#version 440 core

in vec2 fTexcoord;
in vec3 fNormals;
in vec3 fPosition;
in vec4 shadowCoord1;
in vec4 shadowCoord2;
in vec4 worldPosition;
in float visibility;

struct Light {
	vec3 position;
	vec3 intensity;
	vec4 direction;
	float angle;
};

out vec4 outColor;

uniform mat4 model;
uniform sampler2D shadowMap1;
uniform sampler2D shadowMap2;
uniform sampler2D shadowMap3;
uniform sampler2D tex;
uniform mat4 depthBias;
uniform Light light1;
uniform Light light2;
layout (location = 10) uniform float camHeight;

vec2 poissonDisk[16] = vec2[](
	vec2( -0.94201624, -0.39906216 ),
	vec2 ( 0.94558609, -0.76890725 ),
	vec2 ( -0.094184101, -0.92938870 ),
	vec2 ( 0.34495938, 0.29387760 ),
	vec2 ( -0.91588581, 0.45771432 ),
	vec2 ( -0.81544232, -0.87912464 ),
	vec2 ( -0.38277543, 0.27676845 ),
	vec2 ( 0.97484398, 0.75648379 ),
	vec2 ( 0.44323325, -0.97511554 ),
	vec2 ( 0.53742981, -0.47373420 ),
	vec2 ( -0.26496911, -0.41893023 ),
	vec2 ( 0.79197514, 0.19090188 ),
	vec2 ( -0.24188840, 0.99706507 ),
	vec2 ( -0.81409955, 0.91437590 ),
	vec2 ( 0.19984126, 0.78641367 ),
	vec2 ( 0.14383161, -0.14100790 )
);

const float satFilterSize = 5;

float linearizeDepth(float depth) {
	float near = 1.0f;
	float far = 70.0f;
    return (2.0 * near ) / (far + near - depth * (far - near));
}

float calcShadowPCF(vec4 shadowCoord, sampler2D shadowMap, float filterSize) {
	float bias = 0.001;
	vec2 texSize = 1.0 / textureSize(shadowMap, 0);
	vec3 projCoord = shadowCoord.xyz / shadowCoord.w;
	float currentDepth = projCoord.z - bias;

	float shadow = 0;

	for (int x = 0; x < 16; x += 1) {
		vec2 offset = poissonDisk[x] * filterSize;
		float pcfValue = texture(shadowMap, projCoord.xy + offset * texSize).r;
		shadow += currentDepth > pcfValue ? 1.0 : 0.0;
	}

	shadow = shadow / 16;
	return 1.0 - shadow;
}

//http://developer.download.nvidia.com/whitepapers/2008/PCSS_Integration.pdf
float calcShadowPCSSPCFPoisson(vec4 shadowCoord, sampler2D shadowMap) {

	const float blockerSearchNumSamples = 16;
	const float lightSizeUV = 0.8;
	const float nearPlane = 9.5;
	const float pcfNumSamples = 16;

	vec2 texSize = 1.0 / textureSize(shadowMap, 0);
	vec3 projCoord = shadowCoord.xyz / shadowCoord.w;
	float receiverDist = projCoord.z;

	//find blocker
	float blockerSearchFilterSize = lightSizeUV * (receiverDist - nearPlane) / receiverDist;
	float numOccluder = 0;
	float avgBlockerDist = 0;

	for (int x = 0; x < blockerSearchNumSamples; x += 1) {
		float dist = texture(shadowMap, projCoord.xy + poissonDisk[x] * blockerSearchFilterSize * texSize).r;
		if (receiverDist > dist) {
			avgBlockerDist += dist;
			numOccluder += 1;
		}

	}

	/*if (numOccluder == 0)
		return 1;*/

	//calc filtersize
	avgBlockerDist = avgBlockerDist / numOccluder;
	float penumbra = (lightSizeUV * (receiverDist - avgBlockerDist)) / avgBlockerDist;
	float filterSize = (nearPlane * penumbra /receiverDist);
	//filterSize = floor(abs(filterSize));
	filterSize = min(filterSize, 4);

	float shadow = calcShadowPCF(shadowCoord, shadowMap, filterSize);

	return shadow;

}

float getChebyshevUpperBound(vec2 moments, float t) {
	float p = (t <= moments.x) ? 1.0 : 0.0;
	float variance = float(moments.y - (moments.x * moments.x));
	variance = max(variance, 0.0002);
	float d = float(t - moments.x);
	float p_max = variance / (variance + d*d);

	float minv = 0.2;
	float maxv = 1.0;
	p_max = clamp((p_max - minv) / (maxv - minv), 0, 1);
	return max(p, p_max);
}

float calcShadowVSM(vec4 shadowCoord, sampler2D shadowMap) {
	vec3 projCoord = shadowCoord.xyz / shadowCoord.w;
	vec2 moments = texture(shadowMap, projCoord.xy).xy;
	//vec4 value = texture(shadowMap, projCoord.xy);
	//vec2 moments = value.xy + value.zw * (1.0 / 256.0);
	float currentDepth = projCoord.z;

	return getChebyshevUpperBound(moments, currentDepth);
}

float calcShadowSAVSM(vec4 shadowCoord, sampler2D shadowMap, float filtersize) {
	vec3 projCoord = shadowCoord.xyz / shadowCoord.w;
	float currentDepth = linearizeDepth(projCoord.z);

	vec2 texSize = textureSize(shadowMap, 0);
	vec2 texSizeInv = 1.0 / texSize;
	float distributeValue = 256.0f;
	float distInv = 1.0f / distributeValue;

	float recHalf = filtersize / 2.0;

	vec4 upperLeft = texture(shadowMap, projCoord.xy + vec2(-recHalf, recHalf) * texSizeInv);
	vec4 upperRight = texture(shadowMap, projCoord.xy + vec2(recHalf, recHalf) * texSizeInv);
	vec4 bottomLeft = texture(shadowMap, projCoord.xy + vec2(-recHalf, -recHalf) * texSizeInv);
	vec4 bottomRight = texture(shadowMap, projCoord.xy + vec2(recHalf, -recHalf) * texSizeInv);

	vec4 value = bottomRight - bottomLeft - upperRight + upperLeft;
	value = vec4(value.rg + (value.ba * distInv), 0, 0);
	value = value / pow(filtersize, 2);

	return getChebyshevUpperBound(value.xy, currentDepth);
}

float calcShadowPCSSSAVSM(vec4 shadowCoord, sampler2D shadowMap, sampler2D satShadowMap) {

	const float BLOCKER_STEP_COUNT = 4;
	const float lightSizeUV = 0.2;
	const float nearPlane = 9.5;

	vec2 texSize = 1.0 / textureSize(shadowMap, 0);
	vec3 projCoord = shadowCoord.xyz / shadowCoord.w;
	float receiverDist = linearizeDepth(projCoord.z);

	//find blocker
	float blockerSearchFilterSize = lightSizeUV * (receiverDist - nearPlane) / receiverDist;
	float blockerStep = blockerSearchFilterSize / BLOCKER_STEP_COUNT;
	float numOccluder = 0;
	float avgBlockerDist = 0;

	for (float x = -BLOCKER_STEP_COUNT; x <= BLOCKER_STEP_COUNT; x+=1) {
		for (float y = -BLOCKER_STEP_COUNT; y <= BLOCKER_STEP_COUNT; y+=1) {
			vec4 moments = texture(shadowMap, projCoord.xy + vec2(x, y) * blockerStep * texSize);
			float dist = moments.x + moments.y * (1.0 / 256.0);
			if (receiverDist > dist) {
				avgBlockerDist += dist;
				numOccluder += 1;
			}
		}
	}

	if (numOccluder == 0)
		return 1;

	//calc filtersize
	avgBlockerDist = avgBlockerDist / numOccluder;
	float penumbra = (lightSizeUV * (receiverDist - avgBlockerDist)) / avgBlockerDist;
	float filterSize = (nearPlane * penumbra /receiverDist);
	filterSize = clamp(filterSize, 4, 20);

	float shadow = calcShadowSAVSM(shadowCoord, satShadowMap, filterSize);

	return shadow;

}

vec3 calcDirectionalLight(Light dLight, vec4 shadowCoord, sampler2D shadowMap) {
	vec4 lightDirection = dLight.direction;
	vec3 toLightDir = normalize(-lightDirection).xyz;
	vec3 norm = normalize(fNormals);
	float diff = max(dot(norm, toLightDir), 0.0);
	vec3 diffuse = vec3(diff);
	float shadow = calcShadowPCSSPCFPoisson(shadowCoord, shadowMap);
	diffuse = shadow * diffuse;
	return diffuse;
}

vec3 calcSpotLight(Light spotLight, vec4 shadowCoord, sampler2D shadowMap) {
	vec4 lightDirection = spotLight.direction;
	vec3 lightPosition = spotLight.position;
	float lightCutOff = cos(radians(spotLight.angle));
	float lightCutOffInner = cos(radians(spotLight.angle - 5.0f));
	float lightDiffuse = 0.5f;
	float lightConstant = 1.0f;
	float lightLinear =  0.014f;
	float lightQuadratic = 0.0004f;

	vec3 toLightDir = normalize(lightPosition - fPosition);
	float theta = dot(toLightDir, normalize(-lightDirection).xyz);
	float cutOffDiff = lightCutOffInner - lightCutOff;
	float intensity = clamp((theta - lightCutOff) / cutOffDiff, 0.0, 1.0);
	//float shadow = calcShadowSAVSM(shadowCoord, shadowMap, satFilterSize);
    float shadow = calcShadowPCSSSAVSM(shadowCoord, shadowMap3, shadowMap);
	//float shadow = calcShadowVSM(shadowCoord, shadowMap3);
	//float shadow = calcShadowPCSSPCFPoisson(shadowCoord, shadowMap3);
	//float shadow = 1;

	if(theta >= lightCutOff) {
		// Diffuse
		vec3 norm = normalize(fNormals);
		float diff = max(dot(norm, toLightDir), 0.0);
		vec3 diffuse = vec3(lightDiffuse * diff);

		// Attenuation
		float distance = length(lightPosition - fPosition);
		float attenuation = 1.0f / (lightConstant + lightLinear * distance + lightQuadratic * (distance * distance));
		diffuse = shadow * (diffuse * attenuation);

		return diffuse * intensity;
	} else {
		return vec3(0.0f, 0.0f, 0.0f);
	}
}

void main(void) {
	vec3 ambient = vec3(0.3f);
	vec3 diffuse = vec3(0);
	//if(fPosition.y < 0) discard;

	if (camHeight >= -0.1) {
		vec4 lightDirection = light1.direction;
		if (lightDirection.w == 1.0) {
			diffuse = calcSpotLight(light1, shadowCoord1, shadowMap1);
		} else { //directional light
			diffuse = calcDirectionalLight(light1, shadowCoord1, shadowMap1);
		}
	} else {
		vec4 lightDirection = light2.direction;
		if (lightDirection.w == 1.0) {
			diffuse += calcSpotLight(light2, shadowCoord2, shadowMap2);
		} else { //directional light
			diffuse += calcDirectionalLight(light2, shadowCoord2, shadowMap2);
		}
	}


	vec4 textureColor = texture(tex, fTexcoord);
	outColor = vec4(vec4(ambient + diffuse, 1.0f) * textureColor);
	if (camHeight < 0) {
		float normalizedDist = float(min(-worldPosition.y, 299.0f)) / 300.0f;
		float waterDist = clamp(1.0-normalizedDist , 0.0, 1.0);

		//float waterDist = normalized == 0 ? 0 : 0.3;

		const vec4 fogBlendColor = vec4(0.3 * waterDist * waterDist, 0.5 * waterDist, 0.55 * waterDist, 1.0);

		outColor = mix(fogBlendColor, outColor, visibility);
	}

}
