#version 130

#define NUM_LIGHTS 5

// ----------------------------------------------------------------------------
// Constants and Structs
// ----------------------------------------------------------------------------

struct FloatArray {
    float Float;
};

const float PI = 3.14159265359;
const vec2 PMheight = vec2(0.04,-0.02);


// ----------------------------------------------------------------------------
// Input Textures from the main program
// ----------------------------------------------------------------------------

uniform sampler2D albedoMap;
uniform sampler2D normalMap;
uniform sampler2D roughnessMap;
uniform sampler2D metallicMap;
uniform sampler2D heightMap;
uniform sampler2D aoMap;
uniform sampler2D emissionMap;


// ----------------------------------------------------------------------------
// Attributes from the Vertex Shader
// ----------------------------------------------------------------------------

in vec2 Vertex_UV;
in vec3 Vertex_Normal;
in vec4 Vertex_Position;
in vec3 Vertex_Eyevector;
in vec3 Vertex_LightDir[NUM_LIGHTS];
in vec3 Vertex_LightColor[NUM_LIGHTS];
in float Vertex_LightRange[NUM_LIGHTS];
in vec3 Vertex_AmbientColor;

out vec4 FragColor;


// ----------------------------------------------------------------------------
// Attributes from the main program
// ----------------------------------------------------------------------------

uniform float levelscale;
uniform float gamma;

uniform float fogStart;
uniform float fogRange;
uniform float fogDensity;
uniform vec3 fogColor;

uniform float AttA;
uniform float AttB;
uniform float flicker;
uniform FloatArray lightradius[NUM_LIGHTS];

uniform int isMetal;

// ----------------------------------------------------------------------------

uniform int flagDM;
uniform int flagFG;
uniform int flagPB;
uniform int flagTM;
uniform int flagTL;

// ----------------------------------------------------------------------------

uniform int texAL;
uniform int texNM;
uniform int texRO;
uniform int texME;
uniform int texHM;
uniform int texAO;
uniform int texEM;


// ----------------------------------------------------------------------------
// Normal functions
// ----------------------------------------------------------------------------

mat3 cotangent_frame(vec3 N, vec3 p, vec2 uv)
{
	vec3 dp1 = dFdx(p);
	vec3 dp2 = dFdy(p);
	vec2 duv1 = dFdx(uv);
	vec2 duv2 = dFdy(uv);
 
	vec3 dp2perp = cross(dp2, N);
	vec3 dp1perp = cross(N, dp1);
	vec3 T = dp2perp * duv1.x + dp1perp * duv2.x;
	vec3 B = dp2perp * duv1.y + dp1perp * duv2.y;
 
	float invmax = inversesqrt(max(dot(T, T), dot(B, B)));
	return mat3(T * invmax, B * invmax, N);
}

// ----------------------------------------------------------------------------

vec3 perturb_normal(vec3 N, vec3 V, vec3 map, vec2 texcoord)
{
	map = map * 255.0 / 127.0 - 128.0 / 127.0;
	mat3 TBN = cotangent_frame(N, -V, texcoord);
	return normalize(TBN * map);
}


// ----------------------------------------------------------------------------
// Tonemapping functions
// ----------------------------------------------------------------------------

vec3 ToneMapFilmic(vec3 color)
{
	vec4 vh = vec4(color, gamma);
	vec4 va = 1.425 * vh + 0.05;
	vec4 vf = (vh * va + 0.004) / (vh * (va + 0.55) + 0.0491) - 0.0821;
	return vf.rgb / vf.www;
}

// ----------------------------------------------------------------------------

vec3 ToneMapSimple(vec3 color)
{
	return pow(color / (color + vec3(1.0)), vec3(1.0 / gamma));
	
}
	
// ----------------------------------------------------------------------------

vec3 ToneMapExposure(vec3 color)
{
	color = exp(-1.0 / ( 2.72 * color + 0.15 ));
	color = pow(color, vec3(1. / gamma));
	return color;
}

// ----------------------------------------------------------------------------

vec3 ToneMapPBR(vec3 color)
{
	// HDR tonemapping
	color = color / (color + vec3(1.0));
	// gamma correct
	color = pow(color, vec3(1.0 / gamma));
	
	return color;
}

// ----------------------------------------------------------------------------

vec3 Uncharted(vec3 x)
{
	float A = 0.15;
	float B = 0.50;
	float C = 0.10;
	float D = 0.20;
	float E = 0.02;
	float F = 0.30;
	
	return ((x * (A * x + C * B) + D * E) / (x * (A * x + B) + D * F)) - E / F;
}

// ----------------------------------------------------------------------------

vec3 ToneMapUncharted(vec3 color)
{
	color = Uncharted(color * 4.5) * (1.0 / Uncharted(vec3(11.2)));
	color = pow(color, vec3(1.0 / gamma));
	return color;

}

// ----------------------------------------------------------------------------

vec3 ToneMapSCurve(vec3 x)
{
	//x = pow(x, vec3(1.0 / 2.2));
	
	float a = 2.51f;
	float b = 0.03f;
	float c = 2.43f;
	float d = 0.59f;
	float e = 0.14f;
	return clamp((x * (a * x + b)) / (x * (c * x + d) + e), 0.0, 1.0);
}


// ----------------------------------------------------------------------------
// PBR functions
// ----------------------------------------------------------------------------

float DistributionGGX(vec3 N, vec3 H, float roughness)
{
	float a = roughness * roughness;
	float a2 = a * a;
	float NdotH = max(dot(N, H), 0.0);
	float NdotH2 = NdotH * NdotH;

	float nom = a2;
	float denom = (NdotH2 * (a2 - 1.0) + 1.0);
	denom = PI * denom * denom;

	return nom / denom;
}

// ----------------------------------------------------------------------------

float GeometrySchlickGGX(float NdotV, float roughness)
{
	float r = (roughness + 1.0);
	float k = (r * r) / 8.0;

	float nom = NdotV;
	float denom = NdotV * (1.0 - k) + k;

	return nom / denom;
}

// ----------------------------------------------------------------------------

float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness)
{
	float NdotV = max(dot(N, V), 0.0);
	float NdotL = max(dot(N, L), 0.0);
	float ggx2 = GeometrySchlickGGX(NdotV, roughness);
	float ggx1 = GeometrySchlickGGX(NdotL, roughness);

	return ggx1 * ggx2;
}

// ----------------------------------------------------------------------------

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


// ----------------------------------------------------------------------------
// Parallax Occlusion Mapping
// ----------------------------------------------------------------------------

vec2 ParallaxOcclusionMapping(vec2 texCoords, vec3 viewDir)
{ 
	// number of depth layers
	const vec2 heightScale = vec2(0.00625,-0.00625);
	const float minLayers = 8;
	const float maxLayers = 32;
	float numLayers = mix(maxLayers, minLayers, abs(dot(vec3(0.0, 0.0, 1.0), viewDir)));
	
	// calculate the size of each layer
	float layerDepth = 1.0 / numLayers;
	
	// depth of current layer
	float currentLayerDepth = 0.0;
	
	// the amount to shift the texture coordinates per layer (from vector P)
	vec2 P = viewDir.xy / viewDir.z * heightScale; 
	vec2 deltaTexCoords = P / numLayers;
  
	// get initial values
	vec2  currentTexCoords = texCoords;
	float currentDepthMapValue = texture(heightMap, currentTexCoords).r;
      
	while(currentLayerDepth < currentDepthMapValue)
	{
		// shift texture coordinates along direction of P
		currentTexCoords -= deltaTexCoords;
		// get depthmap value at current texture coordinates
		currentDepthMapValue = texture(heightMap, currentTexCoords).r;  
		// get depth of next layer
		currentLayerDepth += layerDepth;  
	}
    
	// get texture coordinates before collision (reverse operations)
	vec2 prevTexCoords = currentTexCoords + deltaTexCoords;

	// get depth after and before collision for linear interpolation
	float afterDepth = currentDepthMapValue - currentLayerDepth;
	float beforeDepth = texture(heightMap, prevTexCoords).r - currentLayerDepth + layerDepth;
 
	// interpolation of texture coordinates
	float weight = afterDepth / (afterDepth - beforeDepth);
	vec2 finalTexCoords = prevTexCoords * weight + currentTexCoords * (1.0 - weight);

	return finalTexCoords;
}


// ----------------------------------------------------------------------------
// Light functions
// ----------------------------------------------------------------------------

float LinearizeDepth(float depth) // Note that this ranges from [0,1] instead of up to 'far plane distance' since we divide by 'far'
{
	float near = 0.1; 
	float far = fogRange*1.0/levelscale; 
	float z = depth * 2.0 - 1.0; // Back to NDC 
	return (2.0 * near) / (far + near - z * (far - near));	
}

// ----------------------------------------------------------------------------

float CalcAtt(float distance)
{
	return 1.0 / (1.0 + AttA * distance + AttB * distance * distance);
}

// ----------------------------------------------------------------------------

float spotlight(int i, float attenuation, vec3 L, float intensity)
{
	float clampedCosine = max(0.0, 1.0 * dot(-L, gl_LightSource[i].spotDirection));
	attenuation = attenuation * pow(clampedCosine, gl_LightSource[i].spotExponent*intensity);

	return attenuation;
}


// ----------------------------------------------------------------------------
// Main Fragment Shader
// ----------------------------------------------------------------------------

void main()
{
	vec2 uv = Vertex_UV;
		
	// Parallax Mapping
	if(texHM > 0){uv = ParallaxOcclusionMapping(uv, normalize(Vertex_Eyevector.xyz));}
	
	// 1. Albedo Texture
	vec4 albedo = vec4(0.5, 0.5, 0.5, 1.0);
	if(texAL > 0){albedo = texture(albedoMap, uv);}
	
	// 2. Normalmap Texture
	vec3 nrm = Vertex_Normal;
	if(texNM > 0){nrm = texture(normalMap, uv).rgb;}
		
	// 3. Roughness Texture
	float roughness = 0.7;
	if(texRO > 0){roughness = texture(roughnessMap, uv).r;}

	// 4. Metallic Texture
	float metallic = 0.2;
	if(texME > 0){metallic = texture(metallicMap, uv).r;}

	// 5. Ambient Occlusion Texture
	float ao = 1.0;
	if(texAO > 0){ao = texture(aoMap, uv).r;}
		
	// 6. Emissive Texture
	vec3 emission = vec3(0.0);
	if(texEM > 0){emission = texture(emissionMap, uv).rgb * (1.0 + (flicker / 2.0)) * 4.0;}

	vec3 Lo = vec3(0.0);
	
	// ambient and emission lighting
	vec3 color = (emission + Vertex_AmbientColor) * albedo.rgb;
	vec3 r = vec3(0.0);
	
	// PBR active
	if(flagPB > 0)
	{
	
		vec3 N = Vertex_Normal.xyz;
		vec3 V = normalize(Vertex_Eyevector.xyz);
		vec3 PN = perturb_normal(N, V, nrm, uv);

		// calculate reflectance at normal incidence; if dia-electric (like plastic) use F0 
		// of 0.04 and if it's a metal, use the albedo color as F0 (metallic workflow)    
		vec3 F0 = vec3(0.04);
		F0 = mix(F0, albedo.rgb, metallic);
		if(isMetal > 0){F0 = albedo.rgb;}
		
		for(int i = 0; i < NUM_LIGHTS; ++i) 
		{
			// calculate per-light radiance
			vec3 L = normalize(Vertex_LightDir[i]);
			vec3 X = normalize(gl_LightSource[0].spotDirection.xyz);
			
			
			vec3 H = normalize(V + L);
			float distance = length(Vertex_LightDir[i]) * 1.0 / levelscale;
			float attenuation = CalcAtt(distance) * Vertex_LightRange[i];
			
			// first light = player spotlight
			if(i == 0 && flagTL == 1)
			{
				attenuation = spotlight(0, attenuation, L, 1.0);
				attenuation = pow(attenuation / (attenuation + 1.0 + flicker), 1.0 / gamma) * (2.0 - 2.0 / Vertex_LightRange[i]);
			}
			
			if(i == 0 && flagTL == 0)
			{
				attenuation = 1.0 / (1.0 + 0.0 * distance + 0.5 * distance * distance);
			}
			
			// light color (scaled)
			vec3 radiance = Vertex_LightColor[i] * attenuation;
				
			// Cook-Torrance BRDF
			float NDF = DistributionGGX(PN, H, roughness);
			float G = GeometrySmith(PN, V, L, roughness);
			vec3 F = fresnelSchlick(max(dot(H, V), 0.0), F0);
	           
			vec3 nominator = NDF * G * F; 
			float denominator = 2 * max(dot(PN, V), 0.0) * max(dot(PN, L), 0.0) + 0.001; // 0.001 to prevent divide by zero.
			vec3 specular = nominator / denominator;
	        
			// kS is equal to Fresnel
			vec3 kS = F;
			// for energy conservation, the diffuse and specular light can't
			// be above 1.0 (unless the surface emits light); to preserve this
			// relationship the diffuse component (kD) should equal 1.0 - kS.
			vec3 kD = clamp(vec3(1.0) - kS, 0.0, 1.0);
			// multiply kD by the inverse metalness such that only non-metals 
			// have diffuse lighting, or a linear blend if partly metal (pure metals
			// have no diffuse light).
			kD *= 1.0 - metallic;
			
	
			// non-player light: check backface lighting
			float NdotL = 1.0;
			if(i > 0)
			{
				float NdotL = max(dot(PN, L), 0.0);
				if(NdotL > 0.0)
				{
					Lo += (kD * albedo.rgb / PI + specular) * radiance * NdotL * attenuation;
					r += attenuation;

				}
			}
				// player light: simpler light equotation
			else
			{
				Lo += Lo += (albedo.rgb / PI + specular) * radiance * attenuation;
				r += attenuation;
			}

		}
	}
	
	// PBR off
	else
	{
		for(int i = 0; i < NUM_LIGHTS; ++i) 
		{
			vec3 L = normalize(Vertex_LightDir[i]);
			vec3 N = Vertex_Normal.xyz;
			float NdotL = max(dot(N, L), 0.0);

			float distance = length(Vertex_LightDir[i]) * 1.0 / levelscale;
			float attenuation = CalcAtt(distance) * Vertex_LightRange[i];

			// first light = player spotlight
			if(i == 0 && flagTL == 1)
			{
				attenuation = spotlight(0, attenuation, L, 2.0);
				attenuation = pow(attenuation / (attenuation + 1.0 + flicker), 1.0 / gamma) * (2.0 - 2.0 / Vertex_LightRange[i]);
			}
			
			if(i == 0 && flagTL == 0)
			{
				attenuation = 1.0 / (1.0 + 0.0 * distance + 0.5 * distance * distance);
			}				

			if(NdotL > 0.0)
			{
				Lo += albedo.rgb * Vertex_LightColor[i] * attenuation * NdotL;
				r += attenuation;
			}

		}
	}
	
	// put it all together
	color += (color + Lo) * ao;
	
	// Tonemapping
	if(flagTM == 1){color = ToneMapFilmic(color);}
	if(flagTM == 2){color = ToneMapSimple(color);}
	if(flagTM == 3){color = ToneMapExposure(color);}
	if(flagTM == 4){color = ToneMapPBR(color);}
	if(flagTM == 5){color = ToneMapUncharted(color);}
	if(flagTM == 6){color = ToneMapSCurve(color);}
	
	// fog
	if(flagFG > 0)
	{
		float plane;
		plane = length(Vertex_Position);   // range-based (radial), flat would be plane = abs(position.z)
		float fogFactor = clamp((fogRange - plane) / (fogRange - fogStart), 0.0, 1.0);
		
		// distant lights shine through the fog a little bit
		vec3 fogFactorX = max(clamp(r, 0.0, 0.5), vec3(fogFactor)) * (1.0 - fogDensity);
		
		color = (fogColor * (1.0 - fogFactorX) + (color.rgb * fogFactorX));

	}
	
	// simple depthmap
	if(flagDM > 0)
	{
		float depth = 1.0 - LinearizeDepth(gl_FragCoord.z);
		color.rgb = vec3(depth);
	}
	
	FragColor = vec4(color, albedo.a);

}