// Scene-referred automatic exposure for NOISE.
//
// Deferred writes log2 luminance into colortex5.a. Iris/OptiFine build a mip
// pyramid before composite, turning every meter tap into the dense average of
// a large image region. The final robust average still runs only once per
// fullscreen vertex. Exposure history lives in two reserved R8 metadata texels.
// Logarithmic encoding covers 0.25-4.0 exposure with sub-percent half-step error.
#ifndef NOISE_EXPOSURE_GLSL
#define NOISE_EXPOSURE_GLSL

#include "/lib/noise_common.glsl"
#include "/lib/noise_options.glsl"
#include "/lib/noise_render_scale.glsl"

const float NOISE_EXPOSURE_HISTORY_MARKER = 173.0 / 255.0;

float noiseEncodeExposureHistory(float exposure) {
	return clamp((log2(clamp(exposure, 0.25, 4.0)) + 2.0) * 0.25,
		0.0, 1.0);
}

float noiseDecodeExposureHistory(float encodedExposure) {
	return exp2(clamp(encodedExposure, 0.0, 1.0) * 4.0 - 2.0);
}

bool noiseExposureHistoryValid(float marker, int frameIndex) {
	return frameIndex > 2 &&
		abs(marker - NOISE_EXPOSURE_HISTORY_MARKER) < 0.75 / 255.0;
}

// Robust centre-weighted log-average metering. Unlike sparse full-resolution
// taps, every sample below represents hundreds or thousands of source pixels.
// A second pass winsorises bright windows and black voids in photographic EV,
// following the same two-stage idea used by FastPBR's exposure scan.
float noiseMeterSceneLuminance(
	sampler2D logLuminanceTexture,
	vec2 viewSize
) {
	vec2 safeViewSize = max(viewSize, vec2(1.0));
	vec2 renderScale = noiseRenderScale2(safeViewSize);
	vec2 viewportSize = max(floor(safeViewSize * renderScale), vec2(1.0));
	float minimumViewportDimension = max(min(viewportSize.x, viewportSize.y), 1.0);
	float meterLod = max(floor(log2(minimumViewportDimension)) - 2.0, 0.0);
	vec2 halfMipFootprint = 0.55 * exp2(meterLod) / safeViewSize;
	vec2 logicalMargin = clamp(halfMipFootprint / max(renderScale, vec2(1e-4)),
		vec2(0.025), vec2(0.18));
	float averageLogLuminance = 0.0;
	float totalWeight = 0.0;

	for (int sampleY = 0; sampleY < 5; ++sampleY) {
		for (int sampleX = 0; sampleX < 8; ++sampleX) {
			vec2 gridUv = (vec2(sampleX, sampleY) + 0.5) / vec2(8.0, 5.0);
			vec2 logicalUv = mix(logicalMargin, vec2(1.0) - logicalMargin, gridUv);
			vec2 physicalUv = logicalUv * renderScale;
			float centreDistance = length((logicalUv - 0.5) * vec2(1.0, 0.84));
			float sampleWeight = mix(1.0, 0.76,
				smoothstep(0.12, 0.66, centreDistance));
			float logLuminance = clamp(textureLod(logLuminanceTexture,
				physicalUv, meterLod).a, -8.0, 2.5);
			averageLogLuminance += logLuminance * sampleWeight;
			totalWeight += sampleWeight;
		}
	}
	averageLogLuminance /= max(totalWeight, 1e-4);

	float robustLogLuminance = 0.0;
	totalWeight = 0.0;
	for (int sampleY = 0; sampleY < 5; ++sampleY) {
		for (int sampleX = 0; sampleX < 8; ++sampleX) {
			vec2 gridUv = (vec2(sampleX, sampleY) + 0.5) / vec2(8.0, 5.0);
			vec2 logicalUv = mix(logicalMargin, vec2(1.0) - logicalMargin, gridUv);
			vec2 physicalUv = logicalUv * renderScale;
			float centreDistance = length((logicalUv - 0.5) * vec2(1.0, 0.84));
			float sampleWeight = mix(1.0, 0.76,
				smoothstep(0.12, 0.66, centreDistance));
			float logLuminance = clamp(textureLod(logLuminanceTexture,
				physicalUv, meterLod).a, -8.0, 2.5);
			logLuminance = clamp(logLuminance,
				averageLogLuminance - 2.20, averageLogLuminance + 1.40);
			robustLogLuminance += logLuminance * sampleWeight;
			totalWeight += sampleWeight;
		}
	}

	return exp2(robustLogLuminance / max(totalWeight, 1e-4));
}

float noiseAuthoredExposure(float daylight, float rainStrength) {
	#ifdef NETHER
	return 1.00;
	#elif defined(END)
	return 1.06;
	#else
	return mix(1.10, 0.68, noiseNight(daylight)) *
		mix(1.0, 0.82, rainStrength);
	#endif
}

float noiseExposureReferenceLuminance(float daylight, float rainStrength) {
	#ifdef NETHER
	return 0.075;
	#elif defined(END)
	return 0.055;
	#else
	return mix(0.035, 0.220, daylight) * mix(1.0, 0.78, rainStrength);
	#endif
}

float noiseTargetExposure(
	float meteredLuminance,
	float daylight,
	float rainStrength,
	int cameraMedium
) {
	float authoredExposure = noiseAuthoredExposure(daylight, rainStrength);
	#if EYE_ADAPTATION == 0
	return authoredExposure;
	#else
	float referenceLuminance = noiseExposureReferenceLuminance(daylight,
		rainStrength);
	float correctionStops = log2(referenceLuminance /
		max(meteredLuminance, 0.0015));
	// A sub-linear response keeps the authored mood while still giving caves
	// and bright exteriors several useful stops of automatic compensation.
	float targetExposure = authoredExposure *
		exp2(clamp(correctionStops, -1.25, 2.50) * 0.64);

	#ifdef NETHER
	targetExposure = clamp(targetExposure, 0.52, 2.60);
	#elif defined(END)
	targetExposure = clamp(targetExposure, 0.62, 3.00);
	#else
	targetExposure = clamp(targetExposure, 0.50, 3.20);
	#endif

	if (cameraMedium == 1) {
		// Water absorbs light strongly; allow adaptation without bleaching the
		// characteristic blue-green medium.
		targetExposure = clamp(targetExposure * 1.06, 0.62, 3.35);
	} else if (cameraMedium == 2) {
		// Lava occupies most of the view and must close exposure immediately.
		targetExposure = clamp(targetExposure, 0.30, 0.82);
	} else if (cameraMedium != 0) {
		targetExposure = clamp(targetExposure, 0.55, 2.40);
	}
	return targetExposure;
	#endif
}

float noiseAdaptExposure(float previousExposure, float targetExposure,
	float frameDuration) {
	#if EYE_ADAPTATION == 0
	return targetExposure;
	#else
	float previousStops = log2(clamp(previousExposure, 0.25, 4.0));
	float targetStops = log2(clamp(targetExposure, 0.25, 4.0));
	float stopDelta = targetStops - previousStops;
	// Sub-perceptual changes do not restart adaptation while foliage and shadow
	// edges move through the meter.
	const float deadZoneStops = 0.08;
	if (abs(stopDelta) <= deadZoneStops) return exp2(previousStops);
	float stableTargetStops = targetStops - sign(stopDelta) * deadZoneStops;
	float stableDelta = stableTargetStops - previousStops;
	float distanceStops = abs(stableDelta);
	float frameDurationSafe = clamp(frameDuration, 1.0 / 240.0, 0.05);

	// Unreal-style hybrid traversal in EV: large changes move linearly, then the
	// last 1.5 stops converge exponentially. Matching slopes at the transition
	// prevents the near-target jitter produced by a purely linear controller.
	const float transitionDistanceStops = 1.50;
	float speedStopsPerSecond = stableDelta < 0.0 ? 1.65 : 0.48;
	float stepMagnitude;
	if (distanceStops > transitionDistanceStops) {
		stepMagnitude = speedStopsPerSecond * frameDurationSafe;
	} else {
		float exponentialRate = speedStopsPerSecond / transitionDistanceStops;
		stepMagnitude = distanceStops *
			(1.0 - exp(-exponentialRate * frameDurationSafe));
	}
	float appliedStep = sign(stableDelta) * min(stepMagnitude, distanceStops);
	return exp2(previousStops + appliedStep);
	#endif
}

#endif
