/*---------------------------------------------------------------------
    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)
    // =====================================================================

    // 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.
        vec3 edgeDist = min(voxelCoord, vec3(giVolumeSize) - voxelCoord);
        float edgeFade = clamp01(min(edgeDist.x, min(edgeDist.y, edgeDist.z)) / float(GI_EDGE_FADE));

        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 result = IC_SampleWorldCache(wsrcSampler, voxelCoord) * 0.28;
        result += IC_SampleWorldCache(wsrcSampler,
                   voxelCoord + tangent   *  r) * 0.11;
        result += IC_SampleWorldCache(wsrcSampler,
                   voxelCoord - tangent   *  r) * 0.11;
        result += IC_SampleWorldCache(wsrcSampler,
                   voxelCoord + bitangent *  r) * 0.11;
        result += IC_SampleWorldCache(wsrcSampler,
                   voxelCoord - bitangent *  r) * 0.11;
        vec3 diagA = (tangent + bitangent) * (r * 0.7071);
        vec3 diagB = (tangent - bitangent) * (r * 0.7071);
        result += IC_SampleWorldCache(wsrcSampler,
                   voxelCoord + diagA) * 0.07;
        result += IC_SampleWorldCache(wsrcSampler,
                   voxelCoord - diagA) * 0.07;
        result += IC_SampleWorldCache(wsrcSampler,
                   voxelCoord + diagB) * 0.07;
        result += IC_SampleWorldCache(wsrcSampler,
                   voxelCoord - diagB) * 0.07;
        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) {
        vec3 voxelPos = GiSceneToVoxel(worldPos + normal * GI_VOXEL_SIZE * 0.75);
        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;
            if (!GiTraceRay(voxelPos, dir, IC_TRACE_STEPS, hit, hd, tt)) continue;

            vec3 hitPos = voxelPos + dir * hd;
            vec3 hitN = GiEstimateVoxelNormal(hit);
            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
