// ===========================
// 🔧 Helper Macros
// ===========================

#define diagonal3(m) vec3((m)[0].x, (m)[1].y, m[2].z)
#define projMAD(m, v) (diagonal3(m) * (v) + (m)[3].xyz)
#define viewMAD(m, v) (mat3(m) * (v) + (m)[3].xyz)

#ifdef DISTANT_HORIZONS
    #define diagonal3_dh(m) vec3((m)[0].x, (m)[1].y, m[2].z)
    #define projMAD_dh(m, v) (diagonal3_dh(m) * (v) + (m)[3].xyz)
#endif

// ===========================
// 🌍 World Space Conversions
// ===========================

vec3 toWorldSpace(vec3 p3)
{
    p3 = mat3(gbufferModelViewInverse) * p3 + gbufferModelViewInverse[3].xyz;

#ifdef PIXEL_LOCK
    p3 = floor((p3 + cameraPosition) * 16 + 0.001) / 16 - cameraPosition + 0.5 / 16;
#endif

    return p3;
}

vec3 toWorldSpaceNoLock(vec3 p3)
{
    p3 = mat3(gbufferModelViewInverse) * p3 + gbufferModelViewInverse[3].xyz;
    return p3;
}

vec3 worldToView(vec3 worldPos)
{
    return (gbufferModelView * vec4(worldPos, 0.0)).xyz;
}

vec3 viewToWorld(vec3 viewPos)
{
    return (gbufferModelViewInverse * vec4(viewPos, 0.0)).xyz;
}

vec3 viewToSceneSpace(vec3 viewpos, mat4 mvInv)
{
    return projMAD(mvInv, viewpos);
}

vec3 viewToSceneSpace(vec3 viewpos)
{
    return viewToSceneSpace(viewpos, gbufferModelViewInverse);
}

// ===========================
// 🌑 Shadow Space Conversions
// ===========================

vec3 shadowSpace0()
{
    vec3 p3 = gbufferModelViewInverse[3].xyz;
    p3 = mat3(shadowModelView) * p3 + shadowModelView[3].xyz;
    p3 = diagonal3(shadowProjection) * p3 + shadowProjection[3].xyz;
    return p3;
}

vec3 toShadowSpaceProjected(vec3 p3)
{
    p3 = toWorldSpace(p3);
    p3 = mat3(shadowModelView) * p3 + shadowModelView[3].xyz;
    p3 = diagonal3(shadowProjection) * p3 + shadowProjection[3].xyz;
    return p3;
}

// ===========================
// 🎯 Clip Space Conversions
// ===========================
#ifdef VOXY

vec3 toClipSpace3_voxy(vec3 viewSpacePosition)
{
    return projMAD(vxProj, viewSpacePosition) / -viewSpacePosition.z * 0.5 + 0.5;
}
#endif
vec3 toClipSpace3(vec3 viewSpacePosition)
{
    return projMAD(gbProjection_proxy, viewSpacePosition) / -viewSpacePosition.z * 0.5 + 0.5;
}

vec4 toClipSpace3_shadow(vec3 viewSpacePosition)
{
    return vec4(projMAD(gl_ProjectionMatrix, viewSpacePosition), 1.0);
}

vec4 toClipSpace4(vec3 viewSpacePosition)
{
    return vec4(projMAD(gl_ProjectionMatrix, viewSpacePosition), -viewSpacePosition.z);
}

vec3 toClipSpace3Prev(vec3 viewSpacePosition)
{
    return projMAD(gbufferPreviousProjection, viewSpacePosition) / -viewSpacePosition.z * 0.5 + 0.5;
}

#ifdef DISTANT_HORIZONS
    #define diagonal3_dh(m) vec3((m)[0].x, (m)[1].y, m[2].z)
    #define projMAD_dh(m, v) (diagonal3_dh(m) * (v) + (m)[3].xyz)

vec3 toClipSpace3DH(vec3 viewSpacePosition)
{
    return projMAD_dh(dhProjection, viewSpacePosition) / -viewSpacePosition.z * 0.5 + 0.5;
}

vec3 toClipSpace3_org(vec3 viewSpacePosition)
{
    return projMAD_dh(gbProjection_proxy, viewSpacePosition) / -viewSpacePosition.z * 0.5 + 0.5;
}

vec3 toClipSpace3_alt(vec3 viewSpacePosition)
{
    return projMAD_dh(gbProjection_proxy, viewSpacePosition) / -viewSpacePosition.z * 0.5 + 0.5;
}
#endif

// ===========================
// 🖥️ Screen Space Conversions
// ===========================

vec3 toScreenSpace(vec3 p)
{
    vec4 iProjDiag = vec4(gbProjectionInverse_proxy[0].x, gbProjectionInverse_proxy[1].y, gbProjectionInverse_proxy[2].zw);
    vec3 p3 = p * 2. - 1.;
    vec4 fragposition = iProjDiag * p3.xyzz + gbProjectionInverse_proxy[3];
    return fragposition.xyz / fragposition.w;
}
vec3 toScreenSpace_og(vec3 p)
{
    vec4 iProjDiag = vec4(gbufferProjectionInverse[0].x, gbufferProjectionInverse[1].y, gbufferProjectionInverse[2].zw);
    vec3 p3 = p * 2. - 1.;
    vec4 fragposition = iProjDiag * p3.xyzz + gbufferProjectionInverse[3];
    return fragposition.xyz / fragposition.w;
}

#ifdef DISTANT_HORIZONS
vec3 toScreenSpaceDH(vec3 position)
{
    position = position * 2.0 - 1.0;
    position.xy = vec2(dhProjectionInverse[0].x, dhProjectionInverse[1].y) * position.xy + dhProjectionInverse[3].xy;
    return vec3(position.xy, dhProjectionInverse[3].z) /
           (dhProjectionInverse[2].w * position.z + dhProjectionInverse[3].w);
}

vec3 toScreenSpace_org(vec3 p)
{
    vec4 iProjDiag = vec4(gbProjectionInverse_proxy[0].x, gbProjectionInverse_proxy[1].y, gbProjectionInverse_proxy[2].zw);
    vec3 p3 = p * 2. - 1.;
    vec4 fragposition = iProjDiag * p3.xyzz + gbProjectionInverse_proxy[3];
    return fragposition.xyz / fragposition.w;
}

vec3 toScreenSpace_alt(vec2 texcoord, float depthA, float depthB, bool hand)
{
    #ifdef DISTANT_HORIZONS
    if (depthA >= 1.0)
    {
        return toScreenSpaceDH(vec3(texcoord, depthB));
    }
    else
    {
        return toScreenSpace_org(vec3(texcoord, depthA));
    }
    #else
    return toScreenSpace_org(vec3(texcoord, depthA));
    #endif
}
#endif
#ifdef VOXY
vec3 toScreenSpaceDH(vec3 position)
{
    position = position * 2.0 - 1.0;
    position.xy = vec2(vxProjInv[0].x, vxProjInv[1].y) * position.xy + vxProjInv[3].xy;
    return vec3(position.xy, vxProjInv[3].z) /
           (vxProjInv[2].w * position.z + vxProjInv[3].w);
}

vec3 toScreenSpace2(vec3 p)
{
    vec4 iProjDiag = vec4(gbProjectionInverse_proxy[0].x, gbProjectionInverse_proxy[1].y, gbProjectionInverse_proxy[2].zw);
    vec3 p3 = p * 2. - 1.;
    vec4 fragposition = iProjDiag * p3.xyzz + gbProjectionInverse_proxy[3];
    return fragposition.xyz / fragposition.w;
}
float ld4(const float dist)
{
    return (2.0 * nearorg) / (farorg + nearorg - dist * (far - nearorg));
}
vec3 toScreenSpace_og3(vec3 p)
{
    vec4 iProjDiag = vec4(gbufferProjectionInverse[0].x, gbufferProjectionInverse[1].y, gbufferProjectionInverse[2].zw);
    vec3 p3 = p * 2. - 1.;
    vec4 fragposition = iProjDiag * p3.xyzz + gbufferProjectionInverse[3];
    return fragposition.xyz / fragposition.w;
}

vec3 toScreenSpace_alt(vec2 texcoord, float depthA, float depthB, bool hand) {
    // 1. Calculate NDC (Normalized Device Coordinates)
    // We compute this once as the XY components are shared.
    vec3 ndc = vec3(texcoord, depthA) * 2.0 - 1.0;

    // 2. Reconstruct View Space Position (A)
    // Optimized inverse projection (Standard unproject)
    vec4 iProjDiag = vec4(gbufferProjectionInverse[0].x, gbufferProjectionInverse[1].y, gbufferProjectionInverse[2].zw);
    vec4 fragPosRaw = iProjDiag * ndc.xyzz + gbufferProjectionInverse[3];
    vec3 A = fragPosRaw.xyz / fragPosRaw.w;

    // 3. Fast Early Returns
    // If we hit the background or this is the hand, we always use A.
    // No need to calculate p3, check2, or B.
    if (depthB >= 1.0 || hand) {
        return A;
    }

    // 4. Calculate World Space Position (p3)
    // Required only for the distance check.
    vec3 p3 = mat3(gbufferModelViewInverse) * A + gbufferModelViewInverse[3].xyz;

    // 5. Optimized Distance Check (Square Root & Division Eliminated)
    // Original: (max(len, abs) / far) < (1/far * (farorg - 16))
    // Optimized: max(len, abs) < (farorg - 16)
    float distLimit = farorg - 16.0;

    // Check Y (vertical) first as it is cheapest (scalar absolute)
    if (abs(p3.y) < distLimit) {
        // Check XZ (horizontal) using dot product to avoid expensive sqrt()
        if (dot(p3.xz, p3.xz) < distLimit * distLimit) {
            return A; // "check2" passed
        }
    }

    // 6. Calculate B (Voxel/Shadow Space)
    // We only reach here if all previous checks failed.
    // We reuse ndc.xy, but we need depthB for Z.
    vec3 ndcB = vec3(ndc.xy, depthB * 2.0 - 1.0); 

    // Optimized 'vx' projection logic
    vec2 posXY = vec2(vxProjInv[0].x, vxProjInv[1].y) * ndcB.xy + vxProjInv[3].xy;
    float wB   = vxProjInv[2].w * ndcB.z + vxProjInv[3].w;

    return vec3(posXY, vxProjInv[3].z) / wB;
}
#endif

vec2 WorldToScreenPos(vec3 worldPos)
{
    vec4 clipSpacePos = gbProjection_proxy * vec4(worldPos, 1.0);
    vec3 ndc = clipSpacePos.xyz / clipSpacePos.w; // Perspective divide
    return ndc.xy * 0.5 + 0.5;                    // NDC [-1,1] -> UV [0,1]
}

// ===========================
// 🔁 Screen <-> View Space
// ===========================

vec3 ViewSpaceFromScreenSpace(vec3 pos, mat4 inverseProjection)
{
    pos = pos * 2.0 - 1.0;
    vec3 viewPosition = vec3(vec2(inverseProjection[0].x, inverseProjection[1].y) * pos.xy + inverseProjection[3].xy, inverseProjection[3].z);
    viewPosition /= inverseProjection[2].w * pos.z + inverseProjection[3].w;
    return viewPosition;
}

vec3 ScreenSpaceFromViewSpace(vec3 viewPosition, mat4 projection)
{
    vec3 screenPosition = vec3(projection[0].x, projection[1].y, projection[2].z) * viewPosition + projection[3].xyz;
    return screenPosition * (0.5 / -viewPosition.z) + 0.5;
}

float ViewSpaceFromScreenSpace(float positionScreen, mat4 projectionInverse)
{
    return projectionInverse[3].z / (projectionInverse[2].w * (positionScreen * 2.0 - 1.0) + projectionInverse[3].w);
}

// ===========================
// ⏮️ Reprojection Functions
// ===========================

vec3 Reproject(vec3 position)
{
    bool isHand = position.z < 0.6;
    position = ViewSpaceFromScreenSpace(position, gbProjectionInverse_proxy);

    vec3 worldPosition = toWorldSpaceNoLock(position);
    worldPosition += (!isHand && position.z < 1.0) ? (cameraPosition - previousCameraPosition) : vec3(0.0);

    vec3 previousViewPos = mat3(gbufferPreviousModelView) * worldPosition + gbufferPreviousModelView[3].xyz;
    return ScreenSpaceFromViewSpace(previousViewPos, gbufferPreviousProjection);
}
#ifdef VOXY

vec3 Reprojectv(vec3 position)
{
    bool isHand = position.z < 0.6;
    position = ViewSpaceFromScreenSpace(position, vxProjInv);

    vec3 worldPosition = toWorldSpaceNoLock(position);
    worldPosition += (!isHand && position.z < 1.0) ? (cameraPosition - previousCameraPosition) : vec3(0.0);

    vec3 previousViewPos = mat3(gbufferPreviousModelView) * worldPosition + gbufferPreviousModelView[3].xyz;
    return ScreenSpaceFromViewSpace(previousViewPos, vxProjPrev);
}

#endif
vec3 ReprojectSimple(vec3 position)
{
    position = ViewSpaceFromScreenSpace(position, gbProjectionInverse_proxy);

    vec3 worldPosition = toWorldSpaceNoLock(position);
    worldPosition += (cameraPosition - previousCameraPosition);

    vec3 previousViewPos = mat3(gbufferPreviousModelView) * worldPosition + gbufferPreviousModelView[3].xyz;
    return ScreenSpaceFromViewSpace(previousViewPos, gbufferPreviousProjection);
}
vec3 ReprojectWorld(vec3 worldPosition)
{
    vec3 previousViewPos = mat3(gbufferPreviousModelView) * worldPosition + gbufferPreviousModelView[3].xyz;
    return ScreenSpaceFromViewSpace(previousViewPos, gbufferPreviousProjection);
}

#ifdef DISTANT_HORIZONS
vec3 ReprojectDH(vec3 position)
{
    bool isHand = position.z < 0.6;
    position = ViewSpaceFromScreenSpace(position, dhProjectionInverse);

    vec3 worldPosition = toWorldSpaceNoLock(position);
    //  worldPosition += (!isHand && position.z < 1.0) ? (cameraPosition - previousCameraPosition) : vec3(0.0);

    vec3 previousViewPos = mat3(gbufferPreviousModelView) * worldPosition + gbufferPreviousModelView[3].xyz;
    return ScreenSpaceFromViewSpace(previousViewPos, dhPreviousProjection);
}

vec3 Reproject2DH(vec3 position)
{
    position = ViewSpaceFromScreenSpace(position, dhProjectionInverse);

    vec3 worldPosition = toWorldSpaceNoLock(position);
    worldPosition += (cameraPosition - previousCameraPosition);

    vec3 previousViewPos = mat3(gbufferPreviousModelView) * worldPosition + gbufferPreviousModelView[3].xyz;
    return ScreenSpaceFromViewSpace(previousViewPos, dhPreviousProjection);
}
vec3 ReprojectWorldDH(vec3 worldPosition)
{
    vec3 previousViewPos = mat3(gbufferPreviousModelView) * worldPosition + gbufferPreviousModelView[3].xyz;
    return ScreenSpaceFromViewSpace(previousViewPos, dhPreviousProjection);
}
vec3 reprojectDH(vec3 sceneSpace, bool hand)
{
    vec3 prevScreenPos = hand ? vec3(0.0) : cameraPosition - previousCameraPosition;
    prevScreenPos = sceneSpace + prevScreenPos;
    prevScreenPos = viewMAD(gbufferPreviousModelView, prevScreenPos);
    prevScreenPos = viewMAD(dhPreviousProjection, prevScreenPos) * (0.5 / -prevScreenPos.z) + 0.5;
    return prevScreenPos;
}

#endif
