#version 420

// Important input textures
uniform sampler2DArray  normalTex;
uniform samplerCube     bgTex;
uniform sampler2D       randTex;

// Index of refraction
uniform float idxRefr;
uniform float shininess;

// Important matrices
uniform mat4 rotation;
uniform mat4 project;

// Inputs from vertex shader
in vec3 viewVec;
in vec2 fragTexCoord;
in float vertID;

// Output refractive color for this surface
out vec4 result;

// Create the basis vectors from a given input vector
void OrthoBasis( in vec3 normal, out vec3 tangent, out vec3 binormal )
{
	binormal = normalize( abs(normal.x) > abs(normal.z) ? 
				          vec3(-normal.y, normal.x, 0) : 
				          vec3(0,        -normal.z, normal.y) );
	tangent  = cross( binormal, normal );          
}

// Take a point in Cartesian coordinate into our ortho-normal basis
vec3 ToBasis( vec3 input, vec3 norm, vec3 tang, vec3 binorm )
{
	return input.x*tang + input.y*binorm + input.z*norm;
}

// This takes the random number seed for this pixel and increments it,
//   returning the next pseudorandom number in the sequence
float RandomNumber( inout uint prev )
{
  const uint LCG_A = 1664525u;
  const uint LCG_C = 1013904223u;
  prev = (LCG_A * prev + LCG_C);
  return ( float(prev & 0x00FFFFFF) / float(0x01000000) );
}

// Computes a random normal based on the Phong distribution.
//    See Walter et al's "Microfacet Models for Refraction through Rough Surfaces"
vec3 ComputeRandomPhongNormal( inout uint rndSeed, in vec3 norm, in float shininess )
{
	// Compute an orthonormal basis from our input surface normal
	vec3 tangent, binormal;
	OrthoBasis( norm, tangent, binormal );

	// Find two random numbers based on our input seed
	float e1 = RandomNumber( rndSeed );
	float e2 = RandomNumber( rndSeed );
	
	// Compute random theta & phi
	float theta_m = acos( pow( e1, 1.0f/(shininess+2.0) ) );
	float phi_m   = 2.0 * 3.14159265358979323846 * e2;
	
	// Using theta/phi spherical coords, convert to tangent space
	vec3 rndTanSpaceVec = vec3( cos( phi_m ) * sin( theta_m ),
	                            sin( phi_m ) * sin( theta_m ),
	                            cos( theta_m ) );
	
	// Now that we have a random tangent-space vector, convert that back
	//   to our eye space coordinate system using our ONB from above
	return ToBasis( rndTanSpaceVec, norm, tangent, binormal );
}


// Approximate the Fresnel coefficients using Schlick's approximation
vec2 SchlickFresnelApprox( float cosAng )
{
	float oneMinus = 1.0-cosAng;
	float approx = 0.05 + 0.95*(oneMinus*oneMinus*oneMinus*oneMinus*oneMinus);
	return vec2( approx, 1.0-approx );
}

// Take an eye-space position and project it into a 2D image coordinate
vec2 ProjectToTexCoord( vec4 eyeSpacePos )
{
	vec4 projLoc = project * eyeSpacePos;
	return ( 0.5*(projLoc.xy / projLoc.w) + 0.5 );
}

// A refraction function similar to the GLSL built-in, except this gives a
//    useful approximation vector when total internal reflection occurs.
vec4 refraction( vec3 incident, vec3 normal, float ni_nt )
{
    vec4 returnVal;
    float ni_nt_sqr = ni_nt*ni_nt;
    float tmp = 1.0;
	float IdotN = dot( -incident, normal );
	float cosSqr = 1.0 - ni_nt_sqr*(1.0 - IdotN*IdotN);
	return ( cosSqr <= 0.0 ? 
			 vec4( normalize(reflect( incident, normal )), -1.0 ) :
			 vec4( normalize( ni_nt * incident + (ni_nt* IdotN - sqrt( cosSqr )) * normal ), 1.0 ) );
}


void main( void ) 
{
	// Get a per-sample random number.  (See shaders from IntermediateStochasticOBJRasterizer for info)
	ivec2 rngSize    = textureSize( randTex, 0 );
	ivec2 vertOffset = ivec2( int( vertID ) % 1024, int( vertID ) / 1024 );
	ivec2 sampOffset = ivec2( gl_SampleID % 8, gl_SampleID / 8 );
	ivec2 sampCoord  = ivec2( gl_FragCoord.x*8, gl_FragCoord.y*4 ) + sampOffset + vertOffset; // + ivec2( randOffset*rngSize );
	vec2 rngTexCoord = vec2( sampCoord.x % rngSize.x, sampCoord.y % rngSize.y ) / vec2( rngSize );
	uint rndSeed = uint( 0xFFFF *  texture( randTex, rngTexCoord ).x );

	// Get our front normal, according to the precomputed G-buffer
    vec4 frontNorm = texture( normalTex, vec3(fragTexCoord, 0) );
    
    // If we didn't hit the object ,stop now.  Really... This should be < 0,
    //    but any sample where we're interpolating and don't have a value
    //    above 0.1 won't be able to be re-normalized into a useful normal vector.
    if ( dot(frontNorm.xyz,frontNorm.xyz) <= 0.1 )
		discard;
		
	// Find the distances to the front and back surface along the view ray
	float distFront = frontNorm.w;
	float distBack  = texture( normalTex, vec3(fragTexCoord, 1) ).w;
		
	// Find the viewing vector.  We already have this, but it's interpolated,
	//    so re-normalize it.
	vec3 V   = normalize( viewVec );
		
	// Find the front facing position.  The w coef of the normal texture stores 
	//    eye-space depth of the front fragment.
	vec4 P_1 = vec4( distFront * V, 1.0 );
    
	// Compute our sampled surface normal
	vec3 N_1 = ComputeRandomPhongNormal( rndSeed, normalize( frontNorm.xyz ), shininess );
	// vec3 N_1 = normalize( frontNorm.xyz );
	float NdotV = dot( -V, N_1 );
	
	// Find the distances to the front and back surface along the view ray
	float deltaDist = distBack - distFront;
	
	// Get our Fresnel terms
	vec2 fresnel   = SchlickFresnelApprox( NdotV );
	
	// Compute our reflection direction & reflection color
	vec4 dirRefl   = rotation * vec4( reflect( V, N_1 ), 0.0 );
	vec4 reflColor = texture( bgTex, normalize(dirRefl.xyz) );
    
	// Compute our refraction direction
	vec3 T_1 = normalize( refract( V, N_1, 1.0/idxRefr ).xyz );
	
	// Compute our approximate exitant location & surface normal
	vec4 P_2_tilde = vec4( T_1 * deltaDist + P_1.xyz, 1.0 );
	vec3 N_2       = texture( normalTex, vec3(ProjectToTexCoord(P_2_tilde), 1) ).xyz;
	float dotN2    = dot( N_2.xyz, N_2.xyz );
	
	// Did we hit a black texel in the back normal map? (deltaDist was too big! Oops)
	//     Conceptually, this means we pass through the side of the object (not front/back)
	//     In this case, we'll use a surface 'normal' perpindicular to the view direction (but 
	//     generally along the same direction as our refracted T_1)
	N_2 = ( dot( N_2.xyz, N_2.xyz ) <= 0.0 ) ?          // Hit black pixel?
	      vec3( T_1.xy, 0.0 ) / length(T_1.xy) :        // Yes.  Use approximation.
	      N_2;                                          // No.   Use stored normal.
	
	// Perturb the back-facing normal
	N_2 = ComputeRandomPhongNormal( rndSeed, normalize( N_2.xyz ), shininess );
	
	// Refract at the second surface
	vec4 T_2 = refraction( T_1, -normalize( N_2 ), idxRefr );
	
	// Lookup the environment map color in the refraction direction
	vec4 dirRefr   = rotation * vec4( T_2.xyz, 0.0 );
	vec4 refrColor = texture( bgTex, normalize(dirRefr.xyz) );
    
    // Output our final result
    result = fresnel.x * reflColor + fresnel.y * refrColor;
}