precision highp float;
varying vec2 vUV;

// Samplers
uniform highp sampler2D depthSampler;
uniform sampler2D noiseSampler;
uniform vec2 noiseScale;
uniform vec3 sampleKernel[sampleKernelSize];
uniform mat4 projectionMatrix;

uniform float strength;
uniform float radius;
uniform vec2 nearFar;

float bias = -0.015;

// #include ViewSpacePosition.glsl
// #include NormalsFromDepth.glsl

float ambientOcclusionValue(vec3 viewSpaceFragment, vec3 viewSpaceNormal, vec2 screenUV, vec2 texSizeF) {
    vec3 noise = texture2D(noiseSampler, screenUV * noiseScale).rgb;
    vec3 randomVec = noise * 2.0 - 1.0;
    float occlusion = 0.0;

    vec3 tangent = normalize(randomVec - viewSpaceNormal * dot(randomVec, viewSpaceNormal));
    vec3 biTangent = cross(viewSpaceNormal, tangent);
    mat3 tbn = mat3(tangent, biTangent, viewSpaceNormal);

    for (int i = 0; i < sampleKernelSize; i++) {
        vec3 samplePosition = tbn * sampleKernel[i];
        samplePosition = samplePosition * radius + viewSpaceFragment;

        vec4 offset = vec4(samplePosition, 1.0);
        offset = projectionMatrix * offset;
        offset.xy /= offset.w;
        offset.xy = offset.xy * 0.5 + vec2(0.5);

        // Use texelFetch for precise depth sampling (texSizeF cached outside loop)
        ivec2 samplePixelCoord = ivec2(offset.xy * texSizeF);
        float viewDepth = fetchDepthAt(samplePixelCoord);
        // Use sample's offset UV, not screenUV
        float sampleDepth = viewSpacePositionFromDepth(viewDepth, offset.x, offset.y).z;

        float rangeCheck = smoothstep(0.0, 1.0, radius / abs(viewSpaceFragment.z - sampleDepth));
        occlusion += (sampleDepth <= samplePosition.z + bias ? 1.0 * rangeCheck : 0.0);
    }

    occlusion = min((occlusion / float(sampleKernelSize)) * strength, 1.0);

    return occlusion;
}

void main(void)
{
    vec2 sampleUV = vUV;
    // Use texelFetch for precise depth sampling - bypasses UV precision issues on Linux/Android
    ivec2 texSize = getDepthTextureSize();
    vec2 texSizeF = vec2(texSize);
    ivec2 pixelCoord = ivec2(sampleUV * texSizeF);
    float fragmentDepth = fetchDepthAt(pixelCoord);
    vec3 viewSpacePosition = viewSpacePositionFromDepth(fragmentDepth, sampleUV.x, sampleUV.y);
    #ifdef expensiveNormals
        vec3 viewSpaceNormal = expensiveViewSpaceNormalFromNeighbours(sampleUV, viewSpacePosition, fragmentDepth);
    #else
        vec3 viewSpaceNormal = viewSpaceNormalFromNeighbours(sampleUV, viewSpacePosition);
    #endif

    // Check both upper bound (>= 0.9995 means far plane/no geometry)
    // AND lower bound (<= 0.0001 means cleared to black/no geometry written)
    float fragmentWritten = step(0.0001, fragmentDepth) * (1.0 - step(0.9995, fragmentDepth));
    float ambientOcclusion = 0.0;
    if (fragmentWritten == 1.0) {
        float distanceFade = 1.0 - smoothstep(nearFar.y * 0.1, nearFar.y * 0.9, viewSpacePosition.z);
        float depthDifferenceScaling = 1.0 + pow((1.0 - (viewSpacePosition.z / nearFar.y)), 16.0) * 5.0;
        float aoContribution = distanceFade * depthDifferenceScaling;
        ambientOcclusion = ambientOcclusionValue(viewSpacePosition, viewSpaceNormal, sampleUV, texSizeF) * aoContribution;
    }

    gl_FragColor = vec4(vec3(ambientOcclusion), 1.0);
}