#version 330 compatibility
#include "includes.glsl"

//No touchy
#define Pi 3.141592653589
#define Epsilon .0001

//Buffers
uniform sampler2D noisetex;
uniform sampler2D depthtex0;
uniform sampler2D colortex0; // Image
uniform sampler2D colortex1; // Voxels
uniform sampler2D colortex5; // Screen normals
uniform sampler2D colortex6; // Screen Specular
uniform sampler2D colortex7; // Blocklights
uniform sampler2D colortex8; // Textures
uniform sampler2D colortex9; // Specular textures

//Game Data
uniform float frameTimeCounter;
uniform vec3 cameraPosition;
uniform float near;
uniform float far;
uniform vec3 skyColor;
uniform float sunAngle;

/* DRAWBUFFERS:0 */
layout(location = 0) out vec4 fragColor;
/*
    const int colortex0Format = RGBA16F;
*/

//Shader Data
uniform mat4 gbufferModelView;
uniform mat4 gbufferModelViewInverse;
uniform mat4 gbufferProjectionInverse;
uniform mat4 gbufferProjection;
uniform int heldItemId;
uniform int heldItemId2;
in vec2 texcoord;
vec3 Normal;

//Basic Definitions
struct Ray{
    vec3 orgn;
    vec3 Dir;
};

struct Material{
    float Roughness;
    float Reflectance;
    float Emission;
    float RefractionProbability;
};

Ray ViewRay;
vec3 EyeCameraPosition = cameraPosition + gbufferModelViewInverse[3].xyz;
int Seed;
int PrevID;
float depth;
vec2 BufferCoord;

vec3 LightColor[LightsBinSize+3]; // +3 for sun, moon, and hand
vec3 LightPos[LightsBinSize+3];
vec2 LightProp[LightsBinSize+3];

ivec2 imgSize = ivec2(VoxelBufferSize, VoxelBufferSize);

//Code
vec3 ProjectNDivide(mat4 Matrix, vec3 Pos){
    vec4 HgnsPos = Matrix*vec4(Pos,1);
    return HgnsPos.xyz/HgnsPos.w;
}

void LoadLights(){
    //x: Lumenosity, y: Size
    float TimeAngle = (Pi/2.)-.0-(sunAngle*2.*Pi);
    vec3 HandLightPos = normalize(vec3(.4,-.3,3));
    if (heldItemId2!=-1) HandLightPos.x = -HandLightPos.x;
    vec3 HandOff = (mat3(gbufferModelViewInverse)*HandLightPos)*0.0;
    LightColor[0] = vec3(1,.7, .55);
    LightPos[0] = vec3(sin(TimeAngle),cos(TimeAngle),.04)*4500.;
    LightProp[0] = vec2(2100000.*SunLightBrightness, 100);

    LightColor[1] = vec3(.6,.8,1); //Multiply by moon phase brightness in next update
    LightPos[1] = -LightPos[0]; //opposite sun
    LightProp[1] = vec2(1500000.*MoonLightBrightness, 150);

    //Prevent Sun & Moon rendering if below horizon and fade
    LightProp[0].x = LightProp[0].x*min(pow(max(LightPos[0].y/1000., 0.), .5), 1.0);
    LightProp[1].x = LightProp[1].x*min(pow(max(LightPos[1].y/1000., 0.), .5), 1.0);

    LightColor[2] = vec3(1,1,1);
    LightProp[2] = vec2(0, .1398); // value calculated from pixel sizes (sqrt(2^2 + 1^1))
    LightPos[2] = vec3(RealVoxDist/2)+mod(EyeCameraPosition,1.)+HandOff;

    if (max(heldItemId, heldItemId2)>3999) LightProp[2].x = 1.6;
    if (max(heldItemId, heldItemId2)==4000) LightColor[2] = vec3(1,.7,.4);
    if (max(heldItemId, heldItemId2)==4001) LightColor[2] = vec3(.4,.7,1);
    if (max(heldItemId, heldItemId2)==4002) LightColor[2] = vec3(1,.2,.2);
    if (max(heldItemId, heldItemId2)==4004) LightColor[2] = vec3(1,.4,1);
    if (max(heldItemId, heldItemId2)==4005) LightColor[2] = vec3(.4,1,.5);

    /*for (int i=0; i<LightsBinSize; i++){
        // r: Voxel X, g: Voxel Y, b: Voxel Z
        ivec2 ReadLoc = ivec2(i%imgSize.x, i/imgSize.x);
        vec4 Data = imageLoad(colorimg7, ReadLoc);
        int LightID = int(Data.r*100.) + int(Data.g*100.*float(RealVoxDist)) + int(Data.b*100.*float(RealVoxDist*RealVoxDist));
        LightColor[i+3] = imageLoad(colorimg8, ivec2(LightID%imgSize.x, LightID/imgSize.x)).rgb; // get from voxel
        LightPos[i+3] = (Data.rgb*100.)+.5;
        LightProp[i+3] = vec2(60., 1.0);
    }*/
}

bool GetVoxel(in int ID){ // change!!
    ivec2 LoadPos = ivec2(ID%imgSize.x, ID/imgSize.x);
    return (texelFetch(colortex1, LoadPos, 0).x < 0.01);
}

float GetRand(in float mi, in float ma){
    Seed = Seed * 747796405 + 2891336453;
    int result = ((Seed >> ((Seed >> 28) + 4)) ^ Seed) * 277803737;
    result = (result >> 22) ^ result;
    return (mod(float(result)/149729.4323, (ma-mi)) + mi); 
}

float GetRandNormDist(){
    float theta = 2.0*Pi*GetRand(0.0,1.0);
    return sqrt(-2.0 * log(GetRand(-1.0,1.0))) * cos(theta);
}

vec3 GetRandVec(){
    return normalize(vec3(GetRand(-1.0,1.0), GetRand(-1.0,1.0), GetRand(-1.0,1.0)));
}

void GetTexture(in int ID, vec3 inNormal, out vec3 Color, out vec4 Material){
    Color = texelFetch(colortex8, ivec2(ID%imgSize.x, ID/imgSize.x), 0).rgb;
    Material = texelFetch(colortex9, ivec2(ID%imgSize.x, ID/imgSize.x), 0);
}

vec3 VoxelNormal(in int ID){
    //         HitPoint - (Bottom Corner of Voxel + Half) -- Center of Voxel
    vec3 Dir = ViewRay.orgn-(floor(ViewRay.orgn+(ViewRay.Dir*.005)) + .5);

    // Compare each coordinate and take max, as that will be the face of the normal, then normalize via "/abs(Dir.coord)"
    if (abs(Dir.x)>max(abs(Dir.y),abs(Dir.z))) {return vec3(Dir.x/abs(Dir.x),0,0);}
    if (abs(Dir.y)>abs(Dir.z)) {return vec3(0,Dir.y/abs(Dir.y),0);}
    return vec3(0,0,Dir.z/abs(Dir.z));
}

float VoxelIntersection(in Ray Cast, inout int HitID, in float MaxDist){
    int BlockID;
    vec3 Coord = Cast.orgn;
    float t = 0.;

    // Calculate first Intersection for each coordinate
    vec3 tCoord = (( floor(Coord)+vec3(Cast.Dir.x>0., Cast.Dir.y>0., Cast.Dir.z>0.) ) - Coord) / Cast.Dir;
    // This tells the program how far along the ray it will have to travel to move 1 along each axis
    vec3 StepLen = abs(1.0/Cast.Dir);

    for (int i=0; i<TraceDist; i++){
        // DDA //
        // Get minimum between all three intersections -might change to manual calculation for a little speed up, not requiring if's
        float mi = min(tCoord.x, min(tCoord.y, tCoord.z));
        // Set "t" to the minimum
        t = mi;
        if (t>MaxDist) return -1.;
        // Offset intersection point to the next whole number coordinate
        if (tCoord.x == mi) {tCoord.x+=StepLen.x;}
        if (tCoord.y == mi) {tCoord.y+=StepLen.y;}
        if (tCoord.z == mi) {tCoord.z+=StepLen.z;}

        // Get world location of t and check if it's a voxel
        Coord = Cast.orgn+(Cast.Dir*(t+Epsilon*2.0));
        if (Coord.x<0. || Coord.y<0. || Coord.z<0.) continue;
        if (Coord.x>RealVoxDist || Coord.y>RealVoxDist || Coord.z>RealVoxDist) continue;
        
        BlockID = int(floor(Coord.x)) + (int(floor(Coord.y))*RealVoxDist) + (int(floor(Coord.z))*RealVoxDist*RealVoxDist);
        if (GetVoxel(BlockID)){
            // If so set the intersection and return "t"
            HitID = BlockID;
            return t-(Epsilon*1.);
        }
    }
    return -1.; // If past the maximum intersection, return -1 to prevent it thinking it hit something
}

// Setup for the atmospheric update
float PlaneIntersection(){
    // Use 164 for the y position of clouds
    float t = ( 164.0 - ((ViewRay.orgn.y-float(RealVoxDist/2)) + floor(EyeCameraPosition.y))) / ViewRay.Dir.y;

    vec2 NoiseRead = ((ViewRay.orgn+(ViewRay.Dir*t)+floor(EyeCameraPosition)).xz) /20000.0;
    NoiseRead = floor(NoiseRead*ViewSize)/ViewSize;
    if ( sqrt(texture(noisetex, NoiseRead).r) < 0.9 ) {t = -1.0;}
    return t;
}

/*  Currently this function doesn't do much, but for more complex affects in the future
        it will be helpful to have this.
*/
float GetIntersection(in Ray Cast, inout int HitID, in float Max){
    float Closest = Max-Epsilon;
    HitID = -1;
    Closest = VoxelIntersection(Cast, HitID, Closest);
    return Closest;
}

vec3 SingleLight(in Ray Cast, in int ID, in vec3 inNormal){
    if (PixelLock != 0) Cast.orgn = (floor(Cast.orgn*float(PixelLock))+.5)/float(PixelLock);
    vec3 Rand = normalize(inNormal + GetRandVec());
    // Make light softness cubed by normalizing based off of the maximum length coordinate
    Rand /= sqrt(pow(max( max(abs(Rand.x), abs(Rand.y)), abs(Rand.z) ), 2.0));
    Cast.Dir = (LightPos[ID]+(Rand*LightProp[ID].y*LightSize)) - Cast.orgn;

    float Dist = length(Cast.Dir);
    Cast.Dir /= Dist;

    float a = dot(Cast.Dir, inNormal);
    if (a<0.) {return vec3(0);}

    int HitID;
    GetIntersection(Cast, HitID, Dist);
    if (HitID != -1) return vec3(0);

    return (LightProp[ID].x*6.*LightColor[ID]*a*float(LightsBinSize+3)) / (max(Dist*Dist, 8.)*float(LightSamples));
}

vec3 GetLight(in vec3 inNormal){
    vec3 LightCol;
    for (int i=0; i<LightSamples/(LightsBinSize+3); i++){
        for (int ID=0; ID<(LightsBinSize+3); ID++){
            LightCol += SingleLight(ViewRay, ID, inNormal);
        }
    }
    int ID;
    for (int i=0; i<LightSamples % (LightsBinSize+3); i++){
        ID = int( GetRand(.5, float(LightsBinSize+3)+.5) );
        LightCol += SingleLight(ViewRay, ID, inNormal);
    }
    return LightCol;
}

float CalcFresnel(in float x, in float R0){
    return clamp(R0 + (1.0-R0)*pow(1.0-x, 4.0), 0.0, 1.0);
}

void Bounce(in vec3 inNormal, in vec4 Mat){
    // Change to normal distribution (makes accurate)
    vec3 Rand = normalize(inNormal + GetRandVec());
    //Rand = GetRandVec();
    
    // Flip ray if negative
    Rand = normalize(Rand*(dot(Rand,inNormal)));
    ViewRay.Dir = normalize(mix(reflect(ViewRay.Dir, inNormal), Rand, (1.0-Mat.g)*dot(ViewRay.Dir, -inNormal)));
    #ifdef ReflectLock
        if (PixelLock != 0) ViewRay.orgn = (floor((ViewRay.orgn+.02*-ViewRay.Dir)*float(PixelLock))+.5)/float(PixelLock);
    #endif
}

vec3 SampleSky(in Ray SampleRay){
    // The center y relative to the player at y=0, calculated assuming the surface of earth is about 4000 miles (6437376m)
    //     from it's center and the average surface y is about y=80 (6437376 + 80)
    const float SurfaceY = 6437456.0;
    const float radius = SurfaceY + 96.5606;
    vec3 L = vec3(0,SurfaceY-cameraPosition.y,0);
    float tca = dot(L, SampleRay.Dir);
    if (tca < 0.0) return vec3(0);
    float d2 = dot(L,L)-(tca*tca);
    if (d2 > radius*radius) return vec3(0);
    float thc = sqrt((radius*radius) - d2);
    float t0 = tca - thc;
    float t1 = tca + thc;
    if (t0 > t1) {t0 = t1; t1 = tca - thc;}
    if (t0 < 0.0) t0 = t1;
    if (t0 < 0.0) return vec3(0);
    return vec3(t0);
}

void ScreenSpaceReflection(inout vec3 Color, inout vec4 Material, inout vec3 Normal){
    vec3 RayPlayerPos = ViewRay.orgn-(float(RealVoxDist/2)+mod(EyeCameraPosition, 1));
    vec3 RayViewPos = mat3(gbufferModelView)*RayPlayerPos;
    float RayDepth = length(RayViewPos);
    vec3 RayNDC = ProjectNDivide(gbufferProjection, RayViewPos);
    vec3 RayScreenPos = RayNDC*0.5+0.5;
    vec3 RayScreenView = mat3(gbufferModelViewInverse)*ProjectNDivide(gbufferProjectionInverse,vec3(RayScreenPos.xy,texture(depthtex0,RayScreenPos.xy))*2.-1.);
    // "mixt" will be used to blend SSR with voxel in the future
    float mixt = 0.0;
    if (RayDepth > 0.0 && length(RayScreenView-RayPlayerPos) < .005) {
        Color = mix(texture(colortex0, RayScreenPos.xy).rgb, Color, mixt);
        Material = mix(texture(colortex6, RayScreenPos.xy), Material, mixt);
        Normal = mix(texture(colortex5, RayScreenPos.xy).rgb*2.0-1.0, Normal, mixt);
    }
}

vec3 PathTrace(){
    LoadLights();
    PrevID = -1;

    //Translate from Screen-space to View-space
    ViewRay.orgn += ( mat3(gbufferModelViewInverse)*ProjectNDivide(gbufferProjectionInverse, vec3(BufferCoord,texture(depthtex0,vec2(BufferCoord)))*2.0-1.0) ) - (ViewRay.Dir*0.1);
    vec3 VoxNormal = texture(colortex5, BufferCoord).xyz*2.-1.;

    // Sample screen
    vec3 TexColor;
    vec4 TexMat = texture(colortex6, BufferCoord);
    // Convert multiplier to roughness
    //if (texture(depthtex0, BufferCoord).x == 1.0) return SampleSky(ViewRay)/20000000.;
    TexMat.r *= pow(1.0-CalcFresnel(dot(ViewRay.Dir, -VoxNormal), TexMat.g), 2.0);

    vec3 PixColor = texture(colortex0, BufferCoord).rgb;
    vec3 RayColor = PixColor;
    if (TexMat == vec4(0,0,0,0)) return PixColor;

    // Perform explicit light sample and add blocklight value for glowing
    PixColor *= GetLight(VoxNormal)+(TexMat.b*2.0*BlockLightBrightness);
    Bounce(VoxNormal, TexMat);
    
    for (int b=0; b<Bounces; b++){
        int HitID = -1;
        float t = GetIntersection(ViewRay, HitID, float(RealVoxDist));
        if (HitID == -1){
            TexColor = skyColor;
            return PixColor+(TexColor*RayColor);
        }
        ViewRay.orgn += ViewRay.Dir*t;
        vec3 TexColor;
        vec4 TexMat;
        if (HitID != -2){
            VoxNormal = VoxelNormal(HitID);
            TexColor = skyColor;
            TexMat = vec4(1,1,0,1);

            //x: Roughness, y: Reflectance (R0), z: Lumenosity, w: unused
            GetTexture(HitID, VoxNormal, TexColor, TexMat);
            ScreenSpaceReflection(TexColor, TexMat, VoxNormal);
        }
        if (b>0) TexColor *= GiStrength;

        // Convert multiplier to roughness
        TexMat.r *= pow(1.0-CalcFresnel(dot(ViewRay.Dir, -VoxNormal), TexMat.g), 2.0);

        RayColor *= TexColor;
        PixColor += RayColor*(GetLight(VoxNormal)+(TexMat.b*60.*BlockLightBrightness));
        Bounce(VoxNormal, TexMat);
        PrevID = HitID;
    }
    return PixColor;
}

float LinearDepthFast(in float nonlin){
    return (near*far) / (nonlin * (near-far) + far);
}

void main(){
    // Get seed for random numbers
    Seed = int(length(texture(noisetex, mod(texcoord*vec2(frameTimeCounter/7.0, frameTimeCounter/3.0), 1.0)).rgb*vec3(12378, 78932, 7923)));
    // Linearize depths
    depth = LinearDepthFast(texture(depthtex0, texcoord).x);
    float focusDepth = (near*far) / (texture(depthtex0, vec2(.5,.5)).x * (near-far) + far);

    // TAA & DOF //
    #ifdef RenderMode
        BufferCoord = texcoord;
        // Perform correct intersection at first pass rather than beam optimization for correct TA and DOF
    #else
        // Simplified TA and DOF for real-time
        // Get random offset for DOF
        BufferCoord = texcoord + (DOFStrength * vec2(GetRand(-0.015,0.015),GetRand(-0.015,0.015)) * sqrt(abs(focusDepth-depth)/ViewSize) * float(depth > 0.113));
        // Get depth at offset coord
        float newDepth = LinearDepthFast(texture(depthtex0, BufferCoord).x);
        // Check if hand or if it is very far apart, in which case we don't include the offset
        if (depth < 0.113 || sqrt(abs(depth-newDepth)) > 4.) BufferCoord = texcoord;
        #ifdef TAA
            BufferCoord += vec2(GetRand(-1.0,1.0),GetRand(-1.0,1.0)) / ViewSize;
        #endif
        depth = LinearDepthFast(texture(depthtex0, BufferCoord).x);
    #endif

    // Get ray direction
    vec3 ndc = vec3(BufferCoord,texture(depthtex0,vec2(BufferCoord)))*2.0 - 1.0;
    Ray ProjectedRay = Ray(
        vec3(RealVoxDist/2)+mod(EyeCameraPosition, 1),
        normalize(mat3(gbufferModelViewInverse)*ProjectNDivide(gbufferProjectionInverse,ndc))
    );

    // Used for Debug
    //fragColor = vec4(vec3(texture(colortex6, texcoord).a), 1);
    //fragColor = imageLoad(colorimg7, ivec2(texcoord*imgSize));
    //fragColor = vec4(vec3(depth/10.), 1);
    //fragColor = vec4(vec3( mat3(gbufferModelViewInverse)*ProjectNDivide(gbufferProjectionInverse, vec3(BufferCoord,texture(depthtex0,vec2(BufferCoord)))*2.0-1.0) )/10.,1);
    //return;
    
    vec3 Color = vec3(0);
    for (int s=0; s<Samples; s++){
        ViewRay = ProjectedRay;
        Color += PathTrace();
    }

    fragColor = vec4(Color.rgb/float(Samples), 1);
}