// Copyright Ngonart OU. All Rights Reserved.
// UE5 Modular Game Framework — PlanarCaptureCameraUtils implementation

#include "Capture/PlanarCaptureCameraUtils.h"
#include "Kismet/KismetMathLibrary.h"

FTransform UPlanarCaptureCameraUtils::ComputeMirroredTransform(
	const FTransform& ViewerCameraTransform,
	const FTransform& MirrorPlaneTransform)
{
	// Mirror plane normal (local Z axis of the mirror surface)
	const FVector MirrorNormal = MirrorPlaneTransform.GetUnitAxis(EAxis::Z);
	const FVector MirrorLocation = MirrorPlaneTransform.GetLocation();

	// Reflect viewer position across the mirror plane
	const FVector ViewerPos = ViewerCameraTransform.GetLocation();
	const FVector ToViewer = ViewerPos - MirrorLocation;
	const float DistanceToPlane = FVector::DotProduct(ToViewer, MirrorNormal);
	const FVector ReflectedPosition = ViewerPos - 2.0f * DistanceToPlane * MirrorNormal;

	// Reflect viewer rotation across the mirror plane
	const FVector ViewerForward = ViewerCameraTransform.GetUnitAxis(EAxis::X);
	const FVector ReflectedForward = ViewerForward - 2.0f * FVector::DotProduct(ViewerForward, MirrorNormal) * MirrorNormal;

	const FVector ViewerUp = ViewerCameraTransform.GetUnitAxis(EAxis::Z);
	const FVector ReflectedUp = ViewerUp - 2.0f * FVector::DotProduct(ViewerUp, MirrorNormal) * MirrorNormal;

	const FRotator ReflectedRotation = FRotationMatrix::MakeFromXZ(ReflectedForward, ReflectedUp).Rotator();

	return FTransform(ReflectedRotation, ReflectedPosition, ViewerCameraTransform.GetScale3D());
}

FTransform UPlanarCaptureCameraUtils::ComputePortalTransform(
	const FTransform& ViewerCameraTransform,
	const FTransform& SourceSurfaceTransform,
	const FTransform& TargetSurfaceTransform)
{
	// Compute viewer position relative to source surface
	const FVector ViewerPos = ViewerCameraTransform.GetLocation();
	const FVector RelativePos = SourceSurfaceTransform.InverseTransformPosition(ViewerPos);

	// Compute viewer rotation relative to source surface
	const FQuat ViewerRot = ViewerCameraTransform.GetRotation();
	const FQuat SourceRotInv = SourceSurfaceTransform.GetRotation().Inverse();
	const FQuat RelativeRot = SourceRotInv * ViewerRot;

	// Apply relative transform to target surface
	const FVector TargetPos = TargetSurfaceTransform.TransformPosition(RelativePos);
	const FQuat TargetRot = TargetSurfaceTransform.GetRotation() * RelativeRot;

	return FTransform(TargetRot, TargetPos, ViewerCameraTransform.GetScale3D());
}

FMatrix UPlanarCaptureCameraUtils::ComputeObliqueProjectionMatrix(
	float FOV,
	float AspectRatio,
	float NearPlane,
	float FarPlane,
	const FPlane& ClipPlane,
	const FTransform& SurfaceTransform)
{
	// Build standard perspective projection matrix
	FMatrix ProjectionMatrix;
	const float HalfFOVRad = FMath::DegreesToRadians(FOV * 0.5f);
	const float YScale = 1.0f / FMath::Tan(HalfFOVRad);
	const float XScale = YScale / AspectRatio;

	ProjectionMatrix.SetIdentity();
	ProjectionMatrix.M[0][0] = XScale;
	ProjectionMatrix.M[1][1] = YScale;
	ProjectionMatrix.M[2][2] = (NearPlane == FarPlane) ? 0.0f : FarPlane / (FarPlane - NearPlane);
	ProjectionMatrix.M[2][3] = 1.0f;
	ProjectionMatrix.M[3][2] = (NearPlane == FarPlane) ? NearPlane : -NearPlane * FarPlane / (FarPlane - NearPlane);
	ProjectionMatrix.M[3][3] = 0.0f;

	// Transform clip plane into view space (inverse of surface transform)
	const FMatrix ViewMatrix = SurfaceTransform.ToMatrixNoScale().Inverse();
	const FPlane ViewSpaceClipPlane = ClipPlane.TransformBy(ViewMatrix);

	// Compute oblique near-plane using the standard technique:
	// Calculate the clip-space corner that maximizes the dot product with the plane normal.
	// Then modify the third row of the projection matrix to make the near plane pass through the clip plane.

	FVector4 ClipPlaneVec(ViewSpaceClipPlane.X, ViewSpaceClipPlane.Y, ViewSpaceClipPlane.Z, ViewSpaceClipPlane.W);

	// Find the vertex of the view frustum in clip space that is closest to the plane
	FVector4 Q;
	Q.X = (FMath::Sign(ClipPlaneVec.X) + ProjectionMatrix.M[0][2]) / ProjectionMatrix.M[0][0];
	Q.Y = (FMath::Sign(ClipPlaneVec.Y) + ProjectionMatrix.M[1][2]) / ProjectionMatrix.M[1][1];
	Q.Z = 1.0f;
	Q.W = (1.0f + ProjectionMatrix.M[2][2]) / ProjectionMatrix.M[3][2];

	// Scale the clip plane so its distance from the origin matches the Q vertex
	const float DotVal = ClipPlaneVec.X * Q.X + ClipPlaneVec.Y * Q.Y + ClipPlaneVec.Z * Q.Z + ClipPlaneVec.W * Q.W;
	const float Scale = 2.0f / DotVal;
	ClipPlaneVec *= Scale;

	// Replace the third row of the projection matrix with the clip plane
	ProjectionMatrix.M[2][0] = ClipPlaneVec.X;
	ProjectionMatrix.M[2][1] = ClipPlaneVec.Y;
	ProjectionMatrix.M[2][2] = ClipPlaneVec.Z;
	ProjectionMatrix.M[2][3] = ClipPlaneVec.W;

	return ProjectionMatrix;
}

float UPlanarCaptureCameraUtils::ComputeScreenCoverage(
	const FBox& SurfaceBounds,
	const FTransform& ViewerTransform,
	float ViewerFOV,
	int32 ScreenWidth,
	int32 ScreenHeight)
{
	if (!SurfaceBounds.IsValid || ScreenWidth <= 0 || ScreenHeight <= 0)
	{
		return 0.0f;
	}

	const float HalfFOVRad = FMath::DegreesToRadians(ViewerFOV * 0.5f);
	const FVector ViewerPos = ViewerTransform.GetLocation();
	const FVector ViewerForward = ViewerTransform.GetUnitAxis(EAxis::X);

	// Project all 8 corners of the bounding box to screen space
	const FVector Corners[8] = {
		FVector(SurfaceBounds.Min.X, SurfaceBounds.Min.Y, SurfaceBounds.Min.Z),
		FVector(SurfaceBounds.Min.X, SurfaceBounds.Min.Y, SurfaceBounds.Max.Z),
		FVector(SurfaceBounds.Min.X, SurfaceBounds.Max.Y, SurfaceBounds.Min.Z),
		FVector(SurfaceBounds.Min.X, SurfaceBounds.Max.Y, SurfaceBounds.Max.Z),
		FVector(SurfaceBounds.Max.X, SurfaceBounds.Min.Y, SurfaceBounds.Min.Z),
		FVector(SurfaceBounds.Max.X, SurfaceBounds.Min.Y, SurfaceBounds.Max.Z),
		FVector(SurfaceBounds.Max.X, SurfaceBounds.Max.Y, SurfaceBounds.Min.Z),
		FVector(SurfaceBounds.Max.X, SurfaceBounds.Max.Y, SurfaceBounds.Max.Z),
	};

	float MinScreenX = FLT_MAX, MinScreenY = FLT_MAX;
	float MaxScreenX = -FLT_MAX, MaxScreenY = -FLT_MAX;
	int32 BehindCameraCount = 0;

	for (const FVector& Corner : Corners)
	{
		const FVector ToCorner = Corner - ViewerPos;
		const float DotForward = FVector::DotProduct(ToCorner, ViewerForward);

		if (DotForward <= 0.0f)
		{
			BehindCameraCount++;
			continue;
		}

		const float Distance = ToCorner.Size();
		const FVector Right = ViewerTransform.GetUnitAxis(EAxis::Y);
		const FVector Up = ViewerTransform.GetUnitAxis(EAxis::Z);

		const float DotRight = FVector::DotProduct(ToCorner.GetSafeNormal(), Right);
		const float DotUp = FVector::DotProduct(ToCorner.GetSafeNormal(), Up);

		const float AngleX = FMath::Atan2(DotRight, DotForward);
		const float AngleY = FMath::Atan2(DotUp, DotForward);

		const float ScreenX = (AngleX / HalfFOVRad) * (ScreenWidth * 0.5f) + (ScreenWidth * 0.5f);
		const float ScreenY = (ScreenHeight * 0.5f) - (AngleY / HalfFOVRad) * (ScreenHeight * 0.5f);

		MinScreenX = FMath::Min(MinScreenX, ScreenX);
		MinScreenY = FMath::Min(MinScreenY, ScreenY);
		MaxScreenX = FMath::Max(MaxScreenX, ScreenX);
		MaxScreenY = FMath::Max(MaxScreenY, ScreenY);
	}

	if (BehindCameraCount >= 8)
	{
		return 0.0f;
	}

	// Clamp to screen bounds
	MinScreenX = FMath::Clamp(MinScreenX, 0.0f, static_cast<float>(ScreenWidth));
	MinScreenY = FMath::Clamp(MinScreenY, 0.0f, static_cast<float>(ScreenHeight));
	MaxScreenX = FMath::Clamp(MaxScreenX, 0.0f, static_cast<float>(ScreenWidth));
	MaxScreenY = FMath::Clamp(MaxScreenY, 0.0f, static_cast<float>(ScreenHeight));

	const float ScreenArea = static_cast<float>(ScreenWidth * ScreenHeight);
	const float BoundingArea = (MaxScreenX - MinScreenX) * (MaxScreenY - MinScreenY);

	return FMath::Clamp(BoundingArea / ScreenArea, 0.0f, 1.0f);
}

bool UPlanarCaptureCameraUtils::IsSurfaceVisibleToViewer(
	const FBox& SurfaceBounds,
	const FTransform& ViewerTransform,
	float ViewerFOV,
	float ViewerAspectRatio,
	float ViewerNearPlane,
	float ViewerFarPlane)
{
	if (!SurfaceBounds.IsValid)
	{
		return false;
	}

	// Quick dot-product check: is the surface center roughly in front of the viewer?
	const FVector SurfaceCenter = SurfaceBounds.GetCenter();
	const FVector ViewerPos = ViewerTransform.GetLocation();
	const FVector ToSurface = SurfaceCenter - ViewerPos;
	const FVector ViewerForward = ViewerTransform.GetUnitAxis(EAxis::X);

	if (FVector::DotProduct(ToSurface.GetSafeNormal(), ViewerForward) < 0.0f)
	{
		return false;
	}

	// Distance check
	if (ToSurface.Size() > ViewerFarPlane)
	{
		return false;
	}
	if (ToSurface.Size() < ViewerNearPlane)
	{
		return false;
	}

	// Screen coverage check — if coverage is above 0, it's visible
	const float Coverage = ComputeScreenCoverage(SurfaceBounds, ViewerTransform, ViewerFOV, 1920, 1080);
	return Coverage > 0.001f;
}

float UPlanarCaptureCameraUtils::ComputeCompositeScore(
	float ScreenCoverage,
	float FacingAngle,
	float DistanceToViewer,
	float MaxDistance,
	float ScriptedPriority)
{
	const float DistanceFactor = (MaxDistance > 0.0f)
		? FMath::Clamp(1.0f - (DistanceToViewer / MaxDistance), 0.0f, 1.0f)
		: 1.0f;

	const float Score = (ScreenCoverage * 0.5f)
		+ (FMath::Abs(FacingAngle) * 0.3f)
		+ (DistanceFactor * 0.1f)
		+ (ScriptedPriority * 0.1f);

	return FMath::Clamp(Score, 0.0f, 1.0f);
}
