#include "/lib/all_the_libs.glsl"
#include "/lib/distort.glsl"
#include "/global/sky.glsl"

#define TAU 6.28318530718
#define MAX_ITER 4

uniform sampler2D gcolor;
uniform sampler2D lightmap;

// Uniforms needed for slope bias


// --- Distant Horizons Uniforms ---
#ifdef DISTANT_HORIZONS
uniform sampler2D dhDepthTex0;
#endif

varying vec2 texcoord;

// --- Noise Helper for Soft Shadows ---
float InterleavedGradientNoise(vec2 position) {
    vec3 magic = vec3(0.06711056, 0.00583715, 52.9829189);
    return fract(magic.z * fract(dot(position, magic.xy)));
}

vec3 projectAndDivide(mat4 projectionMatrix, vec3 position){
  vec4 homPos = projectionMatrix * vec4(position, 1.0);
  return homPos.xyz / homPos.w;
}

float water_fog() {
    vec2 ScreenPos = gl_FragCoord.xy * resolutionInv;
    float TerrainDepth = texture2D(depthtex1, ScreenPos).x;
    TerrainDepth = linearize_depth(TerrainDepth);
    float ScreenDepth = linearize_depth(gl_FragCoord.z);
    // Keep fog very low so water is clear
    return smoothstep(3, 15, TerrainDepth - ScreenDepth) * WATER_FOG_STRENGTH *2.0;
}

float get_cloud_shadow(vec3 worldPos, vec3 sunDir) {
    if (sunDir.y < 0.05) return 1.0; 

    float totalWorldTime = float(worldDay) * 24000.0 + float(worldTime);
    vec3 wind = vec3(totalWorldTime * 0.002, 0.0, totalWorldTime * 0.05);
    
    // 1. Calculate Coverage Weather
    vec2 weatherCoords = fract(vec2(totalWorldTime * 0.00000095 * DYNAMIC_COVERAGE_MULT, totalWorldTime * 0.00000093 * DYNAMIC_COVERAGE_MULT));
    float weatherNoise = texture2D(noisetex, weatherCoords).r; 
    float dynamicCoverageOffset = (weatherNoise - 0.9) * 0.25;
    float finalOffset = mix(dynamicCoverageOffset, 0.6, rainStrength);

    vec2 noiseCoords = fract(vec2(totalWorldTime * 0.000004 * DYNAMIC_DENSITY_MULT, totalWorldTime * 0.000003 * DYNAMIC_DENSITY_MULT));
    float densNoiseVal = texture2D(noisetex, noiseCoords).r; 
    float dynamicDensity = (densNoiseVal - 0.5) * DYNAMIC_DENSITY_RANGE * 2.0; 
    float densityMultiplier = mix(dynamicDensity + (nightStrength * 8.0), 60.0, rainStrength);

    // 3. Fast Density Raymarch
    float cloudBottom = CLOUD_LOWER;
    float cloudTop = CLOUD_UPPER * UPPER_CLOUD_MULTIPLIER;
    float layerThickness = cloudTop - cloudBottom;
    
    float distToClouds = (cloudBottom - worldPos.y) / sunDir.y;
    vec3 startPos = worldPos + (sunDir * distToClouds);
    vec3 stepVector = (sunDir * (layerThickness / sunDir.y)) / 4.0; 
    vec3 currentPos = startPos;

    float totalDensity = 0.0;

    for (int i = 0; i < 4; i++) {
        vec3 coveragePos = vec3(currentPos.x, 150.0, currentPos.z) * 0.003 + wind * vec3(0.005, 0.0, 0.005); 
        float coverage = fbm_volumetric(coveragePos) + finalOffset;
        
        coverage = smoothstep(0.15, 0.75, coverage);
        
        if (coverage > 0.01) {
            totalDensity += (coverage * densityMultiplier) * 0.015; 
        }
        
        currentPos += stepVector;
    }

    return 1.0 - clamp(totalDensity, 0.0, 0.9); 
}

#define TAU 6.28318530718
#define MAX_ITER 4

//Core logic from David Hoskins, converted to a helper, you can find the original shader here : https://www.shadertoy.com/view/MdlXz8 
float get_caustic_sample(vec2 p, float t) {
    vec2 i = p;
    float c = 1.0;
    float inten = 0.0065;

    for (int n = 0; n < MAX_ITER; n++) {
        float t_layer = t * (1.0 - (3.0 / float(n + 1)));
        i = p + vec2(cos(t_layer - i.x) + sin(t_layer + i.y), 
                     sin(t_layer - i.y) + cos(t_layer + i.x));
        c += 1.0 / length(vec2(p.x / (sin(i.x + t_layer) / inten), 
                               p.y / (cos(i.y + t_layer) / inten)));
    }
    c /= float(MAX_ITER);
    return pow(abs(1.15 - pow(c, 1.2)), 10.0);
}

vec3 get_nostalgic_caustics_3d(vec3 worldPos) {

    vec2 p = mod(worldPos.xz * 1.2, TAU) - 250.0;
    float t = frameTimeCounter * 0.3 + 23.0;

    // Multiply by 9.5 for the bright spots
    vec3 caustics = vec3(get_caustic_sample(p,t)) * 9.5; 

    // THE FIX: Remove the solid waterColor addition!
    // Just return the raw light pattern, clamped so it doesn't break math
    return clamp(caustics, 0.0, 1.5); 

}

float get_nostalgic_caustics(vec3 worldPos) {
    vec2 p = mod(worldPos.xz, 1000.0) * 1.5; 
    float t = frameTimeCounter * 0.4;

    p.x += sin(p.y * 1.2 + t) * 0.5;
    p.y += sin(p.x * 1.2 + t) * 0.5;

    // Standard Caustic Layer
    float grid1 = sin(p.x * 2.5 + t) + sin(p.y * 2.5 - t);
    float grid2 = sin(p.x * 5.0 - t) + sin(p.y * 5.0 + t);
    float baseCaustics = 1.0 - abs((grid1 + grid2 * 0.2) * 0.4);
    baseCaustics = pow(max(0.0, baseCaustics), 4.0);

    // --- HOTSPOT LAYER ---
    float sparkleGrid = sin(p.x * 6.0 + t * 2.0) * sin(p.y * 6.0 - t * 1.5);
    float hotspots = pow(max(0.0, sparkleGrid), 16.0) * 2.5; 

    return baseCaustics + (hotspots * baseCaustics);
}

// --- UTILS ---

float get_eclipse_factor() {
    // 1. Recreate the exact same positions you used in the skybox
    vec3 baseJupPos = vec3(1500.0, 800.0, 0.0); 
    vec3 jupiterPos = rotateAxis(baseJupPos, vec3(0.0, 1.0, 0.0), frameTimeCounter * ORBIT_SPEED);
    vec3 sunPos = vec3(0.0, 1300.0, 2500.0); 

    // 2. Get their directions from the center of the world (camera origin)
    vec3 dirSun = normalize(sunPos);
    vec3 dirJup = normalize(jupiterPos);

    // 3. Calculate their apparent sizes in the sky (Radius / Distance)
    float sunAngularSize = 300.0 / length(sunPos);      // Approx 0.106
    float jupAngularSize = 800.0 / length(jupiterPos);  // Approx 0.403
    
    // The maximum distance between their centers before they start touching
    float touchDist = sunAngularSize + jupAngularSize;

    // 4. Check the actual distance between them on the sky dome
    float currentDist = distance(dirSun, dirJup);

    // 5. Output an eclipse factor: 0.0 (normal day) -> 1.0 (total eclipse)
    // We map the distance so that when they touch, it starts dimming, 
    // and when Jupiter mostly covers the Sun, it hits 1.0.
    float eclipseAmount = smoothstep(touchDist, touchDist * 0.3, currentDist);
    
    return eclipseAmount;
}

float check_is_underwater(vec2 texcoord) {
    float depthAll = texture2D(depthtex0, texcoord).r;
    float depthSolid = texture2D(depthtex1, texcoord).r;
    if (depthAll < depthSolid - 0.0001) return 1.0;
    if (isEyeInWater == 1 && depthAll > depthSolid - 0.0001) return 1.0;
    return 0.0;
}
vec3 GetShadowColorSoft(vec3 shadowCoord, float NdotL) {
    if (clamp(shadowCoord, 0.0, 1.0) != shadowCoord) return vec3(1.0);

    // 1. Robust Bias
    float baseBias = SHADOW_BIAS / SHADOW_MAP_RESOLUTION;
    float bias = baseBias * tan(acos(NdotL)); 
    bias = clamp(bias, baseBias, baseBias * 8.0);

    // 2. Controlled Softness
    float shadowMapDepth = texture2D(shadowtex1, shadowCoord.xy).r;
    float distToCaster = max(shadowCoord.z - shadowMapDepth, 0.0);
    
    // If shadows are too pixelated, reduce the 400.0 to 200.0
    float spread = (distToCaster * 400.0 + 1.0) / SHADOW_MAP_RESOLUTION;
    spread = clamp(spread, 0.5 * SHADOW_MAP_RESOLUTION, 20.0 / SHADOW_MAP_RESOLUTION);

    // 3. 32-Tap Golden Spiral
    // We use a high-frequency noise to break up the banding
    float noise = InterleavedGradientNoise(gl_FragCoord.xy);
    float angle = noise * 6.2831853;
    mat2 rot = mat2(cos(angle), -sin(angle), sin(angle), cos(angle));

    vec3 shadowAccum = vec3(0.0);
    const int SAMPLES = SOFT_SHADOWS_STEPS; // Optimized middle ground

    for(int i = 0; i < SAMPLES; i++) {
        // Fermat's Spiral for perfect distribution
        float r = sqrt(float(i + 0.5) / float(SAMPLES));
        float theta = float(i) * 2.3999632; // Golden Angle
        vec2 offset = rot * vec2(cos(theta), sin(theta)) * r * spread;
        
        vec2 sampleCoord = shadowCoord.xy + offset;
        float dAll = texture2D(shadowtex0, sampleCoord).r;
        float dOpaque = texture2D(shadowtex1, sampleCoord).r;
        
        float currentZ = shadowCoord.z - bias;

        if (currentZ < dAll) {
            shadowAccum += 1.0;
        } else if (currentZ < dOpaque) {
            shadowAccum += texture2D(shadowcolor0, sampleCoord).rgb;
        }
    }

    return shadowAccum / float(SAMPLES);
}

vec3 GetShadowColorSimple(vec3 shadowCoord, float NdotL) {
    // 1. No loops, no noise, no trig.
    // 2. Single bias
    float bias = 0.0007; 
    
    float depthOpaque = texture2D(shadowtex1, shadowCoord.xy).r;
    if (shadowCoord.z - bias < depthOpaque) return vec3(1.0);
    
    // Fallback to check for transparents (water/glass)
    float depthAll = texture2D(shadowtex0, shadowCoord.xy).r;
    if (shadowCoord.z - bias < depthAll) return texture2D(shadowcolor0, shadowCoord.xy).rgb;

    return vec3(0.0);
}

vec3 GetShadowColor(vec3 shadowCoord, float NdotL) {

    if (shadowCoord.x < 0.0 || shadowCoord.x > 1.0 ||

        shadowCoord.y < 0.0 || shadowCoord.y > 1.0 ||

        shadowCoord.z < 0.0 || shadowCoord.z > 1.0) {

        return vec3(1.0);

    }

   

    // 1. Calculate the base bias from your distort.glsl library

    float baseBias = SHADOW_BIAS/SHADOW_MAP_RESOLUTION;

   

    // 2. Gentle slope bias to fix the "rake" stripes on the ground

    float slope = sqrt(1.0 - NdotL * NdotL) / max(NdotL, 0.001);

    float slopeBias = clamp(slope, 0.0, 3.0);

    float finalBias = baseBias * (1.0 + slopeBias * 1.5);

   

    // 3. The Nostalgic Haze Setup

    float blurRadius = 2.5 / SHADOW_MAP_RESOLUTION;

   

    float noise = InterleavedGradientNoise(gl_FragCoord.xy);

    float angle = noise * 6.2831853;

    mat2 rotationMatrix = mat2(cos(angle), -sin(angle), sin(angle), cos(angle));



    // A fast, 4-tap cross pattern rotated by the noise

    vec2 offsets[4] = vec2[4](

        vec2( 1.0,  1.0),

        vec2(-1.0, -1.0),

        vec2(-1.0,  1.0),

        vec2( 1.0, -1.0)

    );



    vec3 shadowTotal = vec3(0.0);

   

    for(int i = 0; i < 4; i++) {

        vec2 offset = rotationMatrix * offsets[i] * blurRadius;

        vec2 sampleCoord = shadowCoord.xy + offset;

       

        float depthAll = texture2D(shadowtex0, sampleCoord).r;

        float depthOpaque = texture2D(shadowtex1, sampleCoord).r;

       

        if (shadowCoord.z - finalBias < depthAll) {

            shadowTotal += vec3(1.0); // Sun hits it fully

        }

        else if (shadowCoord.z - finalBias < depthOpaque) {

            // Sun is blocked by translucent block (glass/water)

            shadowTotal += texture2D(shadowcolor0, sampleCoord).rgb;

        }

        else {

            shadowTotal += vec3(0.0); // Blocked by solid block

        }

    }

   

    return shadowTotal / 4.0;

}

void main() {
    vec3 Color = texture2D(colortex0, texcoord).rgb;
    float depth = texture2D(depthtex1, texcoord).r;

    


    bool isDH = false;
    #ifdef DISTANT_HORIZONS
    float depthDH = texture2D(dhDepthTex1, texcoord).r;
    if (depth >= 1.0 && depthDH < 1.0) {
        depth = depthDH;
        isDH = true;
    }
    #endif

    
    vec3 NDCPos = vec3(texcoord.xy, depth) * 2.0 - 1.0;
    vec3 viewPos;

  #ifdef DISTANT_HORIZONS
    if (isDH) {
       
        viewPos = projectAndDivide(dhProjectionInverse, NDCPos);
    } else 
    #endif
    {

        viewPos = projectAndDivide(gbufferProjectionInverse, NDCPos);
    }

    vec3 feetPlayerPos = (gbufferModelViewInverse * vec4(viewPos, 1.0)).xyz;
    vec3 shadowViewPos = (shadowModelView * vec4(feetPlayerPos, 1.0)).xyz;
    vec4 shadowClipPos = shadowProjection * vec4(shadowViewPos, 1.0);
    shadowClipPos.z -= 0.001; // bias
    shadowClipPos.xyz = distortShadowClipPos(shadowClipPos.xyz); // distortion
    vec3 shadowNDCPos = shadowClipPos.xyz / shadowClipPos.w;
    vec3 shadowCoords = shadowNDCPos * 0.5 + 0.5;
 

    // --- Calculate NdotL for the Slope Bias ---
    vec3 viewNormal = texture2D(colortex9, texcoord).rgb * 2.0 - 1.0;
    vec3 viewLightDir = normalize(shadowLightPosition);


    #ifdef DIMENSION_END
        vec3 starPos = vec3(0, 300.0, 1500.0); 
        viewLightDir = normalize(mat3(gbufferModelView) * normalize(starPos));
    #endif
    float NdotL = clamp(dot(viewNormal, viewLightDir), 0.0, 1.0);

   float isUnderwater = check_is_underwater(texcoord);
        
    // Updated to pass NdotL so it compiles!
    float isSunlit = GetShadowColor(shadowCoords, NdotL).r; 

    // Grab the color of the transparent layer (water/glass)
    vec3 transColor = texture2D(colortex0, texcoord).rgb;
    bool isWater = (transColor.b > transColor.r + 0.05); // Simple color mask

    vec3 absoluteWorldPos = feetPlayerPos + cameraPosition; 

    float fogFade = 1.0; 

    if (depth < 1.0) {
        vec2 lmCoord = texture2D(lightmap, texcoord).rg;
        float blockLight = lmCoord.x;

        vec3 shadowFloor = vec3((blockLight) * (1.0 + rainStrength * 10.0)); 
        
        vec3 incomingLight;

        #ifdef SOFT_SHADOWS
        incomingLight = GetShadowColorSoft(shadowCoords, NdotL);
        #elif defined FAST_SHADOWS
        incomingLight = GetShadowColorSimple(shadowCoords, NdotL); 
        #else
        incomingLight = GetShadowColor(shadowCoords, NdotL);
        #endif

      
        // 2. Get the sun direction in world space
        vec3 sunDirWorld = normalize(mat3(gbufferModelViewInverse) * shadowLightPosition);
        
        // 3. Calculate the shadow cast by the clouds
        float cloudShadow = get_cloud_shadow(absoluteWorldPos, sunDirWorld);
        
        
        
        // --- 2. CALCULATE WATER DEPTH JUST ONCE ---
        if (isUnderwater > 0.5 && (isWater || isEyeInWater == 1)) {
            
            float floorDepth = texture2D(depthtex1, texcoord).r;
            float surfaceDepth = texture2D(depthtex0, texcoord).r;
            
            vec4 cPos = vec4(texcoord * 2.0 - 1.0, floorDepth * 2.0 - 1.0, 1.0);
            vec4 vPos = gbufferProjectionInverse * cPos;
            if (abs(vPos.w) > 0.00001) vPos /= vPos.w; 
            
            vec4 cPosSurf = vec4(texcoord * 2.0 - 1.0, surfaceDepth * 2.0 - 1.0, 1.0);
            vec4 vPosSurf = gbufferProjectionInverse * cPosSurf;
            if (abs(vPosSurf.w) > 0.00001) vPosSurf /= vPosSurf.w;

            float waterDepth = distance(vPos.xyz, vPosSurf.xyz);
            if (isEyeInWater == 1) {
                waterDepth = length(vPos.xyz); 
            }

            // Notice we do NOT write "float fogFade" here. We just update it!
            fogFade = exp(-waterDepth * 0.55); 

            vec4 floorPos = gbufferModelViewInverse * vPos; 
            
            // Notice we do NOT write "vec3 absoluteWorldPos" here either!
            absoluteWorldPos = floorPos.xyz + cameraPosition;
        }
        incomingLight *= fogFade;
        // 4. Darken the incoming shadow map light!
        #if defined(VOLUMETRIC_CLOUDS) && defined(CLOUD_SHADOW) 
        incomingLight *= cloudShadow;
        #endif

        float nightShadowFactor = 0.4; 
        float shadowPower = mix(1.0, nightShadowFactor, nightStrength);
        vec3 finalShadow = max(incomingLight, shadowFloor);
        finalShadow = mix(vec3(1.0), finalShadow, shadowPower); 

        if(thunderStrength>0.01){
            finalShadow=vec3(blockLight * 15.0);
        }

        vec3 SUN_COLOR = vec3(1.0, 0.95, 0.85) * dayStrength;           // Warm daylight
        SUN_COLOR += vec3(1.0, 0.45, 0.15) * sunsetStrength;            // Hot orange/pink sunset
        SUN_COLOR += vec3(0.9, 0.35, 0.25) * sunriseStrength;           // Peachy sunrise
        
        // Darken during rain/storms
        SUN_COLOR *= (1.0 - rainStrength * 0.8);
        
        // Boost the multiplier slightly so the terrain doesn't get too dark
        SUN_COLOR *= SUN_COLOR_MULT * 1.5; 

        #ifdef DIMENSION_END
            SUN_COLOR = vec3(1.0) * SUN_COLOR_MULT_END;
        #endif
        
        const vec3 MOON_COLOR = vec3(0.820, 0.761, 0.647); 
        vec3 currentLightColor = mix(SUN_COLOR, MOON_COLOR, nightStrength);
        float timeWeight = dayStrength + sunriseStrength + sunsetStrength + (nightStrength * 0.5);

        #ifdef GODRAYS
            vec3 finalSunlight = currentLightColor * finalShadow;
        #else
            vec3 finalSunlight = currentLightColor * finalShadow * timeWeight;
        #endif

         #ifdef DIMENSION_END
            float eclipse = get_eclipse_factor();
            finalSunlight *= mix(1.0,0.05,eclipse);
        #endif 
        

    
        vec3 dayShadow = vec3(0.05, 0.05, 0.15); 
        #ifdef DIMENSION_END
            dayShadow = vec3(0.1, 0.02, 0.15); // Deep Nostalgic Purple for the End
        #endif
        vec3 nightLight = vec3(1.20, 1.20, 1.35); 
        float timeMix = clamp(timeWeight, 0.0, 1.0);
        vec3 ambientColor = mix(nightLight, dayShadow, timeMix); 

        Color *= (ambientColor + finalSunlight);
        
        float gray = dot(Color, vec3(0.299, 0.587, 0.114));
        Color = mix(vec3(gray), Color, 1.15); 
        
        Color *= 0.6; 
        Color *= vec3(1.05, 1.0, 0.95);
        Color = clamp(Color,0.0,1.0);

        #ifdef SPECULAR
        //skip hand
        if(depth >= 0.56)   {
            // 1. Reconstruct vectors needed for specular
            vec3 viewDir = normalize(-viewPos); // viewPos is already calculated in your code
            vec3 lightDir = normalize(shadowLightPosition);
            vec3 reflectDir = reflect(-lightDir, viewNormal);

            // 2. Grab the smoothness/f0 data you saved in terrain.fsh
            vec4 specData = texture2D(colortex8, texcoord);
            float smoothness = specData.r;
            float f0 = specData.g;
            bool isMetal = f0 > 0.5; // Or however you define metalness

            // 3. Calculate the Specular Strength
            float NdotH = max(0.0, dot(reflectDir, viewDir));
            float specularStrength = pow(NdotH, 10.0 + (100.0 * smoothness)) * smoothness;

            // 4. THE FIX: Mask it by the shadow (incomingLight)
            // Use the shadow variable you calculated earlier in the file
            specularStrength *= incomingLight.r; 

            // 5. Calculate the final color using your original formula
            vec3 specularCol = (isMetal ? Color : vec3(1.0)) * specularStrength * 2.0;

            // 6. Add it to the final scene color
            #ifdef DIMENSION_END
                Color += specularCol * currentLightColor * timeWeight * 0.06;
            #else
                Color += specularCol * currentLightColor * timeWeight;
            #endif
        }

        #endif
    }

 #ifdef WATER_CAUSTICS
        // --- CAUSTIC LOGIC ---
    

        if (isUnderwater > 0.5 && isSunlit > 0.1 && (isWater || isEyeInWater == 1)) {
            
        
            #ifdef REALISTC_CAUSTICS
            float rPattern = get_nostalgic_caustics_3d(absoluteWorldPos + 0.08).r;
            float gPattern = get_nostalgic_caustics_3d(absoluteWorldPos).g;
            float bPattern = get_nostalgic_caustics_3d(absoluteWorldPos - 0.08).b;
            #else
            float rPattern = get_nostalgic_caustics(absoluteWorldPos + 0.04);
            float gPattern = get_nostalgic_caustics(absoluteWorldPos);
            float bPattern = get_nostalgic_caustics(absoluteWorldPos - 0.04);
            #endif            
            vec3 causticColor;
            causticColor.r = rPattern * 1.1; 
            causticColor.g = gPattern;
            causticColor.b = bPattern * 1.2; 

            vec3 sunTint = vec3(1.0, 0.9, 0.7); 
            vec3 finalCaustic = causticColor * sunTint;

            float intensity = (dayStrength + 0.001) * CAUSTICS_STRENGTH * 5.5 * (1.0-rainStrength);
            vec3 blendedCaustic = (Color.rgb * finalCaustic * 4.5) + (finalCaustic * 0.15);

            // Apply it with our new Fog Fade mask!
            Color.rgb += blendedCaustic * isSunlit * intensity * fogFade;

            if (gPattern > 0.8) {
                Color.rgb += vec3(0.1, 0.08, 0.05) * gPattern * isSunlit;
            }
        }
    #endif

    gl_FragColor = vec4(Color, 1.0);
}