#version 430
#extension GL_NV_uniform_buffer_std430_layout : enable

uniform mat4 P;
uniform mat4 V;
uniform mat4 M;
uniform vec3 cam_pos;
uniform int window_width;
uniform int window_height;

uniform vec3 la;
uniform vec3 ld;
uniform vec3 ls;

uniform float smoothing;
uniform vec3 sphere_center;

const uint P_SPHERE = 0x00;
const uint P_TORUS = 0x01;
const uint P_CONE = 0x02;
const uint P_CUBE = 0x03;
const uint P_ROUNDED_CUBE = 0x04;

struct GLSLPrimitive
{
	vec4 position;
	vec4 diffuse_color;
	vec4 specular_color;
	vec4 ambient_color;
	
	int specular_exponent;

	uint type;

	float radius;
	float height;
	float inner_radius;
	float outer_radius;
	float size;
	uint dummy;
};

layout(std430, binding = 0) buffer PrimitiveBuffer 
{
	GLSLPrimitive primitives[];
};

out vec4 FragColor;

struct NormalEstimationData 
{
	vec3 normal;
	float minDistance;
	float secondMinDistance;
	float smoothMinDistance;
	GLSLPrimitive closestPrimitive;
	GLSLPrimitive secondClosestPrimitive;
};



float sphereSDF(vec3 p, vec3 center, float r) 
{
    return length(p - center) - r;
}

float torusSDF(vec3 p, vec3 center, float R, float r) 
{
	vec2 q = vec2(length(p.xz - center.xz) - R, p.y - center.y);
	return length(q) - r;
}

float coneSDF(vec3 p, vec3 center, float h, float r) 
{
	vec2 d = abs(vec2(length(p.xz), p.y)) - vec2(r, h);
	return length(d) < 0.0 ? max(d.x, -d.y) : length(max(d, vec2(0)));
}

float cubeSDF(vec3 p, vec3 c, float s) 
{
	float x = max(p.x - c.x - (s / 2.0), c.x - p.x - (s / 2.0));
	float y = max(p.y - c.y - (s / 2.0),c.y - p.y - (s / 2.0));
	float z = max(p.z - c.z - (s / 2.0),c.z - p.z - (s / 2.0));

	float d = x;
	d = max(d, y);
	d = max(d, z);

	return d;
}

float roundedCubeSDF(vec3 p, vec3 center, float s, float r) {
	return length(max(abs(p - center) - vec3(s), vec3(0))) - r;
}

float sdf(vec3 pos, GLSLPrimitive prim)
{
	float d;
	switch (prim.type)
	{
		case P_SPHERE:
		{
			d = sphereSDF(pos, prim.position.xyz, prim.radius);
			break;
		}
		case P_TORUS:
		{
			d = torusSDF(pos, prim.position.xyz, prim.outer_radius, prim.inner_radius);
			break;
		}
		case P_CONE:
		{
			d = coneSDF(pos, prim.position.xyz, prim.height, prim.radius);
			break;
		}
		case P_CUBE:
		{
			d = cubeSDF(pos, prim.position.xyz, prim.size);
			break;
		}
		case P_ROUNDED_CUBE:
		{
			d = roundedCubeSDF(pos, prim.position.xyz, prim.size, prim.radius);
			break;
		}
		default:
		{
			d = 0.0;
			break;
		}
	}
	return d;
}

float smoothMinSDF(float d1, float d2, float k) 
{
	if (k == 0.0) k = 0.000001;
	float h = max(k - abs(d1 - d2), 0) / k;
	return min(d1, d2) - h * h * h * k * 1.0 / 6.0;
}

float smoothMaxSDF(float d1, float d2, float k) 
{
	if (k == 0.0) k = 0.000001;
	float h = min(k - abs(d1 - d2), 0) / k;
	return min(d1, d2) - h * h * h * k * 1.0 / 6.0;
}

void estimateSmoothNormals(vec3 p, float smoothness, float epsilon, inout vec3 normals, inout vec4 color)
{
	float d =  sdf(p, primitives[0]);
	float nx = sdf(vec3(p.x + epsilon, p.y, p.z), primitives[0]);
	float ny = sdf(vec3(p.x, p.y + epsilon, p.z), primitives[0]);
	float nz = sdf(vec3(p.x, p.y, p.z + epsilon), primitives[0]);

	float blendFactor = clamp(0.5 + 0.5 * (sdf(p, primitives[0]) - sdf(p, primitives[1])) / smoothness, 0.0, 1.0);
	vec4 blendedColor = mix(primitives[0].diffuse_color, primitives[1].diffuse_color, blendFactor);

	blendFactor = clamp(0.5 + 0.5 * (sdf(p, primitives[1]) - sdf(p, primitives[2])) / smoothness, 0.0, 1.0);
	vec4 blendedColor1 = mix(primitives[1].diffuse_color, primitives[2].diffuse_color, blendFactor);

	blendFactor = clamp(0.5 + 0.5 * (sdf(p, primitives[2]) - sdf(p, primitives[0])) / smoothness, 0.0, 1.0);
	vec4 blendedColor2 = mix(primitives[2].diffuse_color, primitives[0].diffuse_color, blendFactor);

	vec4 blendedColor3 = mix(blendedColor, blendedColor1, blendFactor);
	vec4 blendedColor4 = mix(blendedColor3, blendedColor2, blendFactor);


	for (int i =  1; i < primitives.length(); i++)
	{
		d = smoothMinSDF(d, sdf(p, primitives[i]), smoothness);
		nx = smoothMinSDF(nx, sdf(vec3(p.x + epsilon, p.y, p.z), primitives[i]), smoothness);
		ny = smoothMinSDF(ny, sdf(vec3(p.x, p.y + epsilon, p.z), primitives[i]), smoothness);
		nz = smoothMinSDF(nz, sdf(vec3(p.x, p.y, p.z + epsilon), primitives[i]), smoothness);
	}
	nx -= d;
	ny -= d;
	nz -= d;

	normals = normalize(vec3(nx, ny, nz));
}

vec3 estimateNormals(vec3 p, GLSLPrimitive prim, float epsilon)
{
	float x = sdf(vec3(p.x + epsilon, p.y, p.z), prim) - sdf(vec3(p.x - epsilon, p.y, p.z), prim);
	float y = sdf(vec3(p.x, p.y + epsilon, p.z), prim) - sdf(vec3(p.x, p.y - epsilon, p.z), prim);
	float z = sdf(vec3(p.x, p.y, p.z + epsilon), prim) - sdf(vec3(p.x, p.y, p.z - epsilon), prim);
	return normalize(vec3(x, y, z));
}

//vec3 estimateNormalsMax(vec3 p, float smoothness) {
//	float epsilon = 0.001;
//	float d = max(torusSDF(p, torus.center, torus.R, torus.r), sphereSDF(p, sphere.center, sphere.r));
//	float nx = max(torusSDF(vec3(p.x + epsilon, p.y, p.z), torus.center, torus.R, torus.r), sphereSDF(vec3(p.x + epsilon, p.y, p.z), sphere.center, sphere.r)) - d;
//	float ny = max(torusSDF(vec3(p.x, p.y + epsilon, p.z), torus.center, torus.R, torus.r), sphereSDF(vec3(p.x, p.y + epsilon, p.z), sphere.center, sphere.r)) - d;
//	float nz = max(torusSDF(vec3(p.x, p.y, p.z + epsilon), torus.center, torus.R, torus.r), sphereSDF(vec3(p.x, p.y, p.z + epsilon), sphere.center, sphere.r)) - d;
//	return normalize(vec3(nx, ny, nz));
//}

vec4 blendColors(vec4 color1, vec4 color2, float blendFactor)
{
	return mix(color1, color2, blendFactor);
}

float weightFunction(float distance, float k, float epsilon) 
{
	if (k <= 0.0) 
	{
		// When k is zero, return 1 if the distance is zero (object is closest), else return 0.
		return distance <= 0.001 ? 1.0 : 0.0;
	}
	else 
	{
		// As k increases, the transition becomes smoother.
		return (k * k * k) / distance;
	}
}

void main(void)
{
	vec3 normals;
	vec3 color = la;
	vec4 ray_pos = vec4(cam_pos, 1.0);
	float ray_dist = 0.0;
	float max_dist = 100.0;
	float epsilon = 0.0001;
	int steps = 0;
	int max_steps = 1000;
	vec3 light_pos = vec3(0.0, 1.0, 0.0);
	int spec_exponent = 40;
	float k = smoothing;

	vec2 ndc_pos = 2.0 * vec2(gl_FragCoord.x / window_width, gl_FragCoord.y / window_height) - 1.0;
	vec4 cam_dir = inverse(P) * vec4(ndc_pos, 1.0, 1.0);
	cam_dir /= cam_dir.w;
	cam_dir = vec4(cam_dir.xyz, 0.0);

	vec4 ray_dir = normalize(inverse(V) * cam_dir);

	vec4 accumulatedDiffuse = vec4(0.0);
	vec4 accumulatedSpecular = vec4(0.0);
	vec4 accumulatedAmbient = vec4(0.0);
	float accumulatedSpecExp = 0.0;
	float totalWeight = 0.0;

	int closestIndex = -1;

	while (ray_dist < max_dist && steps < max_steps && primitives.length() != 0)
	{
		steps++;
		float minDist = 999999.0;

		for (int i = 0; i < primitives.length(); i++)
		{
			float dist = sdf(ray_pos.xyz, primitives[i]);
			float weight = weightFunction(dist, smoothing, epsilon);
			if (dist < sdf(ray_pos.xyz, primitives[closestIndex]))
			{
				closestIndex = i;
			}
			minDist = smoothMinSDF(minDist, dist, k);

			accumulatedDiffuse += primitives[i].diffuse_color * weight;
			accumulatedSpecular += primitives[i].specular_color * weight; 
			accumulatedAmbient += primitives[i].ambient_color * weight;
			accumulatedSpecExp += primitives[i].specular_exponent * weight;
			totalWeight += weight;
		}

		

		if (minDist <= epsilon)
		{
			if (totalWeight > 0.0) {
				accumulatedDiffuse /= totalWeight;
				accumulatedSpecular /= totalWeight;
				accumulatedAmbient /= totalWeight;
				accumulatedSpecExp /= totalWeight;
			}

			vec4 ambient_color, diffuse_color;

			ambient_color = vec4(accumulatedAmbient.rgb, 1.0);
			diffuse_color = vec4(accumulatedDiffuse.rgb, 1.0);

			vec4 ambient = vec4(la, 1.0) * ambient_color;

			//normals = estimateNormals(ray_pos.xyz, primitives[closestIndex], epsilon);
			estimateSmoothNormals(ray_pos.xyz, k, epsilon, normals, diffuse_color);

			vec3 nw = normalize(normals);
			vec3 lw = normalize(light_pos - ray_pos.xyz);
			float dist = length(vec4(ray_pos.xyz - light_pos, 1.0));
			vec4 diffuse = max(dot(nw, lw), 0) * diffuse_color * vec4(ld, 1.0);

			float spec;
			vec3 vw = normalize(cam_pos - ray_pos.xyz);

			vec3 halfwayDir = normalize(lw + vw);
			spec = pow(max(dot(halfwayDir, nw), 0),  int(accumulatedSpecExp));

			vec4 specular = spec * vec4(accumulatedSpecular.rgb * ls, 1.0);

			color = (ambient + (1.0 / (dist * dist) * (diffuse + specular))).xyz;
			break;
		}
		ray_pos += ray_dir * minDist;
		ray_dist += minDist;
	}

	FragColor = vec4(color, 1.0);
}