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

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

//////////Shadowcomp 1//////////Shadowcomp 1//////////Shadowcomp 1//////////
#ifdef SHADOWCOMP

#define OPTIMIZATION_ACT_BEHIND_PLAYER
#define OPTIMIZATION_ACT_HALF_RATE_SPREADING
//#define OPTIMIZATION_ACT_SHARED_MEMORY

layout (local_size_x = 8, local_size_y = 8, local_size_z = 8) in;
#if COLORED_LIGHTING_INTERNAL == 128
	const ivec3 workGroups = ivec3(16, 8, 16);
#elif COLORED_LIGHTING_INTERNAL == 192
	const ivec3 workGroups = ivec3(24, 12, 24);
#elif COLORED_LIGHTING_INTERNAL == 256
	const ivec3 workGroups = ivec3(32, 16, 32);
#elif COLORED_LIGHTING_INTERNAL == 384
	const ivec3 workGroups = ivec3(48, 24, 48);
#elif COLORED_LIGHTING_INTERNAL == 512
	const ivec3 workGroups = ivec3(64, 32, 64);
#elif COLORED_LIGHTING_INTERNAL == 768
	const ivec3 workGroups = ivec3(96, 32, 96);
#elif COLORED_LIGHTING_INTERNAL == 1024
	const ivec3 workGroups = ivec3(128, 32, 128);
#endif

#ifdef OPTIMIZATION_ACT_SHARED_MEMORY
	shared vec4 sharedLight[10][10][10];
#endif

//Common Variables//
writeonly uniform image3D floodfill_img;
writeonly uniform image3D floodfill_img_copy;

//Common Functions//
vec4 GetLightSample(sampler3D lightSampler, ivec3 pos) {
	#ifdef OPTIMIZATION_ACT_SHARED_MEMORY
		ivec3 localPos = pos - ivec3(gl_WorkGroupID) * ivec3(8); // Convert global pos to local shared pos
		if (all(greaterThanEqual(localPos, ivec3(0))) && all(lessThan(localPos, ivec3(8)))) {
			return sharedLight[localPos.x][localPos.y][localPos.z];
		} else {
			return texelFetch(lightSampler, pos, 0); // fallback to global fetch
		}
	#else
		return texelFetch(lightSampler, pos, 0);
	#endif
}

vec4 GetLightCalculated(sampler3D lightSampler, ivec3 pos, ivec3 voxelVolumeSize, uint voxel) {
	vec4 light_px = GetLightSample(lightSampler, clamp(pos + ivec3( 1,  0,  0), ivec3(0), voxelVolumeSize - 1));
	vec4 light_py = GetLightSample(lightSampler, clamp(pos + ivec3( 0,  1,  0), ivec3(0), voxelVolumeSize - 1));
	vec4 light_pz = GetLightSample(lightSampler, clamp(pos + ivec3( 0,  0,  1), ivec3(0), voxelVolumeSize - 1));
	vec4 light_nx = GetLightSample(lightSampler, clamp(pos + ivec3(-1,  0,  0), ivec3(0), voxelVolumeSize - 1));
	vec4 light_ny = GetLightSample(lightSampler, clamp(pos + ivec3( 0, -1,  0), ivec3(0), voxelVolumeSize - 1));
	vec4 light_nz = GetLightSample(lightSampler, clamp(pos + ivec3( 0,  0, -1), ivec3(0), voxelVolumeSize - 1));

    vec4 lightAvg = light_px + light_py + light_pz + light_nx + light_ny + light_nz;
    lightAvg /= 6.42; // Slightly higher than 6 to prevent the light from travelling too far

    // Directional/"boxy" propagation: instead of always blurring the strongest
    // neighbor down into a shared average, let it carry through mostly on its
    // own (mild falloff per step). Through an open shaft (e.g. straight below
    // a ceiling lamp) this keeps a defined geometric column/box shape rather
    // than smoothing sideways into a round blob. Solid neighbors still return
    // 0 light exactly as before, so this never leaks through walls.
    vec4 lightMax = max(max(max(light_px, light_py), max(light_pz, light_nx)), max(light_ny, light_nz));
    // ("ruangan penuh lampu jadi putih rata" fix): falloff per step dinaikkan
    // dari 0.978 (hanya ~2.2% redam per blok) ke 0.94 (~6% per blok) supaya
    // cahaya sumber lampu meredup lebih cepat seiring jarak - dua lampu yang
    // berdekatan tidak lagi saling menumpuk jadi satu bidang putih tanpa
    // gradasi, tapi menyisakan area yang lebih gelap di antara keduanya.
    lightMax *= 0.94;

    vec4 light = mix(lightAvg, lightMax, clamp(float(VOXEL_LIGHT_SHAFT_STYLE) * 0.01, 0.0, 1.0));

	if (voxel >= 200u) {
		vec3 tint = specialTintColor[min(voxel - 200u, specialTintColor.length() - 1u)];
		light.rgb *= tint;
		light.a *= dot(tint, vec3(0.333333));
	}

	return light;
}

//Includes//
#include "/lib/voxelization/lightVoxelization.glsl"

#if SOLAR_GI == 1 && defined OVERWORLD
    // ================================================================
    //  Solar GI (ported from ForkReimagined "SolarGI v1.8")
    //  ------------------------------------------------------------
    //  Injects shadow-map-verified sunlight bounce directly into ACT's
    //  existing flood-fill volume (floodfill_img/floodfill_img_copy).
    //  Allocates no new images and performs no image writes outside this
    //  compute pass. Self-contained: local uniform declarations below
    //  instead of touching /lib/uniforms.glsl, so nothing else in the
    //  pack is affected if this block is ever disabled/removed.
    // ================================================================
    #include "/lib/util/spaceConversion.glsl"

    uniform vec3 sunPosition;
    uniform vec3 shadowLightPosition; // sunPosition while the sun drives the shadow pass, moonPosition at night

    float sunVisibility2 = pow2(clamp(1.0 - nightFactor * 1.35, 0.0, 1.0));
    #include "/lib/colors/lightAndAmbientColors.glsl"

    // Unbound has no SUN_ANGLE_EW azimuth-rotation setting (unlike the source
    // pack this was ported from), so this is just the plain world-space sun
    // direction - equivalent to GetSunVector() in commonFunctions.glsl before
    // its gbufferModelView (view-space) transform.
    vec3 GetSolarGIWorldSunDir() {
        const vec2 sunRotationData = vec2(cos(sunPathRotation * 0.01745329251994), -sin(sunPathRotation * 0.01745329251994));
        float ang = fract(timeAngle - 0.25);
        ang = (ang + (cos(ang * 3.14159265358979) * -0.5 + 0.5 - ang) / 3.0) * 6.28318530717959;
        return normalize(vec3(-sin(ang), cos(ang) * sunRotationData));
    }

    float GetSolarGISunGate(vec3 worldSunDir) {
        // Do not infer the shadow source from timeAngle. Iris explicitly exposes the
        // celestial body currently driving the shadow pass via shadowLightPosition.
        vec3 activeShadowDir = normalize(shadowLightPosition);
        vec3 nativeSunDir = normalize(sunPosition);
        float sourceMatch = dot(activeShadowDir, nativeSunDir);

        // shadowLightPosition == sunPosition during the solar shadow phase and points
        // toward the moon during the lunar phase. This hard-rejects the moon shadow
        // map before any Solar GI visibility lookup is allowed.
        float actualSunShadowGate = smoothstep(0.90, 0.999, sourceMatch);

        // Separately taper irradiance near the physical horizon.
        float elevationGate = smoothstep(0.015, 0.13, worldSunDir.y);
        float dayFactor = actualSunShadowGate * elevationGate * (1.0 - rainFactor * 0.75);
        return clamp(dayFactor, 0.0, 1.0);
    }

    float SolarGIRawShadowVisibility(vec3 playerPos, out float sourceAgreement) {
        vec3 shadowPos = PlayerToShadow(playerPos);
        float receiverClipZ = shadowPos.z;
        sourceAgreement = 0.0;
        float distortFactor = length(shadowPos.xy) * shadowMapBias + (1.0 - shadowMapBias);
        shadowPos.xy /= distortFactor;
        shadowPos.z *= 0.2;
        shadowPos = shadowPos * 0.5 + 0.5;

        if (shadowPos.x <= 0.002 || shadowPos.y <= 0.002 || shadowPos.x >= 0.998 || shadowPos.y >= 0.998 || shadowPos.z <= 0.0 || shadowPos.z >= 1.0) return 0.0;

        ivec2 basePixel = ivec2(clamp(shadowPos.xy, vec2(0.0), vec2(0.999999)) * float(shadowMapResolution));
        float receiverDepth = shadowPos.z - SOLAR_GI_SHADOW_BIAS;

        // A small cross-PCF dramatically reduces shadow-map edge toggling without the cost of a full 3x3 kernel.
        const ivec2 offsets[5] = ivec2[5](ivec2(0, 0), ivec2(1, 0), ivec2(-1, 0), ivec2(0, 1), ivec2(0, -1));
        float visibility = 0.0;

        for (int i = 0; i < 5; i++) {
            ivec2 sp = clamp(basePixel + offsets[i], ivec2(0), ivec2(shadowMapResolution - 1));
            float storedDepth = texelFetch(shadowtex0, sp, 0).x;

            // A Solar GI source must be the FIRST surface seen from the sun, not merely a
            // point that happens to pass a conventional shadow comparison. This is the key
            // cave-leak guard: cave walls far behind terrain can no longer become sources.
            float storedClipZ = (storedDepth * 2.0 - 1.0) / 0.2;
            float depthSeparationBlocks = abs(storedClipZ - receiverClipZ)
                                        / max(abs(shadowProjection[2].z), 0.000001);
            // ("light bounce kedip-kedip pas diam/gerak" fix): band pelan-pelan
            // diperlebar dari (0.55, 1.25) ke (0.45, 1.7) supaya transisi
            // "sumber ini masih dianggap valid atau tidak" jadi gradasi halus,
            // bukan lompatan biner - sebelumnya kamera bergerak sedikit di
            // dekat sudut/tepi geometri bisa membuat sourceAgreement melompat
            // dari 0 ke 1 dalam 1 frame, yang kelihatan sebagai kedipan.
            float depthMatch = 1.0 - smoothstep(0.45, 1.7, depthSeparationBlocks);
            sourceAgreement = max(sourceAgreement, depthMatch);

            float compareVis = clamp((storedDepth - receiverDepth) * 65536.0, 0.0, 1.0);
            visibility += compareVis * depthMatch;
        }

        return visibility * 0.2;
    }

    float SolarGIHistoryPresence(vec4 prevLight) {
        // Lets brand-new GI sources fade in gradually from zero instead of popping to
        // full strength on first detection, while still rejecting high-alpha non-solar
        // ACT lighting (torches, lava, etc. already stored in the same volume).
        float prevA = clamp(prevLight.a, 0.0, 1.0);
        float prevEnergy = max(prevA, GetLuminance(prevLight.rgb));
        float present = smoothstep(0.00001, 0.00125, prevEnergy);
        float rejectHighAlpha = 1.0 - smoothstep(0.08, 0.28, prevA);
        return present * rejectHighAlpha;
    }

    float SolarGIHistoryRetention() {
        #if SOLAR_GI_FADE_TIME <= 0
            return 0.0;
        #else
            // FPS-independent half-life: after SOLAR_GI_FADE_TIME seconds, 50% of the
            // previous source remains.
            float dt = clamp(frameTime, 0.001, 0.10);
            float effectiveHalfLife = max(0.01, SOLAR_GI_FADE_TIME * mix(0.35, 1.0, SOLAR_GI_STABILITY));
            return exp2(-dt / effectiveHalfLife);
        #endif
    }

    vec4 StabilizeSolarGISource(vec4 newLight, vec4 prevLight) {
        if (SOLAR_GI_FADE_TIME <= 0.0) return newLight;

        float retention = SolarGIHistoryRetention();
        float historyPresence = SolarGIHistoryPresence(prevLight);
        vec4 prevGI = prevLight * historyPresence;

        // True temporal low-pass from BOTH zero and existing history: fades in from
        // nothing when no prior source exists, and decays from the current filtered
        // value (not forever rising) if the source disappears before reaching full
        // brightness.
        vec4 filtered = mix(newLight, prevGI, retention);
        return max(filtered, vec4(0.0));
    }

    bool SolarGIReflectiveVoxel(uint voxel) {
        // 0 = air/non-solid, 200+ = translucent/tinted ACT geometry.
        // 1..199 = standard solid geometry and special solid/light blocks.
        return voxel > 0u && voxel < 200u;
    }

    vec4 GetSolarGISource(ivec3 pos, uint voxel, vec4 prevLight) {
        if (voxel != 0u) return vec4(0.0); // only air voxels can hold bounced GI
        if (any(lessThanEqual(pos, ivec3(0))) || any(greaterThanEqual(pos, voxelVolumeSize - 1))) return vec4(0.0);

        const ivec3 faceOffsets[6] = ivec3[6](
            ivec3( 1, 0, 0), ivec3(-1, 0, 0),
            ivec3( 0, 1, 0), ivec3( 0,-1, 0),
            ivec3( 0, 0, 1), ivec3( 0, 0,-1)
        );

        vec3 worldSunDir = GetSolarGIWorldSunDir();
        float dayStrength = GetSolarGISunGate(worldSunDir);
        if (dayStrength <= 0.0001) {
            // If Iris is not actually rendering shadows from the sun, purge Solar GI
            // history immediately - long fade times must never bridge the sun/moon switch.
            return vec4(0.0);
        }

        vec3 playerPos = vec3(pos) + vec3(0.5) - cameraPositionBestFract - 0.5 * vec3(voxelVolumeSize);
        float visibilityAccum = 0.0;
        float strongestVisibility = 0.0;
        float strongestSourceAgreement = 0.0;
        int visibleFaceCount = 0;

        for (int face = 0; face < 6; face++) {
            ivec3 offset = faceOffsets[face];
            uint neighborVoxel = GetVoxelVolume(pos + offset);
            if (!SolarGIReflectiveVoxel(neighborVoxel)) continue;

            vec3 faceNormal = -normalize(vec3(offset));
            float rawIncidence = max(dot(faceNormal, worldSunDir), 0.0);
            if (rawIncidence <= 0.0001) continue;
            // A raw Lambert term is too binary for ACT's block-sized voxel cells near the
            // horizon - preserve directionality while preventing sunrise/sunset GI from
            // collapsing into sparse, extremely bright vertical-face patches.
            float incidence = mix(0.22, 1.0, sqrt(rawIncidence));

            vec3 surfacePos = playerPos + vec3(offset) * 0.47;
            float faceAgreement;
            float faceVisibility = SolarGIRawShadowVisibility(surfacePos, faceAgreement);
            strongestSourceAgreement = max(strongestSourceAgreement, faceAgreement * incidence);
            if (faceVisibility <= 0.001) continue;

            float weightedVis = faceVisibility * incidence;
            visibilityAccum += weightedVis;
            strongestVisibility = max(strongestVisibility, weightedVis);
            visibleFaceCount++;
        }

        if (visibleFaceCount == 0 || visibilityAccum <= 0.001) {
            float historyMarker = SolarGIHistoryPresence(prevLight);
            if (historyMarker <= 0.00001 || SOLAR_GI_FADE_TIME <= 0.0) return vec4(0.0);

            // Only preserve long Solar-GI history when the candidate still agrees with the
            // first surface in the shadow map, so stale history purges fast in caves.
            if (strongestSourceAgreement < 0.18) {
                float dt = clamp(frameTime, 0.001, 0.10);
                float invalidRetention = exp2(-dt / 0.18);
                return prevLight * (invalidRetention * historyMarker);
            }

            float retention = SolarGIHistoryRetention();
            return prevLight * (retention * historyMarker);
        }

        float multiFaceGain = 1.0 + 0.18 * float(max(visibleFaceCount - 1, 0));
        float visibility = min(max(strongestVisibility, visibilityAccum / float(visibleFaceCount)) * multiFaceGain, 1.35);
        float energy = visibility * dayStrength * SOLAR_GI_STRENGTH;
        if (energy <= 0.001) return vec4(0.0);

        // No real per-block texture albedo captured here (see comment in common.glsl) -
        // bounce color comes from the sun/sky light color only.
        vec3 sunBounce = max(lightColor, vec3(0.0));
        float sunLum = max(GetLuminance(sunBounce), 0.0001);
        sunBounce *= min(1.0, 1.15 / sunLum);
        sunLum = GetLuminance(sunBounce);
        sunBounce = mix(vec3(sunLum), sunBounce, SOLAR_GI_SATURATION);

        float reflectance = 0.75; // fixed mid-albedo reflectance (no captured surface albedo to derive it from)
        float bounceLum = max(GetLuminance(sunBounce), 0.0001);
        sunBounce *= min(1.0, 1.45 / bounceLum);
        sunBounce *= energy * reflectance * 0.58;

        vec4 source = vec4(pow2(sunBounce), clamp(GetLuminance(sunBounce) * 0.026, 0.0, 0.085));
        return StabilizeSolarGISource(source, prevLight);
    }
#endif

//Program//
void main() {
	ivec3 pos = ivec3(gl_GlobalInvocationID);
	vec3 posM = vec3(pos) / vec3(voxelVolumeSize);
	vec3 posOffset = floor(previousCameraPosition) - floor(cameraPosition);
	ivec3 previousPos = pos - ivec3(posOffset);
    ivec3 localPos = ivec3(gl_LocalInvocationID);

	#ifdef OPTIMIZATION_ACT_SHARED_MEMORY
		vec4 prevLight = vec4(0.0);
		if (int(framemod2) == 0)
			prevLight = texelFetch(floodfill_sampler, previousPos, 0);
		else
			prevLight = texelFetch(floodfill_sampler_copy, previousPos, 0);

		sharedLight[localPos.x][localPos.y][localPos.z] = prevLight;

		barrier();

		ivec3 alignedOffset = ivec3(posOffset) / 8 * 8;
		previousPos = pos - alignedOffset;
	#endif

	#ifdef OPTIMIZATION_ACT_BEHIND_PLAYER
		ivec3 absPosFromCenter = abs(pos - voxelVolumeSize / 2);
		if (absPosFromCenter.x + absPosFromCenter.y + absPosFromCenter.z > 16) {
			vec4 viewPos = gbufferProjectionInverse * vec4(0.0, 0.0, 1.0, 1.0);
			viewPos /= viewPos.w;
			vec3 nPlayerPos = normalize(mat3(gbufferModelViewInverse) * viewPos.xyz);
			if (dot(normalize(posM - 0.5), nPlayerPos) < 0.0) {
				#ifdef COLORED_LIGHT_FOG
					if (int(framemod2) == 0) {
						imageStore(floodfill_img_copy, pos, GetLightSample(floodfill_sampler, previousPos));
					} else {
						imageStore(floodfill_img, pos, GetLightSample(floodfill_sampler_copy, previousPos));
					}
				#endif
				return;
			}
		}
	#endif

	vec4 previousFrameLight = vec4(0.0);
	if (int(framemod2) == 0) previousFrameLight = GetLightSample(floodfill_sampler, previousPos);
	else previousFrameLight = GetLightSample(floodfill_sampler_copy, previousPos);

	vec4 light = vec4(0.0);	
	uint rawData = GetVoxelVolumeRaw(pos);
	uint voxel = rawData & 32767u;
	uint isColorwheelGeometry = (rawData - voxel) >> 15u;

	if (voxel == 1u) { // Solid Blocks
		light = vec4(0.0);
	} else if (voxel == 0u || voxel >= 200u) { // Air, Non-solids, Translucents
		if (int(framemod2) == 0) {
			#ifdef OPTIMIZATION_ACT_HALF_RATE_SPREADING
				if (posM.z > 0.5) light = GetLightSample(floodfill_sampler, previousPos);
				else
			#endif
			light = GetLightCalculated(floodfill_sampler, previousPos, voxelVolumeSize, voxel);
		} else {
			#ifdef OPTIMIZATION_ACT_HALF_RATE_SPREADING
				if (posM.z < 0.5) light = GetLightSample(floodfill_sampler_copy, previousPos);
				else
			#endif
			light = GetLightCalculated(floodfill_sampler_copy, previousPos, voxelVolumeSize, voxel);
		}
	} else { // Light Sources
		vec4 color = GetSpecialBlocklightColor(int(voxel));
		if (isColorwheelGeometry == 1u) color.a = 1.0;
		light = max(light, vec4(pow2(color.rgb), color.a));
	}

    #if SOLAR_GI == 1 && defined OVERWORLD
        light = max(light, GetSolarGISource(pos, voxel, previousFrameLight));
    #endif

	light = clamp(light, vec4(0.0), vec4(1000.0)); // Prevents a random NaN from consuming the volume

	if (int(framemod2) == 0) {
		imageStore(floodfill_img_copy, pos, light);
	} else {
		imageStore(floodfill_img, pos, light);
	}
}

#endif