#version 330 core

#define UNIT_SIZE 8

// Ouput data
layout(location = 0) out vec4 color;

uniform sampler2DArray TextureAtlas;
uniform sampler2DArray NormalAtlas;
uniform sampler2DArray HOSAtlas;

uniform mat4 View;
uniform vec3 FogColor;

#ifdef GEOMETRY
in GeometryData
#else
in VertexData
#endif
{
    vec3 Position_modelspace;
#ifdef GEOMETRY
    flat uint Textures[3];
    vec3 TextureRatio;
#else
    flat uint Texture;
#endif
    vec3 FaceNormal_modelspace;
#ifdef PBR
    vec3 FaceNormal_worldspace;
    vec3 EyeDirection_cameraspace;
    vec3 LightDirection_cameraspace;
#endif
#ifdef FOG
    float Depth;
#endif
} vs;

vec3 expand(vec3 v) {
    return (v - 0.5) * 2;
}

vec2 hash2D(vec2 s) {
    // magic numbers
    return fract(sin(mod(vec2(dot(s, vec2(127.1, 311.7)), dot(s, vec2(269.5, 183.3))), 3.14159)) * 43758.5453);
}

vec4 textureStochasticGrad(sampler2DArray sample, vec3 UV, vec2 dx, vec2 dy) {
#ifdef STOCHASTIC
    // triangular by approx 2*sqrt(3)
    vec2 skewUV = mat2(1.0, 0.0, -0.57735027, 1.15470054) * (UV.xy * 3.46400);

    // vertex id and barrycentric coords
    vec2 vxID = vec2(floor(skewUV));
    vec3 barry = vec3(fract(skewUV), 0);
    barry.z = 1.0 - barry.x - barry.y;

    vec3 BW_vx0;
    vec3 BW_vx1;
    vec3 BW_vx2;
    vec3 BW_vx3;

    if(barry.z > 0) {
        BW_vx0 = vec3(vxID, 0);
        BW_vx1 = vec3(vxID + vec2(0, 1), 0);
        BW_vx2 = vec3(vxID + vec2(1, 0), 0);
        BW_vx3 = barry.zyx;
    } else {
        BW_vx0 = vec3(vxID + vec2(1, 1), 0);
        BW_vx1 = vec3(vxID + vec2(1, 0), 0);
        BW_vx2 = vec3(vxID + vec2(0, 1), 0);
        BW_vx3 = vec3(-barry.z, 1.0 - barry.y, 1.0 - barry.x);
    }

    return textureGrad(sample, vec3(UV.xy + hash2D(BW_vx0.xy), UV.z), dx, dy) * BW_vx3.x +
           textureGrad(sample, vec3(UV.xy + hash2D(BW_vx1.xy), UV.z), dx, dy) * BW_vx3.y +
           textureGrad(sample, vec3(UV.xy + hash2D(BW_vx2.xy), UV.z), dx, dy) * BW_vx3.z;
#else
    return textureGrad(sample, UV, dx, dy);
#endif
}
vec4 textureStochastic(sampler2DArray sample, vec3 UV) {
#ifdef STOCHASTIC
    return textureStochasticGrad(sample, UV, dFdx(UV.xy), dFdy(UV.xy));
#else
    return texture(sample, UV);
#endif
}

vec4 getTexture(sampler2DArray sample, vec2 UV) {
#ifdef GEOMETRY
#ifdef BLEND
    vec4 colx = textureStochastic(sample, vec3(UV, vs.Textures[0]));
    if(vs.Textures[1] == vs.Textures[0]) {
        return vs.Textures[2] == vs.Textures[0] ? colx :
            mix(colx, textureStochastic(sample, vec3(UV, vs.Textures[2])), vs.TextureRatio.z);
    } else {
        vec4 coly = textureStochastic(sample, vec3(UV, vs.Textures[1]));
        return vs.Textures[2] == vs.Textures[0] ? mix(colx, coly, vs.TextureRatio.y) : (
               vs.Textures[2] == vs.Textures[1] ? mix(coly, colx, vs.TextureRatio.x) :
                colx * vs.TextureRatio.x + coly * vs.TextureRatio.y + textureStochastic(sample, vec3(UV, vs.Textures[2])) * vs.TextureRatio.z);
    }
#else
    int mainTexture = vs.TextureRatio.x >= vs.TextureRatio.y ?
        (vs.TextureRatio.x >= vs.TextureRatio.z ? 0 : 2) :
        (vs.TextureRatio.y >= vs.TextureRatio.z ? 1 : 2);
    return textureStochastic(sample, vec3(UV, vs.Textures[mainTexture]));
#endif
#else
    return textureStochastic(sample, vec3(UV, vs.Texture));
#endif
}
vec4 getTextureGrad(sampler2DArray sample, vec2 UV, vec2 dx, vec2 dy) {
#ifdef GEOMETRY
#ifdef BLEND
    vec4 colx = textureStochasticGrad(sample, vec3(UV, vs.Textures[0]), dx, dy);
    if(vs.Textures[1] == vs.Textures[0]) {
        return vs.Textures[2] == vs.Textures[0] ? colx :
            mix(colx, textureStochasticGrad(sample, vec3(UV, vs.Textures[2]), dx, dy), vs.TextureRatio.z);
    } else {
        vec4 coly = textureStochasticGrad(sample, vec3(UV, vs.Textures[1]), dx, dy);
        return vs.Textures[2] == vs.Textures[0] ? mix(colx, coly, vs.TextureRatio.y) : (
               vs.Textures[2] == vs.Textures[1] ? mix(coly, colx, vs.TextureRatio.x) :
                colx * vs.TextureRatio.x + coly * vs.TextureRatio.y + textureStochasticGrad(sample, vec3(UV, vs.Textures[2]), dx, dy) * vs.TextureRatio.z);
    }
#else
    int mainTexture = vs.TextureRatio.x >= vs.TextureRatio.y ?
        (vs.TextureRatio.x >= vs.TextureRatio.z ? 0 : 2) :
        (vs.TextureRatio.y >= vs.TextureRatio.z ? 1 : 2);
    return textureStochasticGrad(sample, vec3(UV, vs.Textures[mainTexture]), dx, dy);
#endif
#else
    return textureStochasticGrad(sample, vec3(UV, vs.Texture), dx, dy);
#endif
}

vec4 getTriTexture(sampler2DArray sample, vec2 UVx, vec2 UVy, vec2 UVz, vec3 w) {
    vec4 x = getTexture(sample, UVx);
    vec4 y = getTexture(sample, UVy);
    vec4 z = getTexture(sample, UVz);
    return x*w.x + y*w.y + z*w.z;
}

vec4 getBiTexture(sampler2DArray sample, vec2 UVa, vec2 UVb, vec2 dxa, vec2 dxb, vec2 dya, vec2 dyb, vec2 w) {
    vec4 x = getTextureGrad(sample, UVa, dxa, dya);
    vec4 y = getTextureGrad(sample, UVb, dxb, dyb);
    return x*w.x + y*w.y;
}

void main() {
    float texScale = 1. / UNIT_SIZE;
    float transitionSpeed = 2;
#ifdef TRIPLANAR
    // Triplanar
    float plateauSize = 0.001;

    vec3 blendWeights = abs(vs.FaceNormal_modelspace);
    blendWeights = blendWeights - plateauSize;
    blendWeights = pow(max(blendWeights, 0), vec3(transitionSpeed));
    blendWeights = blendWeights / (blendWeights.x + blendWeights.y + blendWeights.z);
    vec2 UVx = vs.Position_modelspace.yz * texScale;
    vec2 UVy = vs.Position_modelspace.zx * texScale;
    vec2 UVz = vs.Position_modelspace.xy * texScale;

    vec4 tex = getTriTexture(TextureAtlas, UVx, UVy, UVz, blendWeights);

#ifdef PBR
    // Whiteout normal blend
    vec3 texNx = expand(getTexture(NormalAtlas, UVx).rgb);
    vec3 texNy = expand(getTexture(NormalAtlas, UVy).rgb);
    vec3 texNz = expand(getTexture(NormalAtlas, UVz).rgb);
    // Swizzle world normals into tangent space and apply Whiteout blend
    texNx = vec3(texNx.xy + vs.FaceNormal_worldspace.zy, abs(texNx.z) * vs.FaceNormal_worldspace.x);
    texNy = vec3(texNy.xy + vs.FaceNormal_worldspace.xz, abs(texNy.z) * vs.FaceNormal_worldspace.y);
    texNz = vec3(texNz.xy + vs.FaceNormal_worldspace.xy, abs(texNz.z) * vs.FaceNormal_worldspace.z);
    // Swizzle tangent normals to match world orientation and triblend
    vec3 worldNormal = normalize(texNx.zyx * blendWeights.x + texNy.xzy * blendWeights.y +texNz.xyz * blendWeights.z);

    vec3 texHOS = getTriTexture(HOSAtlas, UVx, UVy, UVz, blendWeights).rgb;
#endif
#else
#ifdef BIPLANAR
    // Biplanar
    vec3 pos = vs.Position_modelspace * texScale;
    vec3 dpdx = dFdx(pos);
    vec3 dpdy = dFdy(pos);
    vec3 blendWeights = abs(normalize(vs.FaceNormal_modelspace));

    // determine major axis (in x; yz are following axis)
    ivec3 ma = (blendWeights.x>blendWeights.y && blendWeights.x>blendWeights.z) ? ivec3(0,1,2) :
               (blendWeights.y>blendWeights.z) ? ivec3(1,2,0) : ivec3(2,0,1);
    // determine minor axis (in x; yz are following axis)
    ivec3 mi = (blendWeights.x<blendWeights.y && blendWeights.x<blendWeights.z) ? ivec3(0,1,2) :
               (blendWeights.y<blendWeights.z) ? ivec3(1,2,0) : ivec3(2,0,1);
    // determine median axis (in x;  yz are following axis)
    ivec3 me = ivec3(3) - mi - ma;
    vec2 UVa = vec2(pos[ma.y], pos[ma.z]);
    vec2 UVb = vec2(pos[me.y], pos[me.z]);
    vec2 dxa = vec2(dpdx[ma.y], dpdx[ma.z]);
    vec2 dxb = vec2(dpdx[me.y], dpdx[me.z]);
    vec2 dya = vec2(dpdy[ma.y], dpdy[ma.z]);
    vec2 dyb = vec2(dpdy[me.y], dpdy[me.z]);

    // blend factors
    vec2 w = vec2(blendWeights[ma.x],blendWeights[me.x]);
    w = clamp((w-0.5773)/(1.0-0.5773), 0.0, 1.0);
    w = pow(w, vec2(transitionSpeed/8));
    w = w / (w.x + w.y);

    vec4 tex = getBiTexture(TextureAtlas, UVa, UVb, dxa, dxb, dya, dyb, w);

#ifdef PBR
    // Whiteout normal blend
    vec3 texNa = expand(getTextureGrad(NormalAtlas, UVa, dxa, dya).rgb);
    vec3 texNb = expand(getTextureGrad(NormalAtlas, UVb, dxb, dyb).rgb);

    // Swizzle world normals into tangent space and apply Whiteout blend
    texNa = normalize(vec3(texNa.xy + vs.FaceNormal_worldspace.zy, abs(texNa.z) * vs.FaceNormal_worldspace.x).zyx * blendWeights.x +
        vec3(texNa.xy + vs.FaceNormal_worldspace.xz, abs(texNa.z) * vs.FaceNormal_worldspace.y).xzy * blendWeights.y +
        vec3(texNa.xy + vs.FaceNormal_worldspace.xy, abs(texNa.z) * vs.FaceNormal_worldspace.z).xyz * blendWeights.z);
    texNb = normalize(vec3(texNb.xy + vs.FaceNormal_worldspace.zy, abs(texNb.z) * vs.FaceNormal_worldspace.x).zyx * blendWeights.x +
        vec3(texNb.xy + vs.FaceNormal_worldspace.xz, abs(texNb.z) * vs.FaceNormal_worldspace.y).xzy * blendWeights.y +
        vec3(texNb.xy + vs.FaceNormal_worldspace.xy, abs(texNb.z) * vs.FaceNormal_worldspace.z).xyz * blendWeights.z);
    // Swizzle tangent normals to match world orientation and biblend
    vec3 worldNormal = normalize((texNa * w.x + texNb * w.y) / (w.x + w.y));

    vec3 texHOS = getBiTexture(HOSAtlas, UVa, UVb, dxa, dxb, dya, dyb, w).rgb;
#endif
#else
    // Cheap planar
    vec3 blendWeights = abs(vs.FaceNormal_modelspace);
    vec3 nrm = normalize(pow(blendWeights, vec3(80 / sqrt(UNIT_SIZE))));
    vec2 UV = (vec2(vs.Position_modelspace.xy * nrm.z) + vec2(vs.Position_modelspace.yz * nrm.x) + vec2(vs.Position_modelspace.zx * nrm.y)) * texScale;

    vec4 tex = getTexture(TextureAtlas, UV);
#ifdef PBR
    vec3 texN = expand(getTexture(NormalAtlas, UV).rgb);
    // Swizzle tangent normals to match world orientation
    vec3 worldNormal = normalize(vec3(texN.xy + vs.FaceNormal_worldspace.zy, abs(texN.z) * vs.FaceNormal_worldspace.x).zyx * blendWeights.x +
                                vec3(texN.xy + vs.FaceNormal_worldspace.xz, abs(texN.z) * vs.FaceNormal_worldspace.y).xzy * blendWeights.y +
                                vec3(texN.xy + vs.FaceNormal_worldspace.xy, abs(texN.z) * vs.FaceNormal_worldspace.z).xyz * blendWeights.z);

    vec3 texHOS = getTexture(HOSAtlas, UV).rgb;
#endif
#endif
#endif

// Colors
#ifdef PBR
    // Texture properties
    vec3 TextureDiffuseColor = vec3(tex);
    vec3 TextureAmbientColor = vec3(.1) * TextureDiffuseColor * texHOS.y;
    vec3 TextureSpecularColor = vec3(.8) * texHOS.z;

    vec3 Normal_cameraspace = normalize((View * vec4(worldNormal,0)).xyz);

    // Light emission properties
    // You probably want to put them as uniforms
    vec3 LightColor = vec3(1, 0.9, 0.75);
    float LightPower = 1.25f;

    // Distance to the light
    float distance = 1.0f;//length( LightPosition_worldspace - Position_worldspace );

    // Direction of the light (from the fragment to the light)
    vec3 l = normalize(vs.LightDirection_cameraspace);
    // Cosine of the angle between the normal and the light direction,
    // clamped above 0
    //  - light is at the vertical of the triangle -> 1
    //  - light is perpendiular to the triangle -> 0
    //  - light is behind the triangle -> 0
    float cosTheta = clamp(dot(Normal_cameraspace,l), 0,1 );

    // Eye vector (towards the camera)
    vec3 E = normalize(vs.EyeDirection_cameraspace);
    // Direction in which the triangle reflects the light
    vec3 R = reflect(-l,Normal_cameraspace);
    // Cosine of the angle between the Eye vector and the Reflect vector,
    // clamped to 0
    //  - Looking into the reflection -> 1
    //  - Looking elsewhere -> < 1
    float cosAlpha = clamp( dot( E,R ), 0,1 );

    float visibility=1.0;

    // MAYBE: shadow

    color = vec4(
        // Ambient : simulates indirect lighting
        TextureAmbientColor +
        // Diffuse : "color" of the object
        visibility * TextureDiffuseColor * LightColor * LightPower * cosTheta / (distance * distance) +
        // Specular : reflective highlight, like a mirror
        visibility * TextureSpecularColor * LightColor * LightPower * pow(cosAlpha,5) / (distance * distance),
        // Restore alpha
        tex.a);
#else
    color = tex;
#endif
#ifdef FOG
    float ratio = exp(vs.Depth * 0.69)-1;
    color = mix(color, vec4(pow(FogColor, vec3(2.2)), 1), clamp(ratio, 0, 1));
#endif
}