float3 safeNormalize(float3 value)
{
	float len = max(length(value), 0.0001);
	return value / len;
}

float luma709(float3 color)
{
	return dot(color, float3(0.2126, 0.7152, 0.0722));
}

float2 chroma709(float3 color)
{
	float y = luma709(color);
	return float2((color.b - y) * 0.5647, (color.r - y) * 0.7132);
}

float3 matteSampleColor(float2 uv, ShaderContext context)
{
	float2 pixel = 1.0 / max(context.outputResolution, float2(1.0, 1.0));
	float blur = max(screenPreBlur, 0.0);
	float3 center = saturate(sampleVideo(saturate(uv)).rgb);
	if (blur <= 0.0001)
		return center;

	// Pre-blur only the color used for screen comparison; the final image keeps
	// its original detail and alpha is refined in a later pass.
	float2 radius = pixel * blur;
	float3 color = center * 0.36;
	color += saturate(sampleVideo(saturate(uv + float2(radius.x, 0.0))).rgb) * 0.16;
	color += saturate(sampleVideo(saturate(uv - float2(radius.x, 0.0))).rgb) * 0.16;
	color += saturate(sampleVideo(saturate(uv + float2(0.0, radius.y))).rgb) * 0.16;
	color += saturate(sampleVideo(saturate(uv - float2(0.0, radius.y))).rgb) * 0.16;
	return color;
}

float keyDistanceAt(float2 uv, ShaderContext context)
{
	float3 color = matteSampleColor(uv, context);
	float3 keyColor = saturate(screenColor.rgb);
	float chromaDistance = distance(chroma709(color), chroma709(keyColor)) * 2.65;
	// Direction distance is less sensitive to brightness, while chroma distance
	// follows broadcast-style color difference; screenBalance blends the two.
	float directionDistance = length(safeNormalize(max(color, float3(0.0001, 0.0001, 0.0001))) - safeNormalize(max(keyColor, float3(0.0001, 0.0001, 0.0001)))) * 0.55;
	return lerp(directionDistance, chromaDistance, saturate(screenBalance));
}

float rawAlphaAt(float2 uv, ShaderContext context)
{
	float keyDistance = keyDistanceAt(uv, context);
	float matteCenter = threshold + erodeDilate;
	float matteFeather = max(softness, 0.0005);
	float alpha = smoothstep(matteCenter - matteFeather, matteCenter + matteFeather, keyDistance);
	return saturate(alpha);
}

float matteAlphaAt(float2 uv)
{
	return saturate(sampleVideo(saturate(uv)).a);
}

float refinedAlphaFromMatte(float2 uv, ShaderContext context)
{
	float2 pixel = 1.0 / max(context.outputResolution, float2(1.0, 1.0));
	float blur = max(matteBlur, 0.0);
	float aaRadius = max(blur, 0.65);
	float centerAlpha = matteAlphaAt(uv);
	float alpha = centerAlpha * 0.30;

	if (aaRadius > 0.0001)
	{
		// A small fixed kernel smooths edges and collects min/max alpha for
		// black/white cleanup without needing dynamic loops or arrays.
		float2 radius = pixel * aaRadius;
		float2 halfRadius = radius * 0.5;
		float alphaMin = centerAlpha;
		float alphaMax = centerAlpha;
		float sampleAlpha = matteAlphaAt(uv + float2(halfRadius.x, 0.0));
		alpha += sampleAlpha * 0.065;
		alphaMin = min(alphaMin, sampleAlpha);
		alphaMax = max(alphaMax, sampleAlpha);
		sampleAlpha = matteAlphaAt(uv - float2(halfRadius.x, 0.0));
		alpha += sampleAlpha * 0.065;
		alphaMin = min(alphaMin, sampleAlpha);
		alphaMax = max(alphaMax, sampleAlpha);
		sampleAlpha = matteAlphaAt(uv + float2(0.0, halfRadius.y));
		alpha += sampleAlpha * 0.065;
		alphaMin = min(alphaMin, sampleAlpha);
		alphaMax = max(alphaMax, sampleAlpha);
		sampleAlpha = matteAlphaAt(uv - float2(0.0, halfRadius.y));
		alpha += sampleAlpha * 0.065;
		alphaMin = min(alphaMin, sampleAlpha);
		alphaMax = max(alphaMax, sampleAlpha);
		sampleAlpha = matteAlphaAt(uv + float2(radius.x, 0.0));
		alpha += sampleAlpha * 0.06;
		alphaMin = min(alphaMin, sampleAlpha);
		alphaMax = max(alphaMax, sampleAlpha);
		sampleAlpha = matteAlphaAt(uv - float2(radius.x, 0.0));
		alpha += sampleAlpha * 0.06;
		alphaMin = min(alphaMin, sampleAlpha);
		alphaMax = max(alphaMax, sampleAlpha);
		sampleAlpha = matteAlphaAt(uv + float2(0.0, radius.y));
		alpha += sampleAlpha * 0.06;
		alphaMin = min(alphaMin, sampleAlpha);
		alphaMax = max(alphaMax, sampleAlpha);
		sampleAlpha = matteAlphaAt(uv - float2(0.0, radius.y));
		alpha += sampleAlpha * 0.06;
		alphaMin = min(alphaMin, sampleAlpha);
		alphaMax = max(alphaMax, sampleAlpha);
		sampleAlpha = matteAlphaAt(uv + radius);
		alpha += sampleAlpha * 0.05;
		alphaMin = min(alphaMin, sampleAlpha);
		alphaMax = max(alphaMax, sampleAlpha);
		sampleAlpha = matteAlphaAt(uv - radius);
		alpha += sampleAlpha * 0.05;
		alphaMin = min(alphaMin, sampleAlpha);
		alphaMax = max(alphaMax, sampleAlpha);
		sampleAlpha = matteAlphaAt(uv + float2(radius.x, -radius.y));
		alpha += sampleAlpha * 0.05;
		alphaMin = min(alphaMin, sampleAlpha);
		alphaMax = max(alphaMax, sampleAlpha);
		sampleAlpha = matteAlphaAt(uv + float2(-radius.x, radius.y));
		alpha += sampleAlpha * 0.05;
		alphaMin = min(alphaMin, sampleAlpha);
		alphaMax = max(alphaMax, sampleAlpha);

		alpha = lerp(alpha, alphaMin, saturate(blackCleanup));
		alpha = lerp(alpha, alphaMax, saturate(whiteCleanup));
	}
	else
	{
		alpha = centerAlpha;
	}

	// Final matte shaping happens after blur/cleanup so clip and contrast affect
	// the refined edge rather than the raw screen-distance estimate.
	alpha = saturate((alpha - clipBlack) / max(clipWhite - clipBlack, 0.0001));
	alpha = saturate((alpha - 0.5) * max(matteContrast, 0.0001) + 0.5);
	alpha = pow(max(alpha, 0.0), max(matteGamma, 0.0001));
	return saturate(alpha);
}

float spillAmountForColor(float3 color)
{
	float3 keyColor = saturate(screenColor.rgb);
	// Measure spill as color energy aligned with the screen color minus the
	// strongest opposing channel, leaving neutral highlights mostly intact.
	float keyComponent = dot(color, safeNormalize(max(keyColor, float3(0.0001, 0.0001, 0.0001))));
	float opposingComponent = max(max(color.r * (1.0 - keyColor.r), color.g * (1.0 - keyColor.g)), color.b * (1.0 - keyColor.b));
	return saturate(keyComponent - opposingComponent + despillBias);
}

float3 despillColor(float3 color, float alpha)
{
	float3 keyColor = safeNormalize(max(screenColor.rgb, float3(0.0001, 0.0001, 0.0001)));
	float spill = spillAmountForColor(color) * despill * (1.0 - alpha * 0.35);
	float neutral = luma709(color);
	float3 neutralized = color - keyColor * spill;
	neutralized = max(neutralized, float3(0.0, 0.0, 0.0));
	neutralized = lerp(neutralized, float3(neutral, neutral, neutral), spill * 0.18);
	neutralized = lerp(neutralized, neutralized * saturate(spillTint.rgb), saturate(spill));
	return saturate(neutralized);
}

float cropMaskAt(float2 uv, ShaderContext context)
{
	float2 feather = 1.0 / max(context.outputResolution, float2(1.0, 1.0));
	float left = smoothstep(saturate(cropLeft), saturate(cropLeft) + feather.x, uv.x);
	float right = 1.0 - smoothstep(1.0 - saturate(cropRight) - feather.x, 1.0 - saturate(cropRight), uv.x);
	float top = smoothstep(saturate(cropTop), saturate(cropTop) + feather.y, uv.y);
	float bottom = 1.0 - smoothstep(1.0 - saturate(cropBottom) - feather.y, 1.0 - saturate(cropBottom), uv.y);
	return saturate(left * right * top * bottom);
}

float4 buildRawMatte(ShaderContext context)
{
	float4 src = context.sourceColor;
	float3 color = saturate(src.rgb);
	float alpha = rawAlphaAt(context.uv, context);
	return float4(color, alpha);
}

float4 refineMatte(ShaderContext context)
{
	float4 raw = sampleVideo(context.uv);
	float alpha = refinedAlphaFromMatte(context.uv, context);
	return float4(saturate(raw.rgb), alpha);
}

float4 applyKey(ShaderContext context)
{
	float4 keyed = sampleVideo(context.uv);
	float3 color = saturate(keyed.rgb);
	float alpha = saturate(keyed.a);
	float spill = spillAmountForColor(color);
	float3 despilled = despillColor(color, alpha);
	float cropMask = cropMaskAt(context.uv, context);
	alpha *= cropMask;

	// Edge recovery is strongest around 50% alpha, where fringing usually lives,
	// and fades away for solid foreground/background pixels.
	float edgeAmount = saturate(1.0 - abs(alpha * 2.0 - 1.0));
	despilled = lerp(despilled, despilled * saturate(edgeColor.rgb), edgeAmount * saturate(edgeRecover));

	if (viewMode == 1)
		return float4(alpha, alpha, alpha, 1.0);
	if (viewMode == 2)
		return float4(spill, spill * 0.55, 0.0, 1.0);
	if (viewMode == 3)
		return float4(despilled, 1.0);
	if (viewMode == 4)
	{
		float rawAlpha = rawAlphaAt(context.uv, context) * cropMask;
		return float4(rawAlpha, alpha, spill, 1.0);
	}

	float3 premultiplied = saturate(despilled) * alpha;
	return float4(premultiplied, alpha);
}

float4 shadeVideo(ShaderContext context)
{
	return applyKey(context);
}
