#version 140

//uniforms
uniform float time;
uniform mat4 light_position;
uniform mat4 light_direction;
uniform mat4 light_color;
uniform vec4 light_intensity;
uniform int light_amount;
uniform vec3 ambient_light_color;
uniform float ambient_light_intensity;

//attributes
in vec3 world_position;
in vec3 view_direction;
in vec3 vertex_color;
in vec3 vertex_normal;
in vec2 vertex_texcoord;

//material properties
// material uniforms
uniform vec3 mat_color;
uniform sampler2D mat_albedo;
uniform sampler2D mat_normal;
uniform float mat_normal_strength;
uniform sampler2D mat_roughness;
uniform float mat_roughness_strength;
uniform sampler2D mat_metallic;
uniform float mat_metallic_strength;
uniform sampler2D mat_emissive;
uniform float mat_emissive_strength;
uniform float mat_transparency_strength;
uniform sampler2D skybox;

// fragment outputs
out vec4 color;

//constants
const float PI = 3.14159265359;

// Helper Functions for PBR
vec2 getSphereMapUV(vec3 dir) {
    float u = atan(dir.z, dir.x) / (2.0 * 3.14159265) + 0.5;
    float v = asin(dir.y) / 3.14159265 + 0.5;
    return vec2(u, v);
}

float DistributionGGX(vec3 N, vec3 H, float roughness) {
    float a = roughness * roughness;
    float a2 = a * a;
    float NdotH = max(dot(N, H), 0.0);
    float NdotH2 = NdotH * NdotH;

    float nom = a2;
    float denom = (NdotH2 * (a2 - 1.0) + 1.0);
    denom = PI * denom * denom;

    return nom / max(denom, 0.000001); // Prevent division by zero
}

float GeometrySchlickGGX(float NdotV, float roughness) {
    float r = (roughness + 1.0);
    float k = (r * r) / 8.0;

    float nom = NdotV;
    float denom = NdotV * (1.0 - k) + k;

    return nom / denom;
}

float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness) {
    float NdotV = max(dot(N, V), 0.0);
    float NdotL = max(dot(N, L), 0.0);
    float ggx1 = GeometrySchlickGGX(NdotL, roughness);
    float ggx2 = GeometrySchlickGGX(NdotV, roughness);

    return ggx1 * ggx2;
}

vec3 fresnelSchlick(float cosTheta, vec3 F0) {
    return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
}

// Main PBR calculation function
// PBR calculations including skybox lighting
vec4 calculatePBRColor(vec3 viewDir) {
    // Fetch material properties
    vec4 albedo_texel = texture(mat_albedo, vertex_texcoord);
    float albedo_alpha = albedo_texel.a;
    vec3 albedo = albedo_texel.rgb * mat_color;
    vec3 normal = normalize(vertex_normal + (texture(mat_normal, vertex_texcoord).rgb - 0.5) * mat_normal_strength);
    float roughness = texture(mat_roughness, vertex_texcoord).r * mat_roughness_strength;
    float metallic = texture(mat_metallic, vertex_texcoord).r * mat_metallic_strength;
    vec3 emissive = texture(mat_emissive, vertex_texcoord).rgb * mat_emissive_strength;

    // Calculate reflectance at normal incidence
    vec3 F0 = vec3(0.04);
    F0 = mix(F0, albedo, metallic);

    vec3 result = vec3(0.0);
    for(int i = 0; i < light_amount; i++) {
        // Light calculations for each active light
        vec4 lightDirUniform = light_direction[i];
        vec3 lightDir = normalize(light_position[i].xyz - world_position);
        if(lightDirUniform.w == 1.0) {
            lightDir = normalize(lightDirUniform.xyz);
        }
        vec3 halfDir = normalize(lightDir + viewDir);
        float distance = length(light_position[i].xyz - world_position);
        float attenuation = 1.0 / (distance * distance);
        vec3 radiance = light_color[i].xyz * light_intensity[i] * attenuation;

        // Cook-Torrance BRDF
        float NDF = DistributionGGX(normal, halfDir, roughness);
        float G = GeometrySmith(normal, viewDir, lightDir, roughness);
        vec3 F = fresnelSchlick(max(dot(halfDir, viewDir), 0.0), F0);

        vec3 kS = F;
        vec3 kD = vec3(1.0) - kS;
        kD *= 1.0 - metallic;

        float NdotL = max(dot(normal, lightDir), 0.0);

        // Combine terms
        vec3 numerator = NDF * G * F;
        float denominator = 4.0 * max(dot(normal, viewDir), 0.0) * NdotL + 0.0001;
        vec3 specular = numerator / denominator;

        // Ambient and diffuse lighting
        vec3 ambient = ambient_light_color * ambient_light_intensity * albedo;
        vec3 diffuse = kD * albedo / PI;
        vec3 reflection = (diffuse + specular) * radiance * NdotL;

        // Accumulate result from this light
        result += ambient + reflection;
    }

    // Calculate reflection vector for environmental lighting
    vec3 reflectionVector = reflect(-viewDir, normal);
    vec2 uv = getSphereMapUV(reflectionVector);
    vec3 envReflection = texture(skybox, uv).rgb;

    // Apply fresnel effect to the environmental reflection
    vec3 fresnelEffect = fresnelSchlick(max(dot(viewDir, normal), 0.0), F0);
    vec3 envReflectionWithFresnel = envReflection * fresnelEffect * (1.0 - metallic);

    // Combine PBR lighting with environmental reflection
    vec3 finalColor = result + emissive + envReflectionWithFresnel;

    return vec4(finalColor, albedo_alpha * mat_transparency_strength);
}

void main() {
    color = calculatePBRColor(normalize(view_direction));
}
