#ifndef INCLUDE_BLOCKLIGHT_POINT_SHADOWS
    #define INCLUDE_BLOCKLIGHT_POINT_SHADOWS

    /*--------------------------------------------------------------------
        Block-Light Point Shadows  (BLOCKLIGHT_POINT_SHADOWS)
        --------------------------------------------------------------
        The regular ACT flood-fill (see shadowcomp.glsl / GetLightVolume in
        lightVoxelization.glsl) treats every torch/lantern/lamp as a source
        that diffuses through open voxels one blur/max step per frame -
        good for a soft ambient glow, but it has no concept of "this light
        came from THAT lamp and was blocked by THAT wall": two lamps in the
        same room just diffuse into and average with each other, and
        nothing in that system casts a hard shadow.

        This samples light-emitting voxels straight out of the SAME grid
        (voxel_sampler, via GetVoxelVolume - the exact texture
        lightVoxelization.glsl/UpdateVoxelMap already writes material IDs
        into) directly around the shading point, and fires one real DDA
        visibility ray per light found, using the identical "solid" rule
        the flood-fill itself uses (voxel == 1u). So instead of one
        blended average, two lamps in a room stay two separate pools of
        light with their own falloff and their own shadow, and a lamp
        behind a wall reads as genuinely occluded rather than bleeding
        through.

        A full brute-force cube scan of the grid per shaded pixel would be
        far too expensive, so the search is stochastic: a handful of
        jittered probe directions per pixel per frame, temporally rotated
        (the same `dither` value already used elsewhere in this file for
        TAA-friendly noise), converging to a stable result over a few
        frames exactly like this pack's other stochastic techniques
        (SSGI, IC probes, GI bounce rays) - KEEP TAA ON for this to read
        cleanly instead of as sparkly noise.
    --------------------------------------------------------------------*/

    // Cheap 1D -> 2D hash. Kept local to this file (rather than reusing
    // GiHash22 from giFloodfill.glsl) since giFloodfill.glsl isn't
    // included in every program that includes this file.
    vec2 BlpsHash21(float seed) {
        vec3 p3 = fract(vec3(seed) * vec3(0.1031, 0.1030, 0.0973));
        p3 += dot(p3, p3.yzx + 33.33);
        return fract((p3.xx + p3.yz) * p3.zy);
    }

    // Is this raw material ID an emissive "light" voxel in the ACT grid?
    // Mirrors the exact classification shadowcomp.glsl already uses:
    // voxel == 1u is the sole solid/opaque voxel, voxel == 0u is air,
    // voxel >= 200u is translucent/tinted glass - everything else (2-199)
    // is a light source (see GetVoxelIDs/GetSpecialBlocklightColor).
    bool ACT_IsLightVoxel(uint v) {
        return v >= 2u && v < 200u;
    }

    // Same occlusion rule the ACT flood-fill itself uses for this grid:
    // the only voxel that blocks light propagation is a plain solid block
    // (ID 1). Keeping this identical to shadowcomp.glsl's own rule is
    // what stops this contribution from reading as a second, disagreeing
    // light model layered on top of the existing one.
    bool ACT_IsOccluder(ivec3 p) {
        return GetVoxelVolume(p) == 1u;
    }

    // 3D DDA (Amanatides-Woo) visibility trace through the ACT voxel grid,
    // same algorithm as GiTraceRay in giFloodfill.glsl adapted to this
    // grid's coordinate space and occupancy rule. Returns true if the
    // straight line from startVoxelPos to endVoxelPos is NOT blocked by a
    // solid voxel before it gets there.
    bool ACT_TraceShadowRay(vec3 startVoxelPos, vec3 endVoxelPos) {
        vec3 toEnd = endVoxelPos - startVoxelPos;
        float dist = length(toEnd);
        if (dist < 0.001) return true;

        vec3 safeDir = toEnd / dist;
        safeDir += vec3(equal(safeDir, vec3(0.0))) * 1e-6;

        ivec3 voxel = ivec3(floor(startVoxelPos));
        vec3 stepDir = sign(safeDir);
        vec3 tDelta = abs(1.0 / safeDir);
        vec3 voxelBound = vec3(voxel) + max(stepDir, vec3(0.0));
        vec3 tMax = (voxelBound - startVoxelPos) / safeDir;
        float t = 0.0;

        int maxSteps = int(min(dist + 2.0, float(BLOCKLIGHT_POINT_SHADOWS_RADIUS) * 1.8 + 4.0));
        for (int i = 0; i < maxSteps; i++) {
            if (t > dist) return true; // Reached the light without crossing an occluder
            if (!CheckInsideVoxelVolume(vec3(voxel))) return true; // Left the grid - not this system's job to shadow that
            if (ACT_IsOccluder(voxel)) return false;

            if (tMax.x < tMax.y) {
                if (tMax.x < tMax.z) { voxel.x += int(stepDir.x); t = tMax.x; tMax.x += tDelta.x; }
                else { voxel.z += int(stepDir.z); t = tMax.z; tMax.z += tDelta.z; }
            } else {
                if (tMax.y < tMax.z) { voxel.y += int(stepDir.y); t = tMax.y; tMax.y += tDelta.y; }
                else { voxel.z += int(stepDir.z); t = tMax.z; tMax.z += tDelta.z; }
            }
        }
        return true; // Ran out of steps before finding either the light or an occluder - err on the side of visible
    }

    // ("ray traced blocklight, jangan flat diamond, harus punya arah" fix,
    // teknik geometry term dipelajari dari VTXS Screen Space Demo's SSLL
    // system - lihat geometryWeight()/ssll_ResolveSample() di
    // src/ray_tracing/ssll.glsl: setiap sample cahaya di sana dibobot
    // dengan dot(lightDir, shadingNormal), bukan cuma 1/(r*r) jarak
    // murni). Sebelumnya GetBlockLightPointShadows sudah mem-visibility-
    // ray-kan tiap lampu secara individual (jadi tiap lampu sudah dapat
    // shadow-nya sendiri), TAPI kontribusinya masih dijumlah dengan bobot
    // yang sama ke segala arah permukaan - dua sisi sebuah blok yang sama
    // jaraknya ke lampu tapi menghadap arah berbeda akan terang sama rata,
    // persis gejala "flat diamond" lightmap vanilla, cuma sekarang sudah
    // ada shadow-nya. Menambahkan cosine term di sini (dot(surfaceNormal,
    // lightDir)) adalah bagian yang benar-benar memberi ARAH: sisi blok
    // yang menghadap ke lampu jadi nyata lebih terang daripada sisi yang
    // membelakangi, seperti cahaya titik sungguhan. Dicampur (bukan
    // dikali penuh) via BLOCKLIGHT_NORMAL_STRENGTH (setting yang sudah
    // ada, dipakai juga oleh BLOCKLIGHT_NORMAL_SHADING di atas) supaya
    // sisi yang membelakangi tidak jatuh ke hitam total (lampu asli tetap
    // memantul sedikit dari sekitar), dan supaya kedua sistem berbagi satu
    // slider yang sama alih-alih menambah setting baru.
    //
    // Stochastically searches for nearby light-emitting voxels around
    // `voxelPos` (same ACT grid-space position already computed for the
    // diffuse GetLightVolume() sample), shadow-rays each one found, and
    // returns their summed, occlusion-tested, direction-aware contribution.
    vec3 GetBlockLightPointShadows(vec3 voxelPos, vec3 surfaceNormal, float dither) {
        vec3 result = vec3(0.0);
        ivec3 originVoxel = ivec3(floor(voxelPos));

        float radius = float(BLOCKLIGHT_POINT_SHADOWS_RADIUS);
        float rotation = fract(dither + goldenRatio * mod(float(frameCounter), 3600.0));

        int found = 0;
        ivec3 foundVoxel[BLOCKLIGHT_POINT_SHADOWS_MAX];

        for (int i = 0; i < BLOCKLIGHT_POINT_SHADOWS_PROBES && found < BLOCKLIGHT_POINT_SHADOWS_MAX; i++) {
            // Jittered, temporally-rotated point on a sphere - a different
            // subset of directions each frame, so TAA converges the full
            // sphere over several frames instead of one static pattern.
            vec2 h = BlpsHash21(float(i) * 12.9898 + rotation * 78.233);
            float u = h.x * 2.0 - 1.0;
            float thetaAng = h.y * 6.28318530718;
            float r = sqrt(max(0.0, 1.0 - u * u));
            vec3 dir = vec3(r * cos(thetaAng), u, r * sin(thetaAng));

            // Walk outward in a few coarse jumps - this loop only needs to
            // FIND a nearby light source, the real visibility test is the
            // DDA trace done afterward.
            for (int step = 1; step <= 4; step++) {
                float dist = radius * (float(step) * 0.25);
                ivec3 p = ivec3(floor(voxelPos + dir * dist));
                if (p == originVoxel) continue;
                if (!CheckInsideVoxelVolume(vec3(p))) break;

                uint v = GetVoxelVolume(p);
                if (ACT_IsLightVoxel(v)) {
                    bool dup = false;
                    for (int f = 0; f < found; f++) {
                        if (foundVoxel[f] == p) { dup = true; break; }
                    }
                    if (!dup) {
                        foundVoxel[found] = p;
                        found++;
                    }
                    break; // Found something along this direction, stop walking it
                } else if (v == 1u) {
                    break; // Wall in the way along this probe direction
                }
            }
        }

        for (int f = 0; f < found; f++) {
            vec3 lightVoxelCenter = vec3(foundVoxel[f]) + 0.5;
            float dist = distance(voxelPos, lightVoxelCenter);
            if (dist > radius) continue;

            if (ACT_TraceShadowRay(voxelPos, lightVoxelCenter)) {
                uint v = GetVoxelVolume(foundVoxel[f]);
                vec4 lightColor = GetSpecialBlocklightColor(int(v));

                // Inverse-square-ish falloff off the real 3D distance to
                // this specific source, so each lamp gets its own falloff
                // shape instead of sharing the flood-fill's diffusion-based
                // one.
                float falloff = 1.0 / (1.0 + dist * dist * 0.35);

                // ("cahaya kepotong kotak / putih-ke-oren tiba-tiba" fix):
                // sebelumnya kontribusi ini dipakai penuh sampai persis
                // `radius`, lalu di baris "if (dist > radius) continue;" di
                // atas langsung dibuang total - jadi warna asli lampu ini
                // (mis. biru-putih Sea Lantern) berhenti SETEBAL SATU FRAME
                // pas melewati radius, dan yang tersisa cuma base blocklight
                // hangat/oren dari floodfill ACT di baliknya. Perbatasan
                // tegas itu yang terlihat sebagai "kotak"/cut-off. Ditambah
                // fade halus mulai dari 70% radius menuju radius penuh,
                // supaya warna lampu ini meluruh smooth ke base light
                // sebelum benar-benar dibuang, bukan lenyap sekaligus.
                float edgeFade = 1.0 - smoothstep(radius * 0.7, radius, dist);
                falloff *= edgeFade;

                // Directional geometry term (VTXS SSLL-style dot(lightDir,
                // normal) weighting - see comment on the function above):
                // the face pointing toward this specific lamp gets more of
                // its light than a face pointing away, instead of every
                // face around the lamp reading the same brightness at the
                // same distance. dist is already known > 0 here (radius
                // check above guarantees it), so this is a safe divide.
                vec3 lightDir = (lightVoxelCenter - voxelPos) / max(dist, 0.001);
                float NdotL = clamp(dot(surfaceNormal, lightDir), -1.0, 1.0);
                float dirWeight = mix(1.0, 0.35 + 0.65 * (NdotL * 0.5 + 0.5), BLOCKLIGHT_NORMAL_STRENGTH);

                result += pow2(lightColor.rgb) * falloff * dirWeight;
            }
        }

        return result * BLOCKLIGHT_POINT_SHADOWS_I;
    }

#endif //INCLUDE_BLOCKLIGHT_POINT_SHADOWS
