/////////////////////////////////////
// Unbound Visual + PT v9.6 by EminGT //
/////////////////////////////////////

//Common//
#include "/lib/common.glsl"

//////////Prepare 1//////////Prepare 1//////////Prepare 1//////////
#ifdef PREPARE

#if defined CUSTOM_GI_VOXEL_INTERNAL && defined OVERWORLD

layout (local_size_x = 8, local_size_y = 8, local_size_z = 8) in;
#if GI_VOXEL_RESOLUTION == 64
    const ivec3 workGroups = ivec3(8, 8, 8);
#elif GI_VOXEL_RESOLUTION == 128
    const ivec3 workGroups = ivec3(16, 16, 16);
#elif GI_VOXEL_RESOLUTION == 192
    const ivec3 workGroups = ivec3(24, 24, 24);
#elif GI_VOXEL_RESOLUTION == 256
    const ivec3 workGroups = ivec3(32, 32, 32);
#endif

//Common Variables//
uniform sampler3D gi_voxel_sampler;
writeonly uniform image3D gi_radiance_a_img;

#if GI_EMISSIVE_BLOCKS == 1
    uniform sampler3D gi_emission_sampler;
#endif

#if GI_TEMPORAL_ACCUM == 1
    uniform sampler3D gi_history_sampler;
#endif

// sunPosition is the standard Iris/OptiFine view-space direction-to-sun
// uniform. This codebase normally computes its own sunVec via
// GetSunVector() instead, but that function is only valid in vertex
// shaders (it's passed down as a "flat in" varying elsewhere) - a compute
// pass has no varyings, so the built-in view-space uniform is used here.
uniform vec3 sunPosition;
uniform vec3 moonPosition;

//Includes//
#include "/lib/util/spaceConversion.glsl"
#define GI_NEED_READ_SAMPLERS
#include "/lib/voxelization/giFloodfill.glsl"

// Minimal, self-contained shadow-space transform (same math as
// GetShadowPos() in lib/lighting/shadowSampling.glsl). Not including that
// file directly: it also defines helpers that reference gl_FragCoord,
// which doesn't exist in a compute shader and would fail to compile even
// if unused.
vec3 GiGetShadowPos(vec3 playerPos) {
    vec3 shadowPos = PlayerToShadow(playerPos);
    float distb = sqrt(shadowPos.x * shadowPos.x + shadowPos.y * shadowPos.y);
    float distortFactor = distb * shadowMapBias + (1.0 - shadowMapBias);
    shadowPos.xy /= distortFactor;
    shadowPos.z *= 0.2;
    return shadowPos * 0.5 + 0.5;
}

// Self-contained, lightweight sun/moon color estimate (deliberately not
// reusing the full atmospheric color pipeline here, to keep this compute
// pass simple and independent) - warms up near the horizon, cools to
// near-white near zenith.
vec3 GiApproxSunColor(vec3 sunDirWorld) {
    float elevation = clamp(sunDirWorld.y, 0.0, 1.0);
    // Near-white bounce color: barely warm at horizon, pure white at zenith.
    // The old warm-gold tint compounded into an orange/brown cast across all
    // lit surfaces. Keeping it close to neutral white means bounce light
    // faithfully reflects the albedo of whatever surface it hits instead
    // of imposing its own warm tint on everything.
    vec3 horizonColor = vec3(1.00, 0.98, 0.96);
    vec3 zenithColor  = vec3(1.00, 1.00, 1.00);
    return mix(horizonColor, zenithColor, pow(elevation, 0.4));
}

//Program//
void main() {
    ivec3 pos = ivec3(gl_GlobalInvocationID);

    vec4 albedoOpacity = texelFetch(gi_voxel_sampler, pos, 0);
    float opacity = albedoOpacity.a;

    // Voxel CENTER in scene(player)-space, true inverse of GiSceneToVoxel().
    // GiSceneToVoxel() writes into texel floor(voxelCoord), i.e. integer
    // index `pos` covers the continuous voxel-space span from pos to pos+1 -
    // its center is pos+0.5, not pos. Using bare `pos` here (missing the
    // +0.5) sampled the shadow map and estimated the sun direction from a
    // point up to half a voxel off from the voxel's actual location, which
    // near walls/edges was enough to flip whether a voxel tested as
    // sunlit or shadowed - i.e. bounce light landing on the wrong side of
    // a surface ("posisi salah").
    // GI_Y_OFFSET is 0.0 (grid centered symmetrically on the camera, matching
    // ACT/Solar GI) - see giFloodfill.glsl for why.
    vec3 scenePos = (vec3(pos) + 0.5 - 0.5 * vec3(giVolumeSize) - vec3(0.0, GI_Y_OFFSET, 0.0)) * GI_VOXEL_SIZE - cameraPositionFract;

    // Day-only bounce gate ("Malam nggak boleh ada light bounce").
    // The bounce MUST come from the sun - moonlight is far too weak/diffuse
    // to carry a visible flood-fill, and injecting it makes night scenes
    // bright and washed white. Compute the sun's own elevation in world
    // space and fade the whole sunlight term to zero as soon as the sun
    // drops below the horizon: at night the grid gets only emissive blocks
    // (torch/glowstone), so interior scenes stay lit but the landscape
    // stops glowing. A soft ramp around the horizon gives a natural
    // sunset/dusk border instead of a hard on/off snap.
    vec3 sunWorldDir = normalize(mat3(gbufferModelViewInverse) * sunPosition);
    float bounceDayFactor = clamp(sunWorldDir.y * 6.0 + 0.15, 0.0, 1.0);

    #if GI_TEMPORAL_ACCUM == 1 && GI_SINGLE_BOUNCE == 0
        // Reproject: find where THIS voxel's world position landed in LAST
        // frame's grid. Last frame's grid was centered using
        // previousCameraPositionFract, not this frame's cameraPositionFract,
        // so re-using `pos` directly (as if the grid hadn't moved at all)
        // reads slightly the wrong world location every frame the camera
        // moves even a little - this was the missing piece that made
        // GI_TEMPORAL_ACCUM a no-op before (gi_history_sampler was declared
        // but never actually sampled here).
        vec3 cameraDelta = cameraPosition - previousCameraPosition;
        vec3 prevScenePos = scenePos + cameraDelta;
        vec3 prevVoxelPos = GiSceneToVoxelWithFract(prevScenePos, previousCameraPositionFract);

        vec3 history = vec3(0.0);
        if (GiCheckInsideVolume(prevVoxelPos)) {
            vec3 historyRaw = GiSampleRadianceTrilinear(gi_history_sampler, prevVoxelPos);

            // --- Temporal neighborhood clamp (ghost rejection) -----------
            // A reprojected sample can point at a voxel whose lighting has
            // since moved on (a torch placed/removed, sun angle changed, or
            // the region just hasn't been refreshed by this frame's gather
            // yet). Trusting it unconditionally every frame is what reads as
            // bounce light "dragging"/ghosting behind a moving light source
            // instead of tracking it. Bound the sample to the min/max
            // radiance of its 6 face-adjacent history texels: an outlier
            // relative to its own neighbourhood gets pulled back in line
            // with where the region is actually converging right now, so
            // stale light gets corrected out within a couple of frames
            // instead of persisting for the whole decay curve. Cheap (6
            // extra texelFetches), same idea as the neighbourhood-clamping
            // used to fight ghosting in temporal image filters generally,
            // just applied here to the 3D radiance grid instead of a 2D
            // frame buffer.
            ivec3 baseTexel = clamp(ivec3(floor(prevVoxelPos - 0.5)), ivec3(0), giVolumeSize - 1);
            vec3 nMin = historyRaw;
            vec3 nMax = historyRaw;
            for (int i = 0; i < 6; i++) {
                ivec3 nOff;
                if (i == 0) nOff = ivec3( 1, 0, 0);
                else if (i == 1) nOff = ivec3(-1, 0, 0);
                else if (i == 2) nOff = ivec3( 0, 1, 0);
                else if (i == 3) nOff = ivec3( 0,-1, 0);
                else if (i == 4) nOff = ivec3( 0, 0, 1);
                else nOff = ivec3( 0, 0,-1);
                ivec3 np = clamp(baseTexel + nOff, ivec3(0), giVolumeSize - 1);
                vec3 nRad = GiLoadRadiance(gi_history_sampler, np).rgb;
                nMin = min(nMin, nRad);
                nMax = max(nMax, nRad);
            }
            vec3 historyClamped = clamp(historyRaw, nMin, nMax);

            // --- Motion-adaptive decay (faster convergence) --------------
            float giMotionVoxels = length(cameraDelta) / float(GI_VOXEL_SIZE);
            float giMotionFactor = clamp(giMotionVoxels * 0.8, 0.0, 1.0);
            float giAdaptiveDecay = mix(float(GI_TEMPORAL_ACCUM_DECAY), float(GI_TEMPORAL_ACCUM_DECAY) * 0.5, giMotionFactor);

            // --- Geometry change detection: zero history when occupancy changes ---
            // If the voxel's world position now contains different geometry
            // (block placed/broken), the old radiance is invalid. Check if
            // the current voxel's opacity differs from what the history
            // voxel had - if so, discard history entirely to prevent
            // stale light from "fading in" at the wrong location.
            vec4 prevAlbedo = GiLoadAlbedoOpacity(clamp(baseTexel, ivec3(0), giVolumeSize - 1));
            bool wasSolid = prevAlbedo.a > 0.5;
            bool isSolid = opacity > 0.5;
            if (isSolid != wasSolid) {
                // Geometry changed: block placed or broken
                // Use 50% history to avoid hard flash, but mostly fresh
                giAdaptiveDecay *= 0.2;
            }

            history = historyClamped * giAdaptiveDecay * bounceDayFactor;
        }
    #endif

    if (opacity < 0.5) {
        // Air / non-solid: no direct light of its own to inject, but it may
        // still be carrying bounced light traveling through it from
        // previous frames - keep that (decayed) instead of always zeroing
        // it, so indirect light in open space also converges/stabilizes
        // over time instead of being rebuilt from scratch every frame.
        #if GI_TEMPORAL_ACCUM == 1 && GI_SINGLE_BOUNCE == 0
            imageStore(gi_radiance_a_img, pos, vec4(history, 0.0));
        #else
            imageStore(gi_radiance_a_img, pos, vec4(0.0));
        #endif
        return;
    }

    vec3 albedo = albedoOpacity.rgb;

    // Pick whichever of sun/moon is currently above the horizon, matching
    // the "Sun/Moon Bounce" naming convention used elsewhere in this pack.
    vec3 skyLightViewDir = sunPosition.y > 0.0 ? sunPosition : moonPosition;
    vec3 skyLightWorldDir = normalize(mat3(gbufferModelViewInverse) * skyLightViewDir);
    float horizonFade = clamp(skyLightWorldDir.y * 8.0 + 0.0625, 0.0, 1.0);

    // Offset test point slightly towards the light source so the voxel center
    // doesn't self-shadow against the block's own front face in shadow space.
    vec3 sunLitTestPos = scenePos + skyLightWorldDir * (GI_VOXEL_SIZE * 0.5);
    vec3 shadowPos = GiGetShadowPos(sunLitTestPos);
    shadowPos.z -= 0.0005; // depth bias against shadow map precision jitter
    float vis0 = shadow2D(shadowtex0, shadowPos).x;
    float visibility = vis0;
    bool isThinFeature = false;
    vec3 normalEstimate = GiEstimateVoxelNormalEx(pos, isThinFeature);

    vec3 direct = vec3(0.0);
    if (visibility > 0.5 && bounceDayFactor > 0.0) {
        // A voxel this thin (see GiEstimateVoxelNormalEx) has no real single
        // face to test NdotL against - the default normal it falls back to
        // is an arbitrary "up", which would let a horizontally-thin pillar
        // read as fully sun-facing purely because the sun is high in the
        // sky, regardless of which side (if either) actually faces it. A
        // fixed, moderate exposure avoids that false full-brightness read
        // while the shadow visibility test above still correctly gates it
        // off entirely once the feature is actually in shade.
        float NdotL = isThinFeature ? 0.6 : max(0.0, dot(normalEstimate, skyLightWorldDir));
        vec3 sunColor = GiApproxSunColor(skyLightWorldDir) * horizonFade;
        direct = albedo * sunColor * NdotL * bounceDayFactor;
    }

    // No ambient sky floor here on purpose: the bounce light must come ONLY
    // from actual sunlight hitting a surface and being re-emitted (true GI),
    // not from an artificial wash applied to every sun-facing voxel. The
    // `direct` term below (gated by shadow visibility + NdotL) is the sole
    // source that then propagates through the gather passes.
    vec3 radianceOut = direct;

    #if GI_EMISSIVE_BLOCKS == 1
        // Self-emitted light (glowstone, lava, torches, lamps, etc). Not
        // gated by sun visibility/shadow like `direct` above - these blocks
        // emit their own light regardless of whether they're lit by the
        // sun, including fully indoors/underground/covered by glass.
        radianceOut += texelFetch(gi_emission_sampler, pos, 0).rgb;
    #endif

    #if GI_TEMPORAL_ACCUM == 1 && GI_SINGLE_BOUNCE == 0
        radianceOut += history;
    #endif

    // Alpha is used as a one-frame source marker in single-bounce mode:
    // alpha=1 means this voxel was injected directly by the sun/emissive
    // source; propagated results are written with alpha=0 and therefore can
    // never become a new bounce emitter.
    float directSourceLuma = GetLuminance(direct);
    #if GI_EMISSIVE_BLOCKS == 1
        directSourceLuma += GetLuminance(texelFetch(gi_emission_sampler, pos, 0).rgb);
    #endif
    float giSourceMarker = (GI_SINGLE_BOUNCE == 1 && directSourceLuma > 0.001) ? 1.0 : 0.0;
    imageStore(gi_radiance_a_img, pos, vec4(radianceOut, giSourceMarker));
}

#else

layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
const ivec3 workGroups = ivec3(1, 1, 1);
void main() {}

#endif

#endif
