#ifdef END
#include "/lib/atmospherics/enderBeams.glsl"
#endif

#if defined END && defined ENDER_BEAMS
    #define LS_SAMPLE_COUNT 8
    #define LS_MAX_DIST 256.0   // 2x the default beam reach
#else
    #define LS_SAMPLE_COUNT 7
    #define LS_MAX_DIST 128.0
#endif

float GetLightShaftsFalloff(vec3 viewPos, float z0) {
	vec3 lightVec = sunVec * ((timeAngle < 0.5325 || timeAngle > 0.9675) ? 1.0 : -1.0);
	float VoL = dot(normalize(viewPos.xyz), lightVec);

	#ifdef OVERWORLD
	float factor = mix(LIGHT_SHAFT_MORNING_FALLOFF, LIGHT_SHAFT_DAY_FALLOFF, timeBrightness);
		  factor = mix(LIGHT_SHAFT_NIGHT_FALLOFF, factor, sunVisibility);
		  factor*= mix(1.0, LIGHT_SHAFT_WEATHER_FALLOFF, rainStrength) * 0.1;
		  factor = min(factor, 0.999);

	float reverseFactor = 1.0 - factor;

	float falloff = clamp(VoL * 0.5 + 0.5, 0.0, 1.0);
	falloff = factor / (1.0 - reverseFactor * falloff) - factor;
	falloff = clamp(falloff * 1.015 / reverseFactor - 0.015, 0.0, 1.0);
	falloff = mix(1.0, falloff, 0.03125 * eBS + 0.96875);
	#endif

	#ifdef NETHER
	float falloff = 1.0;
	#endif
	
	#ifdef END
	// Glow + rays share one virtual axis (default = zenith). END_GLOW_TILT/AZIMUTH aim it.
	float gT = radians(float(END_GLOW_TILT));
	float gA = radians(float(END_GLOW_AZIMUTH));
	vec3 glowAxis = vec3(sin(gT) * cos(gA), cos(gT), sin(gT) * sin(gA));
	vec3 worldViewDir = normalize(mat3(gbufferModelViewInverse) * normalize(viewPos.xyz));
	float VoU = dot(worldViewDir, glowAxis);
	float falloff = pow(VoU * 0.5 + 0.5, 16.0 / END_GLOW_SIZE) * 0.75 + 0.25;
	#endif

	#ifdef END_GLOW_COVER_REDUCE
	// Pull the glow's sky-distance cutoff inward so it doesn't bleed onto nearer
	// surfaces/roofs. END_GLOW_REACH closer to 1.0 = only the farthest sky glows.
	falloff *= float(z0 > END_GLOW_REACH);
	#else
	falloff *= float(z0 > 0.56);
	#endif

	return falloff;
}

bool IsRayMarcherHit(float currentDist, float maxDist, float linearZ0, float linearZ1, vec3 translucent) {
	bool isMaxReached = currentDist >= maxDist;
	bool opaqueReached = currentDist > linearZ1;
	bool solidTransparentReached = currentDist > linearZ0 && translucent == vec3(0.0);
	
	return isMaxReached || opaqueReached || solidTransparentReached;
}

vec3 DistortShadow(vec3 shadowPos) {
	float distb = sqrt(dot(shadowPos.xy, shadowPos.xy));
	float distortFactor = 1.0 - shadowMapBias + distb * shadowMapBias;
	
	shadowPos.xy *= 1.0 / distortFactor;
	shadowPos.z = shadowPos.z * 0.2;
	shadowPos = shadowPos * 0.5 + 0.5;

	return shadowPos;
}

bool IsSampleInShadowmap(vec3 shadowPos) {
	return length(shadowPos.xy * 2.0 - 1.0) < 1.0;
}

vec3 SampleShadow(vec3 sampleShadowPos) {
	float shadow0 = shadow2D(shadowtex0, sampleShadowPos.xyz).z;
	
	vec3 shadowCol = vec3(0.0);
	#ifdef SHADOW_COLOR
	if (shadow0 < 1.0) {
		float shadow1 = shadow2D(shadowtex1, sampleShadowPos.xyz).z;
		if (shadow1 > 0.0) {
			shadowCol = texture2D(shadowcolor0, sampleShadowPos.xy).rgb;
			shadowCol *= shadowCol * shadow1;
			#ifdef WATER_CAUSTICS
			shadowCol *= 16.0 - 15.0 * (1.0 - (1.0 - shadow0) * (1.0 - shadow0));
			#endif
		}
	}
	#endif

	shadow0 *= shadow0;
	shadowCol *= shadowCol;
	
	vec3 shadow = clamp(shadowCol * (1.0 - shadow0) + shadow0, vec3(0.0), vec3(16.0));

	return shadow;
}

vec3 GetShadowTint(vec3 translucent, vec3 waterTint, float currentDist, float linearZ0) {
	vec3 shadowTint = vec3(1.0);
	if (currentDist > linearZ0) {
		shadowTint = translucent;
	} else {
		if (isEyeInWater == 1.0) {
			#ifdef WATER_SHADOW_COLOR
			shadowTint = vec3(currentDist) / 48.0 * (1.0 + eBS);
			#else
			shadowTint = waterTint * currentDist / 512.0 * (1.0 + eBS);
			#endif
		}
	}

	return shadowTint;
}

float Get3DNoise(vec3 shadow, vec3 worldPos) {
	// End rays radiate from the virtual glow axis (default = zenith) to form a sunburst
	// converging at that point, instead of vertical columns. The pattern depends only on
	// view direction, so each pixel gets a crisp, stable radial spoke.
	float gT = radians(float(END_GLOW_TILT));
	float gA = radians(float(END_GLOW_AZIMUTH));
	vec3 a = vec3(sin(gT) * cos(gA), cos(gT), sin(gT) * sin(gA));
	vec3 d = normalize(worldPos);
	vec3 ref = abs(a.y) < 0.99 ? vec3(0.0, 1.0, 0.0) : vec3(1.0, 0.0, 0.0);
	vec3 t1 = normalize(cross(a, ref));
	vec3 t2 = cross(a, t1);
	float az = atan(dot(d, t2), dot(d, t1));        // spoke angle around the axis
	float u = az * (1.0 / 6.2831853) + 0.5;          // 0..1 around (wraps in noisetex)
	// Two octaves of irregular azimuth noise -> soft, uneven rays (not laser-straight).
	// Integer spoke counts keep the +/-PI seam continuous.
	float n1 = sin(az * float(END_GLOW_RAYS) + texture2D(noisetex, vec2(u * 2.0, time * 0.01)).r * 6.2831853);
	float n2 = sin(az * float(END_GLOW_RAYS) * 0.5 - time * 0.5);
	float rays = (n1 * 0.6 + n2 * 0.4) * 0.5 + 0.5;  // 0..1, smooth
	// Gentle modulation centred on 0.5 (same average brightness as the old smoke), so
	// the rays are a soft ripple rather than hard spokes.
	return mix(0.42, 0.58, rays);
}

vec3 ApplyLightShaftMultiplier(vec3 totalShadow, float falloff) {
	#ifdef OVERWORLD
	totalShadow *= falloff;
	totalShadow *= lightCol * 0.25;
	#endif

	#ifdef NETHER
	totalShadow *= falloff;
	#endif

	#ifdef END
	// Split the End shaft so the glow RGB only colors the zenith lobe. The ambient
	// floor (0.25) keeps the atmosphere tint (endCol), so changing the glow color no
	// longer recolors the whole dimension's haze.
	float endLobe = max(falloff - 0.25, 0.0);
	vec3 endGlowCol = vec3(END_GLOW_R, END_GLOW_G, END_GLOW_B) / 255.0;
	totalShadow *= endCol.rgb * (0.1 * 0.25) + endGlowCol * (0.15 * END_GLOW_I) * endLobe;
	#endif

	#ifdef END
	totalShadow *= LIGHT_SHAFT_STRENGTH_END * (1.0 - rainStrength * eBS * 0.875) * shadowFade;
	#else
	totalShadow *= LIGHT_SHAFT_STRENGTH * (1.0 - rainStrength * eBS * 0.875) * shadowFade;
	#endif
	
	#ifdef IS_IRIS
	totalShadow *= clamp((cameraPosition.y - bedrockLevel + 6.0) / 8.0, 0.0, 1.0);
	#else
	#if MC_VERSION >= 11800
	totalShadow *= clamp((cameraPosition.y + 70.0) / 8.0, 0.0, 1.0);
	#else
	totalShadow *= clamp((cameraPosition.y + 6.0) / 8.0, 0.0, 1.0);
	#endif
	#endif
	
	#ifdef SKY_UNDERGROUND
	totalShadow *= mix(clamp((cameraPosition.y - 48.0) / 16.0, 0.0, 1.0), 1.0, eBS);
	#endif

	return totalShadow;
}

vec3 ApplyBlocklightFogMultiplier(vec3 totalBlocklight, float maxDist) {
	float blocklightIntensity = length(totalBlocklight);

	vec3 blocklightColor = normalize(totalBlocklight * totalBlocklight + 1e-5);

	totalBlocklight = blocklightIntensity * blocklightColor;
	
	totalBlocklight *= MCBL_FOG_STRENGTH * 0.5 / maxDist;
	totalBlocklight *= BLOCKLIGHT_I * BLOCKLIGHT_I;

	#ifdef OVERWORLD
	totalBlocklight *= 1.0 + 3.0 * rainStrength * eBS;
	#endif

	#ifdef NETHER
	vec3 netherTint = netherColSqrt.rgb + 1e-5;
	netherTint /= max(max(netherTint.r, netherTint.g), netherTint.b);
	netherTint = netherTint + 0.25;

	totalBlocklight *= netherTint;
	#endif
	
	#ifdef END
	totalBlocklight *= 4.0;
	#endif

	return totalBlocklight;
}

//Light shafts based on Robobo1221's implementation
vec3 GetLightShafts(vec3 viewPos, float z0, float z1, float dither, vec3 translucent) {
	vec3 vl = vec3(0.0);
	vec3 totalShadow = vec3(0.0);
	vec3 totalBlocklight = vec3(0.0);

	// Temporally-stable dither for the ender beams. TAA can't reproject the volumetric
	// beams (no motion vectors), so the frame-jittered dither below makes their sharp
	// edges shimmer. blueNoise is per-pixel (spatially decorrelated) but steady in time.
	float beamDither = dither;

	#ifdef TAA
	#if TAA_MODE == 0
	dither = fract(dither + frameCounter * 0.618);
	#else
	dither = fract(dither + frameCounter * 0.5);
	#endif
	#endif
	
	float falloff = GetLightShaftsFalloff(viewPos, z0);

	float maxDist = LS_MAX_DIST;
	
	float linearZ0 = GetLinearDepth(z0, gbufferProjectionInverse);
	float linearZ1 = GetLinearDepth(z1, gbufferProjectionInverse);

	#if defined DISTANT_HORIZONS && defined END
	// Stop the End shaft at Distant Horizons terrain so it doesn't over-extend past the
	// regular render distance and create a seam where DH rendering begins.
	if (z1 >= 1.0) {
		float dhZ = texture2D(dhDepthTex0, texCoord).r;
		if (dhZ < 1.0) {
			float dhLinear = GetLinearDepth(dhZ, dhProjectionInverse);
			linearZ0 = min(linearZ0, dhLinear);
			linearZ1 = min(linearZ1, dhLinear);
		}
	}
	#endif

	vec3 worldPos = ToWorld(viewPos);
	vec3 viewDir = normalize(viewPos.xyz);
	vec3 worldDir = normalize(worldPos.xyz);

	worldDir /= -viewDir.z;

	#if defined END && defined ENDER_BEAMS
	// True unit world direction (NO perspective divisor): sample the beams as
	// world-anchored vertical columns so they stay put when you turn, instead of
	// feeling like a flat plane painted in front of the camera. beamDist is then a
	// world-space distance along this direction (uniform reach in every direction).
	vec3 beamWorldDir = normalize(worldPos.xyz);
	// Render beams as a background-sky layer: only on sky pixels, so the landscape
	// (near terrain AND Distant Horizons terrain) always sits in front of them.
	bool beamIsSky = (z0 >= 1.0);
	#ifdef DISTANT_HORIZONS
	if (beamIsSky && texture2D(dhDepthTex0, texCoord).r < 1.0) beamIsSky = false;
	#endif
	#endif
	
	vec3 waterTint = waterColor.rgb / (waterColor.a * waterColor.a);
	waterTint = mix(vec3(1.0), waterTint, pow(waterAlpha, 0.25));
	
	#ifndef MCBL_FOG
	if (falloff < 0.0) {
		return vec3(0.0);
	}
	#endif

	#ifdef NETHER
	vec3 enableShadows = SampleShadow(vec3(0.0));
	totalShadow += max(enableShadows - vec3(1.0), vec3(0.0));
	#endif

	#if defined END && defined ENDER_BEAMS
	vec3 enderBeams = vec3(0.0);
	float beamVdotU = dot(normalize(viewPos.xyz), upVec);
	#endif

	for(int i = 0; i < LS_SAMPLE_COUNT; i++) {
		float currentDist = exp2(i + dither) - 0.95;

		if (IsRayMarcherHit(currentDist, maxDist, linearZ0, linearZ1, translucent)) break;

		#if defined END && LIGHT_SHAFT_END_NEAR > 0.0
		// Near-clip: fade the shaft in past LIGHT_SHAFT_END_NEAR blocks so it doesn't
		// start right at the camera. Smooth ramp over the following ~32 blocks.
		float nearClip = smoothstep(LIGHT_SHAFT_END_NEAR, LIGHT_SHAFT_END_NEAR + 32.0, currentDist);
		#else
		float nearClip = 1.0;
		#endif
		
		vec3 shadowTint = GetShadowTint(translucent, waterTint, currentDist, linearZ0);

		vec3 sampleWorldPos = worldDir * currentDist;
		vec3 sampleShadowPos = ToShadow(sampleWorldPos);
		sampleShadowPos = DistortShadow(sampleShadowPos);

		sampleShadowPos.z += 0.0512 / shadowMapResolution;

		vec3 shadow = vec3(0.0);
		vec3 blocklight = vec3(0.0);

		#ifdef END
		#ifdef END_GODRAYS
		float noise3D = Get3DNoise(shadow, sampleWorldPos);
		#else
		float noise3D = 0.5;
		#endif
		#ifdef ENDER_BEAMS
		// Occlude beams at the nearest surface (linearZ0 = glass/opaque), so they don't
		// render at full strength behind transparent blocks and make glass look hollow.
		// Denoise: sub-sample the beam noise END_BEAM_SAMPLES times within this march step
		// (stratified, stable dither) and average. The beam pattern is too fine for the
		// sparse 8-step march alone, so this trades GPU cost for a smooth result.
		vec3 beamStep = vec3(0.0);
		for (int j = 0; j < END_BEAM_SAMPLES; j++) {
			float beamDist = exp2(float(i) + (float(j) + beamDither) / float(END_BEAM_SAMPLES)) - 0.95;
			if (beamIsSky) beamStep += DrawEnderBeams(beamVdotU, beamWorldDir * beamDist);
		}
		enderBeams += beamStep / float(END_BEAM_SAMPLES) * nearClip;
		#endif
		#endif

		#ifndef NETHER
		bool isShadowSampleValid = IsSampleInShadowmap(sampleShadowPos);
		#ifdef MCBL_FOG
		isShadowSampleValid = isShadowSampleValid && falloff > 0.0;
		#endif

		if (isShadowSampleValid) {
			shadow = SampleShadow(sampleShadowPos);
		} else {
			shadow = vec3(1.0);
		}
		shadow *= shadowTint;
		#ifdef END
		#ifdef END_GODRAYS
		// Use only the vertical noise to structure the shaft; bypass the shadow map so
		// terrain shadows don't form a second shaft toward the End's sun direction.
		shadow = shadowTint * mix(0.5, noise3D, LIGHT_SHAFT_END_SMOKE);
		#else
		shadow = shadowTint * 0.5; // godrays off: clean glow, no streaks
		#endif
		#endif

		totalShadow += shadow * nearClip;
		#endif

		#if defined MULTICOLORED_BLOCKLIGHT && defined MCBL_FOG
		vec3 blocklightSampleWorldPos = sampleWorldPos;
		#ifdef WORLD_CURVATURE
		blocklightSampleWorldPos.y += dot(blocklightSampleWorldPos.xz, blocklightSampleWorldPos.xz) / WORLD_CURVATURE_SIZE;
		#endif

		vec3 voxelMapPos = WorldToVoxel(blocklightSampleWorldPos);

		if (IsInVoxelMapVolume(voxelMapPos)) {
			voxelMapPos /= voxelMapSize;

			int iFrameMod2 = int(frameCounter % 2);

			if (iFrameMod2 == 0) {
				blocklight = texture3D(lighttex0, voxelMapPos).rgb;
			} else {
				blocklight = texture3D(lighttex1, voxelMapPos).rgb;
			}

			blocklight *= currentDist;
			blocklight *= step(length(sampleWorldPos.xz), maxDist);
		}
		blocklight *= shadowTint;
		#ifdef END
		blocklight *= noise3D;
		#endif

		totalBlocklight += blocklight;
		#endif
	}

	totalShadow = ApplyLightShaftMultiplier(totalShadow, falloff);
	
	#if defined MULTICOLORED_BLOCKLIGHT && defined MCBL_FOG
	totalBlocklight = ApplyBlocklightFogMultiplier(totalBlocklight, maxDist);
	#endif

	vl = (totalShadow + totalBlocklight) / float(LS_SAMPLE_COUNT);
	vl *= 1.0 - max(blindFactor, darknessFactor);

	vl = pow(vl / 32.0, vec3(0.25));
	#if defined END && defined ENDER_BEAMS
	vl += enderBeams / float(LS_SAMPLE_COUNT) * END_BEAM_I;
	#endif

	#if defined END && defined END_BOTTOM_DARKEN
	// Darken the End light-shaft atmosphere toward the bottom of the world. This is the
	// layer that gets added back over the sky after the deferred darkening, so without
	// this the lower sky stays bright/purple. Top-oriented beams (near zenith) are left
	// untouched since the factor is ~1 there.
	float endDownDot = dot(normalize(viewPos), upVec);
	float endDownAmount = pow(clamp(-endDownDot * 0.5 + 0.5, 0.0, 1.0), 16.0 / END_BOTTOM_DARK_SIZE);
	vl *= mix(1.0, END_BOTTOM_DARK, endDownAmount);
	#endif

	if(dot(vl, vl) > 0.0) vl += (dither - 0.25) / 128.0;
	
	return vl;
}