#shader vertex
#version 440 core

layout(location = 0) in vec3 VertexPosition;
layout(location = 1) in vec2 UVPosition;

out flat vec2 UV;

void main()
{
	UV = UVPosition;
    gl_Position = vec4(VertexPosition, 1.0f);
}

#shader fragment
#version 420 core

in vec2 UV;
out vec4 outColor;

uniform sampler2D textureFrame;
uniform sampler2D textureDepth;
uniform sampler2D textureNorm;
uniform sampler2D textureMetallic;
uniform sampler2D textureAlbedo;

uniform mat4 proj;
uniform mat4 invProj;
uniform mat4 view;
uniform mat4 invView;

uniform float rayStep = 0.2f;
uniform int iterationCount = 100;
uniform float distanceBias = 0.05f;
uniform bool enableSSR = true;
uniform int sampleCount = 4;
uniform bool isSamplingEnabled = true;
uniform bool isExponentialStepEnabled = false;
uniform bool isAdaptiveStepEnabled = true;
uniform bool isBinarySearchEnabled = true;
uniform bool debugDraw = false;
uniform float samplingCoefficient = 0.001f;

vec3 ReflectedVector;
float Metallic;
const float reflectionSpecularFalloffExponent = 5.0;
float random(vec2 uv) {
	return fract(sin(dot(uv, vec2(12.9898, 78.233))) * 43758.5453123); //simple random function
}

vec3 generatePositionFromDepth(vec2 texturePos, float depth) {
	vec4 ndc = vec4((texturePos - 0.5) * 2, depth, 1.f);
	vec4 inversed = invProj * ndc;// going back from projected
	inversed /= inversed.w;
	return inversed.xyz;
}

vec2 generateProjectedPosition(vec3 pos) {
	vec4 samplePosition = proj * vec4(pos, 1.f);
	samplePosition.xy = (samplePosition.xy / samplePosition.w) * 0.5 + 0.5;
	return samplePosition.xy;
}

vec3 SSR(vec3 position, vec3 reflection) {
	vec3 step = rayStep * reflection;
	vec3 marchingPosition = position + step;
	float delta;
	float depthFromScreen;
	vec2 screenPosition;

	int i = 0;
	for (; i < iterationCount; i++) {
		screenPosition = generateProjectedPosition(marchingPosition);
		depthFromScreen = abs(generatePositionFromDepth(screenPosition, textureLod(textureDepth, screenPosition, 2).x).z);
		delta = abs(marchingPosition.z) - depthFromScreen;

		float depth = textureLod(textureDepth, screenPosition, 2).r;
		if (depthFromScreen > 1000.0f)
		{
			vec2 dCoords = smoothstep(0.2, 0.6, abs(vec2(0.5, 0.5) - screenPosition.xy));
			float screenEdgefactor = clamp(1.0 - (dCoords.x + dCoords.y), 0.0, 1.0);
			float ReflectionMultiplier = pow(Metallic, 3.0) *
				screenEdgefactor *
				-ReflectedVector.z;
			return texture(textureFrame, screenPosition).xyz * ReflectionMultiplier;
		}
		if (abs(delta) < distanceBias) {
			vec3 color = vec3(1);
			if (debugDraw)
				color = vec3(0.5 + sign(delta) / 2, 0.3, 0.5 - sign(delta) / 2);
			vec2 dCoords = smoothstep(0.2, 0.6, abs(vec2(0.5, 0.5) - screenPosition.xy));
			float screenEdgefactor = clamp(1.0 - (dCoords.x + dCoords.y), 0.0, 1.0);
			float ReflectionMultiplier = pow(Metallic, 3.0) *
				screenEdgefactor *
				-ReflectedVector.z;
			
			return texture(textureFrame, screenPosition).xyz * color * clamp(ReflectionMultiplier, 0.0, 1.0);
		}
		if (isBinarySearchEnabled && delta > 0) {
			break;
		}
		if (isAdaptiveStepEnabled) {
			float directionSign = sign(abs(marchingPosition.z) - depthFromScreen);
			//this is sort of adapting step, should prevent lining reflection by doing sort of iterative converging
			//some implementation doing it by binary search, but I found this idea more cheaty and way easier to implement
			step = step * (1.0 - rayStep * max(directionSign, 0.0));
			marchingPosition += step * (-directionSign);
		}
		else {
			marchingPosition += step;
		}
		if (isExponentialStepEnabled) {
			step *= 1.05;
		}
	}
	if (isBinarySearchEnabled) {
		for (; i < iterationCount; i++) {

			step *= 0.5;
			marchingPosition = marchingPosition - step * sign(delta);

			screenPosition = generateProjectedPosition(marchingPosition);
			depthFromScreen = abs(generatePositionFromDepth(screenPosition, textureLod(textureDepth, screenPosition, 2).x).z);
			delta = abs(marchingPosition.z) - depthFromScreen;

			vec2 dCoords = smoothstep(0.2, 0.6, abs(vec2(0.5, 0.5) - screenPosition.xy));
			float screenEdgefactor = clamp(1.0 - (dCoords.x + dCoords.y), 0.0, 1.0);
			float ReflectionMultiplier = pow(Metallic, 3.0) * screenEdgefactor *
				-ReflectedVector.z;

			if (abs(delta) < distanceBias) {
				return texture(textureFrame, screenPosition).xyz * clamp(ReflectionMultiplier, 0.0, 0.9);
			}
		}
	}

	return vec3(0, 0, 0);
}

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

void main() {
	vec3 position = generatePositionFromDepth(UV, textureLod(textureDepth, UV, 2).x);
	vec4 normal = view * vec4(texture(textureNorm, UV).xyz * 2.0 - 1.0, 0.0);

	float metallic = texture(textureMetallic, UV).r;
	Metallic = metallic;
	vec3 albedo = texture(textureAlbedo, UV).rgb;
	vec3 F0 = vec3(0.04);
	F0 = mix(F0, albedo, metallic);
	vec3 Fresnel = fresnelSchlick(max(dot(normalize(normal.rgb), normalize(position)), 0.0), F0);

	if (!enableSSR || metallic < 0.01) {
		discard;// outColor = mix(texture(textureFrame, UV), normal, 0.9);
	}
	else {
		vec3 reflectionDirection = normalize(reflect(position, normalize(normal.xyz)));

		ReflectedVector = reflectionDirection;
		if (isSamplingEnabled) {
			vec3 firstBasis = normalize(cross(vec3(0.f, 0.f, 1.f), reflectionDirection));
			vec3 secondBasis = normalize(cross(reflectionDirection, firstBasis));
			vec4 resultingColor = vec4(0.f);
			for (int i = 0; i < sampleCount; i++) {
				vec2 coeffs = vec2(random(UV + vec2(0, i)) + random(UV + vec2(i, 0))) * samplingCoefficient;
				vec3 reflectionDirectionRandomized = reflectionDirection + firstBasis * coeffs.x + secondBasis * coeffs.y;
				vec3 tempColor = SSR(position, normalize(reflectionDirectionRandomized));
				
				resultingColor += vec4(tempColor, 1.0f);
			}

			
			if (resultingColor.w != 0)
				resultingColor /= resultingColor.w;

			outColor = vec4(resultingColor.xyz * Fresnel, 1.0) ;
			
		}
		else {
			outColor = vec4(SSR(position, normalize(reflectionDirection)) * Fresnel, 1.0);
			if (outColor.xyz == vec3(0.f)) {
				outColor = texture(textureFrame, UV);
			}
		}
	}
}
