#ifndef FOXY_MATERIAL_REFLECTION_GLSL
#define FOXY_MATERIAL_REFLECTION_GLSL

#if FOXY_MATERIAL_REFLECTIONS == 1
#include "/features/material/reflection_policy.glsl"
#include "/lib/contracts/backend.glsl"

vec2 MaterialReflectionStbn() {
	vec2 stbnPixel = mod(floor(gl_FragCoord.xy), vec2(128.0));
	float stbnFrame = mod(float(frameCounter), 64.0);
	return texture3D(
		cloudStbnVec2,
		vec3((stbnPixel + vec2(0.5)) * (1.0 / 128.0), (stbnFrame + 0.5) * (1.0 / 64.0))
	).rg;
}

// Visible-normal GGX sampling based on Heitz's stretched-view construction.
// The microfacet lobe changes the traced direction itself; rough reflection is
// therefore produced by scene rays over time instead of a large post-hit blur.
vec3 SampleMaterialGgxVndf(
	const in vec3 tangentView,
	const in float alpha,
	const in vec2 noise
) {
	vec3 stretchedView = normalize(vec3(alpha * tangentView.xy, max(tangentView.z, 1.0e-5)));
	float lensq = dot(stretchedView.xy, stretchedView.xy);
	vec3 tangentX = lensq > 1.0e-8
		? vec3(-stretchedView.y, stretchedView.x, 0.0) * inversesqrt(lensq)
		: vec3(1.0, 0.0, 0.0);
	vec3 tangentY = cross(stretchedView, tangentX);
	float radius = sqrt(noise.x);
	float phi = (2.0 * PI) * noise.y;
	float diskX = radius * cos(phi);
	float diskY = radius * sin(phi);
	float visibleBlend = 0.5 * (1.0 + stretchedView.z);
	diskY = mix(
		sqrt(max(0.0, 1.0 - diskX * diskX)),
		diskY,
		visibleBlend
	);
	float diskZ = sqrt(max(0.0, 1.0 - diskX * diskX - diskY * diskY));
	vec3 stretchedNormal =
		tangentX * diskX + tangentY * diskY + stretchedView * diskZ;
	return normalize(vec3(
		alpha * stretchedNormal.xy,
		max(stretchedNormal.z, 0.0)
	));
}

vec3 MaterialRoughReflectionDirection(
	const in vec3 incidentView,
	const in vec3 normalView,
	const in float alpha,
	const in float perceptualRoughness
) {
	vec3 mirrorDirection = normalize(reflect(incidentView, normalView));
	// Preserve exact mirrors and avoid spending temporal variance on a lobe
	// narrower than the packed 4-bit roughness representation can express.
	if (perceptualRoughness <= 0.085) return mirrorDirection;

	vec3 tangentX = abs(normalView.y) < 0.999
		? normalize(cross(vec3(0.0, 1.0, 0.0), normalView))
		: normalize(cross(vec3(1.0, 0.0, 0.0), normalView));
	vec3 tangentY = cross(normalView, tangentX);
	vec3 viewDirection = -incidentView;
	vec3 tangentView = vec3(
		dot(viewDirection, tangentX),
		dot(viewDirection, tangentY),
		max(dot(viewDirection, normalView), 1.0e-5)
	);
	vec3 microTangent = SampleMaterialGgxVndf(
		tangentView,
		clamp(alpha, 0.002, 1.0),
		MaterialReflectionStbn()
	);
	vec3 microNormal = normalize(
		tangentX * microTangent.x +
		tangentY * microTangent.y +
		normalView * microTangent.z
	);
	vec3 sampledDirection = normalize(reflect(incidentView, microNormal));
	// Very rough grazing VNDF samples can fall below the macrosurface. A stable
	// mirror fallback is preferable to isolated black temporal impulses here.
	return dot(sampledDirection, normalView) > 0.001
		? sampledDirection
		: mirrorDirection;
}

float MaterialReflectionSampleWeight(
	const in vec3 normalView,
	const in vec3 incidentView,
	const in vec3 reflectedDir,
	const in float alpha
) {
	float normalViewDot = max(dot(normalView, -incidentView), 1.0e-2);
	float normalLight = max(dot(normalView, reflectedDir), 0.0);
	if (normalLight <= 0.0) return 0.0;
	float alphaSquared = alpha * alpha;
	float viewRoot = sqrt(
		(-normalViewDot * alphaSquared + normalViewDot) * normalViewDot + alphaSquared
	);
	float lightRoot = sqrt(
		(-normalLight * alphaSquared + normalLight) * normalLight + alphaSquared
	);
	float numerator = normalLight * (normalViewDot + viewRoot);
	float denominator = normalViewDot * lightRoot + normalLight * viewRoot;
	return min(numerator / max(denominator, 1.0e-5), 4.0);
}

struct MaterialReflectionTrace {
	vec2 sampleUv;
	float hitCoverage;
	float hit;
	float skyVisible;
};

MaterialReflectionTrace TraceMaterialReflection(const in vec3 viewPos, const in vec3 reflectedDir, const in int traceSteps) {
	MaterialReflectionTrace trace;
	trace.sampleUv = texcoord;
	trace.hitCoverage = 0.0;
	trace.hit = 0.0;
	trace.skyVisible = 0.0;
	vec3 startView = viewPos + reflectedDir * 0.08;
	float rayLength = clamp(max(-viewPos.z * 1.6, 18.0), 18.0, 96.0);
	if (reflectedDir.z > 0.0001) {
		float nearLimit = (-near - startView.z) / reflectedDir.z;
		if (nearLimit <= 0.01) return trace;
		rayLength = min(rayLength, nearLimit * 0.98);
	}
	vec3 endView = startView + reflectedDir * rayLength;
	vec4 startProjected = ProjectViewPos(startView);
	vec4 endProjected = ProjectViewPos(endView);
	if (startProjected.w <= 0.0 || endProjected.w <= 0.0) {
		return trace;
	}

	vec2 screenDelta = endProjected.xy - startProjected.xy;
	float startK = 1.0 / SafeDivisor(startProjected.w);
	float endK = 1.0 / SafeDivisor(endProjected.w);
	vec2 startQzAndK = vec2(startView.z * startK, startK);
	vec2 endQzAndK = vec2(endView.z * endK, endK);
	vec2 temporalPhase = vec2(float(frameCounter) * 0.75487765, float(frameCounter) * 0.56984026);
	float jitter = 0.35 + Hash12(floor(gl_FragCoord.xy) + temporalPhase) * 0.45;
	float previousT = 0.0;
	float previousValidT = 0.0;
	float previousDelta = -1.0e20;
	float previousValid = 0.0;

	for (int i = 0; i < FOXY_MATERIAL_REFLECTION_STEPS; i++) {
		if (i >= traceSteps) break;
		float t = (float(i) + jitter) / float(traceSteps);
		vec2 rawUv = startProjected.xy + screenDelta * t;
		if (rawUv.x <= 0.001 || rawUv.x >= 0.999 || rawUv.y <= 0.001 || rawUv.y >= 0.999) break;
		vec2 uv = PixelCenter(rawUv);
		vec2 sampleUv = WaterCurrentRenderUv(uv);
		float sampleValid;
		float sceneDepth = WaterSceneLinearDepth(uv, sampleUv, sampleValid);
		trace.skyVisible = max(trace.skyVisible, 1.0 - sampleValid);
		if (sampleValid > 0.5) {
			float rayDepth = TraceViewDepth(t, startQzAndK, endQzAndK);
			float previousRayDepth = TraceViewDepth(previousT, startQzAndK, endQzAndK);
			float thickness = clamp(sceneDepth * 0.012 + abs(rayDepth - previousRayDepth) * 1.5, 0.12, 2.2);
			float delta = rayDepth - sceneDepth;
			if (delta >= -thickness * 0.25 && delta <= thickness) {
				// Refine a real front-to-back crossing analytically.  This removes
				// step-sized reflection wobble without another depth lookup.
				float refinedT = t;
				float crossingDenominator = delta - previousDelta;
				if (previousValid > 0.5 && previousDelta < 0.0 && delta >= 0.0 && abs(crossingDenominator) > 1.0e-5) {
					refinedT = mix(previousValidT, t, clamp(-previousDelta / crossingDenominator, 0.0, 1.0));
				}
				vec2 refinedUv = PixelCenter(startProjected.xy + screenDelta * refinedT);
				trace.sampleUv = refinedUv;
				trace.hitCoverage = ScreenEdgeFade(refinedUv);
				trace.hit = 1.0;
				return trace;
			}
			previousDelta = delta;
			previousValidT = t;
			previousValid = 1.0;
		}
		previousT = t;
	}
	return trace;
}

#if FOXY_MATERIAL_REFLECTION_GLOBAL == 1 && FOXY_VOXEL_GI_ACTIVE == 1
// A screen-space miss continues through the existing VRTGI voxel scene. The
// ray is still the current GGX sample and reaches the existing reflection
// filters/history, so this introduces no independent noisy signal.
vec4 TraceGlobalMaterialReflection(
	const in vec3 viewPos,
	const in vec3 worldNormal,
	const in vec3 reflectedWorldDir
) {
	vec3 originPlayerPos = (gbufferModelViewInverse * vec4(viewPos, 1.0)).xyz;
	vec3 voxelRayOrigin = VoxelGridSceneToGrid(
		originPlayerPos,
		cameraPosition
	) + worldNormal * 0.020 + reflectedWorldDir * 0.035;

	uint hitPayload;
	float hitDistance;
	ivec3 hitCell;
	vec3 hitNormal;
	vec3 sphereRadiance;
	vec3 rayTransmittance;
	int traceResult = VoxelGiTrace(
		voxelRayOrigin,
		reflectedWorldDir,
		min(FOXY_VOXEL_GI_MAX_DISTANCE, 48.0),
		min(FOXY_VOXEL_GI_TRACE_ITERATIONS, 48),
		true,
		hitPayload,
		hitDistance,
		hitCell,
		hitNormal,
		sphereRadiance,
		rayTransmittance
	);

	vec3 reflectedRadiance = sphereRadiance;
	float resolved = max(max(sphereRadiance.r, sphereRadiance.g), sphereRadiance.b) > 1.0e-5
		? 1.0
		: 0.0;
	if (traceResult == FOXY_VOXEL_GI_TRACE_SURFACE_HIT) {
		// Hit radiance no longer treats a voxel as a sky emitter. Keep the shared
		// ABI value, but do not fetch the obsolete hemispherical sky statistic.
		vec3 neutralSkyMeanRadiance = vec3(0.0);
		vec3 directLightView = normalize(shadowLightPosition);
		vec3 directLightWorldDirection = normalize(
			mat3(gbufferModelViewInverse) * directLightView
		);
		float directLightCosine = max(
			dot(hitNormal, directLightWorldDirection),
			0.0
		);
		float directLightAltitude = dot(
			 directLightView,
			normalize(upPosition)
		);
		float directLightVisibility = 0.0;
		float hitSkyLight = VoxelSkyLight(hitPayload);
		vec3 directLightRadiance = vec3(0.0);
		#if defined(FOXY_DIM_END)
			directLightWorldDirection = EndSunWorldDirection();
			directLightCosine = max(dot(hitNormal, directLightWorldDirection), 0.0);
			directLightVisibility = directLightCosine > 0.001 ? 1.0 : 0.0;
			directLightRadiance = vec3(0.62, 0.72, 1.00);
		#else
			if (
				directLightAltitude > 0.001 &&
				directLightCosine > 0.001 &&
				hitSkyLight > 0.0001
			) {
				vec3 hitPosition = voxelRayOrigin + reflectedWorldDir * hitDistance;
				directLightRadiance = DecodeSkyLutColor(texelFetch(
					colortex7,
					SkyDirectSunColorTexel(),
					0
				).rgb);
				directLightVisibility = VoxelGiDirectVisibility(
					hitPosition,
					hitNormal,
					directLightCosine
				);
			}
		#endif
		vec3 hitRadiance = VoxelGiHitRadiance(
			hitPayload,
			VoxelGiAlbedo(hitPayload),
			hitNormal,
			hitSkyLight,
			neutralSkyMeanRadiance,
			directLightRadiance,
			directLightCosine,
			directLightVisibility,
			true,
			1.0,
			1.0
		);
		reflectedRadiance += rayTransmittance * hitRadiance;
		resolved = 1.0;
	} else if (traceResult == FOXY_VOXEL_GI_TRACE_DOMAIN_EXIT) {
		// Match diffuse transport: sky is visible only when this reflected ray
		// physically leaves through the top of the traced voxel volume.
		float topExit = step(
			float(VOXEL_GRID_SIZE) - 1.0e-3,
			voxelRayOrigin.y + reflectedWorldDir.y * hitDistance
		);
		#if !defined(FOXY_DIM_NETHER) && !defined(FOXY_DIM_END)
			if (topExit > 0.0) {
				reflectedRadiance += rayTransmittance * DecodeSkyLutColor(
					texture2D(colortex7, SsptSkyLutUv(reflectedWorldDir)).rgb
				) * FOXY_VOXEL_GI_SKY_BRIGHTNESS;
				resolved = 1.0;
			}
		#endif
	}
	return vec4(max(reflectedRadiance, vec3(0.0)), resolved);
}
#endif

vec4 TraceMaterialReflectionSignal(
	const in vec3 sceneColor,
	const in vec2 viewUv,
	const in vec2 renderUv,
	const in vec4 packedSurface
) {
	BackendOpaqueSurface opaqueSurface = BackendResolveOpaqueSurface(
		viewUv,
		renderUv,
		texture2D(depthtex1, renderUv).r,
		gbufferProjectionInverse
	);
	float depthRaw = opaqueSurface.rawDepth;
	PtGbufferSample material = PtDecodeGbuffer(packedSurface, depthRaw);
	if (material.valid < 0.5) return vec4(0.0);

	float perceptualRoughness = sqrt(Saturate(material.roughness));
	float smoothness = MaterialReflectionSmoothness(perceptualRoughness);
	if (MaterialReflectionSurfaceEnabled(
		material.valid,
		material.surfaceClass,
		perceptualRoughness,
		material.metalness
	) < 0.5) return vec4(0.0);

	float traceSelected = 1.0;
	#if FOXY_MATERIAL_REFLECTION_HIGH_QUALITY == 0
		ivec2 tracePixel = ivec2(gl_FragCoord.xy);
		int pixelPhase = (tracePixel.x & 1) | ((tracePixel.y & 1) << 1);
		traceSelected = float(pixelPhase == (frameCounter & 3));
	#endif
	if (traceSelected < 0.5) return vec4(0.0);

	vec3 viewPos = opaqueSurface.viewPosition;
	vec3 incidentView = normalize(viewPos);
	mat3 viewToWorld = mat3(gbufferModelViewInverse);
	vec3 normalView = normalize(vec3(
		dot(viewToWorld[0], material.worldNormal),
		dot(viewToWorld[1], material.worldNormal),
		dot(viewToWorld[2], material.worldNormal)
	));
	vec3 geometricNormalView = normalize(vec3(
		dot(viewToWorld[0], material.worldGeometricNormal),
		dot(viewToWorld[1], material.worldGeometricNormal),
		dot(viewToWorld[2], material.worldGeometricNormal)
	));
	if (dot(geometricNormalView, -incidentView) < 0.0) {
		geometricNormalView = -geometricNormalView;
	}
	normalView = PbrVisibleNormal(
		normalView,
		geometricNormalView,
		-incidentView
	);
	vec3 sampledDirection = MaterialRoughReflectionDirection(
		incidentView,
		normalView,
		material.roughness,
		perceptualRoughness
	);
	float sampleWeight = MaterialReflectionSampleWeight(
		normalView,
		incidentView,
		sampledDirection,
		material.roughness
	);
	vec3 reflectedDir = MaterialReflectionAboveSurface(
		sampledDirection,
		geometricNormalView
	);
	vec3 reflectedWorldDir = normalize(mat3(gbufferModelViewInverse) * reflectedDir);
	vec3 reflectionWorldNormal = normalize(mat3(gbufferModelViewInverse) * normalView);
	int traceSteps = FOXY_MATERIAL_REFLECTION_STEPS;
	#if FOXY_MATERIAL_REFLECTION_HIGH_QUALITY == 0
		traceSteps = min(traceSteps, 8);
	#endif
	MaterialReflectionTrace hit = TraceMaterialReflection(viewPos, reflectedDir, traceSteps);

	float skyAccess = smoothstep(0.22, 0.82, material.lightmap.y);
	skyAccess *= skyAccess;
	float skyWeight = smoothstep(-0.08, 0.12, reflectedWorldDir.y) * skyAccess * hit.skyVisible;
	vec3 localFallback = sceneColor * mix(0.055, 0.12, skyAccess);
	vec3 fallback = localFallback;
	if (skyWeight > 0.0) {
		vec3 skyReflection = DecodeSkyLutColor(texture2D(
			colortex7,
			SkyViewLutUv(reflectedWorldDir)
		).rgb);
		fallback = mix(localFallback, skyReflection, skyWeight);
	}
	vec3 reflectedColor = fallback;
	if (hit.hit > 0.5) {
		vec3 screenReflection = max(ReflectionSample(hit.sampleUv), vec3(0.0));
		reflectedColor = mix(fallback, screenReflection, hit.hitCoverage);
	}
	#if FOXY_MATERIAL_REFLECTION_GLOBAL == 1 && FOXY_VOXEL_GI_ACTIVE == 1
	else {
		float NoV = max(dot(normalView, -incidentView), 0.0);
		vec3 metalF0 = clamp(material.albedo * 1.12 + vec3(0.06), vec3(0.12), vec3(0.96));
		float f0Energy = max(
			mix(
				max(FOXY_PBR_FALLBACK_F0, 0.02),
				max(metalF0.r, max(metalF0.g, metalF0.b)),
				material.metalness
			),
			0.0
		);
		if (MaterialReflectionGlobalEnabled(smoothness, f0Energy, NoV) > 0.5) {
			vec4 globalReflection = TraceGlobalMaterialReflection(
				viewPos,
				reflectionWorldNormal,
				reflectedWorldDir
			);
			reflectedColor = mix(fallback, globalReflection.rgb, globalReflection.a);
		}
	}
	#endif
	return vec4(max(reflectedColor, vec3(0.0)) * sampleWeight, 1.0);
}

#endif

#endif
