//? #version 150
#include "lighting.sdr" //! #include "lighting.sdr"
#include "gamma.sdr" //! #include "gamma.sdr"
#include "shadows.sdr" //! #include "shadows.sdr"
#include "z-compress.sdr" //! #include "z-compress.sdr"

out vec4 fragOut0;

uniform sampler2D ColorBuffer;
uniform sampler2D NormalBuffer;
uniform sampler2D PositionBuffer;
uniform sampler2D SpecBuffer;
uniform sampler2DArray shadow_map;

#ifdef ENV_MAP
uniform samplerCube sEnvmap;
uniform samplerCube sIrrmap;
#endif

layout (std140) uniform globalDeferredData {
	mat4 shadow_mv_matrix;
	mat4 shadow_proj_matrix[4];

	mat4 inv_view_matrix;

	float veryneardist;
	float neardist;
	float middist;
	float fardist;

	float invScreenWidth;
	float invScreenHeight;

	float nearPlane;
};

layout (std140) uniform matrixData {
	mat4 modelViewMatrix;
	mat4 projMatrix;
};

layout (std140) uniform lightData {
	vec3 diffuseLightColor;
	float coneAngle;

	vec3 lightDir;
	float coneInnerAngle;

	vec3 coneDir;
	bool dualCone;

	vec3 scale;
	float lightRadius;

	int lightType;
	bool enable_shadows;

	float sourceRadius;
};

// Nearest point sphere and tube light calculations taken from 
// "Real Shading in Unreal Engine 4" by Brian Karis, Epic Games
// Part of SIGGRAPH 2013 Course: Physically Based Shading in Theory and Practice

vec3 ExpandLightSize(in vec3 lightDir, in vec3 reflectDir) {
	// There's an extra max(...,sourceRadius) call here vs the version in the paper.
	// This prevents the centerToRay calculation from choosing a point behind
	// the reflection ray's origin (i.e. underneath the surface).
	// this is necessary for situations where the fragment being shaded lies inside 
	// the sourceRadius of the light.
	// Instead, we choose a point suffciently far away from the reflection origin (hence max(...,sourceRadius))
	// so that we have a gradual transition as the shaded fragments along the surface
	// shift from being inside to outside the light.
	vec3 centerToRay = max(dot(lightDir, reflectDir),sourceRadius) * reflectDir - lightDir;
	return lightDir + centerToRay * clamp(sourceRadius/length(centerToRay), 0.0, 1.0);
}
void GetLightInfo(vec3 position, in float alpha, in vec3 reflectDir, out vec3 lightDirOut, out float attenuation, out float area_normalisation)
{
	if (lightType == LT_DIRECTIONAL) {
		lightDirOut = normalize(lightDir);
		attenuation = 1.0;
		area_normalisation = 1.0;
	} else {
		vec3 lightPosition = modelViewMatrix[3].xyz;
		// Positional light source
		if (lightType == LT_POINT) {
			lightDirOut = lightPosition - position.xyz;
			float dist = length(lightDirOut);

			// this chunk is unnecessary if sourceRadius= 0.0, but let's avoid a branch.
			// given a sphere of radius sourceRadius centered at lightDirOut,
			// move lightDirOut towards the ray defined by reflectDir
			lightDirOut = ExpandLightSize(lightDirOut, reflectDir);
			dist = length(lightDirOut);
			// Energy conservation term
			float alpha_adjust = clamp(alpha + (sourceRadius/(2*dist)), 0.0, 1.0);
			area_normalisation = alpha/alpha_adjust;
			area_normalisation *= area_normalisation;
			//end chunk


			if(dist > lightRadius) {
				discard;
			}
			attenuation = 1.0 - clamp(sqrt(dist / lightRadius), 0.0, 1.0);
		}
		else if (lightType == LT_TUBE) {  // Tube light
			vec3 beamVec = vec3(modelViewMatrix * vec4(0.0, 0.0, -scale.z, 0.0));
			vec3 beamDir = normalize(beamVec);
			//The actual 'lighting element' is shorter than the light volume cylinder
			//To compensate the light is moved forward along the beam one radius and the length shortened
			//this allows room for clean falloff of the light past the ends of beams.
			vec3 adjustedLightPos = lightPosition - (beamDir * lightRadius);
			vec3 adjustedbeamVec = beamVec - 2.0 * lightRadius * beamDir;
			float beamLength = length(adjustedbeamVec);
			//adjustments having been made, sourceDir needs recalculating
			vec3 sourceDir = adjustedLightPos - position.xyz;

			// Get point on beam nearest to the reflection ray.
			vec3 a_t = reflectDir;
			vec3 b_t = beamDir;
			vec3 b_0 = sourceDir;
			vec3 c = cross(a_t, b_t);
			vec3 d = b_0;
			vec3 r = d - a_t * dot(d, a_t) - c * dot(d,c);
			float neardist = dot(r, r)/dot(b_t, r);
			// Move along the beam by the distance we calculated
			lightDirOut = sourceDir - beamDir * clamp(neardist, 0.0, beamLength);
			// Somebody with a symbolic expression simplifier or a wrinklier brain than me
			// should figure out how to optimise these calcs - qaz


			// this chunk is unnecessary if sourceRadius = 0.0, but let's avoid a branch.
			// Same principle as in LT_POINT, treat chosen location as a spherelight.
			lightDirOut = ExpandLightSize(lightDirOut, reflectDir);
			float dist = length(lightDirOut);
			// Energy conservation term
			float alpha_adjust = min(alpha + (sourceRadius/(2*dist)), 1.0);
			area_normalisation = alpha/alpha_adjust;
			// don't need to square as it's a line rather than a sphere.
			//end chunk

			if(dist > lightRadius) {
				discard;
			}
			attenuation = 1.0 - clamp(sqrt(dist / lightRadius), 0.0, 1.0);
		}
		else if (lightType == LT_CONE) {
			lightDirOut = lightPosition - position.xyz;
			float coneDot = dot(normalize(-lightDirOut), coneDir);
			float dist = length(lightDirOut);
			attenuation = 1.0 - clamp(sqrt(dist / lightRadius), 0.0, 1.0);
			area_normalisation = 1.0;

			if(dualCone) {
				if(abs(coneDot) < coneAngle) {
					discard;
				} else {
					attenuation *= smoothstep(coneAngle, coneInnerAngle, abs(coneDot));
				}
			} else {
				if (coneDot < coneAngle) {
					discard;
				} else {
					attenuation *= smoothstep(coneAngle, coneInnerAngle, coneDot);
				}
			}
		}
		attenuation *= attenuation;
		lightDirOut = normalize(lightDirOut);
	}
}

#ifdef ENV_MAP
void ComputeEnvLight(float alpha, float ao, vec3 light_dir, vec3 eyeDir, vec3 normal, vec4 baseColor, vec4 specColor, out vec3 envLight) {
	// Bit of a hack here, pretend we're doing blinn-phong shading and use a (modified version of) the derivation from
	// Plausible Blinn-Phong Reflection of Standard Cube MIP-Maps - McGuire et al.
	// to determine the mip bias.
	// We use the specular term listed at http://graphicrants.blogspot.com/2013/08/specular-brdf-reference.html
	// 1/(pi*alpha^2) * NdotM^((2/alpha^2)-2)
	// so let s = (2/alpha^2 - 2)
	// 1/2 log(s + 1) = 1/2 log(2/alpha^2 -1)

	const float ENV_REZ = 512; // Ideally this would be #define'd and shader recompiled with envmap rez changes
	const float REZ_BIAS = log2(ENV_REZ * sqrt(3));

	float alphaSqr = alpha * alpha;
	float rough_bias = 0.5 * log2(2/alphaSqr - 1);
	float mip_bias = REZ_BIAS - rough_bias;

	// Sample light, using mip bias to blur it.
	vec3 env_light_dir = vec3(modelViewMatrix * vec4(light_dir, 0.0));
	vec4 specEnvColour = srgb_to_linear(textureLod(sEnvmap, env_light_dir, mip_bias));

	vec3 halfVec = normal;

	// Lots of hacks here to get things to look right. We aren't loading a BRDF split-sum integral texture
	// or properly calculating IBL levels.
	// Fresnel calculation as with standard lights.

	vec3 fresnel = mix(specColor.rgb, FresnelSchlick(halfVec, eyeDir, specColor.rgb), specColor.a);

	// Pseudo-IBL, so use k_IBL
	float k = alpha * alpha/ 2.0f;

	float NdotL = max(dot(light_dir, normal),0);

	float g1vNL = GeometrySchlickGGX(NdotL, k);
	vec3 specEnvLighting = specEnvColour.rgb * fresnel * g1vNL;

	vec3 kD = vec3(1.0)-fresnel;
	kD *= (vec3(1.0) - specColor.rgb);
	vec3 diffEnvColor = srgb_to_linear(texture(sIrrmap, vec3(modelViewMatrix * vec4(normal, 0.0))).rgb);
	vec3 diffEnvLighting = kD * baseColor.rgb * diffEnvColor * ao;
	envLight = (specEnvLighting + diffEnvLighting) * baseColor.a;
}
#endif

void main()
{
	vec2 screenPos = gl_FragCoord.xy * vec2(invScreenWidth, invScreenHeight);
	vec4 position_buffer = texture(PositionBuffer, screenPos);
	vec3 position = vec3(position_buffer.xy, uncompress_depth_value(position_buffer.z));

	if(abs(dot(position, position)) < nearPlane * nearPlane)
		discard;

	vec4 diffuse = texture(ColorBuffer, screenPos);
	vec3 diffColor = diffuse.rgb;
	vec4 normalData = texture(NormalBuffer, screenPos);
	// The vector in the normal buffer could be longer than the unit vector since decal rendering only adds to the normal buffer
	vec3 normal = normalize(normalData.xyz);
	float gloss = normalData.a;
	float roughness = clamp(1.0f - gloss, 0.0f, 1.0f);
	float alpha = roughness * roughness;
	vec3 eyeDir = normalize(-position);
	vec3 reflectDir = reflect(-eyeDir, normal);
	vec4 specColor = texture(SpecBuffer, screenPos);

	vec4 fragmentColor = vec4(1.0);

	if (lightType == LT_AMBIENT) {
		float ao = position_buffer.w;
		fragmentColor.rgb = diffuseLightColor * diffColor * ao;

#ifdef ENV_MAP
		vec3 envLight;
		ComputeEnvLight(alpha, ao, reflectDir, eyeDir, normal, diffuse, specColor, envLight);
		fragmentColor.rgb += envLight;
#endif
	}
	else {
		float fresnel = specColor.a;

		vec3 lightDir;
		float attenuation;
		float area_normalisation;
		GetLightInfo(position, alpha, reflectDir, lightDir, attenuation, area_normalisation);

		if (enable_shadows) {
			vec4 fragShadowPos = shadow_mv_matrix * inv_view_matrix * vec4(position, 1.0);
			vec4 fragShadowUV[4];
			fragShadowUV[0] = transformToShadowMap(shadow_proj_matrix[0], 0, fragShadowPos);
			fragShadowUV[1] = transformToShadowMap(shadow_proj_matrix[1], 1, fragShadowPos);
			fragShadowUV[2] = transformToShadowMap(shadow_proj_matrix[2], 2, fragShadowPos);
			fragShadowUV[3] = transformToShadowMap(shadow_proj_matrix[3], 3, fragShadowPos);

			attenuation *= getShadowValue(shadow_map, -position.z, fragShadowPos.z, fragShadowUV, fardist, middist,
									neardist, veryneardist);
		}

		vec3 halfVec = normalize(lightDir + eyeDir);
		float NdotL = clamp(dot(normal, lightDir), 0.0, 1.0);
		fragmentColor.rgb = computeLighting(specColor.rgb, diffColor, lightDir, normal.xyz, halfVec, eyeDir, roughness, fresnel, NdotL).rgb * diffuseLightColor * attenuation * area_normalisation;
	}

	fragOut0 = max(fragmentColor, vec4(0.0));
}
