#include "HyperTwistTraining/HyperTwistHigherDimensionalTrainingShellActor.h"

#include "Components/InstancedStaticMeshComponent.h"
#include "Components/SceneComponent.h"
#include "Components/StaticMeshComponent.h"
#include "Components/TextRenderComponent.h"
#include "Engine/StaticMesh.h"
#include "HAL/FileManager.h"
#include "HyperTwistDiagnostics/HyperTwistRuntimeDiagnostics.h"
#include "Materials/MaterialInstanceDynamic.h"
#include "Materials/MaterialInterface.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonSerializer.h"
#include "UObject/ConstructorHelpers.h"

DEFINE_LOG_CATEGORY_STATIC(LogHyperTwistHigherDimensionalRuntime, Log, All);

namespace HyperTwistHigherDimensionalTrainingShellActorInternal
{
	const TCHAR* Magic120CellFamilyKey = TEXT("magic120cell");
	const TCHAR* MagicCube5DFamilyKey = TEXT("magiccube5d");
	constexpr int32 PreviewColorLayerCount = 6;
	constexpr int32 Magic120CellElementCount = 120;
	constexpr int32 MagicCube5DOrderThreeElementCount = 242;
	constexpr float ProjectionRefreshIntervalSeconds = 1.0f / 30.0f;

	bool IsEvenPermutation(const int32 A, const int32 B, const int32 C, const int32 D)
	{
		const int32 Values[4] = {A, B, C, D};
		int32 InversionCount = 0;
		for (int32 Left = 0; Left < 4; ++Left)
		{
			for (int32 Right = Left + 1; Right < 4; ++Right)
			{
				if (Values[Left] > Values[Right])
				{
					++InversionCount;
				}
			}
		}

		return InversionCount % 2 == 0;
	}

	float ApplySign(const float Value, const bool bNegative)
	{
		return bNegative ? -Value : Value;
	}
}

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

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

	PresentationFloor = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("PresentationFloor"));
	PresentationFloor->SetupAttachment(SceneRoot);
	PresentationFloor->SetMobility(EComponentMobility::Movable);
	PresentationFloor->SetCollisionEnabled(ECollisionEnabled::NoCollision);
	PresentationFloor->SetCastShadow(false);
	PresentationFloor->SetCanEverAffectNavigation(false);
	PresentationFloor->SetRelativeLocation(FVector(0.0f, 0.0f, -360.0f));
	PresentationFloor->SetRelativeScale3D(FVector(14.0f, 14.0f, 0.05f));

	PreviewInstances = CreateDefaultSubobject<UInstancedStaticMeshComponent>(TEXT("PreviewInstances"));
	PreviewInstances->SetupAttachment(SceneRoot);
	PreviewColorLayers.Add(PreviewInstances);

	for (int32 LayerIndex = 1;
		LayerIndex < HyperTwistHigherDimensionalTrainingShellActorInternal::PreviewColorLayerCount;
		++LayerIndex)
	{
		const FName ComponentName(*FString::Printf(TEXT("PreviewInstances%d"), LayerIndex));
		UInstancedStaticMeshComponent* PreviewLayer =
			CreateDefaultSubobject<UInstancedStaticMeshComponent>(ComponentName);
		PreviewLayer->SetupAttachment(SceneRoot);
		PreviewColorLayers.Add(PreviewLayer);
	}

	for (UInstancedStaticMeshComponent* PreviewLayer : PreviewColorLayers)
	{
		if (PreviewLayer == nullptr)
		{
			continue;
		}

		PreviewLayer->SetMobility(EComponentMobility::Movable);
		PreviewLayer->SetCollisionEnabled(ECollisionEnabled::NoCollision);
		PreviewLayer->SetCastShadow(false);
		PreviewLayer->SetCanEverAffectNavigation(false);
	}

	LabelComponent = CreateDefaultSubobject<UTextRenderComponent>(TEXT("LabelComponent"));
	LabelComponent->SetupAttachment(SceneRoot);
	LabelComponent->SetHorizontalAlignment(EHorizTextAligment::EHTA_Center);
	LabelComponent->SetVerticalAlignment(EVerticalTextAligment::EVRTA_TextCenter);
	LabelComponent->SetWorldSize(42.0f);
	LabelComponent->SetRelativeLocation(FVector(0.0f, 0.0f, 470.0f));
	LabelComponent->SetCollisionEnabled(ECollisionEnabled::NoCollision);
	// The native HUD owns readable status. A fixed world-space label becomes
	// mirrored as soon as the orbit camera reaches its back face.
	LabelComponent->SetVisibility(false);
	LabelComponent->SetHiddenInGame(true);

	static ConstructorHelpers::FObjectFinder<UStaticMesh> CubeMeshFinder(
		TEXT("/Engine/BasicShapes/Cube.Cube"));
	if (CubeMeshFinder.Succeeded())
	{
		CubePreviewMesh = CubeMeshFinder.Object;
		PresentationFloor->SetStaticMesh(CubeMeshFinder.Object);
	}

	static ConstructorHelpers::FObjectFinder<UStaticMesh> SphereMeshFinder(
		TEXT("/Engine/BasicShapes/Sphere.Sphere"));
	if (SphereMeshFinder.Succeeded())
	{
		SpherePreviewMesh = SphereMeshFinder.Object;
	}

	static ConstructorHelpers::FObjectFinder<UMaterialInterface> RedMaterial(
		TEXT("/Game/HyperTwistTraining/Materials/MI_HT_ClassicCube_Right.MI_HT_ClassicCube_Right"));
	static ConstructorHelpers::FObjectFinder<UMaterialInterface> BlueMaterial(
		TEXT("/Game/HyperTwistTraining/Materials/MI_HT_ClassicCube_Back.MI_HT_ClassicCube_Back"));
	static ConstructorHelpers::FObjectFinder<UMaterialInterface> GreenMaterial(
		TEXT("/Game/HyperTwistTraining/Materials/MI_HT_ClassicCube_Front.MI_HT_ClassicCube_Front"));
	static ConstructorHelpers::FObjectFinder<UMaterialInterface> WhiteMaterial(
		TEXT("/Game/HyperTwistTraining/Materials/MI_HT_ClassicCube_Up.MI_HT_ClassicCube_Up"));
	static ConstructorHelpers::FObjectFinder<UMaterialInterface> YellowMaterial(
		TEXT("/Game/HyperTwistTraining/Materials/MI_HT_ClassicCube_Down.MI_HT_ClassicCube_Down"));
	static ConstructorHelpers::FObjectFinder<UMaterialInterface> OrangeMaterial(
		TEXT("/Game/HyperTwistTraining/Materials/MI_HT_ClassicCube_Left.MI_HT_ClassicCube_Left"));
	static ConstructorHelpers::FObjectFinder<UMaterialInterface> FloorMaterial(
		TEXT("/Game/HyperTwistTraining/Materials/MI_HT_ClassicCube_Internal.MI_HT_ClassicCube_Internal"));

	PreviewMaterials =
	{
		RedMaterial.Object,
		BlueMaterial.Object,
		GreenMaterial.Object,
		YellowMaterial.Object,
		OrangeMaterial.Object,
		WhiteMaterial.Object
	};
	if (FloorMaterial.Succeeded())
	{
		PresentationFloor->SetMaterial(0, FloorMaterial.Object);
	}
}

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

	LoadRuntimeState();
	RefreshPreview();

	UE_LOG(
		LogHyperTwistHigherDimensionalRuntime,
		Display,
		TEXT("Higher-dimensional presentation initialized with %d renderable elements (canonical=%d) for family %s."),
		GetRenderableElementCount(),
		GetCanonicalElementCount(),
		*GetNormalizedFamilyKey());
	HyperTwistRuntimeDiagnostics::AppendEvent(
		TEXT("HigherDimensionalPresentation"),
		FString::Printf(
			TEXT("Higher-dimensional presentation initialized with %d renderable elements (canonical=%d) for family %s."),
			GetRenderableElementCount(),
			GetCanonicalElementCount(),
			*GetNormalizedFamilyKey()));
	UE_LOG(
		LogHyperTwistHigherDimensionalRuntime,
		Display,
		TEXT("Higher-dimensional projection palette initialized with %d distinct layer materials."),
		GetDistinctPreviewMaterialCount());
	HyperTwistRuntimeDiagnostics::AppendEvent(
		TEXT("HigherDimensionalPalette"),
		FString::Printf(
			TEXT("Higher-dimensional projection palette initialized with %d distinct layer materials."),
			GetDistinctPreviewMaterialCount()));
}

void AHyperTwistHigherDimensionalTrainingShellActor::EndPlay(
	const EEndPlayReason::Type EndPlayReason)
{
	if (EndPlayReason == EEndPlayReason::Quit || EndPlayReason == EEndPlayReason::LevelTransition)
	{
		SaveRuntimeState();
	}

	Super::EndPlay(EndPlayReason);
}

void AHyperTwistHigherDimensionalTrainingShellActor::OnConstruction(const FTransform& Transform)
{
	Super::OnConstruction(Transform);

	RefreshPreview();
	RefreshLabel();
	RefreshMetadataTags();
}

void AHyperTwistHigherDimensionalTrainingShellActor::Tick(const float DeltaSeconds)
{
	Super::Tick(DeltaSeconds);

	if (!bAutoRotateProjection || FMath::IsNearlyZero(ProjectionRotationSpeedDegrees))
	{
		return;
	}

	ProjectionAngleDegrees = FMath::Fmod(
		ProjectionAngleDegrees + (ProjectionRotationSpeedDegrees * DeltaSeconds),
		360.0f);
	ProjectionRefreshAccumulator += DeltaSeconds;
	if (ProjectionRefreshAccumulator >=
		HyperTwistHigherDimensionalTrainingShellActorInternal::ProjectionRefreshIntervalSeconds)
	{
		ProjectionRefreshAccumulator = 0.0f;
		UpdatePreviewTransforms();
	}
}

void AHyperTwistHigherDimensionalTrainingShellActor::ConfigureForFamily(
	const FString& InFamilyKey)
{
	const bool bUseMagic120Cell = InFamilyKey.Equals(
		HyperTwistHigherDimensionalTrainingShellActorInternal::Magic120CellFamilyKey,
		ESearchCase::IgnoreCase);

	FamilyKey = bUseMagic120Cell
		? HyperTwistHigherDimensionalTrainingShellActorInternal::Magic120CellFamilyKey
		: HyperTwistHigherDimensionalTrainingShellActorInternal::MagicCube5DFamilyKey;
	if (bUseMagic120Cell)
	{
		TrainingShellId = TEXT("phase6c/magic120cell/dedicated-training-shell");
		Title = TEXT("Magic120Cell // exact 120-cell simulator");
		Summary = TEXT("Family-owned 4D simulation with 120 cells, all 7,560 facelets, legal face/edge/vertex twists, exact scrambles, layer focus, and local persistence.");
		MapAssetPath = TEXT("/Game/HyperTwistTraining/Maps/L_HyperTwist_Magic120CellTraining");
		ActivationProfileId = TEXT("magic120cell-cleanroom-runtime-activation");
		HostSurfaceId = TEXT("phase6c/magic120cell/runtime-host-surface");
		LaunchSurfaceId = TEXT("phase6c/magic120cell/dedicated-training-launch-surface");
		ViewContextSurfaceId = TEXT("phase6c/magic120cell/dedicated-training-view-context-surface");
		SessionSurfaceId = TEXT("phase6c/magic120cell/dedicated-training-session-surface");
		InteractiveSceneSurfaceId = TEXT("phase6c/magic120cell/interactive-scene-surface");
		SceneContextId = TEXT("phase6c/magic120cell/interactive-scene-context");
		PuzzleId = TEXT("polychoron/magic120cell");
		RuntimeModeId = TEXT("magic120cell-exact-7560-facelet-runtime-v1");
		ProjectionProfileId = TEXT("magic120cell-4d-projection-distance-v1");
		PrimaryPersistenceBoundaryId = TEXT("magic120cell-persistence-boundary");
		if (!Magic120CellRuntimeState.IsStructurallyValid())
		{
			Magic120CellRuntimeState =
				UHyperTwistMagic120CellRuntimeLibrary::BuildSolvedState();
		}
	}
	else
	{
		TrainingShellId = TEXT("phase6c/magiccube5d/dedicated-training-shell");
		Title = TEXT("MagicCube5D // exact order-3 simulator");
		Summary = TEXT("Family-owned 5D simulation with 243 cubies, all 810 boundary facelets, legal signed-face plane twists, exact scrambles, depth-layer focus, and local persistence.");
		MapAssetPath = TEXT("/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube5DTraining");
		ActivationProfileId = TEXT("magiccube5d-cleanroom-runtime-activation");
		HostSurfaceId = TEXT("phase6c/magiccube5d/runtime-host-surface");
		LaunchSurfaceId = TEXT("phase6c/magiccube5d/dedicated-training-launch-surface");
		ViewContextSurfaceId = TEXT("phase6c/magiccube5d/dedicated-training-view-context-surface");
		SessionSurfaceId = TEXT("phase6c/magiccube5d/dedicated-training-session-surface");
		InteractiveSceneSurfaceId = TEXT("phase6c/magiccube5d/interactive-scene-surface");
		SceneContextId = TEXT("phase6c/magiccube5d/interactive-scene-context");
		PuzzleId = TEXT("hypercube/magiccube5d/order3");
		RuntimeModeId = TEXT("magiccube5d-order3-exact-piece-orientation-runtime-v1");
		ProjectionProfileId = TEXT("magiccube5d-5d-projection-distance-v1");
		PrimaryPersistenceBoundaryId = TEXT("magiccube5d-persistence-boundary");
		if (!MagicCube5DRuntimeState.IsStructurallyValid())
		{
			MagicCube5DRuntimeState =
				UHyperTwistMagicCube5DRuntimeLibrary::BuildSolvedState(3);
		}
	}

	AuthoringManifestRelativePath =
		TEXT("docs/generated/higher_dimensional_training_maps/phase6c_dedicated_family_map_manifest.json");
	bDedicatedFamilyOwnership = true;
	TrainingShellTags =
	{
		TEXT("phase6c"),
		FString::Printf(TEXT("family:%s"), *FamilyKey),
		TEXT("host:dedicated-family-map"),
		FString::Printf(TEXT("projection:%s"), *ProjectionProfileId),
		FString::Printf(TEXT("persistence:%s"), *PrimaryPersistenceBoundaryId),
		TEXT("presentation:runtime-owned")
	};

	VisibleProjectionLayer = FMath::Clamp(VisibleProjectionLayer, -1, GetProjectionLayerCount() - 1);
	RefreshPreview();
	RefreshLabel();
	RefreshMetadataTags();
}

void AHyperTwistHigherDimensionalTrainingShellActor::ToggleAutoRotateProjection()
{
	bAutoRotateProjection = !bAutoRotateProjection;
	LastRuntimeAction = bAutoRotateProjection ? TEXT("auto-rotation resumed") : TEXT("auto-rotation paused");
}

void AHyperTwistHigherDimensionalTrainingShellActor::NudgeProjection(const float DeltaDegrees)
{
	ProjectionAngleDegrees = FMath::Fmod(ProjectionAngleDegrees + DeltaDegrees + 360.0f, 360.0f);
	LastRuntimeAction = FString::Printf(TEXT("projection rotated %.0f degrees"), DeltaDegrees);
	UpdatePreviewTransforms();
}

void AHyperTwistHigherDimensionalTrainingShellActor::CycleProjectionLayer(const int32 Direction)
{
	const int32 LayerCount = GetProjectionLayerCount();
	if (LayerCount <= 0 || Direction == 0)
	{
		return;
	}

	const int32 StateCount = LayerCount + 1;
	const int32 CurrentState = VisibleProjectionLayer + 1;
	const int32 NextState = ((CurrentState + Direction) % StateCount + StateCount) % StateCount;
	VisibleProjectionLayer = NextState - 1;
	LastRuntimeAction = FString::Printf(TEXT("layer focus: %s"), *GetProjectionLayerLabel());
	UpdatePreviewTransforms();
}

void AHyperTwistHigherDimensionalTrainingShellActor::ResetProjection()
{
	ProjectionAngleDegrees = 0.0f;
	VisibleProjectionLayer = -1;
	RuntimeStateSeed = 0;
	if (UsesMagic120CellPreview())
	{
		Magic120CellRuntimeState =
			UHyperTwistMagic120CellRuntimeLibrary::BuildSolvedState();
		RebuildSourcePoints();
	}
	else
	{
		MagicCube5DRuntimeState =
			UHyperTwistMagicCube5DRuntimeLibrary::BuildSolvedState(3);
		RebuildSourcePoints();
	}
	bAutoRotateProjection = true;
	LastRuntimeAction = TEXT("projection and state reset to solved defaults");
	UpdatePreviewTransforms();
}

void AHyperTwistHigherDimensionalTrainingShellActor::CreateScrambledRuntimeState()
{
	RuntimeStateSeed = RuntimeStateSeed >= MAX_int32 - 1 ? 1 : RuntimeStateSeed + 1;
	if (UsesMagic120CellPreview())
	{
		const FHyperTwistMagic120CellScrambleResult Scramble =
			UHyperTwistMagic120CellRuntimeLibrary::GenerateScramble(
				100,
				120000 + RuntimeStateSeed);
		if (!Scramble.bGenerated || !Scramble.bExactStateUpdate)
		{
			LastRuntimeAction =
				TEXT("120-cell scramble rejected: exact-state generation failed");
			return;
		}

		Magic120CellRuntimeState = Scramble.State;
		ProjectionAngleDegrees = FMath::Fmod(
			ProjectionAngleDegrees + 37.0f,
			360.0f);
		LastRuntimeAction = FString::Printf(
			TEXT("exact 120-cell scramble %d applied (%d legal moves)"),
			RuntimeStateSeed,
			Scramble.Moves.Num());
		RebuildSourcePoints();
		UpdatePreviewTransforms();
		return;
	}
	else
	{
		const FHyperTwistMagicCube5DScrambleResult Scramble =
			UHyperTwistMagicCube5DRuntimeLibrary::GenerateScramble(
				3,
				60,
				51000 + RuntimeStateSeed);
		if (!Scramble.bGenerated || !Scramble.bExactStateUpdate)
		{
			LastRuntimeAction = TEXT("5D scramble rejected: exact-state generation failed");
			return;
		}

		MagicCube5DRuntimeState = Scramble.State;
		ProjectionAngleDegrees = FMath::Fmod(
			ProjectionAngleDegrees + 37.0f,
			360.0f);
		LastRuntimeAction = FString::Printf(
			TEXT("exact 5D scramble %d applied (%d legal moves)"),
			RuntimeStateSeed,
			Scramble.Moves.Num());
		RebuildSourcePoints();
		UpdatePreviewTransforms();
		return;
	}
}

bool AHyperTwistHigherDimensionalTrainingShellActor::ApplyMagicCube5DTurn(
	const FHyperTwistMagicCube5DTurnRequest& Request)
{
	if (UsesMagic120CellPreview())
	{
		LastRuntimeAction = TEXT("5D turn rejected on the 120-cell family route");
		return false;
	}
	if (!MagicCube5DRuntimeState.IsStructurallyValid())
	{
		MagicCube5DRuntimeState =
			UHyperTwistMagicCube5DRuntimeLibrary::BuildSolvedState(3);
	}

	const FHyperTwistMagicCube5DTurnResult Turn =
		UHyperTwistMagicCube5DRuntimeLibrary::ApplyTurn(
			MagicCube5DRuntimeState,
			Request);
	if (!Turn.bApplied || !Turn.bExactStateUpdate)
	{
		LastRuntimeAction = TEXT("5D turn rejected: exact-state update failed");
		return false;
	}

	MagicCube5DRuntimeState = Turn.State;
	LastRuntimeAction = FString::Printf(
		TEXT("5D move %s applied%s"),
		*Turn.AppliedNotation,
		MagicCube5DRuntimeState.bIsSolved ? TEXT(" // solved") : TEXT(""));
	RebuildSourcePoints();
	UpdatePreviewTransforms();
	return true;
}

bool AHyperTwistHigherDimensionalTrainingShellActor::ApplyMagic120CellTurn(
	const FHyperTwistMagic120CellTurnRequest& Request)
{
	if (!UsesMagic120CellPreview())
	{
		LastRuntimeAction = TEXT("120-cell turn rejected on the 5D family route");
		return false;
	}
	if (!Magic120CellRuntimeState.IsStructurallyValid())
	{
		Magic120CellRuntimeState =
			UHyperTwistMagic120CellRuntimeLibrary::BuildSolvedState();
	}

	const FHyperTwistMagic120CellTurnResult Turn =
		UHyperTwistMagic120CellRuntimeLibrary::ApplyTurn(
			Magic120CellRuntimeState,
			Request);
	if (!Turn.bApplied || !Turn.bExactStateUpdate)
	{
		LastRuntimeAction = TEXT("120-cell turn rejected: exact-state update failed");
		return false;
	}

	Magic120CellRuntimeState = Turn.State;
	LastRuntimeAction = FString::Printf(
		TEXT("120-cell move %s applied%s"),
		*Turn.AppliedNotation,
		Magic120CellRuntimeState.bIsSolved ? TEXT(" // solved") : TEXT(""));
	RebuildSourcePoints();
	UpdatePreviewTransforms();
	return true;
}

bool AHyperTwistHigherDimensionalTrainingShellActor::SaveRuntimeState()
{
	const FString StatePath = GetRuntimeStatePath();
	if (!IFileManager::Get().MakeDirectory(*FPaths::GetPath(StatePath), true))
	{
		LastPersistenceStatus = TEXT("save failed: state directory unavailable");
		return false;
	}

	const TSharedRef<FJsonObject> StateObject = MakeShared<FJsonObject>();
	StateObject->SetStringField(TEXT("schemaVersion"), TEXT("hypertwist/higher-dimensional-runtime-state/v2"));
	StateObject->SetStringField(TEXT("familyKey"), GetNormalizedFamilyKey());
	StateObject->SetStringField(TEXT("puzzleId"), PuzzleId);
	StateObject->SetNumberField(TEXT("projectionAngleDegrees"), ProjectionAngleDegrees);
	StateObject->SetNumberField(TEXT("visibleProjectionLayer"), VisibleProjectionLayer);
	StateObject->SetNumberField(TEXT("runtimeStateSeed"), RuntimeStateSeed);
	StateObject->SetBoolField(TEXT("autoRotateProjection"), bAutoRotateProjection);
	if (UsesMagic120CellPreview())
	{
		if (!Magic120CellRuntimeState.IsStructurallyValid())
		{
			LastPersistenceStatus = TEXT("save failed: invalid exact 120-cell runtime state");
			return false;
		}
		StateObject->SetStringField(
			TEXT("familyRuntimeStateJson"),
			UHyperTwistMagic120CellRuntimeLibrary::SerializeRuntimeStateToJson(
				Magic120CellRuntimeState));
	}
	else
	{
		if (!MagicCube5DRuntimeState.IsStructurallyValid())
		{
			LastPersistenceStatus = TEXT("save failed: invalid exact 5D runtime state");
			return false;
		}
		StateObject->SetStringField(
			TEXT("familyRuntimeStateJson"),
			UHyperTwistMagicCube5DRuntimeLibrary::SerializeRuntimeStateToJson(
				MagicCube5DRuntimeState));
	}
	StateObject->SetStringField(TEXT("savedAtUtc"), FDateTime::UtcNow().ToIso8601());

	FString SerializedState;
	const TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&SerializedState);
	if (!FJsonSerializer::Serialize(StateObject, Writer)
		|| !FFileHelper::SaveStringToFile(SerializedState, *StatePath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
	{
		LastPersistenceStatus = TEXT("save failed: state file could not be written");
		return false;
	}

	LastPersistenceStatus = TEXT("saved local runtime state");
	LastRuntimeAction = LastPersistenceStatus;
	return true;
}

bool AHyperTwistHigherDimensionalTrainingShellActor::LoadRuntimeState()
{
	const FString StatePath = GetRuntimeStatePath();
	FString SerializedState;
	if (!FFileHelper::LoadFileToString(SerializedState, *StatePath))
	{
		LastPersistenceStatus = TEXT("no saved state; using solved defaults");
		return false;
	}

	TSharedPtr<FJsonObject> StateObject;
	if (!FJsonSerializer::Deserialize(TJsonReaderFactory<>::Create(SerializedState), StateObject)
		|| !StateObject.IsValid())
	{
		LastPersistenceStatus = TEXT("load rejected: malformed state file");
		return false;
	}

	FString StoredFamilyKey;
	if (!StateObject->TryGetStringField(TEXT("familyKey"), StoredFamilyKey)
		|| !StoredFamilyKey.Equals(GetNormalizedFamilyKey(), ESearchCase::IgnoreCase))
	{
		LastPersistenceStatus = TEXT("load rejected: family mismatch");
		return false;
	}

	double StoredProjectionAngle = 0.0;
	double StoredVisibleLayer = -1.0;
	double StoredRuntimeStateSeed = 0.0;
	bool bStoredAutoRotate = true;
	if (!StateObject->TryGetNumberField(TEXT("projectionAngleDegrees"), StoredProjectionAngle)
		|| !StateObject->TryGetNumberField(TEXT("visibleProjectionLayer"), StoredVisibleLayer)
		|| !StateObject->TryGetNumberField(TEXT("runtimeStateSeed"), StoredRuntimeStateSeed)
		|| !StateObject->TryGetBoolField(TEXT("autoRotateProjection"), bStoredAutoRotate))
	{
		LastPersistenceStatus = TEXT("load rejected: incomplete state fields");
		return false;
	}

	FHyperTwistMagicCube5DRuntimeState StoredMagicCube5DState;
	FHyperTwistMagic120CellRuntimeState StoredMagic120CellState;
	FString FamilyRuntimeStateJson;
	if (!StateObject->TryGetStringField(
			TEXT("familyRuntimeStateJson"),
			FamilyRuntimeStateJson))
	{
		LastPersistenceStatus = TEXT("load rejected: exact family state missing");
		return false;
	}
	if (UsesMagic120CellPreview())
	{
		if (!UHyperTwistMagic120CellRuntimeLibrary::
			TryDeserializeRuntimeStateFromJson(
				FamilyRuntimeStateJson,
				StoredMagic120CellState))
		{
			LastPersistenceStatus =
				TEXT("load rejected: exact 120-cell family state invalid");
			return false;
		}
	}
	else
	{
		if (!UHyperTwistMagicCube5DRuntimeLibrary::
				TryDeserializeRuntimeStateFromJson(
					FamilyRuntimeStateJson,
					StoredMagicCube5DState)
			|| StoredMagicCube5DState.Order != 3)
		{
			LastPersistenceStatus =
				TEXT("load rejected: exact 5D family state missing or invalid");
			return false;
		}
	}

	ProjectionAngleDegrees = FMath::Fmod(static_cast<float>(StoredProjectionAngle) + 360.0f, 360.0f);
	VisibleProjectionLayer = FMath::Clamp(
		static_cast<int32>(StoredVisibleLayer),
		-1,
		GetProjectionLayerCount() - 1);
	RuntimeStateSeed = FMath::Max(static_cast<int32>(StoredRuntimeStateSeed), 0);
	bAutoRotateProjection = bStoredAutoRotate;
	if (UsesMagic120CellPreview())
	{
		Magic120CellRuntimeState = MoveTemp(StoredMagic120CellState);
		RebuildSourcePoints();
	}
	else
	{
		MagicCube5DRuntimeState = MoveTemp(StoredMagicCube5DState);
		RebuildSourcePoints();
	}
	LastPersistenceStatus = TEXT("loaded local runtime state");
	LastRuntimeAction = LastPersistenceStatus;
	UpdatePreviewTransforms();
	return true;
}

int32 AHyperTwistHigherDimensionalTrainingShellActor::GetRenderableElementCount() const
{
	int32 ElementCount = 0;
	for (const UInstancedStaticMeshComponent* PreviewLayer : PreviewColorLayers)
	{
		if (PreviewLayer != nullptr)
		{
			ElementCount += PreviewLayer->GetInstanceCount();
		}
	}
	return ElementCount;
}

int32 AHyperTwistHigherDimensionalTrainingShellActor::GetCanonicalElementCount() const
{
	return GetCanonicalElementCountForFamily(GetNormalizedFamilyKey());
}

int32 AHyperTwistHigherDimensionalTrainingShellActor::GetProjectionLayerCount() const
{
	return UsesMagic120CellPreview() ? 6 : 3;
}

int32 AHyperTwistHigherDimensionalTrainingShellActor::GetDistinctPreviewMaterialCount() const
{
	TSet<const UMaterialInterface*> DistinctMaterials;
	for (const UMaterialInterface* Material : PreviewMaterials)
	{
		if (Material != nullptr)
		{
			DistinctMaterials.Add(Material);
		}
	}

	return DistinctMaterials.Num();
}

FString AHyperTwistHigherDimensionalTrainingShellActor::GetProjectionLayerLabel() const
{
	return VisibleProjectionLayer < 0
		? TEXT("all layers")
		: FString::Printf(TEXT("layer %d of %d"), VisibleProjectionLayer + 1, GetProjectionLayerCount());
}

FString AHyperTwistHigherDimensionalTrainingShellActor::GetRuntimeStatusSummary() const
{
	const FString StateSummary = UsesMagic120CellPreview()
		&& Magic120CellRuntimeState.IsStructurallyValid()
			? FString::Printf(
				TEXT("%s | %d exact moves"),
				Magic120CellRuntimeState.bIsSolved ? TEXT("solved") : TEXT("mixed"),
				Magic120CellRuntimeState.AppliedMoveCount)
			: (!UsesMagic120CellPreview()
				&& MagicCube5DRuntimeState.IsStructurallyValid()
			? FString::Printf(
				TEXT("%s | %d exact moves"),
				MagicCube5DRuntimeState.bIsSolved ? TEXT("solved") : TEXT("mixed"),
				MagicCube5DRuntimeState.AppliedMoveCount)
				: TEXT("state unavailable"));
	return FString::Printf(
		TEXT("%d / %d elements | %s | %s | %s"),
		GetRenderableElementCount(),
		GetCanonicalElementCount(),
		*GetProjectionLayerLabel(),
		*StateSummary,
		bAutoRotateProjection ? TEXT("auto-rotating") : TEXT("rotation paused"));
}

FString AHyperTwistHigherDimensionalTrainingShellActor::GetRuntimeStatePath() const
{
	return FPaths::Combine(
		FPaths::ProjectSavedDir(),
		TEXT("HyperTwist"),
		TEXT("HigherDimensional"),
		FString::Printf(TEXT("%s-runtime-state.json"), *GetNormalizedFamilyKey()));
}

int32 AHyperTwistHigherDimensionalTrainingShellActor::GetCanonicalElementCountForFamily(
	const FString& InFamilyKey)
{
	return InFamilyKey.Equals(
		HyperTwistHigherDimensionalTrainingShellActorInternal::Magic120CellFamilyKey,
		ESearchCase::IgnoreCase)
		? HyperTwistHigherDimensionalTrainingShellActorInternal::Magic120CellElementCount
		: HyperTwistHigherDimensionalTrainingShellActorInternal::MagicCube5DOrderThreeElementCount;
}

void AHyperTwistHigherDimensionalTrainingShellActor::RefreshPreview()
{
	RebuildSourcePoints();
	ConfigurePreviewComponents();
	UpdatePreviewTransforms();
}

void AHyperTwistHigherDimensionalTrainingShellActor::RefreshLabel()
{
	if (LabelComponent == nullptr)
	{
		return;
	}

	LabelComponent->SetRelativeLocation(FVector(0.0f, 0.0f, PreviewHeightOffset + PreviewRadius + 120.0f));
	LabelComponent->SetText(BuildLabelText());
	LabelComponent->SetTextRenderColor(
		UsesMagic120CellPreview() ? FColor(54, 244, 255) : FColor(255, 177, 74));
}

void AHyperTwistHigherDimensionalTrainingShellActor::RefreshMetadataTags()
{
	TArray<FName> NewTags;

	const auto AddTag = [&NewTags](const FString& Tag)
	{
		if (!Tag.IsEmpty())
		{
			NewTags.AddUnique(FName(*Tag));
		}
	};

	AddTag(TEXT("HyperTwistHigherDimensionalTrainingShell"));
	AddTag(FString::Printf(TEXT("training-shell-id:%s"), *TrainingShellId));
	AddTag(FString::Printf(TEXT("activation-profile:%s"), *ActivationProfileId));
	AddTag(FString::Printf(TEXT("host-surface:%s"), *HostSurfaceId));
	AddTag(FString::Printf(TEXT("launch-surface:%s"), *LaunchSurfaceId));
	AddTag(FString::Printf(TEXT("view-context-surface:%s"), *ViewContextSurfaceId));
	AddTag(FString::Printf(TEXT("session-surface:%s"), *SessionSurfaceId));
	AddTag(FString::Printf(TEXT("interactive-scene-surface:%s"), *InteractiveSceneSurfaceId));
	AddTag(FString::Printf(TEXT("scene-context:%s"), *SceneContextId));
	AddTag(FString::Printf(TEXT("puzzle:%s"), *PuzzleId));
	AddTag(FString::Printf(TEXT("runtime-mode:%s"), *RuntimeModeId));
	AddTag(FString::Printf(TEXT("projection:%s"), *ProjectionProfileId));
	AddTag(FString::Printf(TEXT("persistence:%s"), *PrimaryPersistenceBoundaryId));
	AddTag(FString::Printf(TEXT("manifest:%s"), *AuthoringManifestRelativePath));
	AddTag(FString::Printf(TEXT("family:%s"), *FamilyKey));
	AddTag(bDedicatedFamilyOwnership
		? TEXT("ownership:dedicated-family")
		: TEXT("ownership:shared-or-unspecified"));

	for (const FString& TrainingShellTag : TrainingShellTags)
	{
		AddTag(TrainingShellTag);
	}

	Tags = MoveTemp(NewTags);
}

void AHyperTwistHigherDimensionalTrainingShellActor::RebuildSourcePoints()
{
	SourcePoints4D.Reset();
	SourceFifthCoordinates.Reset();
	SourceLayerIndices.Reset();
	SourceColorIndices.Reset();

	if (UsesMagic120CellPreview())
	{
		BuildMagic120CellSourcePoints();
	}
	else
	{
		BuildMagicCube5DSourcePoints();
	}
}

void AHyperTwistHigherDimensionalTrainingShellActor::BuildMagic120CellSourcePoints()
{
	using namespace HyperTwistHigherDimensionalTrainingShellActorInternal;
	if (!Magic120CellRuntimeState.IsStructurallyValid())
	{
		Magic120CellRuntimeState =
			UHyperTwistMagic120CellRuntimeLibrary::BuildSolvedState();
	}
	const FHyperTwistMagic120CellProjectionBuildResult Projection =
		UHyperTwistMagic120CellRuntimeLibrary::BuildProjection(
			Magic120CellRuntimeState);
	if (!Projection.bProjected || !Projection.bExactProjection)
	{
		return;
	}

	for (int32 Axis = 0; Axis < 4; ++Axis)
	{
		for (int32 Sign = -1; Sign <= 1; Sign += 2)
		{
			float Coordinates[4] = {0.0f, 0.0f, 0.0f, 0.0f};
			Coordinates[Axis] = static_cast<float>(Sign);
			const int32 Layer = FMath::Clamp(
				FMath::FloorToInt(((Coordinates[3] + 1.001f) / 2.002f) * 6.0f),
				0,
				5);
			AddSourcePoint(
				FVector4(Coordinates[0], Coordinates[1], Coordinates[2], Coordinates[3]),
				0.0f,
				Layer,
				SourcePoints4D.Num() % PreviewColorLayerCount);
		}
	}

	for (int32 SignMask = 0; SignMask < 16; ++SignMask)
	{
		const FVector4 Point(
			ApplySign(0.5f, (SignMask & 1) != 0),
			ApplySign(0.5f, (SignMask & 2) != 0),
			ApplySign(0.5f, (SignMask & 4) != 0),
			ApplySign(0.5f, (SignMask & 8) != 0));
		const int32 Layer = FMath::Clamp(
			FMath::FloorToInt(((Point.W + 1.001f) / 2.002f) * 6.0f),
			0,
			5);
		AddSourcePoint(Point, 0.0f, Layer, SourcePoints4D.Num() % PreviewColorLayerCount);
	}

	const float GoldenRatio = (1.0f + FMath::Sqrt(5.0f)) * 0.5f;
	const float BaseValues[4] =
	{
		0.0f,
		0.5f,
		GoldenRatio * 0.5f,
		1.0f / (2.0f * GoldenRatio)
	};

	for (int32 A = 0; A < 4; ++A)
	{
		for (int32 B = 0; B < 4; ++B)
		{
			if (B == A)
			{
				continue;
			}
			for (int32 C = 0; C < 4; ++C)
			{
				if (C == A || C == B)
				{
					continue;
				}
				for (int32 D = 0; D < 4; ++D)
				{
					if (D == A || D == B || D == C || !IsEvenPermutation(A, B, C, D))
					{
						continue;
					}

					const int32 Permutation[4] = {A, B, C, D};
					for (int32 SignMask = 0; SignMask < 8; ++SignMask)
					{
						float Coordinates[4] = {0.0f, 0.0f, 0.0f, 0.0f};
						int32 SignIndex = 0;
						for (int32 CoordinateIndex = 0; CoordinateIndex < 4; ++CoordinateIndex)
						{
							const float Value = BaseValues[Permutation[CoordinateIndex]];
							if (FMath::IsNearlyZero(Value))
							{
								continue;
							}
							Coordinates[CoordinateIndex] = ApplySign(
								Value,
								(SignMask & (1 << SignIndex)) != 0);
							++SignIndex;
						}

						const int32 Layer = FMath::Clamp(
							FMath::FloorToInt(((Coordinates[3] + 1.001f) / 2.002f) * 6.0f),
							0,
							5);
						AddSourcePoint(
							FVector4(Coordinates[0], Coordinates[1], Coordinates[2], Coordinates[3]),
							0.0f,
							Layer,
							SourcePoints4D.Num() % PreviewColorLayerCount);
					}
				}
			}
		}
	}

	ensureMsgf(
		SourcePoints4D.Num() == Magic120CellElementCount,
		TEXT("Magic120Cell projection source must contain exactly 120 cell centers; observed %d."),
		SourcePoints4D.Num());
	if (SourceColorIndices.Num() == Projection.Projection.Cells.Num())
	{
		for (int32 CellIndex = 0;
			 CellIndex < SourceColorIndices.Num();
			 ++CellIndex)
		{
			SourceColorIndices[CellIndex] =
				Projection.Projection.Cells[CellIndex].RepresentativeColorIndex
				% PreviewColorLayerCount;
		}
	}
}

void AHyperTwistHigherDimensionalTrainingShellActor::BuildMagicCube5DSourcePoints()
{
	using namespace HyperTwistHigherDimensionalTrainingShellActorInternal;
	if (!MagicCube5DRuntimeState.IsStructurallyValid())
	{
		MagicCube5DRuntimeState =
			UHyperTwistMagicCube5DRuntimeLibrary::BuildSolvedState(3);
	}
	const FHyperTwistMagicCube5DProjectionBuildResult Projection =
		UHyperTwistMagicCube5DRuntimeLibrary::BuildProjection(
			MagicCube5DRuntimeState);
	if (!Projection.bProjected || !Projection.bExactProjection)
	{
		return;
	}

	for (const FHyperTwistMagicCube5DProjectedCubie& Cubie :
		 Projection.Projection.Cubies)
	{
		const TArray<int32>& Components = Cubie.Position.Components;
		AddSourcePoint(
			FVector4(
				static_cast<float>(Components[0]),
				static_cast<float>(Components[1]),
				static_cast<float>(Components[2]),
				static_cast<float>(Components[3])),
			static_cast<float>(Components[4]),
			Components[4] + 1,
			Cubie.RepresentativeColorIndex % PreviewColorLayerCount);
	}

	ensureMsgf(
		SourcePoints4D.Num() == MagicCube5DOrderThreeElementCount,
		TEXT("MagicCube5D order-3 projection source must contain exactly 242 non-central cells; observed %d."),
		SourcePoints4D.Num());
}

void AHyperTwistHigherDimensionalTrainingShellActor::UpdatePreviewTransforms()
{
	ClearPreviewInstances();
	if (SourcePoints4D.Num() != SourceFifthCoordinates.Num()
		|| SourcePoints4D.Num() != SourceLayerIndices.Num()
		|| SourcePoints4D.Num() != SourceColorIndices.Num())
	{
		return;
	}

	const bool bMagic120Cell = UsesMagic120CellPreview();
	for (int32 Index = 0; Index < SourcePoints4D.Num(); ++Index)
	{
		if (VisibleProjectionLayer >= 0 && SourceLayerIndices[Index] != VisibleProjectionLayer)
		{
			continue;
		}

		const FVector ProjectedPoint = bMagic120Cell
			? ProjectMagic120CellPoint(SourcePoints4D[Index])
			: ProjectMagicCube5DPoint(SourcePoints4D[Index], SourceFifthCoordinates[Index]);
		const int32 ColorIndex = SourceColorIndices[Index];
		const float ElementScale = bMagic120Cell ? 0.145f : 0.092f;
		AddPreviewInstance(ProjectedPoint, ElementScale, ColorIndex);
	}
}

void AHyperTwistHigherDimensionalTrainingShellActor::ClearPreviewInstances()
{
	for (UInstancedStaticMeshComponent* PreviewLayer : PreviewColorLayers)
	{
		if (PreviewLayer != nullptr)
		{
			PreviewLayer->ClearInstances();
		}
	}
}

void AHyperTwistHigherDimensionalTrainingShellActor::ConfigurePreviewComponents()
{
	using namespace HyperTwistHigherDimensionalTrainingShellActorInternal;

	PreviewDynamicMaterials.SetNum(PreviewColorLayerCount);
	UStaticMesh* PreviewMesh = UsesMagic120CellPreview() ? SpherePreviewMesh : CubePreviewMesh;
	if (PreviewMesh == nullptr)
	{
		PreviewMesh = CubePreviewMesh != nullptr ? CubePreviewMesh : SpherePreviewMesh;
	}

	for (int32 LayerIndex = 0; LayerIndex < PreviewColorLayers.Num(); ++LayerIndex)
	{
		UInstancedStaticMeshComponent* PreviewLayer = PreviewColorLayers[LayerIndex];
		if (PreviewLayer == nullptr)
		{
			continue;
		}

		PreviewLayer->SetStaticMesh(PreviewMesh);
		if (PreviewMaterials.IsValidIndex(LayerIndex) && PreviewMaterials[LayerIndex] != nullptr)
		{
			UMaterialInstanceDynamic* LayerMaterial =
				PreviewDynamicMaterials.IsValidIndex(LayerIndex)
					? PreviewDynamicMaterials[LayerIndex]
					: nullptr;
			if (LayerMaterial == nullptr)
			{
				LayerMaterial = UMaterialInstanceDynamic::Create(
					PreviewMaterials[LayerIndex],
					this,
					FName(*FString::Printf(TEXT("HigherDimensionalLayerMaterial%d"), LayerIndex)));
				PreviewDynamicMaterials[LayerIndex] = LayerMaterial;
			}
			if (LayerMaterial != nullptr)
			{
				LayerMaterial->SetScalarParameterValue(TEXT("GlowStrength"), 0.8f);
				LayerMaterial->SetScalarParameterValue(TEXT("FaceOpacity"), 1.0f);
				PreviewLayer->SetMaterial(0, LayerMaterial);
			}
			else
			{
				PreviewLayer->SetMaterial(0, PreviewMaterials[LayerIndex]);
			}
		}
	}

	if (PresentationFloor != nullptr)
	{
		UMaterialInterface* FloorMaterial = PresentationFloor->GetMaterial(0);
		if (FloorDynamicMaterial == nullptr && FloorMaterial != nullptr)
		{
			FloorDynamicMaterial = UMaterialInstanceDynamic::Create(
				FloorMaterial,
				this,
				TEXT("HigherDimensionalFloorMaterial"));
		}
		if (FloorDynamicMaterial != nullptr)
		{
			const FLinearColor FloorColor = UsesMagic120CellPreview()
				? FLinearColor(0.008f, 0.018f, 0.032f)
				: FLinearColor(0.026f, 0.012f, 0.006f);
			FloorDynamicMaterial->SetVectorParameterValue(TEXT("BaseColor"), FloorColor);
			FloorDynamicMaterial->SetScalarParameterValue(TEXT("GlowStrength"), 0.015f);
			PresentationFloor->SetMaterial(0, FloorDynamicMaterial);
		}
	}
}

void AHyperTwistHigherDimensionalTrainingShellActor::AddSourcePoint(
	const FVector4& Point4D,
	const float FifthCoordinate,
	const int32 LayerIndex,
	const int32 ColorIndex)
{
	SourcePoints4D.Add(Point4D);
	SourceFifthCoordinates.Add(FifthCoordinate);
	SourceLayerIndices.Add(LayerIndex);
	SourceColorIndices.Add(ColorIndex);
}

void AHyperTwistHigherDimensionalTrainingShellActor::AddPreviewInstance(
	const FVector& RelativeLocation,
	const float UniformScale,
	const int32 ColorIndex)
{
	if (PreviewColorLayers.Num() == 0)
	{
		return;
	}

	const int32 LayerIndex = FMath::Abs(ColorIndex) % PreviewColorLayers.Num();
	UInstancedStaticMeshComponent* PreviewLayer = PreviewColorLayers[LayerIndex];
	if (PreviewLayer == nullptr || PreviewLayer->GetStaticMesh() == nullptr)
	{
		return;
	}

	FTransform InstanceTransform;
	InstanceTransform.SetLocation(RelativeLocation);
	InstanceTransform.SetScale3D(FVector(UniformScale));
	PreviewLayer->AddInstance(InstanceTransform);
}

FVector AHyperTwistHigherDimensionalTrainingShellActor::ProjectMagic120CellPoint(
	const FVector4& Point) const
{
	const float Angle = FMath::DegreesToRadians(ProjectionAngleDegrees);
	const float SecondaryAngle = Angle * 0.37f;
	const float CosAngle = FMath::Cos(Angle);
	const float SinAngle = FMath::Sin(Angle);
	const float CosSecondary = FMath::Cos(SecondaryAngle);
	const float SinSecondary = FMath::Sin(SecondaryAngle);

	const float RotatedX = (Point.X * CosAngle) - (Point.W * SinAngle);
	const float RotatedW = (Point.X * SinAngle) + (Point.W * CosAngle);
	const float RotatedY = (Point.Y * CosSecondary) - (Point.Z * SinSecondary);
	const float RotatedZ = (Point.Y * SinSecondary) + (Point.Z * CosSecondary);
	constexpr float ProjectionDistance = 3.2f;
	const float Perspective = ProjectionDistance /
		FMath::Max(ProjectionDistance - RotatedW, 0.35f);

	return FVector(RotatedX, RotatedY, RotatedZ) * PreviewRadius * Perspective
		+ FVector(0.0f, 0.0f, PreviewHeightOffset);
}

FVector AHyperTwistHigherDimensionalTrainingShellActor::ProjectMagicCube5DPoint(
	const FVector4& Point,
	const float FifthCoordinate) const
{
	const float Angle = FMath::DegreesToRadians(ProjectionAngleDegrees);
	const float CosAngle = FMath::Cos(Angle);
	const float SinAngle = FMath::Sin(Angle);
	const float SecondaryAngle = Angle * 0.73f;
	const float CosSecondary = FMath::Cos(SecondaryAngle);
	const float SinSecondary = FMath::Sin(SecondaryAngle);

	const float RotatedX = (Point.X * CosAngle) - (Point.W * SinAngle);
	const float RotatedW = (Point.X * SinAngle) + (Point.W * CosAngle);
	const float RotatedY = (Point.Y * CosSecondary) - (FifthCoordinate * SinSecondary);
	const float RotatedV = (Point.Y * SinSecondary) + (FifthCoordinate * CosSecondary);

	constexpr float FiveDimensionalProjectionDistance = 3.6f;
	const float FiveDimensionalPerspective = FiveDimensionalProjectionDistance /
		FMath::Max(FiveDimensionalProjectionDistance - RotatedV, 0.4f);
	const FVector4 Projected4D(
		RotatedX * FiveDimensionalPerspective,
		RotatedY * FiveDimensionalPerspective,
		Point.Z * FiveDimensionalPerspective,
		RotatedW * FiveDimensionalPerspective);

	constexpr float FourDimensionalProjectionDistance = 4.2f;
	const float FourDimensionalPerspective = FourDimensionalProjectionDistance /
		FMath::Max(FourDimensionalProjectionDistance - Projected4D.W, 0.45f);
	const float SpatialScale = PreviewRadius * 0.42f;
	return FVector(Projected4D.X, Projected4D.Y, Projected4D.Z)
		* SpatialScale
		* FourDimensionalPerspective
		+ FVector(0.0f, 0.0f, PreviewHeightOffset);
}

FText AHyperTwistHigherDimensionalTrainingShellActor::BuildLabelText() const
{
	const FString EffectiveTitle = !Title.IsEmpty()
		? Title
		: (UsesMagic120CellPreview()
			? TEXT("Magic120Cell // exact 120-cell simulator")
			: TEXT("MagicCube5D // exact order-3 simulator"));

	return FText::FromString(FString::Printf(
		TEXT("%s\n%d canonical elements // %s"),
		*EffectiveTitle,
		GetCanonicalElementCount(),
		*GetProjectionLayerLabel()));
}

bool AHyperTwistHigherDimensionalTrainingShellActor::UsesMagic120CellPreview() const
{
	return FamilyKey.Equals(
		HyperTwistHigherDimensionalTrainingShellActorInternal::Magic120CellFamilyKey,
		ESearchCase::IgnoreCase);
}

FString AHyperTwistHigherDimensionalTrainingShellActor::GetNormalizedFamilyKey() const
{
	return UsesMagic120CellPreview()
		? HyperTwistHigherDimensionalTrainingShellActorInternal::Magic120CellFamilyKey
		: HyperTwistHigherDimensionalTrainingShellActorInternal::MagicCube5DFamilyKey;
}
