/*
    VertaLux Classic - VOXEL ray march for off-screen PT reflections.

    TWO-LEVEL ARCHITECTURE:
    - BLOCK GRID (voxelImg5, FIXED scale 1.0, world-aligned): OCCUPANCY.
      A single shadow-pass fragment marks the WHOLE block (position snapped to
      block), so coverage is hole-free and stable regardless of shadow map
      density. Traversed with exact DDA (Amanatides-Woo): every block along the
      ray is visited and the FACE normal is known at the hit.
    - FINE CASCADES (voxelImg..voxelImg4, VOXEL_SCALE x 2^i): COLOR.
      On a block hit, texture detail is read from them (0.1875 = 3 texels);
      an empty fine cell falls back to the block color, never to a hole.
    Past the DDA range, the multi-scale march takes over on the coarse cascades
    for distant content.

    Included by composite1.fsh (#version 430 compatibility) when
    RT_REFLECT_MODE >= 2. Requires settings.glsl + uniform cameraPosition.
    Writer side is shadow.fsh - the addressing formulas must stay IDENTICAL
    between write and read.
*/
#ifndef VLX_VOXELRAY
#define VLX_VOXELRAY

layout (rgba16f) uniform image3D voxelImg;
#if VOXEL_CASCADES > 1
layout (rgba16f) uniform image3D voxelImg1;
#endif
#if VOXEL_CASCADES > 2
layout (rgba16f) uniform image3D voxelImg2;
#endif
#if VOXEL_CASCADES > 3
layout (rgba16f) uniform image3D voxelImg3;
#endif
#if VOXEL_CASCADES > 4
layout (rgba16f) uniform image3D voxelImg4;
#endif
// voxelImg5 = dedicated BLOCK GRID (VOXEL_CASCADES is capped at 5)
layout (rgba16f) uniform image3D voxelImg5;

// Samples p (camera-RELATIVE) in the finest cascade that contains it
// (color/detail). An empty cell is an authoritative miss. outScale < 0 means
// p is outside every cascade.
vec4 sampleClipR(vec3 p, out float outScale) {
    {
        float s = VOXEL_SCALE;
        ivec3 camBlock = ivec3(floor(cameraPosition / s));
        ivec3 cell = ivec3(floor((p + (cameraPosition - vec3(camBlock) * s)) / s)) + 128;
        if (all(greaterThanEqual(cell, ivec3(0))) && all(lessThan(cell, ivec3(256)))) {
            outScale = s; return imageLoad(voxelImg, cell);
        }
    }
#if VOXEL_CASCADES > 1
    {
        float s = VOXEL_SCALE * 2.0;
        ivec3 camBlock = ivec3(floor(cameraPosition / s));
        ivec3 cell = ivec3(floor((p + (cameraPosition - vec3(camBlock) * s)) / s)) + 128;
        if (all(greaterThanEqual(cell, ivec3(0))) && all(lessThan(cell, ivec3(256)))) {
            outScale = s; return imageLoad(voxelImg1, cell);
        }
    }
#endif
#if VOXEL_CASCADES > 2
    {
        float s = VOXEL_SCALE * 4.0;
        ivec3 camBlock = ivec3(floor(cameraPosition / s));
        ivec3 cell = ivec3(floor((p + (cameraPosition - vec3(camBlock) * s)) / s)) + 128;
        if (all(greaterThanEqual(cell, ivec3(0))) && all(lessThan(cell, ivec3(256)))) {
            outScale = s; return imageLoad(voxelImg2, cell);
        }
    }
#endif
#if VOXEL_CASCADES > 3
    {
        float s = VOXEL_SCALE * 8.0;
        ivec3 camBlock = ivec3(floor(cameraPosition / s));
        ivec3 cell = ivec3(floor((p + (cameraPosition - vec3(camBlock) * s)) / s)) + 128;
        if (all(greaterThanEqual(cell, ivec3(0))) && all(lessThan(cell, ivec3(256)))) {
            outScale = s; return imageLoad(voxelImg3, cell);
        }
    }
#endif
#if VOXEL_CASCADES > 4
    {
        float s = VOXEL_SCALE * 16.0;
        ivec3 camBlock = ivec3(floor(cameraPosition / s));
        ivec3 cell = ivec3(floor((p + (cameraPosition - vec3(camBlock) * s)) / s)) + 128;
        if (all(greaterThanEqual(cell, ivec3(0))) && all(lessThan(cell, ivec3(256)))) {
            outScale = s; return imageLoad(voxelImg4, cell);
        }
    }
#endif
    outScale = -1.0;
    return vec4(0.0);
}

// FAR multi-scale march over the cascades, used only BEYOND the DDA range.
// No face normal is available here (callers use -dir).
vec4 marchVoxelFar(vec3 p, vec3 dir, int steps, float maxDist, out vec3 hitPos) {
    const float coarsest = VOXEL_SCALE * float(1 << (VOXEL_CASCADES - 1));
    float travelled = 0.0;
    for (int i = 0; i < steps; i++) {
        float s;
        vec4 vox = sampleClipR(p, s);
        if (s < 0.0) { hitPos = p; return vec4(0.0); }
        // No i==0 skip: the origin is the DDA handoff point, far from the
        // reflector, so a self-hit guard here would punch a one-step gap.
        if (vox.a > 0.5) { hitPos = p; return vox; }
        float distStep = VOXEL_SCALE * exp2(clamp(floor(log2(max(travelled, 1.0))),
                                                  0.0, float(VOXEL_CASCADES - 1)));
        float stepLen = clamp(max(s, distStep), VOXEL_SCALE, coarsest);
        p += dir * stepLen;
        travelled += stepLen;
        if (travelled > maxDist) { hitPos = p; return vec4(0.0); }
    }
    hitPos = p; return vec4(0.0);
}

// ======================= MAIN MARCH =======================
// Exact DDA over the BLOCK GRID: every block crossed by the ray is visited (a
// wall can never be stepped over), hits are stable frame to frame, and the
// exact FACE normal is known at the impact point. Color comes from the fine
// cascades at the entry point, falling back to the block color. p and hitPos
// are camera-RELATIVE. Hits below the self-hit epsilon are ignored.
vec4 marchVoxelPT(vec3 p, vec3 dir, int steps, float maxDist, out vec3 hitPos, out vec3 faceN) {
    faceN = -dir;
    // 128 = full block-grid extent (+/-128). A lower cap would make the range
    // slider inert and hand off to the far march too early.
    float ddaMax = min(maxDist, 128.0);

    // Continuous coordinate in block units, origin = camera block (exact mirror
    // of the shadow.fsh write formula at s = 1.0).
    ivec3 camBlock = ivec3(floor(cameraPosition));
    vec3 g0 = p + (cameraPosition - vec3(camBlock));
    ivec3 cell = ivec3(floor(g0));

    ivec3 istep = ivec3(sign(dir));
    vec3 aDir = max(abs(dir), vec3(1e-5));
    vec3 tDelta = 1.0 / aDir;
    vec3 fr = g0 - vec3(cell);
    vec3 tMax;
    tMax.x = ((dir.x > 0.0) ? (1.0 - fr.x) : fr.x) * tDelta.x;
    tMax.y = ((dir.y > 0.0) ? (1.0 - fr.y) : fr.y) * tDelta.y;
    tMax.z = ((dir.z > 0.0) ? (1.0 - fr.z) : fr.z) * tDelta.z;

    float t = 0.0;
    for (int i = 0; i < 200; i++) {
        // Advance to the next cell boundary (axis with the smallest tMax)
        if (tMax.x < tMax.y && tMax.x < tMax.z) {
            t = tMax.x; tMax.x += tDelta.x; cell.x += istep.x;
            faceN = vec3(-float(istep.x), 0.0, 0.0);
        } else if (tMax.y < tMax.z) {
            t = tMax.y; tMax.y += tDelta.y; cell.y += istep.y;
            faceN = vec3(0.0, -float(istep.y), 0.0);
        } else {
            t = tMax.z; tMax.z += tDelta.z; cell.z += istep.z;
            faceN = vec3(0.0, 0.0, -float(istep.z));
        }
        if (t > ddaMax) break;

        ivec3 ci = cell + 128;
        if (any(lessThan(ci, ivec3(0))) || any(greaterThanEqual(ci, ivec3(256)))) break;

        vec4 vox = imageLoad(voxelImg5, ci);
        // Self-hit epsilon 0.1: call sites already offset the ray origin, so a
        // larger threshold would add ~1 block of dead zone at contact.
        if (vox.a > 0.5 && t > 0.1) {
            hitPos = p + dir * (t + 1e-3);
            // COLOR: fine cascade detail. Fine shells only exist on RASTERIZED
            // faces (sun side), so probe through the block along -faceN until a
            // shell is found; this projects the lit face's texture pattern onto
            // our face instead of leaving a flat "big voxel" look.
            // All probes empty -> average block color (never a hole).
            float sF;
            vec4 fine = sampleClipR(hitPos + dir * 0.06, sF);
            if (fine.a < 0.5) fine = sampleClipR(hitPos - faceN * 0.30, sF);
            if (fine.a < 0.5) fine = sampleClipR(hitPos - faceN * 0.62, sF);
            if (fine.a < 0.5) fine = sampleClipR(hitPos - faceN * 0.94, sF);
            if (fine.a > 0.5) return vec4(fine.rgb, max(vox.a, fine.a));
            return vox;
        }
    }

    // Beyond the DDA range: distant content via the multi-scale march
    if (maxDist > ddaMax + 1.0) {
        vec4 farHit = marchVoxelFar(p + dir * ddaMax, dir, steps, maxDist - ddaMax, hitPos); // local name must not shadow the 'far' uniform
        faceN = -dir;
        return farHit;
    }
    hitPos = p + dir * ddaMax;
    return vec4(0.0);
}

#endif // VLX_VOXELRAY
