/*---------------------------------------------------------------------
    Irradiance Cache System (Lumen-Style)
    --------------------------------------
    Replaces stochastic path tracing with a probe-based irradiance cache
    for noise-free, faster global illumination.

    Architecture:
      - WSRC (World Space Radiance Cache): coarse 3D grid stored in the
        existing voxel radiance buffers (gi_radiance_a/b). Each voxel
        stores incoming radiance from neighbors via flood-fill propagate.
      - SSRC (Screen Space Radiance Cache): screen-space probes at 1/16
        resolution (IC_SCREEN_SCALE=4), built by tracing rays into the
        WSRC. Provides fine near-camera detail that the coarse WSRC misses.
      - Each full-res pixel interpolates from both caches (distance-weighted),
        with temporal blending across frames for convergence.

    Pipeline:
      1. begin.csh          - clears voxel grid + IC history
      2. shadow.fsh         - voxelizes albedo+opacity
      3. prepare1.csh       - direct light injection -> gi_radiance_a
      4. prepare2..3.csh    - WSRC propagate (1-2 bounce passes)
      5. deferred1.fsh      - WSRC+SSRC interpolation + temporal +
                              contact AO + composite into scene color
---------------------------------------------------------------------*/
#ifndef INCLUDE_IRRADIANCE_CACHE
    #define INCLUDE_IRRADIANCE_CACHE

    // =====================================================================
    //  WSRC Sampling (World Space Radiance Cache)
    // =====================================================================

    // CORNER LEAK FIX ("kotak putih di tepi block" / rectangular light-bounce
    // border at wall-ceiling-floor corners): the tangent-plane blur below
    // offsets each of its 8 taps sideways along the SURFACE PLANE to soften
    // per-voxel blockiness. That's correct in the middle of a flat wall, but
    // right at a room corner/edge, offsetting "sideways" by `r` voxels can
    // walk the sample position straight across the corner into the
    // NEIGHBOURING wall/ceiling's own solid voxel column - which holds a
    // completely different (often brighter, e.g. sunlit-outside) radiance
    // value. Blending that in reads as a bright rectangular outline tracing
    // every block edge near a corner, which is exactly the "light bounce
    // leaking" look. Fix: before trusting a tap, check whether it actually
    // landed inside solid geometry (GiLoadAlbedoOpacity(...).a - voxelized
    // albedo+opacity grid, already available via GI_NEED_READ_SAMPLERS).
    // If so, that tap didn't stay on this surface's own plane at all - drop
    // it and give its weight back to the trustworthy center sample instead
    // of letting it leak the neighbouring surface's light through the
    // corner.
    float IC_TapSolidity(vec3 voxelCoord) {
        ivec3 p = ivec3(floor(voxelCoord));
        if (any(lessThan(p, ivec3(0))) || any(greaterThanEqual(p, giVolumeSize))) return 0.0;
        return GiLoadAlbedoOpacity(p).a;
    }

    // Trilinear sample of the WSRC radiance volume.
    // Returns vec3(0.0) outside the volume bounds.
    vec3 IC_SampleWorldCache(sampler3D wsrcSampler, vec3 voxelCoord) {
        if (any(lessThan(voxelCoord, vec3(0.0))) ||
            any(greaterThanEqual(voxelCoord, vec3(giVolumeSize))))
            return vec3(0.0);

        // Soft edge fade (same as GiSampleRadianceTrilinear): ramps to zero
        // near the volume boundary so the grid's outer faces never show up
        // as a dark border shadow that follows the player.
        // Uses smoothstep (not linear) to avoid Mach band visible border.
        vec3 edgeDist = min(voxelCoord, vec3(giVolumeSize) - voxelCoord);
        float edgeT = clamp01(min(edgeDist.x, min(edgeDist.y, edgeDist.z)) / float(GI_EDGE_FADE));
        float edgeFade = edgeT * edgeT * (3.0 - 2.0 * edgeT);

        vec3 texPos = voxelCoord - 0.5;
        ivec3 base = ivec3(floor(texPos));
        vec3 f = clamp01(texPos - vec3(base));
        vec3 result = vec3(0.0);

        for (int z = 0; z <= 1; z++) {
            for (int y = 0; y <= 1; y++) {
                for (int x = 0; x <= 1; x++) {
                    ivec3 p = clamp(base + ivec3(x, y, z),
                                    ivec3(0), giVolumeSize - 1);
                    float w = mix(1.0 - f.x, f.x, float(x))
                            * mix(1.0 - f.y, f.y, float(y))
                            * mix(1.0 - f.z, f.z, float(z));
                    result += w * texelFetch(wsrcSampler, p, 0).rgb;
                }
            }
        }
        return result * edgeFade;
    }

    // Smooth WSRC sample: trilinear + 8-tap tangent-plane blur to soften
    // blocky voxel artifacts. Uses the surface normal to constrain blur
    // along the surface plane (avoids leaking through walls).
    vec3 IC_SampleWorldCacheSmooth(sampler3D wsrcSampler,
                                    vec3 voxelCoord, vec3 normal) {
        float r = float(IC_WORLD_BLUR) * 0.5;

        #if GI_LEAK_REDUCTION == 1
            r = min(r, 1.0);
        #endif

        vec3 up = abs(normal.y) < 0.99
                ? vec3(0.0, 1.0, 0.0)
                : vec3(1.0, 0.0, 0.0);
        vec3 tangent   = normalize(cross(up, normal));
        vec3 bitangent = cross(normal, tangent);

        vec3 centerSample = IC_SampleWorldCache(wsrcSampler, voxelCoord);
        float centerWeight = 0.28;
        vec3 result = centerSample * centerWeight;

        vec3 offsets[8] = vec3[8](
            tangent * r, -tangent * r,
            bitangent * r, -bitangent * r,
            (tangent + bitangent) * (r * 0.7071), -(tangent + bitangent) * (r * 0.7071),
            (tangent - bitangent) * (r * 0.7071), -(tangent - bitangent) * (r * 0.7071)
        );
        float baseWeights[8] = float[8](0.11, 0.11, 0.11, 0.11, 0.07, 0.07, 0.07, 0.07);

        for (int i = 0; i < 8; i++) {
            vec3 tapCoord = voxelCoord + offsets[i];
            // If this tap landed inside solid geometry, the tangent-plane
            // assumption broke down (it crossed a corner into a neighbouring
            // surface rather than staying on this one) - fall back to the
            // center sample instead of reading that other surface's
            // radiance through the wall.
            float solidity = IC_TapSolidity(tapCoord);
            vec3 tapSample = mix(IC_SampleWorldCache(wsrcSampler, tapCoord), centerSample, solidity);
            result += tapSample * baseWeights[i];
        }
        return result;
    }

    // =====================================================================
    //  SSRC Probe Update (Screen Space Radiance Cache)
    // =====================================================================
    //  Called from deferred1.glsl for pixels near the camera or aligned to
    //  the IC_SCREEN_SCALE probe grid. Each probe casts IC_RAYS_PER_PROBE
    //  cosine-weighted rays into the WSRC and stores the average radiance.
    //  The SSRC provides fine near-camera detail that the coarse WSRC
    //  misses (contact bounce, small crevices).

    vec3 IC_UpdateScreenProbe(sampler3D wsrcSampler, sampler3D albedoSampler,
                               vec3 worldPos, vec3 normal, float seed) {
        // ("posisi meleset/bocor setelah GI_VOXEL_SIZE=1.0" fix - sama seperti
        // giSampleScenePos di deferred1.glsl): 0.75 di sini juga satuan voxel,
        // dulu dikonversi lewat GI_VOXEL_SIZE yang masih 0.5-0.65 (~0.38-0.49
        // blok dunia). Diturunkan ke 0.51 supaya konsisten dengan margin yang
        // sama dipakai giSampleScenePos & Derivative's VXGI_ShadowTrace, sekarang
        // GI_VOXEL_SIZE sudah tetap di 1.0. (Jalur ini masih non-aktif secara
        // default - IC_USE_SSRC_PROBES 0 - tapi dibenerin sekalian biar nggak
        // reintroduce bug yang sama kalau probe ini diaktifkan lagi nanti.)
        // Kept in sync with giSampleScenePos's offset in deferred1.glsl
        // (0.51 -> 0.60, "posisi light bounce melenceng" fix).
        vec3 voxelPos = GiSceneToVoxel(worldPos + normal * GI_VOXEL_SIZE * 0.51);
        vec3 acc = vec3(0.0);

        for (int i = 0; i < IC_RAYS_PER_PROBE; i++) {
            vec2 u = GiHash22(vec2(seed + float(i) * 19.19,
                                    seed * 1.71 + float(i) * 7.77));
            vec3 dir = GiCosineSampleHemisphere(normal, u);

            ivec3 hit;
            float hd;
            vec3 tt;
            vec3 hitFaceNormal;
            if (!GiTraceRay(voxelPos, dir, IC_TRACE_STEPS, hit, hd, tt, hitFaceNormal)) continue;

            vec3 hitPos = voxelPos + dir * hd;
            // ("hybrid ray tracing" DDA/normal fix): exact DDA hit-face
            // normal from GiTraceRay, same fix as GiSampleRadianceRayTraced -
            // see giFloodfill.glsl for the full rationale.
            vec3 hitN = hitFaceNormal;
            if (dot(hitN, dir) > 0.0) hitN = -hitN;

            #if GI_LEAK_REDUCTION == 1
                const float pullIn = 1.3;
            #else
                const float pullIn = 0.9;
            #endif
            acc += IC_SampleWorldCacheSmooth(wsrcSampler,
                       hitPos - hitN * pullIn, hitN) * tt;

            // Attenuate by hit albedo (single-bounce for IC)
            acc *= texelFetch(albedoSampler, hit, 0).rgb;
        }

        return acc / max(float(IC_RAYS_PER_PROBE), 1.0);
    }

    // =====================================================================
    //  Contact AO (Screen-Space Ambient Occlusion)
    // =====================================================================
    //  Lightweight hemisphere AO: traces a few short rays in screen space
    //  and checks depth buffer hits. Adds per-pixel contact shadowing that
    //  the coarse voxel grid can't provide.

    #if CONTACT_AO_ENABLED == 1
    float IC_ContactAO(sampler2D depthSampler, vec2 screenCoord,
                        vec3 normal, float dither) {
        float ao = 0.0;

        // Self-contained linear depth reconstruction (does not depend on
        // deferred1's GetLinearDepth so this file compiles standalone).
        float centerDepth = (2.0 * near) / (far + near - texture2D(depthSampler, screenCoord).r * (far - near));
        if (centerDepth < 0.56) return 1.0;

        vec3 viewN = normalize(mat3(gbufferModelView) * normal);
        float dsign = viewN.z >= 0.0 ? 1.0 : -1.0;
        float da = -1.0 / (dsign + viewN.z);
        float db = viewN.x * viewN.y * da;
        vec3 tangent   = vec3(1.0 + dsign * viewN.x * viewN.x * da,
                               dsign * db, -dsign * viewN.x);
        vec3 bitangent = vec3(db, dsign + viewN.y * viewN.y * da, -viewN.y);

        float fovScale = gbufferProjection[1][1];
        float radiusPx = CONTACT_AO_RADIUS * fovScale /
                         max(centerDepth, 0.1);

        for (int i = 0; i < CONTACT_AO_SAMPLES; i++) {
            float angle = (6.2831853 / float(CONTACT_AO_SAMPLES)) *
                          float(i) + dither * 6.2831853;
            float r = sqrt((float(i) + dither) /
                           float(CONTACT_AO_SAMPLES));
            vec2 offset = vec2(cos(angle), sin(angle)) * r * radiusPx /
                          vec2(viewWidth, viewHeight);

            vec2 sampleCoord = screenCoord + offset;
            float sampleDepth = (2.0 * near) / (far + near - texture2D(depthSampler, sampleCoord).r * (far - near));

            float diff = centerDepth - sampleDepth;
            ao += clamp(diff * 4.0, 0.0, 1.0) *
                  clamp(1.0 - abs(diff) * 2.0, 0.0, 1.0);
        }

        ao /= float(CONTACT_AO_SAMPLES);
        return 1.0 - ao * CONTACT_AO_INTENSITY;
    }
    #endif

#endif // INCLUDE_IRRADIANCE_CACHE
