#version 400
out vec4 fragColor;

/*
  Special thanks

  Inspirations:
    Wolfire Games
    Digital Foundry
    id Software
    Casey Muratori

  Learning resources:
    https://learnopengl.com/
*/

/*
  0 -- disabled
  1 -- on
  2 -- bounce light
*/
uniform int ssaoQuality;
/*
  0 -- disabled
  1 -- simplified
  2 -- multisampled
*/
uniform int shadowQuality;

uniform sampler2D shadowMap;
uniform mat4 lightTransform;
uniform mat4 transform;
uniform mat4 view;
uniform mat4 projection;

uniform sampler2D depth;
uniform sampler2D position;
uniform sampler2D albedo;
uniform sampler2D normals;

in vec2 vTexCoord;

in mat4 iview;
in mat4 tview;
in mat4 tilight;

uniform vec3 cameraPosition;
uniform vec3 cameraVector;
uniform vec2 viewportSize;

uniform float noiseTile[16];
uniform vec2 poissonDisk[16];
const int diskSize = 16;

uniform vec3 ssaoKernel[64];
const int kernelSize = 64;
const float kernelScale = 1.0 / 64.0;
const float radius = 10.0;
const float bias = 0.072;

// adapted via http://byteblacksmith.com/improvements-to-the-canonical-one-liner-glsl-rand-for-opengl-es-2-0/
// canonical one-liner appears to have no original source, but discussion here: https://stackoverflow.com/questions/12964279/whats-the-origin-of-this-glsl-rand-one-liner
float rand(vec2 co) {
  float a = 12.9898;
  float b = 78.233;
  // float c = 334.393;
  float d = 43758.5453;
  float dt = dot(co.xy, vec2(a, b));
  float sn = mod(dt, 3.14159);
  return fract(sin(sn) * d);
}

float rand3_good(vec3 co) {
  float a = 12.9898;
  float b = 78.233;
  float c = 334.393;
  float d = 43758.5453;
  float dt = dot(co.xyz, vec3(a, b, c));
  float sn = mod(dt, 3.14159);
  return fract(sin(sn) * d);
}

// a good-enough random generator when combined with an entropy source, avoids sin
float rand3(vec3 co) {
  float dt = dot(co.xyz, vec3(12.9898, 78.233, 334.393));
  return fract(dt);
}

const vec3 straightVector = vec3(0.0, 0.0, -1.0);
vec2 lightingCalculation(float bias, vec4 fragPosLightSpace, vec4 fragNormalLightSpace) {
  float direct = clamp(dot(fragNormalLightSpace.xyz, straightVector) * 1.4 - 0.1, -0.1, 1.0);
  if (shadowQuality == 0) {
    return vec2(direct);
  }
  // perform perspective divide
  vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;
  // transform to [0,1] range
  projCoords = projCoords * 0.5 + 0.5;
  if (projCoords.x < 0.0 || projCoords.x > 1.0 || projCoords.y < 0.0 || projCoords.y > 1.0 || projCoords.z > 1.0) {
    return vec2(direct);
  }

  // get depth of current fragment from light's perspective
  float currentDepth = projCoords.z;

  if (shadowQuality == 1) {
    // get closest depth value from light's perspective (using [0,1] range fragPosLight as coords)
    float closestDepth = texture(shadowMap, projCoords.xy).r;
    // check whether current frag pos is in shadow

    // non-multisampled version
    float shadow = currentDepth > closestDepth - bias ? 0.0 : 1.0;
    return vec2(direct, shadow);
  }

  // const float bias = 0.0;
  vec2 texelSize = 1.0 / vec2(textureSize(shadowMap, 0));
  float shadow = 0.0;
  const int size = 1;
  vec2 poissonSize = texelSize * 2.0;

  for (int x = -size; x <= size; ++x) {
    for (int y = -size; y <= size; ++y) {
      float visibility = 1.0;
      if (texture(shadowMap, projCoords.xy + poissonDisk[y + int(projCoords.x) % diskSize] * poissonSize + vec2(x, y) * texelSize).r >= currentDepth - bias) {
        visibility = 0.0;
      }
      shadow += visibility;
    }
  }
  shadow /= float((size * 2 + 1) * (size * 2 + 1));
  return vec2(direct, 1.0 - shadow);
}

// adapted from http://glampert.com/2014/01-26/visualizing-the-depth-buffer/
float linearizeDepth(in float depth, in float zNear, in float zFar) {
  return (2.0 * zNear) / (zFar + zNear - depth * (zFar - zNear));
}

const vec3 lightVector = vec3(0.0, -1.0, 0.0);
const vec3 lightPosition = vec3(0.0, -500.0, 1000.0);
const vec3 ambientLight = vec3(0.4);
const vec3 diffuseLight = vec3(0.3);
const vec3 sunLight = vec3(0.5);

const vec3 gamma = vec3(2.2);
const float exposure = 1.0;

float[5] ssao(vec3 viewPos, vec3 viewNormal) {
  // SSAO
  int x = int(gl_FragCoord.x * viewPos.z) % 4 + int(gl_FragCoord.y * viewPos.x) % 4 * 4;
  int y = int(gl_FragCoord.y * viewPos.y) % 4 + int(gl_FragCoord.x * viewPos.z) % 4 * 4;
  vec3 bounce = vec3(0.0);
  // vec3 randomVec = normalize(vec3(rand3(viewPos.xyz + viewNormal) * 2.0 - 1.0, rand3(viewPos.zyx + viewNormal.xzy) * 2.0 - 1.0, 0.0));
  // vec3 randomVec = vec3(0.0, 1.0, 0.0);
  vec3 randomVec = normalize(vec3(noiseTile[x] * 2.0 - 1.0, noiseTile[y] * 2.0 - 1.0, 0.0));
  // create TBN change-of-basis matrix: from tangent-space to view-space
  vec3 tangent = normalize(randomVec - viewNormal * dot(randomVec, viewNormal));
  // vec3 tangent = normalize(viewNormal * viewNormal);
  vec3 bitangent = cross(viewNormal, tangent);
  mat3 tbn = mat3(tangent, bitangent, viewNormal);
  // iterate over the sample kernel and calculate occlusion factor
  float occlusion = 0.0;
  float disocclusion = 0.0;

  for (int i = 0; i < kernelSize; i++) {
    // get sample position
    vec3 s = tbn * ssaoKernel[i] * radius + viewPos; // from tangent to view-space

    // project sample position (to sample texture) (to get position on screen/texture)
    vec4 offset = projection * vec4(s, 1.0); // from view to clip-space
    offset.xy = offset.xy / offset.w * 0.5 + 0.5; // transform to range 0.0 - 1.0

    // get sample depth
    float sampleDepth = texture(position, offset.xy).z; // get depth value of kernel sample

    // range check & accumulate
    if (sampleDepth <= s.z - bias) {
      occlusion += smoothstep(0.0, 1.0, radius / abs(viewPos.z - sampleDepth));
      if (ssaoQuality >= 2 && ssaoKernel[i].z > 0.1) {
        // rough bounce lighting
        // get sample color
        vec3 sampleAlbedo = texture(albedo, offset.xy).rgb;
        vec3 samplePos = texture(position, offset.xy).rgb;
        // vec3 sampleNormal = texture(normal, offset.xy).rgb; // get depth value of kernel sample
        float scale = smoothstep(0.0, 1.0, 1.0 - length(samplePos - viewPos) / (radius * 2.0));
        bounce += scale * sampleAlbedo;
      }
    } else {
      disocclusion += pow(smoothstep(-1.0, 0.0, -radius / max(-viewPos.z + sampleDepth, 0.0)), 2.0);
    }
  }
  return float[] (occlusion * kernelScale, disocclusion * kernelScale, bounce.r * kernelScale, bounce.g * kernelScale, bounce.b * kernelScale);
}

const vec3 ihdrScale = vec3(1.0 / 4.0);

void main(void) {
  vec3 albedo = texture(albedo, vTexCoord).xyz;
  vec3 viewPos = texture(position, vTexCoord).xyz;
  vec3 pos = (iview * vec4(viewPos, 1.0)).xyz;
  vec3 viewNormal = texture(normals, vTexCoord).xyz * 2.0 - 1.0;
  vec3 normal = (tview * vec4(texture(normals, vTexCoord).xyz * 2.0 - 1.0, 0.0)).xyz;
  // float dep = max(0.2, texture(depth, vTexCoord).r);
  float shadowDepth = texture(shadowMap, vTexCoord).r;

  vec4 lightPos = lightTransform * vec4(pos, 1.0);
  vec4 lightNormal = normalize(tilight * vec4(normal, 0.0));

  float[5] depthProbe = ssaoQuality == 0 ? float[](0.0, 0.0, 0.0, 0.0, 0.0) : ssao(viewPos, viewNormal);
  float occlusion = depthProbe[0];
  float depthRim = depthProbe[1];
  vec3 bounceLight = vec3(depthProbe[2], depthProbe[3], depthProbe[4]);
  // float occlusion = 0.0;

  float edgeFade = smoothstep(0.0, 0.8,
    clamp(0.0, 1.0, (vTexCoord.x * vTexCoord.y) * (1.0 - (vTexCoord.x * vTexCoord.y)))
  );

  // float edge = -dot(viewNormal, normalize(viewPos.xyz));

  // float bias = max(0.005 * (1.0 - dot(normal, normalize(pos - lightPos.xyz))), 0.005);
  vec2 allLighting = lightingCalculation(0.0, lightPos, lightNormal);
  float direct = allLighting[0];
  float lighting = min(direct, allLighting[1]);
  // float lighting = 0.0;
  // float direct = (dot(normal, lightVector) * 1.25 + 0.75) * 0.75;
  // float direct = 1.0;
  // lighting = min(lighting, direct);
  // float depthFog = max(0.25, 1.0 - linearizeDepth(dep, 10.0, 1500.0)) + 0.5;
  float depthFog = pow(clamp(1.0 - (viewPos.z - 200) / 9000.0, 0.0, 1.0), 2.2);

  float disocclusion = ssaoQuality == 0 ? 1.0 : 1.0 - sqrt(depthFog) * sign(occlusion) * pow(occlusion, 2.0) * edgeFade;

  float shininess = 0.4;
  vec3 lightDir = normalize(lightPosition - pos);
  vec3 viewDir = normalize(cameraPosition - pos);
  vec3 halfwayDir = normalize(lightDir + viewDir);
  float shine = pow(max(dot(normal, halfwayDir), 0.0), 16.0) * shininess;
  float spec = min((lighting + shine * 0.5) * 0.5, shine);
  // float spec = pow(dot(normal, halfwayDir), 3.0);
  // float spec = dot(viewDir, normal);

  float specularity = spec * shininess;
  vec3 bouncedAlbedo = albedo * disocclusion + bounceLight;
  vec3 ambient = pow(ambientLight * albedo + bounceLight * 2.0 * ambientLight, vec3(0.5)) * disocclusion;
  vec3 diffuse = diffuseLight * bouncedAlbedo * direct;
  vec3 sun = sunLight * lighting * albedo * disocclusion + lighting * bounceLight;
  vec3 rimColor = diffuse * pow(depthRim * 0.5, 2.0) + pow(depthRim, 3.0);

  vec3 c = (rimColor + specularity + ambient + diffuse + sun) * disocclusion * depthFog;
  fragColor = vec4(c * ihdrScale, 1.0);
  // fragColor = vec4(bounceLight, 1.0);
  // fragColor = vec4(normalize(bounceLight), 1.0);
  // fragColor = vec4(vec3(diffuse * pow(depthRim, 2.0) * 2.0), 1.0);
  // fragColor = vec4(vec3(pow(spec, edge)), 1.0);
  // fragColor = vec4(vec3(spec), 1.0);
  // fragColor = vec4(vec3(occlusion), 1.0);
  // fragColor = vec4(vec3(disocclusion), 1.0);
  // fragColor = vec4(vec3(disocclusion), 1.0);
  // fragColor = vec4(0.0, 0.0, sampleDepth.z * 0.01, 1.0);
  // fragColor = vec4(viewPos.xyz * 0.01, 1.0);
  // fragColor = vec4(sampleDepth.xy * 0.01, 0.0, 1.0);
  // fragColor = vec4(0.0, 0.0, (viewPos.z - sampleDepth.z) * 0.1, 1.0);
  // fragColor = vec4(s * 0.01, 1.0);
  // fragColor = vec4((viewPos.xyz - sampleDepth.xyz) * 10.0, 1.0);
  // fragColor = vec4(offset.xy, 0.0, 1.0);
  // fragColor = vec4(vTexCoord.xy, 0.0, 1.0);
  // fragColor = vec4(randomVec * 0.5 + 0.5, 1.0);
  // fragColor = vec4(viewPos, 1.0);
  // fragColor = vec4(normal, 1.0);
  // fragColor = vec4(pos, 1.0);
  // fragColor = vec4(tangent, 1.0);
  // fragColor = vec4(lightDir * 2.0 + 1.0, 1.0);
  // fragColor = vec4(spec * vec3(1.0, 1.0, 1.0), 1.0);
  // fragColor = vec4(1.0 - vec3(texture(shadowMap, vTexCoord).r), 1.0);
  // fragColor = vec4(1.0 - vec3(linearDepth), 1.0);
  // fragColor = vec4(vec3(lighting), 1.0);
  // fragColor = vec4(lightNormal.xyz * vec3(0.5, 0.5, 0.5) + vec3(0.5, 0.5, 0.5), 1.0);
  // fragColor = vec4(normal.xyz * vec3(0.5, 0.5, 0.5) + vec3(0.5, 0.5, 0.5), 1.0);

  // fragColor = vec4(pos * 0.001, 1.0);
}
