#include "HyperTwistTraining/HyperTwistHigherDimensionalTrainingGameMode.h"

#include "Camera/CameraComponent.h"
#include "CanvasItem.h"
#include "Components/DirectionalLightComponent.h"
#include "Components/SceneComponent.h"
#include "Engine/Canvas.h"
#include "Engine/DirectionalLight.h"
#include "Engine/Engine.h"
#include "Engine/Font.h"
#include "Engine/World.h"
#include "EngineUtils.h"
#include "GameFramework/SpringArmComponent.h"
#include "HAL/FileManager.h"
#include "HyperTwistDiagnostics/HyperTwistRuntimeDiagnostics.h"
#include "HyperTwistTraining/HyperTwistCoachDashboardActor.h"
#include "HyperTwistTraining/HyperTwistHigherDimensionalTrainingShellActor.h"
#include "HyperTwistUX/HyperTwistHigherDimensionalHUDWidget.h"
#include "HyperTwistUX/HyperTwistPlayerControllerBase.h"
#include "InputCoreTypes.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"

DEFINE_LOG_CATEGORY_STATIC(LogHyperTwistHigherDimensionalGameMode, Log, All);

namespace HyperTwistHigherDimensionalTrainingGameModeInternal
{
	const TCHAR* Magic120CellFamilyKey = TEXT("magic120cell");
	const TCHAR* MagicCube5DFamilyKey = TEXT("magiccube5d");

	FString BuildReadinessResult(const FString& FamilyKey)
	{
		return FamilyKey.Equals(Magic120CellFamilyKey, ESearchCase::IgnoreCase)
			? TEXT("higher-dimensional-runtime-ready-magic120cell")
			: TEXT("higher-dimensional-runtime-ready-magiccube5d");
	}

	int32 WrapSelection(const int32 Value, const int32 Count)
	{
		return Count > 0 ? ((Value % Count) + Count) % Count : 0;
	}

	FString GetMagic120CellAxisFamily(const int32 StickerIndex)
	{
		if (StickerIndex >= 1 && StickerIndex <= 12)
		{
			return TEXT("face axis");
		}
		if (StickerIndex >= 13 && StickerIndex <= 42)
		{
			return TEXT("edge axis");
		}
		if (StickerIndex >= 43 && StickerIndex <= 62)
		{
			return TEXT("vertex axis");
		}
		return TEXT("invalid axis");
	}

	void ResolveMagicCube5DPlane(
		const int32 FaceAxisIndex,
		const int32 PlaneIndex,
		EHyperTwistMagicCube5DAxis& OutAxisA,
		EHyperTwistMagicCube5DAxis& OutAxisB
	)
	{
		int32 OrthogonalAxes[4] = {0, 1, 2, 3};
		int32 WriteIndex = 0;
		for (int32 AxisIndex = 0; AxisIndex < 5; ++AxisIndex)
		{
			if (AxisIndex != FaceAxisIndex && WriteIndex < UE_ARRAY_COUNT(OrthogonalAxes))
			{
				OrthogonalAxes[WriteIndex++] = AxisIndex;
			}
		}

		constexpr int32 PlanePairs[6][2] =
		{
			{0, 1},
			{0, 2},
			{0, 3},
			{1, 2},
			{1, 3},
			{2, 3}
		};
		const int32 SafePlaneIndex = WrapSelection(PlaneIndex, UE_ARRAY_COUNT(PlanePairs));
		OutAxisA = static_cast<EHyperTwistMagicCube5DAxis>(
			OrthogonalAxes[PlanePairs[SafePlaneIndex][0]]);
		OutAxisB = static_cast<EHyperTwistMagicCube5DAxis>(
			OrthogonalAxes[PlanePairs[SafePlaneIndex][1]]);
	}
}

AHyperTwistHigherDimensionalOrbitPawn::AHyperTwistHigherDimensionalOrbitPawn()
{
	PrimaryActorTick.bCanEverTick = true;

	SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot"));
	SetRootComponent(SceneRoot);

	SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
	SpringArm->SetupAttachment(SceneRoot);
	SpringArm->bDoCollisionTest = false;
	SpringArm->bEnableCameraLag = true;
	SpringArm->CameraLagSpeed = 12.0f;
	SpringArm->bUsePawnControlRotation = false;
	SpringArm->TargetArmLength = InitialArmLength;

	CameraComponent = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
	CameraComponent->SetupAttachment(SpringArm, USpringArmComponent::SocketName);
	CameraComponent->bUsePawnControlRotation = false;
}

void AHyperTwistHigherDimensionalOrbitPawn::BeginPlay()
{
	Super::BeginPlay();
	ResetOrbit();
}

void AHyperTwistHigherDimensionalOrbitPawn::Tick(const float DeltaSeconds)
{
	Super::Tick(DeltaSeconds);
	static_cast<void>(DeltaSeconds);

	RefreshOrbitFocusPoint();
	APlayerController* PlayerController = Cast<APlayerController>(GetController());
	if (PlayerController == nullptr)
	{
		return;
	}
	const AHyperTwistPlayerControllerBase* HyperTwistController =
		Cast<AHyperTwistPlayerControllerBase>(PlayerController);
	const FHyperTwistPlayerPreferences* Preferences = HyperTwistController != nullptr
		? &HyperTwistController->GetPlayerPreferences()
		: nullptr;
	const float ZoomMultiplier = Preferences != nullptr
		? Preferences->ZoomSensitivity
		: 1.0f;
	const float OrbitMultiplier = Preferences != nullptr
		? Preferences->OrbitSensitivity
		: 1.0f;
	const float VerticalDirection = Preferences != nullptr && Preferences->bInvertOrbitY
		? -1.0f
		: 1.0f;

	if (SpringArm != nullptr)
	{
		const float MouseWheelDelta = PlayerController->GetInputAnalogKeyState(EKeys::MouseWheelAxis);
		if (!FMath::IsNearlyZero(MouseWheelDelta))
		{
			SpringArm->TargetArmLength = FMath::Clamp(
				SpringArm->TargetArmLength - (MouseWheelDelta * ZoomStep * ZoomMultiplier),
				MinimumArmLength,
				MaximumArmLength);
		}
	}

	if (!ShouldOrbitFromMouseInput())
	{
		return;
	}

	float MouseDeltaX = 0.0f;
	float MouseDeltaY = 0.0f;
	PlayerController->GetInputMouseDelta(MouseDeltaX, MouseDeltaY);
	if (FMath::IsNearlyZero(MouseDeltaX) && FMath::IsNearlyZero(MouseDeltaY))
	{
		return;
	}

	CurrentYawDegrees += MouseDeltaX * OrbitDegreesPerPixel * OrbitMultiplier;
	CurrentPitchDegrees = FMath::Clamp(
		CurrentPitchDegrees
			- (MouseDeltaY * OrbitDegreesPerPixel * OrbitMultiplier * VerticalDirection),
		-78.0f,
		55.0f);
	ApplyOrbitTransform();
}

void AHyperTwistHigherDimensionalOrbitPawn::ResetOrbit()
{
	CurrentYawDegrees = InitialYawDegrees;
	CurrentPitchDegrees = InitialPitchDegrees;
	if (SpringArm != nullptr)
	{
		SpringArm->TargetArmLength = FMath::Clamp(
			InitialArmLength,
			MinimumArmLength,
			MaximumArmLength);
	}
	RefreshOrbitFocusPoint();
	ApplyOrbitTransform();
}

void AHyperTwistHigherDimensionalOrbitPawn::RefreshOrbitFocusPoint()
{
	if (GetWorld() == nullptr)
	{
		return;
	}

	TActorIterator<AHyperTwistHigherDimensionalTrainingShellActor> ActorIt(GetWorld());
	if (ActorIt)
	{
		const FVector NewFocusPoint = ActorIt->GetActorLocation()
			+ FVector(0.0f, 0.0f, ActorIt->PreviewHeightOffset);
		if (!NewFocusPoint.Equals(OrbitFocusPoint))
		{
			OrbitFocusPoint = NewFocusPoint;
			ApplyOrbitTransform();
		}
	}
}

void AHyperTwistHigherDimensionalOrbitPawn::ApplyOrbitTransform()
{
	SetActorLocation(OrbitFocusPoint);
	if (SpringArm != nullptr)
	{
		SpringArm->SetRelativeRotation(FRotator(CurrentPitchDegrees, CurrentYawDegrees, 0.0f));
	}
}

bool AHyperTwistHigherDimensionalOrbitPawn::ShouldOrbitFromMouseInput() const
{
	const APlayerController* PlayerController = Cast<APlayerController>(GetController());
	return PlayerController != nullptr
		&& (PlayerController->IsInputKeyDown(EKeys::MiddleMouseButton)
			|| (PlayerController->IsInputKeyDown(EKeys::RightMouseButton)
				&& (PlayerController->IsInputKeyDown(EKeys::LeftShift)
					|| PlayerController->IsInputKeyDown(EKeys::RightShift))));
}

void AHyperTwistHigherDimensionalTrainingHUD::DrawHUD()
{
	Super::DrawHUD();
	if (Canvas == nullptr)
	{
		return;
	}
	const AHyperTwistHigherDimensionalTrainingPlayerController* Controller =
		Cast<AHyperTwistHigherDimensionalTrainingPlayerController>(
			GetOwningPlayerController());
	if (Controller != nullptr && Controller->IsPuzzleHudReady())
	{
		return;
	}

	AHyperTwistHigherDimensionalTrainingShellActor* TrainingShell = ResolveTrainingShell();
	if (TrainingShell == nullptr)
	{
		return;
	}

	const float PanelX = 18.0f;
	const float PanelY = 18.0f;
	const float PanelWidth = FMath::Min(540.0f, Canvas->SizeX - 36.0f);
	const float PanelHeight = 270.0f;
	FCanvasTileItem Background(
		FVector2D(PanelX, PanelY),
		GWhiteTexture,
		FVector2D(PanelWidth, PanelHeight),
		FLinearColor(0.018f, 0.035f, 0.060f, 0.93f));
	Background.BlendMode = SE_BLEND_Translucent;
	Canvas->DrawItem(Background);

	const FLinearColor AccentColor = TrainingShell->FamilyKey.Equals(
		HyperTwistHigherDimensionalTrainingGameModeInternal::Magic120CellFamilyKey,
		ESearchCase::IgnoreCase)
		? FLinearColor(0.12f, 0.95f, 1.0f)
		: FLinearColor(1.0f, 0.60f, 0.22f);
	FCanvasTileItem Accent(
		FVector2D(PanelX, PanelY),
		GWhiteTexture,
		FVector2D(6.0f, PanelHeight),
		AccentColor);
	Canvas->DrawItem(Accent);

	float CursorY = PanelY + 16.0f;
	DrawStatusLine(TrainingShell->Title, PanelX + 20.0f, CursorY, AccentColor, 1.15f);
	DrawStatusLine(TrainingShell->GetRuntimeStatusSummary(), PanelX + 20.0f, CursorY, FLinearColor::White);
	DrawStatusLine(
		FString::Printf(TEXT("projection: %s"), *TrainingShell->ProjectionProfileId),
		PanelX + 20.0f,
		CursorY,
		FLinearColor(0.72f, 0.80f, 0.88f),
		0.86f);
	DrawStatusLine(
		FString::Printf(TEXT("persistence: %s"), *TrainingShell->LastPersistenceStatus),
		PanelX + 20.0f,
		CursorY,
		FLinearColor(0.72f, 0.80f, 0.88f),
		0.86f);
	DrawStatusLine(
		FString::Printf(TEXT("last action: %s"), *TrainingShell->LastRuntimeAction),
		PanelX + 20.0f,
		CursorY,
		FLinearColor(0.72f, 0.80f, 0.88f),
		0.86f);

	CursorY += 5.0f;
	DrawStatusLine(
		TEXT("SPACE auto-rotate  |  Q / E rotate  |  PAGE UP / DOWN layers"),
		PanelX + 20.0f,
		CursorY,
		FLinearColor::White,
		0.88f);
	DrawStatusLine(
		TEXT("N new state  |  R reset  |  S save  |  L load  |  HOME camera"),
		PanelX + 20.0f,
		CursorY,
		FLinearColor::White,
		0.88f);
	DrawStatusLine(
		TEXT("Wheel zoom  |  MMB or SHIFT+RMB orbit  |  D diagnostics"),
		PanelX + 20.0f,
		CursorY,
		FLinearColor::White,
		0.88f);
}

AHyperTwistHigherDimensionalTrainingShellActor*
AHyperTwistHigherDimensionalTrainingHUD::ResolveTrainingShell()
{
	if (CachedTrainingShell.IsValid())
	{
		return CachedTrainingShell.Get();
	}
	if (GetWorld() == nullptr)
	{
		return nullptr;
	}

	TActorIterator<AHyperTwistHigherDimensionalTrainingShellActor> ActorIt(GetWorld());
	if (ActorIt)
	{
		CachedTrainingShell = *ActorIt;
		return *ActorIt;
	}
	return nullptr;
}

void AHyperTwistHigherDimensionalTrainingHUD::DrawStatusLine(
	const FString& Text,
	const float X,
	float& InOutY,
	const FLinearColor& Color,
	const float Scale)
{
	UFont* Font = GEngine != nullptr ? GEngine->GetSmallFont() : nullptr;
	if (Canvas == nullptr || Font == nullptr)
	{
		return;
	}

	FCanvasTextItem TextItem(FVector2D(X, InOutY), FText::FromString(Text), Font, Color);
	TextItem.Scale = FVector2D(Scale);
	TextItem.EnableShadow(FLinearColor(0.0f, 0.0f, 0.0f, 0.75f));
	Canvas->DrawItem(TextItem);
	InOutY += 23.0f * Scale;
}

AHyperTwistHigherDimensionalTrainingPlayerController::
	AHyperTwistHigherDimensionalTrainingPlayerController()
{
	bBootstrapCoachDashboardOnBeginPlay = false;
	bShowCoachDashboardMouseCursor = true;
	bUseGameAndUiInputMode = true;
	bEnableClickEvents = true;
	bEnableMouseOverEvents = true;
}

FString AHyperTwistHigherDimensionalTrainingPlayerController::GetPauseMenuTitle() const
{
	if (GetWorld() != nullptr)
	{
		TActorIterator<AHyperTwistHigherDimensionalTrainingShellActor> ActorIt(
			GetWorld());
		if (ActorIt)
		{
			return ActorIt->Title;
		}
	}
	return TEXT("Higher-Dimensional Training");
}

FString AHyperTwistHigherDimensionalTrainingPlayerController::GetPauseMenuSubtitle() const
{
	if (GetWorld() != nullptr)
	{
		TActorIterator<AHyperTwistHigherDimensionalTrainingShellActor> ActorIt(
			GetWorld());
		if (ActorIt)
		{
			return ActorIt->GetRuntimeStatusSummary();
		}
	}
	return TEXT("Family-owned runtime, projection, and persistence");
}

void AHyperTwistHigherDimensionalTrainingPlayerController::BeginPlay()
{
	Super::BeginPlay();

	bShowMouseCursor = true;
	FInputModeGameAndUI InputMode;
	InputMode.SetHideCursorDuringCapture(false);
	InputMode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
	SetInputMode(InputMode);
	EnsurePuzzleHud();
}

void AHyperTwistHigherDimensionalTrainingPlayerController::PlayerTick(const float DeltaTime)
{
	Super::PlayerTick(DeltaTime);
	if (IsAssistantPanelOpen()
		|| ActivePauseMenuWidget != nullptr
		|| (GetWorld() != nullptr && GetWorld()->IsPaused()))
	{
		return;
	}

	AHyperTwistHigherDimensionalTrainingShellActor* TrainingShell = ResolveTrainingShell();
	if (TrainingShell == nullptr)
	{
		EnsurePuzzleHud();
		return;
	}

	if (WasInputKeyJustPressed(EKeys::SpaceBar))
	{
		HandleToggleAutoRotate();
	}
	if (WasInputKeyJustPressed(EKeys::PageUp))
	{
		HandleNextProjectionLayer();
	}
	if (WasInputKeyJustPressed(EKeys::PageDown))
	{
		HandlePreviousProjectionLayer();
	}
	if (WasInputKeyJustPressed(EKeys::N))
	{
		HandleScramble();
	}
	if (WasInputKeyJustPressed(EKeys::R))
	{
		HandleReset();
	}
	if (WasInputKeyJustPressed(EKeys::S))
	{
		HandleSave();
	}
	if (WasInputKeyJustPressed(EKeys::L))
	{
		HandleLoad();
	}
	if (WasInputKeyJustPressed(EKeys::Left))
	{
		HandlePreviousPrimarySelection();
	}
	if (WasInputKeyJustPressed(EKeys::Right))
	{
		HandleNextPrimarySelection();
	}
	if (WasInputKeyJustPressed(EKeys::Down))
	{
		HandlePreviousSecondarySelection();
	}
	if (WasInputKeyJustPressed(EKeys::Up))
	{
		HandleNextSecondarySelection();
	}
	if (WasInputKeyJustPressed(EKeys::Q))
	{
		HandlePreviousTertiarySelection();
	}
	if (WasInputKeyJustPressed(EKeys::E))
	{
		HandleNextTertiarySelection();
	}
	if (WasInputKeyJustPressed(EKeys::Tab))
	{
		HandleToggleFaceSide();
	}
	if (WasInputKeyJustPressed(EKeys::Comma))
	{
		HandleNegativeTurn();
	}
	if (WasInputKeyJustPressed(EKeys::Period))
	{
		HandlePositiveTurn();
	}
	if (WasInputKeyJustPressed(EKeys::Home))
	{
		if (AHyperTwistHigherDimensionalOrbitPawn* OrbitPawn =
			Cast<AHyperTwistHigherDimensionalOrbitPawn>(GetPawn()))
		{
			OrbitPawn->ResetOrbit();
			TrainingShell->LastRuntimeAction = TEXT("camera orbit reset");
			RefreshPuzzleHud();
		}
	}
	if (WasInputKeyJustPressed(EKeys::D))
	{
		ToggleCoachDashboardSurface();
	}
	EnsurePuzzleHud();
}

bool AHyperTwistHigherDimensionalTrainingPlayerController::IsPuzzleHudReady() const
{
	return ActivePuzzleHudWidget != nullptr
		&& ActivePuzzleHudWidget->IsInViewport()
		&& ActivePuzzleHudWidget->IsHudSurfaceReady();
}

AHyperTwistHigherDimensionalTrainingShellActor*
AHyperTwistHigherDimensionalTrainingPlayerController::ResolveTrainingShell()
{
	if (CachedTrainingShell.IsValid())
	{
		return CachedTrainingShell.Get();
	}
	if (GetWorld() == nullptr)
	{
		return nullptr;
	}

	TActorIterator<AHyperTwistHigherDimensionalTrainingShellActor> ActorIt(GetWorld());
	if (ActorIt)
	{
		CachedTrainingShell = *ActorIt;
		return *ActorIt;
	}
	return nullptr;
}

void AHyperTwistHigherDimensionalTrainingPlayerController::ToggleCoachDashboardSurface()
{
	if (ActiveDashboardActor != nullptr && ActiveDashboardActor->HasActiveDashboard())
	{
		ActiveDashboardActor->RemoveDashboard();
		return;
	}

	EnsureCoachDashboard();
}

void AHyperTwistHigherDimensionalTrainingPlayerController::EnsurePuzzleHud()
{
	if (!bShowPuzzleHud)
	{
		return;
	}
	if (ActivePuzzleHudWidget == nullptr)
	{
		TSubclassOf<UHyperTwistHigherDimensionalHUDWidget> ResolvedClass =
			PuzzleHudWidgetClass;
		if (*ResolvedClass == nullptr)
		{
			ResolvedClass = UHyperTwistHigherDimensionalHUDWidget::StaticClass();
		}
		ActivePuzzleHudWidget =
			CreateWidget<UHyperTwistHigherDimensionalHUDWidget>(
				this,
				ResolvedClass);
		if (ActivePuzzleHudWidget == nullptr
			|| !ActivePuzzleHudWidget->PrepareHudSurface())
		{
			ActivePuzzleHudWidget = nullptr;
			return;
		}

		ActivePuzzleHudWidget->OnPreviousPrimaryRequested.AddDynamic(
			this,
			&AHyperTwistHigherDimensionalTrainingPlayerController::
				HandlePreviousPrimarySelection);
		ActivePuzzleHudWidget->OnNextPrimaryRequested.AddDynamic(
			this,
			&AHyperTwistHigherDimensionalTrainingPlayerController::
				HandleNextPrimarySelection);
		ActivePuzzleHudWidget->OnPreviousSecondaryRequested.AddDynamic(
			this,
			&AHyperTwistHigherDimensionalTrainingPlayerController::
				HandlePreviousSecondarySelection);
		ActivePuzzleHudWidget->OnNextSecondaryRequested.AddDynamic(
			this,
			&AHyperTwistHigherDimensionalTrainingPlayerController::
				HandleNextSecondarySelection);
		ActivePuzzleHudWidget->OnPreviousTertiaryRequested.AddDynamic(
			this,
			&AHyperTwistHigherDimensionalTrainingPlayerController::
				HandlePreviousTertiarySelection);
		ActivePuzzleHudWidget->OnNextTertiaryRequested.AddDynamic(
			this,
			&AHyperTwistHigherDimensionalTrainingPlayerController::
				HandleNextTertiarySelection);
		ActivePuzzleHudWidget->OnToggleFaceSideRequested.AddDynamic(
			this,
			&AHyperTwistHigherDimensionalTrainingPlayerController::
				HandleToggleFaceSide);
		ActivePuzzleHudWidget->OnNegativeTurnRequested.AddDynamic(
			this,
			&AHyperTwistHigherDimensionalTrainingPlayerController::
				HandleNegativeTurn);
		ActivePuzzleHudWidget->OnPositiveTurnRequested.AddDynamic(
			this,
			&AHyperTwistHigherDimensionalTrainingPlayerController::
				HandlePositiveTurn);
		ActivePuzzleHudWidget->OnPreviousProjectionLayerRequested.AddDynamic(
			this,
			&AHyperTwistHigherDimensionalTrainingPlayerController::
				HandlePreviousProjectionLayer);
		ActivePuzzleHudWidget->OnNextProjectionLayerRequested.AddDynamic(
			this,
			&AHyperTwistHigherDimensionalTrainingPlayerController::
				HandleNextProjectionLayer);
		ActivePuzzleHudWidget->OnToggleAutoRotateRequested.AddDynamic(
			this,
			&AHyperTwistHigherDimensionalTrainingPlayerController::
				HandleToggleAutoRotate);
		ActivePuzzleHudWidget->OnScrambleRequested.AddDynamic(
			this,
			&AHyperTwistHigherDimensionalTrainingPlayerController::HandleScramble);
		ActivePuzzleHudWidget->OnResetRequested.AddDynamic(
			this,
			&AHyperTwistHigherDimensionalTrainingPlayerController::HandleReset);
		ActivePuzzleHudWidget->OnSaveRequested.AddDynamic(
			this,
			&AHyperTwistHigherDimensionalTrainingPlayerController::HandleSave);
		ActivePuzzleHudWidget->OnLoadRequested.AddDynamic(
			this,
			&AHyperTwistHigherDimensionalTrainingPlayerController::HandleLoad);
		ActivePuzzleHudWidget->OnCoachRequested.AddDynamic(
			this,
			&AHyperTwistHigherDimensionalTrainingPlayerController::HandleCoach);
		ActivePuzzleHudWidget->OnMenuRequested.AddDynamic(
			this,
			&AHyperTwistHigherDimensionalTrainingPlayerController::HandleMenu);
		ActivePuzzleHudWidget->AddToViewport(PuzzleHudZOrder);
	}
	else if (!ActivePuzzleHudWidget->IsInViewport())
	{
		ActivePuzzleHudWidget->AddToViewport(PuzzleHudZOrder);
	}
	RefreshPuzzleHud();
}

void AHyperTwistHigherDimensionalTrainingPlayerController::RefreshPuzzleHud()
{
	if (ActivePuzzleHudWidget == nullptr)
	{
		return;
	}
	const AHyperTwistHigherDimensionalTrainingShellActor* TrainingShell =
		ResolveTrainingShell();
	if (TrainingShell == nullptr)
	{
		ActivePuzzleHudWidget->ConfigureSurface(
			UsesMagic120CellRuntime(),
			bSelectedMagicCube5DPositiveFace,
			false,
			TEXT("Higher-Dimensional Puzzle"),
			TEXT("WAITING FOR RUNTIME"),
			TEXT("The family-owned simulation is starting."),
			TEXT("Projection is not ready."),
			TEXT("waiting"),
			TEXT("not loaded"));
		return;
	}

	ActivePuzzleHudWidget->ConfigureSurface(
		UsesMagic120CellRuntime(),
		bSelectedMagicCube5DPositiveFace,
		TrainingShell->bAutoRotateProjection,
		TrainingShell->Title,
		TrainingShell->GetRuntimeStatusSummary(),
		BuildMoveSelectionSummary(),
		FString::Printf(
			TEXT("%s | %s"),
			*TrainingShell->GetProjectionLayerLabel(),
			TrainingShell->bAutoRotateProjection
				? TEXT("projection rotating")
				: TEXT("projection paused")),
		TrainingShell->LastRuntimeAction,
		TrainingShell->LastPersistenceStatus);
}

FString AHyperTwistHigherDimensionalTrainingPlayerController::
	BuildMoveSelectionSummary() const
{
	using namespace HyperTwistHigherDimensionalTrainingGameModeInternal;
	if (UsesMagic120CellRuntime())
	{
		const int32 TurnOrder =
			UHyperTwistMagic120CellRuntimeLibrary::GetTurnOrderForSticker(
				SelectedMagic120StickerIndex);
		return FString::Printf(
			TEXT("Cell %03d / 120 | twist %02d / 62 | %s, order %d"),
			SelectedMagic120CellIndex + 1,
			SelectedMagic120StickerIndex,
			*GetMagic120CellAxisFamily(SelectedMagic120StickerIndex),
			TurnOrder);
	}

	EHyperTwistMagicCube5DAxis PlaneAxisA;
	EHyperTwistMagicCube5DAxis PlaneAxisB;
	ResolveMagicCube5DPlane(
		SelectedMagicCube5DFaceAxis,
		SelectedMagicCube5DPlaneIndex,
		PlaneAxisA,
		PlaneAxisB);
	const EHyperTwistMagicCube5DAxis FaceAxis =
		static_cast<EHyperTwistMagicCube5DAxis>(
			SelectedMagicCube5DFaceAxis);
	return FString::Printf(
		TEXT("Face %s%s | layer mask 0x%X | plane %s%s"),
		bSelectedMagicCube5DPositiveFace ? TEXT("+") : TEXT("-"),
		*UHyperTwistMagicCube5DRuntimeLibrary::GetAxisLabel(FaceAxis),
		SelectedMagicCube5DSliceMask,
		*UHyperTwistMagicCube5DRuntimeLibrary::GetAxisLabel(PlaneAxisA),
		*UHyperTwistMagicCube5DRuntimeLibrary::GetAxisLabel(PlaneAxisB));
}

void AHyperTwistHigherDimensionalTrainingPlayerController::ApplySelectedTurn(
	const bool bPositiveDirection
)
{
	using namespace HyperTwistHigherDimensionalTrainingGameModeInternal;
	AHyperTwistHigherDimensionalTrainingShellActor* TrainingShell =
		ResolveTrainingShell();
	if (TrainingShell == nullptr)
	{
		return;
	}

	if (UsesMagic120CellRuntime())
	{
		FHyperTwistMagic120CellTurnRequest Request;
		Request.CellIndex = SelectedMagic120CellIndex;
		Request.StickerIndex = SelectedMagic120StickerIndex;
		Request.bInverse = !bPositiveDirection;
		TrainingShell->ApplyMagic120CellTurn(Request);
	}
	else
	{
		EHyperTwistMagicCube5DAxis PlaneAxisA;
		EHyperTwistMagicCube5DAxis PlaneAxisB;
		ResolveMagicCube5DPlane(
			SelectedMagicCube5DFaceAxis,
			SelectedMagicCube5DPlaneIndex,
			PlaneAxisA,
			PlaneAxisB);
		FHyperTwistMagicCube5DTurnRequest Request;
		Request.FaceAxis = static_cast<EHyperTwistMagicCube5DAxis>(
			SelectedMagicCube5DFaceAxis);
		Request.bPositiveFace = bSelectedMagicCube5DPositiveFace;
		Request.SliceMask = SelectedMagicCube5DSliceMask;
		Request.RotationAxisA = PlaneAxisA;
		Request.RotationAxisB = PlaneAxisB;
		Request.Direction = bPositiveDirection
			? EHyperTwistMagicCube5DTurnDirection::PositiveQuarterTurn
			: EHyperTwistMagicCube5DTurnDirection::NegativeQuarterTurn;
		TrainingShell->ApplyMagicCube5DTurn(Request);
	}
	RefreshPuzzleHud();
}

bool AHyperTwistHigherDimensionalTrainingPlayerController::
	UsesMagic120CellRuntime() const
{
	if (CachedTrainingShell.IsValid())
	{
		return CachedTrainingShell->FamilyKey.Equals(
			HyperTwistHigherDimensionalTrainingGameModeInternal::
				Magic120CellFamilyKey,
			ESearchCase::IgnoreCase);
	}
	return GetWorld() != nullptr
		&& GetWorld()->GetMapName().Contains(
			TEXT("Magic120Cell"),
			ESearchCase::IgnoreCase);
}

void AHyperTwistHigherDimensionalTrainingPlayerController::
	HandlePreviousPrimarySelection()
{
	using namespace HyperTwistHigherDimensionalTrainingGameModeInternal;
	if (UsesMagic120CellRuntime())
	{
		SelectedMagic120CellIndex = WrapSelection(
			SelectedMagic120CellIndex - 1,
			120);
	}
	else
	{
		SelectedMagicCube5DFaceAxis = WrapSelection(
			SelectedMagicCube5DFaceAxis - 1,
			5);
		SelectedMagicCube5DPlaneIndex = 0;
	}
	RefreshPuzzleHud();
}

void AHyperTwistHigherDimensionalTrainingPlayerController::
	HandleNextPrimarySelection()
{
	using namespace HyperTwistHigherDimensionalTrainingGameModeInternal;
	if (UsesMagic120CellRuntime())
	{
		SelectedMagic120CellIndex = WrapSelection(
			SelectedMagic120CellIndex + 1,
			120);
	}
	else
	{
		SelectedMagicCube5DFaceAxis = WrapSelection(
			SelectedMagicCube5DFaceAxis + 1,
			5);
		SelectedMagicCube5DPlaneIndex = 0;
	}
	RefreshPuzzleHud();
}

void AHyperTwistHigherDimensionalTrainingPlayerController::
	HandlePreviousSecondarySelection()
{
	using namespace HyperTwistHigherDimensionalTrainingGameModeInternal;
	if (UsesMagic120CellRuntime())
	{
		SelectedMagic120StickerIndex =
			WrapSelection(SelectedMagic120StickerIndex - 2, 62) + 1;
	}
	else
	{
		SelectedMagicCube5DSliceMask =
			WrapSelection(SelectedMagicCube5DSliceMask - 2, 7) + 1;
	}
	RefreshPuzzleHud();
}

void AHyperTwistHigherDimensionalTrainingPlayerController::
	HandleNextSecondarySelection()
{
	using namespace HyperTwistHigherDimensionalTrainingGameModeInternal;
	if (UsesMagic120CellRuntime())
	{
		SelectedMagic120StickerIndex =
			WrapSelection(SelectedMagic120StickerIndex, 62) + 1;
	}
	else
	{
		SelectedMagicCube5DSliceMask =
			WrapSelection(SelectedMagicCube5DSliceMask, 7) + 1;
	}
	RefreshPuzzleHud();
}

void AHyperTwistHigherDimensionalTrainingPlayerController::
	HandlePreviousTertiarySelection()
{
	using namespace HyperTwistHigherDimensionalTrainingGameModeInternal;
	if (!UsesMagic120CellRuntime())
	{
		SelectedMagicCube5DPlaneIndex = WrapSelection(
			SelectedMagicCube5DPlaneIndex - 1,
			6);
		RefreshPuzzleHud();
	}
}

void AHyperTwistHigherDimensionalTrainingPlayerController::
	HandleNextTertiarySelection()
{
	using namespace HyperTwistHigherDimensionalTrainingGameModeInternal;
	if (!UsesMagic120CellRuntime())
	{
		SelectedMagicCube5DPlaneIndex = WrapSelection(
			SelectedMagicCube5DPlaneIndex + 1,
			6);
		RefreshPuzzleHud();
	}
}

void AHyperTwistHigherDimensionalTrainingPlayerController::
	HandleToggleFaceSide()
{
	if (!UsesMagic120CellRuntime())
	{
		bSelectedMagicCube5DPositiveFace =
			!bSelectedMagicCube5DPositiveFace;
		RefreshPuzzleHud();
	}
}

void AHyperTwistHigherDimensionalTrainingPlayerController::HandleNegativeTurn()
{
	ApplySelectedTurn(false);
}

void AHyperTwistHigherDimensionalTrainingPlayerController::HandlePositiveTurn()
{
	ApplySelectedTurn(true);
}

void AHyperTwistHigherDimensionalTrainingPlayerController::
	HandlePreviousProjectionLayer()
{
	if (AHyperTwistHigherDimensionalTrainingShellActor* TrainingShell =
			ResolveTrainingShell())
	{
		TrainingShell->CycleProjectionLayer(-1);
		RefreshPuzzleHud();
	}
}

void AHyperTwistHigherDimensionalTrainingPlayerController::
	HandleNextProjectionLayer()
{
	if (AHyperTwistHigherDimensionalTrainingShellActor* TrainingShell =
			ResolveTrainingShell())
	{
		TrainingShell->CycleProjectionLayer(1);
		RefreshPuzzleHud();
	}
}

void AHyperTwistHigherDimensionalTrainingPlayerController::
	HandleToggleAutoRotate()
{
	if (AHyperTwistHigherDimensionalTrainingShellActor* TrainingShell =
			ResolveTrainingShell())
	{
		TrainingShell->ToggleAutoRotateProjection();
		RefreshPuzzleHud();
	}
}

void AHyperTwistHigherDimensionalTrainingPlayerController::HandleScramble()
{
	if (AHyperTwistHigherDimensionalTrainingShellActor* TrainingShell =
			ResolveTrainingShell())
	{
		TrainingShell->CreateScrambledRuntimeState();
		RefreshPuzzleHud();
	}
}

void AHyperTwistHigherDimensionalTrainingPlayerController::HandleReset()
{
	if (AHyperTwistHigherDimensionalTrainingShellActor* TrainingShell =
			ResolveTrainingShell())
	{
		TrainingShell->ResetProjection();
		RefreshPuzzleHud();
	}
}

void AHyperTwistHigherDimensionalTrainingPlayerController::HandleSave()
{
	if (AHyperTwistHigherDimensionalTrainingShellActor* TrainingShell =
			ResolveTrainingShell())
	{
		TrainingShell->SaveRuntimeState();
		RefreshPuzzleHud();
	}
}

void AHyperTwistHigherDimensionalTrainingPlayerController::HandleLoad()
{
	if (AHyperTwistHigherDimensionalTrainingShellActor* TrainingShell =
			ResolveTrainingShell())
	{
		TrainingShell->LoadRuntimeState();
		RefreshPuzzleHud();
	}
}

void AHyperTwistHigherDimensionalTrainingPlayerController::HandleCoach()
{
	ToggleAssistantPanel();
}

void AHyperTwistHigherDimensionalTrainingPlayerController::HandleMenu()
{
	ShowPauseMenu();
}

AHyperTwistHigherDimensionalTrainingGameMode::AHyperTwistHigherDimensionalTrainingGameMode()
{
	DefaultPawnClass = AHyperTwistHigherDimensionalOrbitPawn::StaticClass();
	PlayerControllerClass = AHyperTwistHigherDimensionalTrainingPlayerController::StaticClass();
	HUDClass = AHyperTwistHigherDimensionalTrainingHUD::StaticClass();
	TrainingShellActorClass = AHyperTwistHigherDimensionalTrainingShellActor::StaticClass();
}

void AHyperTwistHigherDimensionalTrainingGameMode::BeginPlay()
{
	Super::BeginPlay();

	HyperTwistRuntimeDiagnostics::AppendEvent(
		TEXT("MapReady"),
		FString::Printf(
			TEXT("Runtime map ready: %s."),
			GetWorld() != nullptr ? *GetWorld()->GetPathName() : TEXT("unknown")));

	const FString FamilyKey = ResolveCurrentFamilyKey();
	EnsureRuntimeLighting(FamilyKey);
	ActiveTrainingShell = FindExistingTrainingShell();
	if (ActiveTrainingShell == nullptr)
	{
		ActiveTrainingShell = SpawnTrainingShell(FamilyKey);
	}
	else
	{
		ActiveTrainingShell->ConfigureForFamily(FamilyKey);
	}

	if (ActiveTrainingShell == nullptr
		|| ActiveTrainingShell->GetRenderableElementCount()
			!= ActiveTrainingShell->GetCanonicalElementCount())
	{
		UE_LOG(
			LogHyperTwistHigherDimensionalGameMode,
			Error,
			TEXT("Dedicated higher-dimensional runtime failed to initialize for family %s."),
			*FamilyKey);
		return;
	}

	PersistRuntimeReadinessDiagnostics(FamilyKey, ActiveTrainingShell);
	UE_LOG(
		LogHyperTwistHigherDimensionalGameMode,
		Display,
		TEXT("Dedicated higher-dimensional runtime ready for %s with %d canonical renderable elements."),
		*FamilyKey,
		ActiveTrainingShell->GetRenderableElementCount());
	if (HyperTwistRuntimeDiagnostics::IsReadyFrameCaptureRequested())
	{
		RequestRuntimeReadyDiagnosticsCapture();
	}
}

FString AHyperTwistHigherDimensionalTrainingGameMode::ResolveCurrentFamilyKey() const
{
	const FString MapName = GetWorld() != nullptr ? GetWorld()->GetMapName() : FString();
	return MapName.Contains(TEXT("Magic120Cell"), ESearchCase::IgnoreCase)
		? HyperTwistHigherDimensionalTrainingGameModeInternal::Magic120CellFamilyKey
		: HyperTwistHigherDimensionalTrainingGameModeInternal::MagicCube5DFamilyKey;
}

AHyperTwistHigherDimensionalTrainingShellActor*
AHyperTwistHigherDimensionalTrainingGameMode::FindExistingTrainingShell() const
{
	if (GetWorld() == nullptr)
	{
		return nullptr;
	}

	TActorIterator<AHyperTwistHigherDimensionalTrainingShellActor> ActorIt(GetWorld());
	if (ActorIt)
	{
		return *ActorIt;
	}
	return nullptr;
}

AHyperTwistHigherDimensionalTrainingShellActor*
AHyperTwistHigherDimensionalTrainingGameMode::SpawnTrainingShell(const FString& FamilyKey)
{
	if (GetWorld() == nullptr)
	{
		return nullptr;
	}

	TSubclassOf<AHyperTwistHigherDimensionalTrainingShellActor> ResolvedClass =
		TrainingShellActorClass;
	if (*ResolvedClass == nullptr)
	{
		ResolvedClass = AHyperTwistHigherDimensionalTrainingShellActor::StaticClass();
	}

	FTransform SpawnTransform;
	AHyperTwistHigherDimensionalTrainingShellActor* TrainingShell =
		GetWorld()->SpawnActorDeferred<AHyperTwistHigherDimensionalTrainingShellActor>(
			ResolvedClass,
			SpawnTransform,
			nullptr,
			nullptr,
			ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
	if (TrainingShell == nullptr)
	{
		return nullptr;
	}

	TrainingShell->ConfigureForFamily(FamilyKey);
	TrainingShell->FinishSpawning(SpawnTransform);
	return TrainingShell;
}

void AHyperTwistHigherDimensionalTrainingGameMode::EnsureRuntimeLighting(const FString& FamilyKey)
{
	if (GetWorld() == nullptr)
	{
		return;
	}

	for (TActorIterator<ADirectionalLight> ActorIt(GetWorld()); ActorIt; ++ActorIt)
	{
		if (ActorIt->GetLightComponent() != nullptr && ActorIt->GetLightComponent()->IsVisible())
		{
			ActivePresentationLight = *ActorIt;
			return;
		}
	}

	FActorSpawnParameters SpawnParameters;
	SpawnParameters.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
	ActivePresentationLight = GetWorld()->SpawnActor<ADirectionalLight>(
		ADirectionalLight::StaticClass(),
		FVector(-340.0f, 120.0f, 470.0f),
		FamilyKey.Equals(HyperTwistHigherDimensionalTrainingGameModeInternal::Magic120CellFamilyKey)
			? FRotator(-34.0f, -22.0f, 0.0f)
			: FRotator(-38.0f, 34.0f, 0.0f),
		SpawnParameters);
	UDirectionalLightComponent* LightComponent = ActivePresentationLight != nullptr
		? Cast<UDirectionalLightComponent>(ActivePresentationLight->GetLightComponent())
		: nullptr;
	if (LightComponent == nullptr)
	{
		UE_LOG(
			LogHyperTwistHigherDimensionalGameMode,
			Error,
			TEXT("Failed to create the self-healing higher-dimensional presentation light."));
		return;
	}

	LightComponent->SetMobility(EComponentMobility::Movable);
	LightComponent->SetIntensity(
		FamilyKey.Equals(HyperTwistHigherDimensionalTrainingGameModeInternal::Magic120CellFamilyKey)
			? 2.8f
			: 3.2f);
	LightComponent->SetLightColor(FLinearColor(0.86f, 0.93f, 1.0f));
	ActivePresentationLight->Tags.AddUnique(FName(TEXT("HyperTwistHigherDimensionalRuntimeLight")));
	UE_LOG(
		LogHyperTwistHigherDimensionalGameMode,
		Display,
		TEXT("Created self-healing runtime presentation lighting for family %s."),
		*FamilyKey);
}

void AHyperTwistHigherDimensionalTrainingGameMode::RequestRuntimeReadyDiagnosticsCapture()
{
	const FString SurfaceId = FString::Printf(
		TEXT("%s-runtime-ready"),
		*ResolveCurrentFamilyKey());
	HyperTwistRuntimeDiagnostics::RequestReadyFrameCapture(SurfaceId);
}

void AHyperTwistHigherDimensionalTrainingGameMode::PersistRuntimeReadinessDiagnostics(
	const FString& FamilyKey,
	const AHyperTwistHigherDimensionalTrainingShellActor* TrainingShell) const
{
	if (TrainingShell == nullptr)
	{
		return;
	}

	const FString DiagnosticsPath = FPaths::Combine(
		FPaths::ProjectLogDir(),
		TEXT("HyperTwistFirstRunLaunch-latest.log"));
	const FString Result =
		HyperTwistHigherDimensionalTrainingGameModeInternal::BuildReadinessResult(FamilyKey);
	const FString Diagnostics = FString::Printf(
		TEXT("[%s] [HigherDimensionalRuntime] family=%s canonical_elements=%d renderable_elements=%d projection=%s persistence=%s\nresult=%s\n"),
		*FDateTime::UtcNow().ToIso8601(),
		*FamilyKey,
		TrainingShell->GetCanonicalElementCount(),
		TrainingShell->GetRenderableElementCount(),
		*TrainingShell->ProjectionProfileId,
		*TrainingShell->PrimaryPersistenceBoundaryId,
		*Result);
	FFileHelper::SaveStringToFile(
		Diagnostics,
		*DiagnosticsPath,
		FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM,
		&IFileManager::Get(),
		FILEWRITE_Append);
}
