#version 330 core
out vec4 FragColor;

in vec3 vsColor;
in vec2 vsTexCoord;

in vec3 vsFragPos;
in vec3 vsNormal;

// uniforms for lighting
uniform vec3 viewPos;

// rendering type
uniform int renderingType;
uniform int objectType;
uniform int light_count;

#define MAX_LIGHTS 16

// light source properties
struct Light {
    int  type;  // 0 ambient, 1 directional, 2 point, 3 spot, 4 point_atn, 5 spot_atn
    vec3 direction;
    vec3 position;

    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
	
    float atn_cnst;
    float atn_lin;
    float atn_quad;
    
    float innerCutoff;
    float cutOff;
    float outerCutOff;
};

struct Material {
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
    vec3 shinyNess;
};

struct MaterialTexture {
    sampler2D diffuse;
    sampler2D ambient;
    sampler2D specular;
    sampler2D normal;
};

uniform Light light[MAX_LIGHTS];
uniform Material material;
uniform MaterialTexture mtrlTex1;
uniform MaterialTexture mtrlTex2;

void calc_ambient( out vec3 ambient_out, vec3 light, vec3 material ){
    ambient_out = light * material;
}

void calc_diffuse( out vec3 diffuse_out, out float diff_out, vec3 normal, vec3 pixel_pos, vec3 light_dir, vec3 light, vec3 material ){
    diff_out     = max( dot( normal, -light_dir ), 0.0 );
    diffuse_out  = light * diff_out * material;
}

void calc_specular( out vec3 specular_out, vec3 normal, vec3 pixel_pos, vec3 view_pos, vec3 light_dir, vec3 light, vec3 material, float shiny ){
    vec3 viewDir        = normalize( view_pos - pixel_pos );
    vec3 reflectDir     = reflect( light_dir, normal );
    float viewedReflect = max( dot( viewDir, reflectDir ), 0.0 );
    float spec          = pow( viewedReflect, shiny );
    specular_out        = light * spec * material;
}

void calc_tbn_matrix( out mat3 tbn, vec3 normal, vec3 pixel_pos, vec2 pixel_texcoord ){
    // Calculate derivatives
    vec3 pos_dx  = dFdx(pixel_pos);
    vec3 pos_dy  = dFdy(pixel_pos);
    vec2 texC_dx = dFdx(pixel_texcoord);
    vec2 texC_dy = dFdy(pixel_texcoord);

    // Calculate tangent and bitangent
    vec3 tangent   = normalize(texC_dy.y * pos_dx - texC_dx.y * pos_dy);
    vec3 bitangent = normalize(texC_dx.x * pos_dy - texC_dy.x * pos_dx);

    // Construct TBN matrix
    tbn = mat3( tangent, bitangent, normal ); // normal interpolated vertex normal (after normalization)
}

void calc_light_dir_and_atn( out vec3 lightDir, out float atn_amb, out float atn_ds, Light light ){
    lightDir = vec3( 0.0, -1.0, 0.0 );
    atn_amb  = 1.0f;
    atn_ds   = 1.0f;

    float spot_intensity = 1.0f;

    if( light.type == 0 ){
        atn_ds   = 0.0f;
    } else if( light.type == 1 ){
        lightDir = light.direction;
        lightDir = normalize( lightDir );
    } else {
        lightDir = vsFragPos - light.position;
        vec3 lightDir_norm = normalize( lightDir );
        if( light.type > 3 ){
            // distance based attenuation
            float distance = length( lightDir );
            atn_amb        = 1.0 / ( light.atn_cnst + ( light.atn_lin + ( light.atn_quad * distance ) * distance ) );
            atn_ds         = atn_amb;
        }

        if( light.type == 3 || light.type == 5 ){
            // spotlight (soft edges)
            // The lightDir_norm is the position relative to the vsFragPos while the light[0].direction is the pointing direction
            float theta    = dot( -lightDir_norm, normalize( -light.direction ) ); 
            float epsilon  = ( light.cutOff - light.outerCutOff );
            spot_intensity = clamp( ( theta - light.outerCutOff ) / epsilon, 0.0, 1.0 );
            atn_ds *= spot_intensity;
        }
        lightDir = lightDir_norm;
    }
}

void main(){
    if( objectType == 0 ){
        if( renderingType < 12 ){
            FragColor = vec4( light[0].diffuse, 1.0 );
        } else {
            FragColor = vec4( light[light_count].diffuse, 1.0 );
        }
    } else {
        // First implementation of the example using only the first texture
        if( renderingType < 2 ){
            FragColor = texture( mtrlTex1.diffuse, vsTexCoord );
        } else if( renderingType == 2 ){
            // Second implementation of the example using texture and color inputs
            FragColor = texture( mtrlTex1.diffuse, vsTexCoord ) * vec4( vsColor, 1.0 );
        } else if( renderingType == 3 ){
            // Third implmentation of the example linearly interpolating between both textures
            FragColor = mix( texture( mtrlTex1.diffuse, vsTexCoord ), texture( mtrlTex2.diffuse, vsTexCoord ), 0.5 );
        } else if( renderingType == 4 ){
            // Fourth implmentation of the example with colored object defined by vec3 and a light
            // It's not really "diffuse" lighting, diffuse is just the default for "what color is this light?"
            FragColor = vec4( light[0].diffuse * material.diffuse, 1.0 );
        } else if( renderingType == 5 ){
            // Fifth implementation of the example with only ambient lighting
            vec3 ambient;
            calc_ambient( ambient, light[0].ambient, material.ambient );
            FragColor    = vec4( ambient, 1.0 );
        } else if( renderingType < 12 ){
            // Implementations after this are going to use the direction the light is pointing relative to the fragment
            vec3 lightDir;
            float atn_amb, atn_ds;
            calc_light_dir_and_atn( lightDir, atn_amb, atn_ds, light[0] );

            if( renderingType == 6 ){
                // Sixth implementation of the example with ambient and diffuse lighting
                vec3 ambient, diffuse;
                float diff;
                calc_ambient( ambient, light[0].ambient, material.ambient );
                calc_diffuse( diffuse, diff, normalize(vsNormal), vsFragPos, lightDir, light[0].diffuse, material.diffuse );
                FragColor     = vec4( ambient * atn_amb + diffuse * atn_ds, 1.0 );
                //FragColor     = vec4( vsNormal, 1.0 );
            } else if( renderingType == 7 ){
                // Seventh implementation of the example with ambient, diffuse, and specular lighting (Phong Lighting)
                vec3 ambient, diffuse, specular;
                float diff;
                vec3 normal_norm = normalize(vsNormal);
                calc_ambient( ambient, light[0].ambient, material.ambient );
                calc_diffuse( diffuse, diff, normal_norm, vsFragPos, lightDir, light[0].diffuse, material.diffuse );
                calc_specular( specular, normal_norm, vsFragPos, viewPos, lightDir, light[0].specular, material.specular, material.shinyNess.x );
                FragColor = vec4( ambient * atn_amb + ( diffuse + specular ) * atn_ds, 1.0 );
            } else if( renderingType == 8 ){
                // Eighth implementation of the example with ambient, diffuse, specular lighting (Phong Lighting), and textures
                // blend all the textures
                vec3 ambTex  = mix( texture( mtrlTex1.ambient,  vsTexCoord ).rgb, texture( mtrlTex2.ambient,  vsTexCoord ).rgb, 0.5 );
                vec3 diffTex = mix( texture( mtrlTex1.diffuse,  vsTexCoord ).rgb, texture( mtrlTex2.diffuse,  vsTexCoord ).rgb, 0.5 );
                vec3 specTex = mix( texture( mtrlTex1.specular, vsTexCoord ).rgb, texture( mtrlTex2.specular, vsTexCoord ).rgb, 0.5 );

                vec3 ambient, diffuse, specular;
                float diff;
                vec3 normal_norm = normalize(vsNormal);
                calc_ambient( ambient, light[0].ambient, (material.ambient * ambTex.r) );
                calc_diffuse( diffuse, diff, normal_norm, vsFragPos, lightDir, light[0].diffuse, (material.diffuse * diffTex) );
                calc_specular( specular, normal_norm, vsFragPos, viewPos, lightDir, light[0].specular, (material.specular * specTex), material.shinyNess.x );
                    
                FragColor = vec4( ambient * atn_amb + ( diffuse + specular ) * atn_ds, 1.0 );
            } else if( renderingType == 9 ){
                // Nineth implementation of the example only ambient, diffuse, specular lighting (Phong Lighting), texture, and normal maps
                // Sample normal map, get normal vector, then normalize and remap (* 2 - 1)
                vec3 normalMapNormal1 = normalize( texture( mtrlTex1.normal, vsTexCoord ).rgb * 2.0 - 1.0 );

                // Construct TBN matrix
                mat3 tbn;
                calc_tbn_matrix( tbn, vsNormal, vsFragPos, vsTexCoord );

                // Transform the normal
                vec3 worldNormal1 = normalize( tbn * normalMapNormal1 );

                vec3 ambTex  = texture( mtrlTex1.ambient,  vsTexCoord ).rgb;
                vec3 diffTex = texture( mtrlTex1.diffuse,  vsTexCoord ).rgb;
                vec3 specTex = texture( mtrlTex1.specular, vsTexCoord ).rgb;

                vec3 ambient, diffuse, specular;
                float diff;
                calc_ambient( ambient, light[0].ambient, (material.ambient * ambTex.r) );
                calc_diffuse( diffuse, diff, worldNormal1, vsFragPos, lightDir, light[0].diffuse, (material.diffuse * diffTex) );
                calc_specular( specular, worldNormal1, vsFragPos, viewPos, lightDir, light[0].specular, (material.specular * specTex.r), material.shinyNess.x );
                    
                FragColor = vec4( ambient * atn_amb + ( diffuse + specular ) * atn_ds, 1.0 );
            } else if( renderingType == 10 ){
                // Tenth implementation of the example with ambient, diffuse, specular lighting (Phong Lighting), texture, and normal maps
                // This implementation (vs 11 below) takes two textures, blends them together, and then uses blended normals and textures
                // Sample normal map, get normal vector, then normalize and remap (* 2 - 1)
                vec3 normalMapNormal1 = normalize( texture( mtrlTex1.normal, vsTexCoord ).rgb * 2.0 - 1.0 );
                vec3 normalMapNormal2 = normalize( texture( mtrlTex2.normal, vsTexCoord ).rgb * 2.0 - 1.0 );

                // Construct TBN matrix
                mat3 tbn;
                calc_tbn_matrix( tbn, vsNormal, vsFragPos, vsTexCoord );

                // Transform the normal
                vec3 worldNormal1 = normalize( tbn * normalMapNormal1 );
                vec3 worldNormal2 = normalize( tbn * normalMapNormal2 );

                vec3 pixelNormal = normalize( mix( worldNormal1, worldNormal2, 0.5 ) );

                // blending two textures together and using an average worldNormal based on both at the pixel (pixelNormal)
                vec3 ambTex  = mix( texture( mtrlTex1.ambient,  vsTexCoord ).rgb, texture( mtrlTex2.ambient,  vsTexCoord ).rgb, 0.5 );
                vec3 diffTex = mix( texture( mtrlTex1.diffuse,  vsTexCoord ).rgb, texture( mtrlTex2.diffuse,  vsTexCoord ).rgb, 0.5 );
                vec3 specTex = mix( texture( mtrlTex1.specular, vsTexCoord ).rgb, texture( mtrlTex2.specular, vsTexCoord ).rgb, 0.5 );

                vec3 ambient, diffuse, specular;
                float diff;
                calc_ambient( ambient, light[0].ambient, (material.ambient * ambTex.r) );
                calc_diffuse( diffuse, diff, pixelNormal, vsFragPos, lightDir, light[0].diffuse, (material.diffuse * diffTex) );
                calc_specular( specular, pixelNormal, vsFragPos, viewPos, lightDir, light[0].specular, (material.specular * specTex.r), material.shinyNess.x );
                    
                FragColor = vec4( ambient * atn_amb + ( diffuse + specular ) * atn_ds, 1.0 );
            } else if( renderingType == 11 ){
                // Eleventh implementation of the example with ambient, diffuse, specular lighting (Phong Lighting), texture, and normal maps
                // This implementation handles diffuse and specular seperately for each texture
                // Sample normal map, get normal vector, then normalize and remap (* 2 - 1)
                vec3 normalMapNormal1 = normalize( texture( mtrlTex1.normal, vsTexCoord ).rgb * 2.0 - 1.0 );
                vec3 normalMapNormal2 = normalize( texture( mtrlTex2.normal, vsTexCoord ).rgb * 2.0 - 1.0 );

                // Construct TBN matrix
                mat3 tbn;
                calc_tbn_matrix( tbn, vsNormal, vsFragPos, vsTexCoord );

                // Transform the normal
                vec3 worldNormal1 = normalize( tbn * normalMapNormal1 );
                vec3 worldNormal2 = normalize( tbn * normalMapNormal2 );

                // blend the textures for ambient, diffuse and specular handle each texture independently
                vec3 ambTex  = mix( texture( mtrlTex1.ambient,  vsTexCoord ).rgb, texture( mtrlTex2.ambient,  vsTexCoord ).rgb, 0.5 );
                vec3 diffTex1 = texture( mtrlTex1.diffuse, vsTexCoord ).rgb;
                vec3 diffTex2 = texture( mtrlTex2.diffuse, vsTexCoord ).rgb;
                vec3 specTex1 = texture( mtrlTex1.specular, vsTexCoord ).rgb;
                vec3 specTex2 = texture( mtrlTex2.specular, vsTexCoord ).rgb;

                vec3 ambient, diffuse1, diffuse2, specular1, specular2;
                float diff1, diff2;
                
                calc_ambient( ambient, light[0].ambient, (material.ambient * ambTex.r) );
                calc_diffuse( diffuse1, diff1, worldNormal1, vsFragPos, lightDir, light[0].diffuse, (material.diffuse * diffTex1) );
                calc_diffuse( diffuse2, diff2, worldNormal2, vsFragPos, lightDir, light[0].diffuse, (material.diffuse * diffTex2) );
                calc_specular( specular1, worldNormal1, vsFragPos, viewPos, lightDir, light[0].specular, (material.specular * specTex1.r), material.shinyNess.x );
                calc_specular( specular2, worldNormal2, vsFragPos, viewPos, lightDir, light[0].specular, (material.specular * specTex2.r), material.shinyNess.x );

                FragColor = vec4( ambient * atn_amb + ( mix( diffuse1, diffuse2, 0.5 ) + mix( specular1, specular2, 0.5 ) ) * atn_ds, 1.0 );
            }
        } else if( renderingType == 12 ){
            // Twelfth implementation of the example with multiple light sources
            // Lights and surfaces have ambient, diffuse, specular lighting (Phong Lighting), and textures
            // Sample normal map, get normal vector, then normalize and remap (* 2 - 1)
            vec3 normalMapNormal1 = normalize( texture( mtrlTex1.normal, vsTexCoord ).rgb * 2.0 - 1.0 );

            // Construct TBN matrix
            mat3 tbn;
            calc_tbn_matrix( tbn, vsNormal, vsFragPos, vsTexCoord );

            // Transform the normal
            vec3 worldNormal1 = normalize( tbn * normalMapNormal1 );

            vec3 ambTex  = texture( mtrlTex1.ambient,  vsTexCoord ).rgb;
            vec3 diffTex = texture( mtrlTex1.diffuse,  vsTexCoord ).rgb;
            vec3 specTex = texture( mtrlTex1.specular, vsTexCoord ).rgb;

            vec4 result = vec4( 0.0, 0.0, 0.0, 0.0 );
            for( int i = 1; i < light_count + 1; i++ ){
                vec3 lightDir, ambient, diffuse, specular;
                float atn_amb, atn_ds, diff;

                calc_light_dir_and_atn( lightDir, atn_amb, atn_ds, light[i] );
                calc_ambient( ambient, light[i].ambient, (material.ambient * ambTex.r) );
                calc_diffuse( diffuse, diff, worldNormal1, vsFragPos, lightDir, light[i].diffuse, (material.diffuse * diffTex) );
                calc_specular( specular, worldNormal1, vsFragPos, viewPos, lightDir, light[i].specular, (material.specular * specTex.r), material.shinyNess.x );
                    
                result += vec4( ambient * atn_amb + ( diffuse + specular ) * atn_ds, 1.0 );
            }
            FragColor = result;
        } else if( renderingType == 13 ){
            float near  = 0.1; 
            float far   = 100.0; 
            float z     = gl_FragCoord.z * 2.0 - 1.0; // back to NDC 
            float depth = sqrt( (2.0 * near ) / (far + near - z * (far - near)) );
            FragColor = vec4( vec3( depth ), 1.0 );
        }
    }
}