/* VertaLux - LPV STAGE 2: light-field PROPAGATION (flood) kernel.
   Kept SEPARATE from lib/lpv.glsl because it touches lightImg AND lightImgB and is included ONLY
   by the flood compute passes (composite_a..composite_d). composite5.fsh / lib/lpv.glsl never see
   it, so the consumer side never hits 'undeclared identifier lightImgB'.

   PREREQUISITES (declared by the INCLUDER - the .csh - BEFORE the #include):
     - settings.glsl (LPV_ATTEN, EMISSIVE_LPV, LPV_PASSES) and common.glsl (luma()) included.
     - layout(rgba16f) uniform image3D lightImg ;     // A
     - layout(rgba16f) uniform image3D lightImgB ;    // B
     - #define LPVF_SRC  <lightImg|lightImgB>          // image READ  this pass
     - #define LPVF_DST  <lightImgB|lightImg>          // image WRITTEN this pass
     - #define LPVF_STEP <8|4|2|1>                      // neighbour distance in cells

   WHY SRC/DST MACROS INSTEAD OF AN image3D PARAMETER?
     Passing an image3D as a function parameter without a format qualifier is spec-legal in GLSL
     4.30 but untested on this Iris/driver combination. The macros reference the uniforms directly,
     so there is one shared body and no image-as-parameter. The .csh picks the ping-pong direction
     by (re)defining LPVF_SRC/LPVF_DST.

   ADDRESSING: direct cell indices (no world space). Ping-pong is EXPLICIT and driven by the .csh
   (Iris custom image3D are persistent, never auto-flipped). The FINAL result must land in
   lightImg (A) because sampleLPV in composite5 reads lightImg -> use an EVEN pass count (4).

   ENCODING (same as the composite.csh injection): .rgb = radiance, .a = flag
   (0 EMPTY, 1 SOLID, 2 SOURCE).

   ONE step (= LPVF_STEP) for a cell c:
     - SOLID (a in [0.5,1.5]) -> writes vec4(0,0,0,1): stays solid, blocks, zero radiance.
     - otherwise: best = self.rgb; for each of the 6 axis neighbours at distance LPVF_STEP:
         if IN BOUNDS, neighbour NOT solid, and NO cell on the SEGMENT c..neighbour is solid,
         candidate = neighbour.rgb * pow(LPV_ATTEN, step); keep it when luma(candidate) >
         luma(best). MAX-by-LUMA preserves hue and never over-counts energy.
       Writes vec4(best, self.a): the flag is PRESERVED (SOURCE=2 never dies because best starts
       from self.rgb; EMPTY=0 keeps a=0 while its .rgb may rise - sampleLPV only skips
       a in [0.5,1.5], so a lit EMPTY cell is read normally).
     EVERY cell is written every pass (no stale data between A and B). Both images are
     clear=false PERSISTENT and fully rewritten each pass; the forward gbuffers bridge reads the
     field from frame N-1, so do NOT switch them to clear=true - that would kill forward colored
     lights.

   ANTI-LEAK = FULL WALK, not a single midpoint tap: cells t=1..step-1 along the axis are tested
   and the neighbour is REJECTED if any of them is solid. A single-block wall anywhere between c
   and the neighbour blocks propagation even at step=8. */
#ifndef LPV_FLOOD_GLSL
#define LPV_FLOOD_GLSL

const int LPVF_RES = 128;   // MUST equal LPV_RES (lib/lpv.glsl); local copy since lpv.glsl is not included here.

// True when encoded cell L is SOLID (occludes the flood). Same test as sampleLPV (a in [0.5,1.5]).
bool lpvSolid(vec4 L) { return L.a > 0.5 && L.a < 1.5; }

// Full walk along unit axis 'dir' from cell 'c' up to (not including) the neighbour at distance
// 'step': true if any intermediate cell (t = 1 .. step-1) is solid -> wall on the path -> block.
// At step==1 the loop does not run (nothing lies between c and an adjacent neighbour, which is
// tested separately), so step 1 never leaks.
bool lpvSegBlocked(ivec3 c, ivec3 dir, int step) {
    for (int t = 1; t < step; t++) {
        if (lpvSolid(imageLoad(LPVF_SRC, c + dir * t))) return true;
    }
    return false;
}

// Tests ONE axis neighbour (unit direction 'dir') at distance LPVF_STEP. Updates best/bestL by
// MAX-luma when the neighbour is in bounds, non-solid, and the segment is not blocked by a solid.
void lpvFloodDir(ivec3 c, ivec3 dir, float att, inout vec3 best, inout float bestL) {
    ivec3 n = c + dir * LPVF_STEP;
    if (any(lessThan(n, ivec3(0))) || any(greaterThanEqual(n, ivec3(LPVF_RES)))) return; // outside grid
    vec4 L = imageLoad(LPVF_SRC, n);
    if (lpvSolid(L)) return;                          // solid neighbour: no radiance to propagate
    if (lpvSegBlocked(c, dir, LPVF_STEP)) return;     // wall on the segment: blocked (no wall jumping)
    vec3  cand = L.rgb * att;
    float cl   = luma(cand);
    if (cl > bestL) { best = cand; bestL = cl; }
}

// ONE flood pass for cell 'c'. Reads LPVF_SRC, writes LPVF_DST, stride = LPVF_STEP.
void lpvFloodCell(ivec3 c) {
    vec4 self = imageLoad(LPVF_SRC, c);

    // SOLID: stays solid with radiance forced to 0 (re-blocks propagation on every pass).
    if (lpvSolid(self)) {
        imageStore(LPVF_DST, c, vec4(0.0, 0.0, 0.0, 1.0));
        return;
    }

    vec3  best  = self.rgb;                 // seeded from self -> a SOURCE never loses its radiance
    float bestL = luma(best);
    float att   = pow(LPV_ATTEN, float(LPVF_STEP));  // attenuation over the step distance

    lpvFloodDir(c, ivec3( 1, 0, 0), att, best, bestL);
    lpvFloodDir(c, ivec3(-1, 0, 0), att, best, bestL);
    lpvFloodDir(c, ivec3( 0, 1, 0), att, best, bestL);
    lpvFloodDir(c, ivec3( 0,-1, 0), att, best, bestL);
    lpvFloodDir(c, ivec3( 0, 0, 1), att, best, bestL);
    lpvFloodDir(c, ivec3( 0, 0,-1), att, best, bestL);

    // PRESERVES the .a flag (SOURCE=2 / EMPTY=0). best >= self.rgb in luma, so the field never
    // decays over time on a static scene (frame-to-frame deterministic = no flicker).
    imageStore(LPVF_DST, c, vec4(best, self.a));
}

// Copies LPVF_SRC->LPVF_DST (pass disabled by LPV_PASSES, or EMISSIVE_LPV=0 fallback): keeps the
// ping-pong PARITY without propagating. Cost = 1 load + 1 store per cell.
void lpvCopyCell(ivec3 c) {
    imageStore(LPVF_DST, c, imageLoad(LPVF_SRC, c));
}

#endif // LPV_FLOOD_GLSL
