#version 400 core

#define PI 3.14159265359
const float PI2 = PI * 2.0;

uniform sampler2D colorBuffer;
uniform sampler2D depthBuffer;
uniform sampler2D normalBuffer;
uniform sampler2D pbrBuffer;
uniform sampler2D occlusionBuffer;
uniform bool haveOcclusionBuffer;

uniform vec2 resolution;
uniform mat4 viewMatrix;
uniform mat4 invViewMatrix;
uniform mat4 invModelMatrix;
uniform mat4 invProjectionMatrix;
uniform float zNear;
uniform float zFar;

uniform vec4 fogColor;
uniform float fogStart;
uniform float fogEnd;

uniform bool useBoxProjection;

#include <unproject.glsl>
#include <gamma.glsl>
#include <fresnel.glsl>
#include <envMapEquirect.glsl>

uniform vec4 ambientVector;
uniform sampler2D ambientTexture;

vec3 ambientEquirectangularMap(in vec3 wN, in float perceptualRoughness)
{
    ivec2 envMapSize = textureSize(ambientTexture, 0);
    float resolution = float(max(envMapSize.x, envMapSize.y));
    float lod = log2(resolution) * perceptualRoughness;
    return textureLod(ambientTexture, envMapEquirect(wN), lod).rgb;
}

uniform samplerCube ambientTextureCube;
vec3 ambientCubemap(in vec3 wN, in float perceptualRoughness)
{
    ivec2 envMapSize = textureSize(ambientTextureCube, 0);
    float resolution = float(max(envMapSize.x, envMapSize.y));
    float lod = log2(resolution) * perceptualRoughness;
    return textureLod(ambientTextureCube, wN, lod).rgb;
}

uniform int ambientFunc;

uniform float ambientEnergy;

uniform sampler2D ambientBRDF;
uniform bool haveAmbientBRDF;

uniform vec3 boxExtents;
uniform float falloffMargin;

// Model-space box projection
vec3 bpcem(in vec3 pos, in vec3 v, in vec3 boxExtents)
{
    vec3 nrdir = v;
    vec3 rbmax = (boxExtents - pos) / nrdir;
    vec3 rbmin = (-boxExtents - pos) / nrdir;
    vec3 rbminmax;
    rbminmax.x = (nrdir.x > 0.0)? rbmax.x : rbmin.x;
    rbminmax.y = (nrdir.y > 0.0)? rbmax.y : rbmin.y;
    rbminmax.z = (nrdir.z > 0.0)? rbmax.z : rbmin.z;
    float fa = min(min(rbminmax.x, rbminmax.y), rbminmax.z);
    vec3 posOnBox = pos + nrdir * fa;
    return posOnBox;
}

layout(location = 0) out vec4 fragColor;

void main()
{
    vec2 gbufTexCoord = gl_FragCoord.xy / resolution;
    
    vec4 col = texture(colorBuffer, gbufTexCoord);
    if (col.a < 1.0)
        discard;
    
    float depth = texture(depthBuffer, gbufTexCoord).x;
    vec3 eyePos = unproject(invProjectionMatrix, vec3(gbufTexCoord, depth));
    vec3 worldPos = (invViewMatrix * vec4(eyePos, 1.0)).xyz;
    vec3 objPos = (invModelMatrix * vec4(worldPos, 1.0)).xyz;

    float alpha = 1.0;

    vec3 falloff = clamp((boxExtents - abs(objPos)) / vec3(falloffMargin, falloffMargin, falloffMargin), vec3(0.0, 0.0, 0.0), vec3(1.0, 1.0, 1.0));
    alpha *= falloff.x * falloff.y * falloff.z;
    
    vec3 albedo = toLinear(col.rgb);
    
    vec3 N = normalize(texture(normalBuffer, gbufTexCoord).rgb);
    vec3 E = normalize(-eyePos);
    vec3 R = reflect(E, N);
    float NE = max(dot(N, E), 0.0);
    
    vec3 worldCamPos = (invViewMatrix[3]).xyz;
    vec3 worldE = normalize(worldPos - worldCamPos);
    vec3 worldN = normalize((vec4(N, 0.0) * viewMatrix).xyz);
    vec3 worldR = reflect(worldE, worldN);
    
    if (useBoxProjection)
    {
        vec3 marginatedExtents = boxExtents - falloffMargin;
        vec3 objPosBoxClamped = clamp(objPos, -marginatedExtents, marginatedExtents);
        vec3 objN = (invModelMatrix * vec4(worldN, 0.0)).xyz;
        worldN = normalize(bpcem(objPosBoxClamped, objN, marginatedExtents));
        vec3 objR = (invModelMatrix * vec4(worldR, 0.0)).xyz;
        worldR = normalize(bpcem(objPosBoxClamped, objR, marginatedExtents));
    }
    
    vec4 pbr = texture(pbrBuffer, gbufTexCoord);
    float roughness = pbr.r;
    float metallic = pbr.g;
    
    float occlusion = haveOcclusionBuffer? texture(occlusionBuffer, gbufTexCoord).r : 1.0;
    
    vec3 f0 = mix(vec3(0.04), albedo, metallic);
    
    float grazingReflectivity = pow(1.0 - NE, 5.0);
    float perceptualRoughness = mix(roughness, 0.0, grazingReflectivity);
    
    // Ambient light
    // TODO: support separate irradiance map
    vec3 irradiance, reflection;
    if (ambientFunc == 1)
    {
        irradiance = ambientEquirectangularMap(worldN, 0.99);
        reflection = ambientEquirectangularMap(worldR, perceptualRoughness);
    }
    else if (ambientFunc == 2)
    {
        irradiance = ambientCubemap(worldN, 0.99);
        reflection = ambientCubemap(worldR, perceptualRoughness);
    }
    else
    {
        irradiance = toLinear(ambientVector.rgb);
        reflection = irradiance;
    }
    
    vec3 F = clamp(fresnelRoughness(NE, f0, roughness), 0.0, 1.0);
    vec3 kD = (1.0 - F) * (1.0 - metallic);
    vec2 brdf = haveAmbientBRDF? texture(ambientBRDF, vec2(NE, roughness)).rg : vec2(1.0, 0.0);
    vec3 specular = reflection * clamp(F * brdf.x + brdf.y, 0.0, 1.0);
    vec3 radiance = (kD * irradiance * albedo + specular) * occlusion * ambientEnergy;
    
    // Fog
    float linearDepth = abs(eyePos.z);
    float fogFactor = clamp((fogEnd - linearDepth) / (fogEnd - fogStart), 0.0, 1.0);
    radiance = mix(toLinear(fogColor.rgb), radiance, fogFactor);

    fragColor = vec4(radiance, alpha);
}
