#version 430 core

out vec4 fragColor;

// input variables
uniform vec2  resolution;	// screen resolution
uniform float time;			// global time

uniform vec2  cameraRotation;	// rotation of camera in x and y
uniform float cameraZoom;		// zoom of camera

uniform sampler2D worldTexture;			// stone
uniform sampler2D environmentTexture;	// texture of background as environemnt
uniform sampler2D coinTexture;			// texture of coins
uniform sampler2D testTexture;			// test

uniform float width;
uniform float height;

uniform int   drawGytebot;
uniform vec2  gytebotLocation;
uniform float gytebotRotation;

layout(std430, binding = 0) readonly buffer CoinBuffer
{
	int coinBuffer[];
};

layout(std430, binding = 1) readonly buffer WallBuffer
{
	float wallBuffer[];
};

// material indexes
const int MaterialNone = 0;
const int MaterialBlue = 1;
const int MaterialEyes = 2;
const int MaterialDark = 3;
const int MaterialCoin = 4;
const int MaterialRockDef = 5;
const int MaterialRockExt = 6;

#define SDF3D vec2	// SDF data type

//  _______ _________ _______  _______  _
// (  ____ \\__   __/(  ___  )(  ____ )( )
// | (    \/   ) (   | (   ) || (    )|| |
// | (_____    | |   | |   | || (____)|| |
// (_____  )   | |   | |   | ||  _____)| |
//       ) |   | |   | |   | || (      (_)
// /\____) |   | |   | (___) || )       _
// \_______)   )_(   (_______)|/       (_)
//
// PLEASE DO NOT TRY TO EDIT ANY OF THE FOLLOWING CODE IF NOT NECESSARY!

const float PI = 3.14159265359;

const int mapBounds = 32;

const float nearPlane = 1e-3;	// camera near plane
const float farPlane  = 1e3;	// camera far plane

const float wallHeight = 0.05;	 // height of walls. NOTE: if raised or lowered coins and the bot may float or sink into the ground
// TODO: adjust coin and bot elevation by wallHeight

const float borderSize = 0.03;	// extended border for map
// recommend values: [ 0.015, 0.030, 0.04 ]

const vec2 epsilon = vec2(1e-3, 0); // small value for creating tiny gaps to prevent errors, not recommend to change

// gytebot exceeds width of 1.0, so adjust scale factor to make him fit in!
const float gytebotScale 	= (mapBounds + 18);
const float invGytebotScale = 1.0 / gytebotScale;

// boundary of walls
const vec3 wallBounding = vec3(width/mapBounds + epsilon.x, wallHeight + epsilon.x, height/mapBounds + epsilon.x);
//                                                   ^ add some space on top for cleaner normal calculations
const float gytebotElevation = -invGytebotScale - 0.02;

// create a "3D" texture lookup by blending all 3 different sides of an cube together
// TODO: Replace with biplanar-mapping
vec3 textureBox2D(in sampler2D sampler, in vec3 normal, in vec3 point)
{
	vec3 x = texture2D(sampler, point.yz).bgr;
	vec3 y = texture2D(sampler, point.zx).bgr;
	vec3 z = texture2D(sampler, point.xy).bgr;

	vec3 blend = pow(abs(normal), vec3(0.5) );

	return (x * blend.x + y * blend.y + z * blend.z) / (blend.x + blend.y + blend.z);
}

float sampleWallMap(in vec2 point)
{
	vec2 f = fract(point * vec2(width, height) );
	int x  = int(point.x * width);
	int y  = int(point.y * height);
	
	// cubic hermite curve to smooth interpolation factors
	f = f * f * (3.0 - 2.0 * f);
	// apply mix factor transformation to 'harden' edges
	f = pow(f, vec2(16.0) );

	float s00 = wallBuffer[(x + 0) + (y + 0) * mapBounds];
	float s10 = wallBuffer[(x + 1) + (y + 0) * mapBounds];
	float s01 = wallBuffer[(x + 0) + (y + 1) * mapBounds];
	float s11 = wallBuffer[(x + 1) + (y + 1) * mapBounds];
	
	// mix top and bottom samples from left to right
	float south = mix(s00, s10, f.x);
	float north = mix(s01, s11, f.x);
	
	// mix top and bottom together and invert
	return 1.0 - mix(south, north, f.y);
}

// extrude a 2D sdf (on x,z) to 3D (x,y,z)
// by Inigo Quilez: https://iquilezles.org/www/articles/distfunctions/distfunctions.htm
float extrudeSdf(in vec3 position, in float sdf)
{
    vec2 w = vec2(sdf, abs(position.y) - wallHeight);
    return min(max(w.x, w.y), 0.0) + length(max(w, 0.0) );
}

// wall sdf
float sdfMap(in vec3 position)
{
	// sample flat 2D-walls
	float sdfWall = sampleWallMap( (position.xz + wallBounding.xz) / (2.0 * wallBounding.xz) - vec2(0.11/vec2(width, height) ) ) * 0.01 - 0.002;
	// extrude 2D-walls to 3D
	return extrudeSdf(position, sdfWall);
}

float sdfBox(vec3 p, vec3 b)
{
  vec3 q = abs(p) - b;
  return length(max(q, 0.0) ) + min(max(q.x, max(q.y, q.z) ), 0.0);
}

float borderBox(in vec3 position)
{
	// compute bounding container, by cutting out a small box out of a big box via boolean difference
	return max(
				 sdfBox(position, wallBounding + vec3(borderSize, 0.02, borderSize) ), // big box
				-sdfBox(position + vec3(0, 0.1, 0), wallBounding - vec3(0.01, -0.05, 0.01) ) // small box
			);
}

// AABB ray-intersection algorithm
// by Inigo Quilez: https://iquilezles.org/www/articles/intersectors/intersectors.htm
vec2 boxIntersection(in vec3 origin, in vec3 direction, inout vec3 normal, in vec3 boxSize)
{
	vec3 m = 1.0 / direction;
    vec3 n = m * origin;   	  // can precompute if traversing a set of aligned boxes
    vec3 k = abs(m) * boxSize;
    vec3 t1 = -n - k;
    vec3 t2 = -n + k;

    float tN = max(max(t1.x, t1.y), t1.z);
    float tF = min(min(t2.x, t2.y), t2.z);

    if(tN > tF || tF < 0.0)
    	return vec2(-1.0); // no intersection

    normal = -sign(direction) * step(t1.yzx, t1.xyz) * step(t1.zxy, t1.xyz);

    return vec2(tN, tF);
}

SDF3D sdfUnion(in SDF3D a, in SDF3D b)
{
	return a.x < b.x ? a : b;
}

float smoothmin(in float d1, in float d2, in float k)
{
    float h = clamp(0.5 + 0.5 * (d2 - d1) / k, 0.0, 1.0);
    return mix(d2, d1, h) - k * h * (1.0 - h);
}

SDF3D sdfGytebot(in vec3 position)
{
	return sdfUnion(
		SDF3D(
			// wheels
			min(
				length(position - vec3( 0, 1.5, 1) ) - 0.58,
				min(
					length(position - vec3( 0.75, 1.5, -0.5) ) - 0.58,
					length(position - vec3(-0.75, 1.5, -0.5) ) - 0.58
				)
			), MaterialDark),
		sdfUnion(
			// eye
			SDF3D( (length( (position + vec3(0, 0, 0.7) ) * vec3(1, 1, 2.0) ) - 0.5) * 0.5, MaterialEyes),
			SDF3D(
				min(
					// head as sliced sphere shell
					max(-dot(position, vec3(0, 0, 1) ) - 0.7, abs(length(position) - 1.0) - 0.05 ),
						max(
							-dot(position, vec3(0, -1, 0) ) - 1.8, // slice
							smoothmin(
								length(position - vec3(0,1,0)) - 0.6, // body, will connect three wheels
								min(
									length(position - vec3( 0, 1.5, 1) ) - 0.6, // back wheel
									min(
										length(position - vec3( 0.75, 1.5, -0.5) ) - 0.6, // right wheel
										length(position - vec3(-0.75, 1.5, -0.5) ) - 0.6  // left wheel
									)
								), 0.2
							)
						)
				), MaterialBlue)
		)
	);
}

mat2 constructRotationMatrix(float theta)
{
	float sinTheta = sin(theta);	// sine of theta
	float cosTheta = cos(theta);	// cosine of theta

	mat2 matrix = mat2(sinTheta, -cosTheta, cosTheta, sinTheta);	// construct rotation matrix

	return matrix;
}

// standard arguments required for composing a mesh
#define stdArgs inout vec3  viewPosition, \
				inout vec3  viewDirection, \
				inout vec3  position, \
				inout vec3  normal, \
				inout float depth, \
				inout int   materialIndex

float sdfCoin(in vec3 position)
{
	vec2 d = abs(vec2(length(position.xy), position.z) ) - vec2(0.43, 0.0001);
	return min(max(d.x, d.y), 0.0) + length(max(d, 0.0) ) / 64.0;
}

vec2 coinUv;
void renderMap(stdArgs)
{
	// get intersection distances with box containing the map
	vec2 bound = boxIntersection(viewPosition, viewDirection, normal, wallBounding);

	if (bound.y > 0.0) // if we hit, i.e tFar is greater than 0 or not behind the camera
	{
		float rayLength    = max(bound.x, epsilon.x);
		vec3  intersection = viewPosition + viewDirection * rayLength;
		float maxDist      = distance(intersection, viewPosition + viewDirection * bound.y);
		float map;

		// march through cube
		for (float dist = epsilon.x; dist < maxDist; dist += map) {
			vec3 location = intersection + viewDirection * dist;
			map           = sdfMap(location);

			if (map < epsilon.x)
			{
				// set material data
				depth    = max(bound.x, epsilon.x) + dist;
				position = location;
				normal   = normalize(map.x - vec3(
						sdfMap(location + epsilon.xyy).x,
						sdfMap(location + epsilon.yxy).x,
						sdfMap(location + epsilon.yyx).x ) );

				materialIndex = MaterialRockDef;

				break; // were done, break out
			}
		}

		// traverse a grid of 2D cells via bransham
		// if we enter a cell check if there is coin in it, if not continue, if yes, render coin.
		// in case there was a coin, but it has no effect, meaning we missed it, continue

		// calculate start- and end-points of bounding box containing the cells
		vec2 endPoint   = vec3(viewPosition + viewDirection * bound.y).xz;
		vec2 startPoint = vec3(viewPosition + viewDirection * max(bound.x, epsilon.x) ).xz;

		// remap to [0, width/height]
		endPoint   = (endPoint   / wallBounding.xz * 0.5 + 0.5) * vec2(width, height);
		startPoint = (startPoint / wallBounding.xz * 0.5 + 0.5) * vec2(width, height);

		vec2 diff = endPoint - startPoint;

		// number of steps we need to travel
		float dist = abs(diff.x) + abs(diff.y);

		// amount of space we need to travel for each cell
		vec2 delta = diff / dist;

		// traverse grid
		for (float i = 0.0; i < ceil(dist); i += 1.0) {
			// position in grid
			vec2 pos = floor(startPoint + delta * i);

			// index of coin
			int index  = int(pos.x + pos.y * width);
			int isCoin = coinBuffer[index];
			// do we have a coin?
			if (isCoin == 1)
			{
				mat2 coinRotation = constructRotationMatrix(time);
				// transform local cell space to world space
				vec2 localSpace  = (pos / vec2(width, height) * 2.0 - 1.0) * wallBounding.xz;
				// construct world space translation
				vec3 translation = vec3(localSpace.x, 0, localSpace.y) + vec3(1.0 / mapBounds, -0.03, 1.0 / mapBounds);

				float t = epsilon.x;
				
				// march a cylinder as a coin
				for (int x = 0; x < 80; x++) {
					vec3 location = (intersection - translation + viewDirection * t) * mapBounds;

					location.xz *= coinRotation;

					float h = sdfCoin(location);
  					
  					if (h < epsilon.x)
  					{
  						// hit it!
  						position = intersection + viewDirection * t;
  						depth    = t + rayLength;
  						normal   = normalize(h - vec3(
										sdfCoin(location + epsilon.xyy).x,
										sdfCoin(location + epsilon.yxy).x,
										sdfCoin(location + epsilon.yyx).x ) );
  						normal.xz *= inverse(coinRotation);

  						materialIndex = MaterialCoin;

  						coinUv = location.xy - vec2(0, 0.5);

  						i = 100.0;
  						break;
  					}
  					t += h;
  					
  					// view is obstructed, break up
  					if (t + rayLength > depth)
  						break;
				}
			}
		}
	}
}

void renderBoundingBox(stdArgs)
{
	float dist = nearPlane;

	for (int i = 0; i < 128; i++) {
		vec3 location = viewPosition + viewDirection * dist;
		float map     = borderBox(location);
		dist         += map;

		if (dist > depth)
			break;
		else if (map < epsilon.x * dist)
		{
			depth    = dist;
			position = location;
			normal   = normalize(map.x - vec3(
					borderBox(location + epsilon.xyy).x,
					borderBox(location + epsilon.yxy).x,
					borderBox(location + epsilon.yyx).x ) );

			materialIndex = MaterialRockExt;

			break;
		}
	}
}

float gytebotOcclusion = 0.0;
int renderGytebot(stdArgs, in vec2 worldGytebotLocation)
{
	float dist = nearPlane;

	vec3 gytebotPosition = viewPosition - vec3(worldGytebotLocation.x, gytebotElevation, worldGytebotLocation.y);

	for (int i = 0; i < 80; i++) {
		vec3  location       = gytebotPosition + viewDirection * dist;
		vec3  scaledLocation = location * gytebotScale;
		SDF3D map            = sdfGytebot(scaledLocation) * vec2(invGytebotScale, 1);

		if (map.x < epsilon.x)
		{
			depth    = dist;
			position = location;
			// compute normal
			normal = normalize(sdfGytebot(scaledLocation).x * invGytebotScale - vec3(
						sdfGytebot(scaledLocation + epsilon.xyy).x * invGytebotScale,
						sdfGytebot(scaledLocation + epsilon.yxy).x * invGytebotScale,
						sdfGytebot(scaledLocation + epsilon.yyx).x * invGytebotScale) );

			materialIndex = int(map.y);

			return 1;
		}
		dist += map.x;
		if (dist > depth)
			break;
	}

	gytebotOcclusion = min(sdfGytebot( (gytebotPosition + viewDirection * depth) * gytebotScale).x * 0.6 + 0.4, 1.0);
	return 0;
}

const vec2 invAtan = vec2(0.1591, 0.3183);

vec2 getSphericalSt(in vec3 direction)
{
    vec2 uv  = vec2(atan(direction.z, direction.x), asin(direction.y) );
         uv *= invAtan;
         uv += 0.5;
    return uv;
}

vec3 shadeMaterial(in vec3 incoming, in vec3 position, in vec3 normal, in vec3 albedo, in float roughness)
{
	vec2 uv         = getSphericalSt(normal);
	vec3 irradiance = textureGrad(environmentTexture, uv, vec2(0.05), vec2(0.05) ).bgr;

	float lod     = pow(roughness, 4.0);
	vec2 specUv   = getSphericalSt(reflect(incoming, normal) );
	vec3 specular = textureGrad(environmentTexture, specUv, vec2(lod), vec2(lod) ).bgr;

	float R = min(pow(1.0 - dot(incoming, normal), 2.0) * (1.0 - roughness * roughness), 1.0);

	return mix(irradiance * albedo, specular, R);
}

void main()
{
	// create rotation matracies
	mat2 horizontalCamerRotation = constructRotationMatrix(cameraRotation.x);
	mat2 verticalCamerRotation   = constructRotationMatrix(cameraRotation.y);
	mat2 gytebotRotationMat      = constructRotationMatrix(gytebotRotation );

	// create camera untransformed data
	vec2 screenCoords  = (gl_FragCoord.xy - 0.5 * resolution.xy) / resolution.y;	// re-map fragment coordinates to [-1, 1]
	vec3 viewDirection = normalize(vec3(screenCoords, 1) );							// construct origin
	vec3 viewPosition  = vec3(0, 0, cameraZoom);

	vec3 normal;
	vec3 position;

	int materialIndex = MaterialNone;

	// camera view depth
	// initially set to the farthest
	float depth = farPlane;

	// rotate camera on y
	viewPosition.yz  *= verticalCamerRotation;
	viewDirection.yz *= verticalCamerRotation;
	// rotate camera on x
	viewPosition.xz  *= horizontalCamerRotation;
	viewDirection.xz *= horizontalCamerRotation;

	// compose the maps walls and coins
	renderMap(viewPosition, viewDirection, position, normal, depth, materialIndex);
	// compose a bounding box
	renderBoundingBox(viewPosition, viewDirection, position, normal, depth, materialIndex);

	vec3 cameraDirection = viewDirection;

	vec2 worldGytebotLocation = (gytebotLocation / vec2(width, height) * 2.0 - 1.0) * wallBounding.xz + vec2(0.03);

	viewPosition.xz  -= worldGytebotLocation;	// move camera to origin, to not have the rotation affect the translation

	// rotate camera, to "fake" a gytebot rotation
	viewPosition.xz  *= gytebotRotationMat;
	viewDirection.xz *= gytebotRotationMat;

	viewPosition.xz += worldGytebotLocation;	// move camera back

	if (drawGytebot == 1)
	{
		if (renderGytebot(viewPosition, viewDirection, position, normal, depth, materialIndex, worldGytebotLocation) == 1)
		{
			// rotate normal back to world space
			normal.xz *= inverse(gytebotRotationMat);
		}
	}

	vec3 color;
	if (materialIndex == MaterialNone)
	{
		// sample environment texture + gamma correction
		color = texture2D(environmentTexture, getSphericalSt(cameraDirection) ).bgr;
	} else if (depth < nearPlane)
	{
		color = vec3(0);
	} else
	{
		vec3 albedo;
		switch(materialIndex) {

#define stdParams cameraDirection, position, normal, albedo

			case MaterialRockDef:
				albedo = textureBox2D(worldTexture, normal, position * 5.0);
				color  = shadeMaterial(stdParams, 0.85);
				color *= min(pow(abs(position.y), 0.5) * 8, 1);
				break;

			case MaterialRockExt:
				albedo = textureBox2D(worldTexture, normal, position * 5.0);
				color = shadeMaterial(stdParams, 0.85);

				if (normal.x < 1e-2 && normal.z < 1e-2 && normal.y > 0.99 && position.y > -wallHeight * 0.4)
				{
					// paint ambient occlusion
					vec2 point = (position.xz + wallBounding.xz) / (2.0 * wallBounding.xz);
					color     *= vec3(sampleWallMap(point - vec2(0.11/vec2(width, height) ) ) );
					color     *= vec3(gytebotOcclusion);

					// paint grid lines
					point *= vec2(width, height);
					color += vec3(step(0.95, fract(point.x) ) + step(0.95, fract(point.y) ) ) * pow(texture2D(worldTexture, position.xz).g, 2.0);
					color *= 1.0 - vec3(step(0.0, sin(point.x * PI) * sin(point.y * PI) ) ) * 0.1;
				}
				break;

			case MaterialBlue:
				albedo = vec3(0.275, 0.796, 1);
				color  = shadeMaterial(stdParams, 0.1);
				break;

			case MaterialDark:
				albedo = vec3(0.1);
				color  = shadeMaterial(stdParams, 0.6);
				break;

			case MaterialEyes:
				albedo = vec3(0.1);
				color  = shadeMaterial(stdParams, 0.1);
				color += smoothstep(0.97, 1.0, dot(normal, vec3(cos(gytebotRotation), 0, sin(gytebotRotation) ) ) ); // emission
				break;

			case MaterialCoin:
				albedo = texture2D(coinTexture, coinUv).bgr;
				color  = shadeMaterial(stdParams, 0.2);
				break;

			default: break;
		}
	}

//	color += texture2D(testTexture, screenCoords).rgb;

	fragColor = vec4(color, depth);
}
