uniform vec2 depthDimensions;

// Fetch and unpack depth value at the given pixel coordinate
float fetchDepthAt(ivec2 pixelCoord) {
    vec4 rgba = texelFetch(depthSampler, pixelCoord, 0);
    return rgba.x;
}

// Get texture size directly from the sampler - avoids precision issues in depthDimensions uniform
ivec2 getDepthTextureSize() {
    return textureSize(depthSampler, 0);
}

#ifdef expensiveNormals

    // https://wickedengine.net/2019/09/22/improved-normal-reconstruction-from-depth/
    // Constructs the normal from a 4 sample neighbor cross
    // Takes the smallest differences in depth in the vertical and horizontal directions from the fragment center and recreates the viewspace positions with the corresponding texel
    // Then these two positions are used to create two vectors from the fragment center, and these two vectors are crossed to get ther surface normal in viewspace
    vec3 expensiveViewSpaceNormalFromNeighbours(vec2 uv, vec3 viewSpaceFragmentCenter, float depthCenter) {
        // Use gl_FragCoord for the center pixel - exact integer position, no UV conversion issues
        // Note: For neighbor calls from compare(), uv is offset but we compute pixel from uv
        ivec2 texSize = getDepthTextureSize();
        vec2 texSizeF = vec2(texSize);
        ivec2 pixelCenter = ivec2(uv * texSizeF);

        // Sample neighbors using integer offsets
        float depthRight = fetchDepthAt(pixelCenter + ivec2(1, 0));
        float depthUp = fetchDepthAt(pixelCenter + ivec2(0, 1));
        float depthDown = fetchDepthAt(pixelCenter + ivec2(0, -1));
        float depthLeft = fetchDepthAt(pixelCenter + ivec2(-1, 0));

        // Calculate neighbor UVs from INTEGER pixel coordinates
        // Use texture size directly to avoid precision issues in depthDimensions uniform
        vec2 right = (vec2(pixelCenter + ivec2(1, 0)) + 0.5) / texSizeF;
        vec2 up = (vec2(pixelCenter + ivec2(0, 1)) + 0.5) / texSizeF;
        vec2 down = (vec2(pixelCenter + ivec2(0, -1)) + 0.5) / texSizeF;
        vec2 left = (vec2(pixelCenter + ivec2(-1, 0)) + 0.5) / texSizeF;

        // Epsilon bias prevents flip-flopping on flat surfaces where depth differences
        // are nearly equal. Without this, floating-point precision differences between
        // GPU drivers (especially Linux/Android vs Windows) cause the comparison to
        // inconsistently flip between adjacent pixels, creating horizontal banding artifacts.
        float epsilon = 0.001;
        bool downIsLess = abs(depthDown - depthCenter) < abs(depthUp - depthCenter) + epsilon;
        bool leftIsLess = abs(depthLeft - depthCenter) < abs(depthRight - depthCenter) + epsilon;

        vec3 point1;
        vec3 point2;

        if (downIsLess && leftIsLess) {
            point1 = viewSpacePositionFromDepth(depthLeft, left.x, left.y);
            point2 = viewSpacePositionFromDepth(depthDown, down.x, down.y);
        } else if (downIsLess) { // right is less
            point1 = viewSpacePositionFromDepth(depthDown, down.x, down.y);
            point2 = viewSpacePositionFromDepth(depthRight, right.x, right.y);
        } else if (!downIsLess && leftIsLess) {
            point1 = viewSpacePositionFromDepth(depthUp, up.x, up.y);
            point2 = viewSpacePositionFromDepth(depthLeft, left.x, left.y);
        } else { // right is less, up is less
            point1 = viewSpacePositionFromDepth(depthRight, right.x, right.y);
            point2 = viewSpacePositionFromDepth(depthUp, up.x, up.y);
        }

        return -normalize(cross(normalize(point1 - viewSpaceFragmentCenter), normalize(point2 - viewSpaceFragmentCenter)));
    }
#else
    // Take the fragment above and to the right of the current fragment and use their depths to reconstruct two positions, subtract these positions with
    // the fragment centers position to get two vectors, take the cross product of these two vectors to get the viewspace surface normal
    vec3 viewSpaceNormalFromNeighbours(vec2 uv, vec3 viewSpaceFragmentCenter) {
        // Convert UV to pixel coordinates for precise sampling
        ivec2 texSize = getDepthTextureSize();
        vec2 texSizeF = vec2(texSize);
        ivec2 pixelCenter = ivec2(uv * texSizeF);

        // Sample neighbors using integer offsets
        float depthRight = fetchDepthAt(pixelCenter + ivec2(1, 0));
        float depthUp = fetchDepthAt(pixelCenter + ivec2(0, 1));

        // Calculate neighbor UVs from INTEGER pixel coordinates to avoid floating-point precision issues
        vec2 right = (vec2(pixelCenter + ivec2(1, 0)) + 0.5) / texSizeF;
        vec2 up = (vec2(pixelCenter + ivec2(0, 1)) + 0.5) / texSizeF;

        vec3 rightViewSpaceFragment = viewSpacePositionFromDepth(depthRight, right.x, right.y);
        vec3 upViewSpaceFragment = viewSpacePositionFromDepth(depthUp, up.x, up.y);

        return -normalize(cross(normalize(rightViewSpaceFragment - viewSpaceFragmentCenter), normalize(upViewSpaceFragment - viewSpaceFragmentCenter)));
    }
#endif