static const float PI = 3.14159265358979323846;

float2 rotateAroundCenter(float2 uv, float radians)
{
	float2 centered = uv - 0.5;
	float s = sin(radians);
	float c = cos(radians);
	return float2(c * centered.x - s * centered.y, s * centered.x + c * centered.y) + 0.5;
}

float mirroredCoordinate(float coordinate)
{
	float wrapped = frac(coordinate * 0.5) * 2.0;
	return wrapped <= 1.0 ? wrapped : 2.0 - wrapped;
}

float2 applyEdgeMode(float2 uv, out bool inside)
{
	inside = uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0;

	if (edgeMode == 1)
		return clamp(uv, 0.0, 1.0);
	if (edgeMode == 2)
		return frac(uv);
	if (edgeMode == 3)
		return float2(mirroredCoordinate(uv.x), mirroredCoordinate(uv.y));

	return uv;
}

float selectedCropAspect()
{
	if (cropAspect == 1)
		return 4.0 / 3.0;
	if (cropAspect == 2)
		return 3.0 / 2.0;
	if (cropAspect == 3)
		return 1.0;
	if (cropAspect == 4)
		return 9.0 / 16.0;
	return 0.0;
}

bool insideCropWindow(float2 uv, float2 resolution)
{
	float targetAspect = selectedCropAspect();
	if (targetAspect <= 0.0)
		return true;

	float outputAspect = resolution.x / max(resolution.y, 1.0);
	float2 cropSize = float2(1.0, 1.0);
	if (outputAspect > targetAspect)
		cropSize.x = targetAspect / outputAspect;
	else
		cropSize.y = outputAspect / targetAspect;

	float2 cropMin = (1.0 - cropSize) * 0.5;
	float2 cropMax = cropMin + cropSize;
	return uv.x >= cropMin.x && uv.x <= cropMax.x && uv.y >= cropMin.y && uv.y <= cropMax.y;
}

float4 shadeVideo(ShaderContext context)
{
	if (!insideCropWindow(context.uv, max(context.outputResolution, float2(1.0, 1.0))))
		return outsideColor;

	float safeZoom = max(zoom, 0.001);
	float2 sourceUv = (context.uv - 0.5) / safeZoom + 0.5;
	sourceUv -= pan;
	sourceUv = rotateAroundCenter(sourceUv, -rotationDegrees * (PI / 180.0));

	bool inside = false;
	float2 sampledUv = applyEdgeMode(sourceUv, inside);
	if (!inside && edgeMode == 0)
		return outsideColor;

	return sampleVideo(sampledUv);
}
