uniform sampler2DShadow shadow;

uniform sampler2D shadowtex0;
uniform sampler2DShadow shadowtex1;
#if defined RSM
uniform sampler2D shadowcolor0;

uniform sampler2D shadowcolor1;
#endif
uniform ivec2 eyeBrightnessSmooth;
uniform mat4 shadowModelView;
uniform mat4 shadowProjection;
#define diagonal3(m) vec3((m)[0].x, (m)[1].y, m[2].z)

// #define SSS_ALT

float phaseg(float x, float g)
{
    float gg = g * g;
    return (gg * -0.25 + 0.25) * pow(-2.0 * (g * x) + (gg + 1.0), -1.5) / PI;
}

vec2 tapLocationBlueNoise(int sampleNumber, int nb, float nbRot, vec2 blueNoise)
{
    float nbInv = 1.0 / nb;                     // Pre-calculate reciprocal if nb is constant
    float alpha = (sampleNumber + 0.5) * nbInv; // Use pre-calculated reciprocal here

    // Incorporate blue noise in angle calculation
    float angle = (alpha * nbRot + blueNoise.x) * TWO_PI;

    // Calculate the position using sine and cosine for better distribution
    float sin_v = sin(angle);
    float cos_v = cos(angle);

    // Slightly vary the radius using blue noise
    float radius = alpha + blueNoise.y * nbInv; // Use pre-calculated reciprocal here as well

    return vec2(cos_v, sin_v) * radius;
}

uniform float biasblend;
uniform float biasblend2;
uniform float biasblend3;
float linZ(float depth)
{
    return (2.0 * near) / (far + near - depth * (far - near));
    // l = (2*n)/(f+n-d(f-n))
    // f+n-d(f-n) = 2n/l
    // -d(f-n) = ((2n/l)-f-n)
    // d = -((2n/l)-f-n)/(f-n)
}
float rayTraceShadow(vec3 dir, vec3 position, float dither)
{
    float quality = 16.0;
    float farMultiplier = far * sqrt(3.0);
    float rayLength = (position.z + dir.z * farMultiplier > -near) ? (-near - position.z) / dir.z : farMultiplier;

    vec3 clipPosition = toClipSpace3(position);
    vec3 endPosition = position + dir * rayLength;
    vec3 clipEndPosition = toClipSpace3(endPosition);
    vec3 direction = clipEndPosition - clipPosition;

    float maxStep = max(abs(direction.x) / texelSize.x, abs(direction.y) / texelSize.y);
    direction = direction / maxStep;

    vec3 step = direction * 3.0;
    vec3 spos = clipPosition + step * dither;
    float shadow = 1.0;

    for (int i = 0; i < int(quality); ++i)
    {
        spos += step;
        float depthSample = texture2D(depthtex0, spos.xy).x;

        if (depthSample < spos.z)
        {
            float depthDifference = abs(linZ(depthSample) - linZ(spos.z)) / linZ(spos.z);
            if (depthDifference < 0.01)
            {
                shadow = 0.0;
                break;
            }
        }
    }

    return shadow;
}

float computeSSS(float sssAmount, vec3 albedo, vec3 normal, vec3 lightPos, float avgDepth, inout float diffuseSun)
{
    if (sssAmount <= 0.0)
    {
        return 0.0;
    }

    float extinction = 1.0 - luma(albedo) * 0.85;

    float angleFactor = dot(normalize(normal), lightPos);

    float extinctionFactor = exp(-avgDepth * 11.0 * extinction);

    float sssBase = extinctionFactor + 3.0 * pow(extinctionFactor, 1.0 / 3.0);

    float phaseFactor = phaseg(angleFactor, 0.85);

    float scattering = clamp((0.7 + 0.3 * 3.14159265 * phaseFactor) * 1.5 / 4.0 * sssAmount, 0.0, 1.0);

    diffuseSun *= 1.0 - sssAmount;

    return clamp(sssBase * scattering, 0.0, 1.0);
}

float cloud_height = 1500.;
float cubeSmooth(float x)
{
    return x * x * (3.0 - 2.0 * x);
}

float cloudTextureCubic(sampler2D tex, vec2 pos)
{
    vec2 texSize = textureSize(tex, 0) * 2.0;
    vec2 texelSize = 1.0 / texSize;

    vec2 fractPos = fract(pos * texSize);
    vec2 smoothPos = vec2(cubeSmooth(fractPos.x), cubeSmooth(fractPos.y));

    float baseTexel = texture(tex, pos).a;
    float rightTexel = texture(tex, pos + vec2(texelSize.x, 0.0)).a;
    float upTexel = texture(tex, pos + vec2(0.0, texelSize.y)).a;
    float diagonalTexel = texture(tex, pos + texelSize).a;

    float mixX1 = mix(baseTexel, rightTexel, smoothPos.x);
    float mixX2 = mix(upTexel, diagonalTexel, smoothPos.x);

    return mix(mixX1, mixX2, smoothPos.y);
}
uniform sampler2D depthtex1;
float cloudCov(in vec3 pos, vec3 samplePos)
{

    const float CloudBaseOffset = 100.0;
    const float CloudTopOffset = 450.0;
    const float CloudBottomOffset = 125.0;
    const float HeightDivisionFactor = 500.0;
    const float BottomHeightDivisionFactor = 100.0;
    const float InverseHeightDivisionFactor = 1.0 / HeightDivisionFactor;
    const float InverseBottomHeightDivisionFactor = 1.0 / BottomHeightDivisionFactor;

    // Height differences simplified with constants
    float heightAboveCloud = max(pos.y - (cloud_height + CloudBaseOffset), 0.0);
    float heightBelowCloudTop = max(-pos.y + (cloud_height + CloudTopOffset), 0.0);
    float heightBelowCloudBottom = min(-pos.y + (cloud_height + CloudBottomOffset), 0.0);

    // Multipliers based on height using multiplication for optimization
    float mult = heightAboveCloud * InverseHeightDivisionFactor;
    float mult2 = heightBelowCloudTop * InverseHeightDivisionFactor;
    float mult3 = heightBelowCloudBottom * InverseBottomHeightDivisionFactor;

    // Cloud coverage calculation with optimizations
    float textureContribution = cloudTextureCubic(depthtex1, samplePos.xz / CloudSize) * 2.0;
    // float textureContribution = texture(depthtex1, samplePos.xz / CloudSize).x * 2.0;

    float coverage = (textureContribution + rainStrength) / (0.8 - rainStrength * 0.8);
    coverage = clamp(coverage, 0.0, 1.0);

    // Final cloud coverage with adjustments for readability and optimization
    float cloud = max(coverage - mult - mult2 + mult3, 0.0);

    return cloud;
}
#if defined RSM
vec3 calculateAlbedo(vec3 ProjShadowPos, vec4 noise2, float distortFac, float ambientA, vec2 lightmap)
{
    vec3 sAlbedo = vec3(0.0);
    float avgDepth2 = 0.0;
    const int iterationsRSM = RSM_SAMPLES;
    int counteRSM = (frameCounter % 40000) * int(iterationsRSM);

    for (int i = 0; i < iterationsRSM; i++)
    {
        int seed = counteRSM + i;
        vec2 ij = fract(R2_samples3(seed) + noise2.rg);
        vec2 offset = tapLocationBlueNoise(i, 16, 80, ij * 512.0);
        vec2 offset2 = tapLocationBlueNoise(i, 16, 80, ij);
        vec2 shadowCoord2 = (ProjShadowPos.xy + offset2);
        float d2 = texture(shadowtex0, (shadowCoord2.xy)).x;
        avgDepth2 += max(ProjShadowPos.z - d2, 0.0);

        vec2 coord = ProjShadowPos.xy + offset * (distortFac / shadowMapResolution);
        float falloff = 1.0 - ((avgDepth2) * 64.0);
        sAlbedo += texelFetch(shadowcolor0, ivec2(coord * shadowMapResolution), 0).rgb * clamp(falloff, 0.0, 1.0);
    }

    sAlbedo /= float(iterationsRSM);
    sAlbedo = ((sAlbedo - luma(sAlbedo)) * 1.0 + sAlbedo);
    sAlbedo *= ambientA * lightmap.y;

    return sAlbedo;
}
#endif
uniform vec3 sunPosWorld;
float cloudShadow(vec3 eyePlayerPos, float noise)
{
    vec3 playerPos = eyePlayerPos + cameraPosition;

    vec3 lowShadowStart = playerPos + sunPosWorld / abs(sunPosWorld.y) * max((1650 - 70) - playerPos.y, 0.0);
    lowShadowStart.x += frameTimeCounter * 35;

    float shadow = cloudCov(lowShadowStart, lowShadowStart * 0.25) * 0.250;
    shadow = clamp(exp(-shadow * 20.0), 0.0, 1.0);

    return shadow;
}

float shadowFunc(float diffuseSun, float sssAmount, vec3 p3, float noise, vec2 lightmap, vec3 fragpos,
                 inout vec3 sAlbedo, vec3 albedo, vec3 normals)
{
    // sssAmount = 2;
    float mix1 = mix(1.0, 100.0, biasblend2);
    mix1 = mix(mix1, 1000, sssAmount);
    float mix2 = -0.0002;

#ifdef DISTANT_HORIZONS
    mix1 = 1000;
    // mix2 = mix(0.01, 0.0001, biasblend);
#endif

    float SSS;

    float shading = 1.0;
    vec3 ProjShadowPos = mat3(shadowModelView) * p3 + shadowModelView[3].xyz;
    ProjShadowPos = diagonal3(shadowProjection) * ProjShadowPos + shadowProjection[3].xyz;

    float distortFac = calcDistort(ProjShadowPos.xy);

    ProjShadowPos.xy *= distortFac;
    vec2 bounds = vec2(1.0, 1.0) - 1.5 / shadowMapResolution;
    vec4 noise2 = blueNoise4(gl_FragCoord.xy);
    float pdepth = 0;
    float avgDepth = 0.3;
    if (all(lessThan(abs(ProjShadowPos.xy), bounds)) && abs(ProjShadowPos.z) < 6)
    {
        shading = 0.0;
        pdepth = 0;
        avgDepth = 0;
        ProjShadowPos = ProjShadowPos * vec3(0.5, 0.5, 0.5 / 6.0) + 0.5;
        float d01 = (float(sssAmount > 0.0) * 0.05 + ((length(fragpos) / far) * 0.15)) + 0.1;

        ProjShadowPos += vec3(shadowProjection[0].x, shadowProjection[1].y, shadowProjection[2].z) *
                         (mat3(shadowModelView) * normals) * distortFac * mix2;

#ifdef SSS_ALT
        diffuseSun = mix(diffuseSun, max(diffuseSun, 0.5), sssAmount > 0.5);

#endif
        float d01k = d01 * 2.2 * distortFac;
        float rdMul0 = d01k / shadowMapResolution;

        const int iterations = SHADOW_SAMPLES;
        // float scaleFac = (mix(distortFac, noise * 10, float(sssAmount > 0.0)));
        float scaleFac = distortFac;

        ///////////////
        vec3 filterDepth = vec3(1, 32, 128.0); // x=min radius, y=max radius, z=max depth

        float RadiusMax = distortFac * (1.0 + filterDepth.y) * 0.1 / shadowMapResolution;

        float blockerCount = 0.0;

        float avgBlockerDepth0 = 0.0;
        float filterDepthRange = filterDepth.y - filterDepth.x;

        //////////////

        const float threshMul = max(2048.0 / shadowMapResolution * shadowDistance / 128.0, 0.95);

        float distortThresh = clamp(sqrt(1.0 - diffuseSun * diffuseSun) / diffuseSun, 0.0, mix1) / scaleFac;
#ifdef DISTANT_HORIZONS
        if (sssAmount == 0)
            distortThresh = (diffuseSun) / scaleFac;
            //  distortThresh = clamp(sqrt(1.0 - diffuseSun * diffuseSun) / diffuseSun, 0.0, mix1) / scaleFac;
#endif
        float diffthresh = (distortThresh) / 6000.0 * threshMul;

        int counter = (frameCounter % 40000) * int(iterations);
        float prtecomp = RadiusMax / (iterations * shadowMapResolution * scaleFac / 2.7);
        for (int i = 0; i < iterations; i++)
        {
            int seed = counter + i;
            vec2 ij = fract(R2_samples3(seed) + noise2.rg);
            vec2 offset = tapLocationBlueNoise(i, iterations, 80, ij);

            vec2 shadowCoord = (ProjShadowPos.xy + offset * RadiusMax);
            float d = texture(shadowtex0, (shadowCoord.xy)).x;

            float weightpu = 3.0 + (i + noise) * prtecomp;
            float b = smoothstep(weightpu * diffthresh / 2.0, weightpu * diffthresh, ProjShadowPos.z - d);

            blockerCount += b;
            avgBlockerDepth0 += d * b;

            float isBlocker = step(0.9, blockerCount);

            float avgBlockerDepth = mix(ProjShadowPos.z, avgBlockerDepth0 / blockerCount, isBlocker);

            float t0 = max((ProjShadowPos.z - avgBlockerDepth) / filterDepth.z, 0.0) * 1500.0;

            pdepth = (t0 * filterDepthRange + filterDepth.x) * isBlocker * rdMul0;

            avgDepth += max(ProjShadowPos.z - d, 0.0);

            float shadowMapFactor = shadowMapResolution * (pdepth / iterations);

            float weight = 1.0 + noise * shadowMapFactor;

            vec3 shadowCoordWeighted = ProjShadowPos + vec3(offset * pdepth, -diffthresh * weight);

            shading += texture(shadowtex1, shadowCoordWeighted);
            avgDepth += max(ProjShadowPos.z - shading, 0.0);
            // shading = mix(1, shading, nightblendShadows);
        }
        avgDepth = sqrt((avgDepth / iterations) * 1000);

        shading /= iterations;
#ifdef CLOUD_SHADOWS
        shading *= cloudShadow(p3, noise);
#endif
        // shading = mix(float(shading > 0.5), shading, biasblend3);

        shading = mix(0.0, shading, shadowblend);

#if defined RSM
        sAlbedo = calculateAlbedo(ProjShadowPos, noise2, distortFac, ambientA, lightmap);
#endif
        SSS = computeSSS(sssAmount, albedo, p3, lightPos, avgDepth, diffuseSun);
    }

    if (diffuseSun * shading > 0.00001)
    {
#ifdef CONTACT_SHADOWS
        if (sssAmount == 0.0)
            shading = min(rayTraceShadow(sunVec, fragpos, noise), shading);
#endif
        shading = mix(0.0, shading, min(eyeBrightnessSmooth.y * recip + lightmap.y, 1.0));
    }

    diffuseSun = max(diffuseSun, SSS);

    diffuseSun = diffuseSun * shading * lightmap.y;

    diffuseSun *= lightmap.y * (1 - rainStrength * 0.9);

    return diffuseSun;
}
