#version 450
/*
 * Copyright 2023 TU Wien, Institute of Visual Computing & Human-Centered Technology.
 * This file is part of the GCG Lab Framework and must not be redistributed.
 *
 * Original version created by Lukas Gersthofer and Bernhard Steiner.
 * Vulkan edition created by Johannes Unterguggenberger (junt@cg.tuwien.ac.at).
 */

layout (binding = 0) uniform UniformBuffer {
	vec4 color;
	mat4 modelMatrix;
	mat4 modelMatrixForNormals;
	mat4 viewProjMatrix;
	vec4 cameraPosition;
	vec4 illumination; // ka, kd, ks, alpha
} ub_data;

layout (binding = 1) uniform DirectionalLight {
	vec4 color;
	vec4 direction;
} dl_data;

struct PointLight {
	vec4 color;
	vec4 position;
	vec4 attenuation;
};

layout (binding = 2) uniform PointLights {
	PointLight lights[2];
} pl_data;

layout (binding = 3) uniform sampler2D diffuse_texture;
layout (binding = 4) uniform sampler2D rendered_normals;
layout (binding = 5) uniform sampler2D rendered_depth;

layout (location = 0) in VertexData {
	vec3 positionWorld;
	vec3 normalWorld;
	vec2 textureCoordinates;
} frag_in;

layout (location = 0) out vec4 out_color;

vec3 rgb2hsv(vec3 color) {
	float cmax = max(max(color.r, color.g), color.b);
	float cmin = min(min(color.r, color.g), color.b);
	float d = cmax - cmin;
	float h = 0;
	if (d > 0) {
		if (cmax == color.r) {
			h = mod((color.g - color.b) / d, 6.0f);
		} else if (cmax == color.g) {
			h = (color.b - color.r) / d + 2.0f;
		} else {
			h = (color.r - color.g) / d + 4.0f;
		}
		h /= 6.0f;
	}
	float s = cmax == 0 ? 0 : d / cmax;
	float v = cmax;
	return vec3(h, s, v);
}

vec3 hsv2rgb(vec3 color) {
	float h = color.x;
	float s = color.y;
	float v = color.z;

	float c = s * v;
	float x = c * (1 - abs(mod(h * 6.0, 2) - 1));
	float m = v - c;

	float r, g, b;
	switch (int(mod(floor(h * 6.0f), 6))) {
		case 0:
			r = c; g = x; b = 0;
			break;
		case 1:
			r = x; g = c; b = 0;
			break;
		case 2:
			r = 0; g = c; b = x;
			break;
		case 3:
			r = 0; g = x; b = c;
			break;
		case 4:
			r = x; g = 0; b = c;
			break;
		default:
			r = c; g = 0; b = x;
			break;
	}
	r += m;
	g += m;
	b += m;
	return vec3(r, g, b);

}
float discretizeForCelShading(float value, float shadowArea, float levelsBetween, float highlightArea) {

	if (value <= shadowArea) {
		value = 0;
	}
	else if (value >= (1 - highlightArea)) {
		value = 1;
	}
	else {
		float stepSize = (1 - highlightArea - shadowArea) / levelsBetween;
		for (int i = 0; i <= levelsBetween; i++) {
			if (value <= shadowArea + i * stepSize) {
				value = shadowArea + (i - 0.5f) * stepSize;
				break;
			}
		}
	}

	return value;
}
vec3 discretizeForCelShading(vec3 color, float shadowArea, float levelsBetween, float highlightArea) {
	vec3 cel_color_hsv = rgb2hsv(color);
	cel_color_hsv.z = discretizeForCelShading(cel_color_hsv.z, shadowArea, levelsBetween, highlightArea);
	return hsv2rgb(cel_color_hsv);
}
vec3 phong(vec3 n, vec3 l, vec3 v, vec3 diffuseC, float diffuseF, vec3 specularC, float specularF, float alpha, bool attenuate, vec3 attenuation) {
	float d = length(l);
	l = normalize(l);
	float att = 1.0;
	if(attenuate) {
		att = 1.0f / (attenuation.x + d * attenuation.y + d * d * attenuation.z);
	}
	vec3 r = reflect(-l, n);
	float nl = discretizeForCelShading(dot(n, l), 0.1f, 5.0f, 0.1f);
	return (diffuseF * diffuseC * nl + specularF * specularC * pow(max(0, dot(r, v)), alpha)) * att;
}

// Gets the reflected color value from a a certain position, from
// a certain direction INSIDE of a cornell box of size 3 which is
// positioned at the origin.
// positionWS:  Position inside the cornell box for which to get the
//              reflected color value for from -directionWS.
// directionWS: Outgoing direction vector (from positionWS towards the
//              outside) for which to get the reflected color value for.
vec3 getCornellBoxReflectionColor(vec3 positionWS, vec3 directionWS) {
	vec3 P0 = positionWS;
	vec3 V  = normalize(directionWS);

	const float boxSize = 1.5;
	vec4[5] planes = {
		vec4(-1.0,  0.0,  0.0, -boxSize), // left
		vec4( 1.0,  0.0,  0.0, -boxSize), // right
		vec4( 0.0,  1.0,  0.0, -boxSize), // top
		vec4( 0.0, -1.0,  0.0, -boxSize), // bottom
		vec4( 0.0,  0.0, -1.0, -boxSize)  // back
	};
	vec3[5] colors = {
		vec3(0.49, 0.06, 0.22),    // left
		vec3(0.0, 0.13, 0.31),    // right
		vec3(0.96, 0.93, 0.85), // top
		vec3(0.64, 0.64, 0.64), // bottom
		vec3(0.76, 0.74, 0.68)  // back
	};

	for (int i = 0; i < 5; ++i) {
		vec3  N = planes[i].xyz;
		float d = planes[i].w;
		float denom = dot(V, N);
		if (denom <= 0) continue;
		float t = -(dot(P0, N) + d)/denom;
		vec3  P = P0 + t*V;
		float q = boxSize + 0.01;
		if (P.x > -q && P.x < q && P.y > -q && P.y < q && P.z > -q && P.z < q) {
			return colors[i];
		}
	}
	return vec3(0.0, 0.0, 0.0);
}

// Computes the reflection direction for an incident vector I about normal N,
// and clamps the reflection to a maximum of 180, i.e. the reflection vector
// will always lie within the hemisphere around normal N.
// Aside from clamping, this function produces the same result as GLSL's reflect function.
vec3 clampedReflect(vec3 I, vec3 N)
{
	return I - 2.0 * min(dot(N, I), 0.0) * N;
}

vec3 fresnelSchlick (float cosTheta, vec3 F0) {
    return F0 + (1.0 - F0) * pow (1.0 - cosTheta, 5.0);
}



float linearizeDepth(float depth,float zNear,float zFar)
{
    return zNear * zFar / (zFar + depth * (zNear - zFar));
}
float normalizeDepth(float linear_depth)
{
	vec3 sceneMin = vec3(-0.5f);
	vec3 sceneMax = vec3(4.5f);
	vec3 nearest = clamp(ub_data.cameraPosition.xyz, sceneMin, sceneMax);
	vec3 furthest = mix(sceneMin, sceneMax, step(ub_data.cameraPosition.xyz, (sceneMin + sceneMax) / 2));
	float low = length(ub_data.cameraPosition.xyz - nearest);
	float high = length(ub_data.cameraPosition.xyz - furthest);
	return (linear_depth - low) / (high - low);
}
float getEdgeFactor()
{
	float thickness = 1.4f;
    ivec2 size = textureSize(rendered_depth, 0);
    vec2 uv = (gl_FragCoord.xy + 0.5) / vec2(size);
    vec2 texel = 1.0 / vec2(size);

	const float kx[25] = float[25](
		-1.0/16.0,  -2.0/16.0,   0.0,        2.0/16.0,   1.0/16.0,
		-4.0/16.0,  -8.0/16.0,   0.0,        8.0/16.0,   4.0/16.0,
		-6.0/16.0, -12.0/16.0,   0.0,       12.0/16.0,   6.0/16.0,
		-4.0/16.0,  -8.0/16.0,   0.0,        8.0/16.0,   4.0/16.0,
		-1.0/16.0,  -2.0/16.0,   0.0,        2.0/16.0,   1.0/16.0
	);

	const float ky[25] = float[25](
		-1.0/16.0,  -4.0/16.0,  -6.0/16.0,  -4.0/16.0,  -1.0/16.0,
		-2.0/16.0,  -8.0/16.0, -12.0/16.0,  -8.0/16.0,  -2.0/16.0,
		0.0,        0.0,        0.0,        0.0,        0.0,
		2.0/16.0,   8.0/16.0,  12.0/16.0,   8.0/16.0,   2.0/16.0,
		1.0/16.0,   4.0/16.0,   6.0/16.0,   4.0/16.0,   1.0/16.0
	);



    vec3 normalsValX = vec3(0.0f);
    vec3 normalsValY = vec3(0.0f);
    float depthValX = 0.0f;
    float depthValY = 0.0f;

	float camera_near = 0.1f;
	float camera_far = 100.0f;

    int i = 0;
    for (int y = -2; y <= 2; y++)
    for (int x = -2; x <= 2; x++, i++)
    {
        vec2 offset = thickness * vec2(x, y) * texel / max(1.5f, pow(linearizeDepth(texture(rendered_depth, uv).x, camera_near, camera_far), 0.5f));

        vec3 n = texture(rendered_normals, uv + offset).xyz;
        normalsValX += n * kx[i];
        normalsValY += n * ky[i];

		float d = normalizeDepth(linearizeDepth(texture(rendered_depth, uv + 1.4f * offset).x, camera_near, camera_far));
		depthValX += d * kx[i];
		depthValY += d * ky[i];
    }

	float normalVal = max(length(normalsValX), length(normalsValY));
	float depthVal = smoothstep(0.08, 0.5, max(abs(depthValX), abs(depthValY)));
    return 1.0 - max(normalVal, depthVal);
}
void main() {
	vec3 n = normalize(frag_in.normalWorld);
	vec3 v = normalize(frag_in.positionWorld - ub_data.cameraPosition.xyz);
	vec3 R = normalize(clampedReflect(v, n));
	vec3 reflectionColor = getCornellBoxReflectionColor(frag_in.positionWorld, R);
	vec3 F0 = vec3(0.1); // <-- some kind of plastic
	vec3 reflectivity = fresnelSchlick(dot(n, -v), F0);
	vec3 diffuseColor = texture(diffuse_texture, frag_in.textureCoordinates).rgb;

	// Start with ambient illumination contribution:
	vec3 color = diffuseColor * ub_data.illumination[0];

	// Add directional light's contribution: (Cel 0.05f, 3.0f, 0.1f)
	color += phong(
		n,
		-dl_data.direction.xyz,
		-v,
		dl_data.color.rgb * diffuseColor, ub_data.illumination[1],
		dl_data.color.rgb,                ub_data.illumination[2],
		ub_data.illumination[3],
		false, vec3(1.0)
	);

	// Add point light's contribution (Cel: 0.05f, 2.0f, 0.7f)
	for	(int i = 0; i < 2; i++)
		color += phong(
			n,
			pl_data.lights[i].position.xyz - frag_in.positionWorld,
			-v,
			pl_data.lights[i].color.rgb * diffuseColor, ub_data.illumination[1],
			pl_data.lights[i].color.rgb,                ub_data.illumination[2],
			ub_data.illumination[3],
			true, pl_data.lights[i].attenuation.xyz
		);

		float contrast = 1.09f;
		float brightnessBoost = 0.04f;
		float saturationBoost = 0.25f;
		vec3 hsv = rgb2hsv(color);

		hsv.y = clamp(hsv.y + saturationBoost * (1.0f - hsv.y), 0.0f, 1.0f);
		hsv.z = (hsv.z - 0.5f) * contrast + 0.5f + brightnessBoost;

	out_color = vec4(hsv2rgb(hsv) * getEdgeFactor(), 1.0f);

}

