#include "HyperTwistSimulation/HyperTwistClassicCubeActor.h"
#include "Engine/World.h"
#include "HyperTwistSimulation/HyperTwistClassicCubeCommandLibrary.h"
#include "HAL/PlatformTime.h"
#include "Materials/MaterialInstanceDynamic.h"
#include "Materials/MaterialInterface.h"
#include "ProceduralMeshComponent/Public/ProceduralMeshComponent.h"
#include "ThirdParty/rob-twophase/cubie.h"
#include "ThirdParty/rob-twophase/face.h"
#include "ThirdParty/rob-twophase/move.h"
#include "UObject/ConstructorHelpers.h"

#include <mutex>

namespace HyperTwistClassicCubeActorInternal
{
	constexpr float RotationDotTolerance = 0.9999f;

	void EnsureSolverFaceletLibrariesInitialized()
	{
		static std::once_flag InitOnce;
		std::call_once(InitOnce, []()
		{
			face::init();
			move::init();
		});
	}
}

AHyperTwistClassicCubeActor::AHyperTwistClassicCubeActor()
{
	PrimaryActorTick.bCanEverTick = true;
	PrimaryActorTick.bStartWithTickEnabled = true;
	PrimaryActorTick.bTickEvenWhenPaused = true;
	RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));

	FaceMaterials.SetNum(static_cast<int32>(EHyperTwistClassicCubeFace::Right) + 1);
	static ConstructorHelpers::FObjectFinder<UMaterialInterface> UpMaterial(
		TEXT("/Game/HyperTwistTraining/Materials/MI_HT_ClassicCube_Up.MI_HT_ClassicCube_Up")
	);
	static ConstructorHelpers::FObjectFinder<UMaterialInterface> DownMaterial(
		TEXT("/Game/HyperTwistTraining/Materials/MI_HT_ClassicCube_Down.MI_HT_ClassicCube_Down")
	);
	static ConstructorHelpers::FObjectFinder<UMaterialInterface> FrontMaterial(
		TEXT("/Game/HyperTwistTraining/Materials/MI_HT_ClassicCube_Front.MI_HT_ClassicCube_Front")
	);
	static ConstructorHelpers::FObjectFinder<UMaterialInterface> BackMaterial(
		TEXT("/Game/HyperTwistTraining/Materials/MI_HT_ClassicCube_Back.MI_HT_ClassicCube_Back")
	);
	static ConstructorHelpers::FObjectFinder<UMaterialInterface> LeftMaterial(
		TEXT("/Game/HyperTwistTraining/Materials/MI_HT_ClassicCube_Left.MI_HT_ClassicCube_Left")
	);
	static ConstructorHelpers::FObjectFinder<UMaterialInterface> RightMaterial(
		TEXT("/Game/HyperTwistTraining/Materials/MI_HT_ClassicCube_Right.MI_HT_ClassicCube_Right")
	);
	static ConstructorHelpers::FObjectFinder<UMaterialInterface> InternalFaceMaterial(
		TEXT("/Game/HyperTwistTraining/Materials/MI_HT_ClassicCube_Internal.MI_HT_ClassicCube_Internal")
	);

	FaceMaterials[static_cast<int32>(EHyperTwistClassicCubeFace::Up)] = UpMaterial.Object;
	FaceMaterials[static_cast<int32>(EHyperTwistClassicCubeFace::Down)] = DownMaterial.Object;
	FaceMaterials[static_cast<int32>(EHyperTwistClassicCubeFace::Front)] = FrontMaterial.Object;
	FaceMaterials[static_cast<int32>(EHyperTwistClassicCubeFace::Back)] = BackMaterial.Object;
	FaceMaterials[static_cast<int32>(EHyperTwistClassicCubeFace::Left)] = LeftMaterial.Object;
	FaceMaterials[static_cast<int32>(EHyperTwistClassicCubeFace::Right)] = RightMaterial.Object;
	InternalMaterial = InternalFaceMaterial.Object;
}

void AHyperTwistClassicCubeActor::OnConstruction(const FTransform& Transform)
{
	Super::OnConstruction(Transform);
	if (bGenerateOnConstruction)
	{
		ResetCube();
	}
}

void AHyperTwistClassicCubeActor::BeginPlay()
{
	Super::BeginPlay();
	SetActorTickEnabled(true);
	EnsureRenderablePresentation();
}

void AHyperTwistClassicCubeActor::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	if (!bIsAnimating)
	{
		return;
	}
	if (!IsValid(ActiveRotation.Pivot))
	{
		FinalizeRotation();
		return;
	}

	const double Elapsed = FPlatformTime::Seconds() - ActiveRotation.StartTime;
	float Alpha = FMath::Clamp(
		static_cast<float>(Elapsed / FMath::Max(ActiveRotation.Duration, SMALL_NUMBER)),
		0.0f,
		1.0f);
	// Smooth step for nicer animation feel
	Alpha = Alpha * Alpha * (3.0f - 2.0f * Alpha);

	const FQuat CurrentRot = FQuat::Slerp(ActiveRotation.PivotStartRot, ActiveRotation.PivotTargetRot, Alpha);
	ActiveRotation.Pivot->SetRelativeRotation(CurrentRot);

	if (Alpha >= 1.0f)
	{
		FinalizeRotation();
	}
}

void AHyperTwistClassicCubeActor::ResetCube()
{
	ClearPieces();
	TrackedPieces.Empty();
	RotationQueue.Empty();
	CurrentScrambleMoves.Empty();
	CompletedMoveHistory.Empty();
	TotalCompletedMoveCount = 0;
	GameplayCompletedMoveCount = 0;
	bIsAnimating = false;
	bHasHintedFace = false;
	HintedFace = EHyperTwistClassicCubeFace::Up;
	ActiveRotation = FActiveRotation();
	GenerateCube();
	UpdateHintMaterialState();
}

bool AHyperTwistClassicCubeActor::ProcessClick(const FVector& RayOrigin, const FVector& RayDirection, bool bCounterClockwise)
{
	if (!GetWorld())
	{
		return false;
	}

	FHitResult Hit;
	FCollisionQueryParams Params(SCENE_QUERY_STAT(CubeClick), false, this);
	const FVector End = RayOrigin + RayDirection * 10000.0f;
	bool bHit = GetWorld()->LineTraceSingleByChannel(Hit, RayOrigin, End, ECC_Visibility, Params);

	if (!bHit || !Hit.GetComponent())
	{
		return false;
	}

	EHyperTwistClassicCubeFace Face = EHyperTwistClassicCubeFace::Up;
	if (!TryResolveFaceFromImpactNormal(Hit.ImpactNormal, Face))
	{
		return false;
	}

	RotateFace(
		Face,
		bCounterClockwise ? EHyperTwistRotationDirection::CounterClockwise
						  : EHyperTwistRotationDirection::Clockwise
	);
	return true;
}

bool AHyperTwistClassicCubeActor::TryResolveFaceFromImpactNormal(
	const FVector& ImpactNormal,
	EHyperTwistClassicCubeFace& OutFace
)
{
	const FVector Normal = ImpactNormal.GetSafeNormal();
	if (Normal.IsNearlyZero())
	{
		return false;
	}

	// Use the dominant axis so small floating-point drift on procedural geometry
	// does not make face selection unstable.
	const FVector AbsNormal = Normal.GetAbs();
	if (AbsNormal.Z >= AbsNormal.X && AbsNormal.Z >= AbsNormal.Y)
	{
		OutFace = Normal.Z >= 0.0f ? EHyperTwistClassicCubeFace::Up : EHyperTwistClassicCubeFace::Down;
	}
	else if (AbsNormal.Y >= AbsNormal.X)
	{
		OutFace = Normal.Y >= 0.0f ? EHyperTwistClassicCubeFace::Front : EHyperTwistClassicCubeFace::Back;
	}
	else
	{
		OutFace = Normal.X >= 0.0f ? EHyperTwistClassicCubeFace::Right : EHyperTwistClassicCubeFace::Left;
	}

	return true;
}

TArray<FString> AHyperTwistClassicCubeActor::GenerateScramble(int32 Length)
{
	TArray<FString> Result;
	const TArray<FString> FaceNames = { TEXT("U"), TEXT("D"), TEXT("F"), TEXT("B"), TEXT("R"), TEXT("L") };
	const TArray<FString> Suffixes = { TEXT(""), TEXT("'"), TEXT("2") };
	const TArray<EHyperTwistClassicCubeFace> FaceOrder = {
		EHyperTwistClassicCubeFace::Up,
		EHyperTwistClassicCubeFace::Down,
		EHyperTwistClassicCubeFace::Front,
		EHyperTwistClassicCubeFace::Back,
		EHyperTwistClassicCubeFace::Right,
		EHyperTwistClassicCubeFace::Left
	};

	// Seed random if not already seeded
	static bool bSeeded = false;
	if (!bSeeded)
	{
		FMath::RandInit(FDateTime::Now().GetTicks());
		bSeeded = true;
	}

	Length = FMath::Max(0, Length);
	int32 PreviousFaceIndex = INDEX_NONE;
	int32 PreviousAxisIndex = INDEX_NONE;
	for (int32 i = 0; i < Length; ++i)
	{
		TArray<int32> CandidateFaceIndices;
		for (int32 FaceIndex = 0; FaceIndex < FaceNames.Num(); ++FaceIndex)
		{
			if (FaceIndex == PreviousFaceIndex)
			{
				continue;
			}

			const int32 AxisIndex = GetFaceAxisIndex(FaceOrder[FaceIndex]);
			if (AxisIndex == PreviousAxisIndex)
			{
				continue;
			}

			CandidateFaceIndices.Add(FaceIndex);
		}

		if (CandidateFaceIndices.IsEmpty())
		{
			for (int32 FaceIndex = 0; FaceIndex < FaceNames.Num(); ++FaceIndex)
			{
				if (FaceIndex != PreviousFaceIndex)
				{
					CandidateFaceIndices.Add(FaceIndex);
				}
			}
		}

		const int32 SelectedFaceIndex =
			CandidateFaceIndices[FMath::RandRange(0, CandidateFaceIndices.Num() - 1)];
		FString Move = FaceNames[SelectedFaceIndex];
		FString Suffix = Suffixes[FMath::RandRange(0, 2)];
		Result.Add(Move + Suffix);
		PreviousFaceIndex = SelectedFaceIndex;
		PreviousAxisIndex = GetFaceAxisIndex(FaceOrder[SelectedFaceIndex]);
	}
	return Result;
}

void AHyperTwistClassicCubeActor::ApplyScramble(const TArray<FString>& MoveStrings)
{
	CurrentScrambleMoves.Empty();

	for (const FString& Move : MoveStrings)
	{
		FHyperTwistClassicCubeMoveDescriptor MoveDescriptor;
		if (!UHyperTwistClassicCubeCommandLibrary::TryParseMoveNotation(Move, MoveDescriptor))
		{
			continue;
		}

		CurrentScrambleMoves.Add(MoveDescriptor.Notation);
		MoveDescriptor.bGameplayMove = false;
		TryQueueMoveDescriptor(MoveDescriptor);
	}

	ProcessRotationQueue();
}

void AHyperTwistClassicCubeActor::ApplyScrambleImmediately(const TArray<FString>& MoveStrings)
{
	CurrentScrambleMoves.Empty();

	for (const FString& Move : MoveStrings)
	{
		FHyperTwistClassicCubeMoveDescriptor MoveDescriptor;
		if (!UHyperTwistClassicCubeCommandLibrary::TryParseMoveNotation(Move, MoveDescriptor))
		{
			continue;
		}

		CurrentScrambleMoves.Add(MoveDescriptor.Notation);
		MoveDescriptor.bGameplayMove = false;
		ExecuteMoveDescriptorImmediately(MoveDescriptor);
	}
}

void AHyperTwistClassicCubeActor::ClearPieces()
{
	for (const FTrackedPiece& Piece : TrackedPieces)
	{
		if (IsValid(Piece.Mesh))
		{
			Piece.Mesh->DestroyComponent();
		}
	}
	TrackedPieces.Empty();

	if (IsValid(ActiveRotation.Pivot))
	{
		ActiveRotation.Pivot->DestroyComponent();
	}
	ActiveRotation = FActiveRotation();
	bIsAnimating = false;
}

void AHyperTwistClassicCubeActor::GenerateCube()
{
	TrackedPieces.SetNum(27);

	struct FPieceDef
	{
		FVector GridPos;
		TArray<EHyperTwistClassicCubeFace> IdentityFaces;
	};

	const TArray<FPieceDef> Pieces = {
		// Centers
		{FVector(0, 0, 1),   {EHyperTwistClassicCubeFace::Up}},
		{FVector(0, 0, -1),  {EHyperTwistClassicCubeFace::Down}},
		{FVector(0, 1, 0),   {EHyperTwistClassicCubeFace::Front}},
		{FVector(0, -1, 0),  {EHyperTwistClassicCubeFace::Back}},
		{FVector(-1, 0, 0),  {EHyperTwistClassicCubeFace::Left}},
		{FVector(1, 0, 0),   {EHyperTwistClassicCubeFace::Right}},
		// Edges
		{FVector(0, 1, 1),   {EHyperTwistClassicCubeFace::Up,    EHyperTwistClassicCubeFace::Front}},
		{FVector(1, 0, 1),   {EHyperTwistClassicCubeFace::Up,    EHyperTwistClassicCubeFace::Right}},
		{FVector(0, -1, 1),  {EHyperTwistClassicCubeFace::Up,    EHyperTwistClassicCubeFace::Back}},
		{FVector(-1, 0, 1),  {EHyperTwistClassicCubeFace::Up,    EHyperTwistClassicCubeFace::Left}},
		{FVector(0, 1, -1),  {EHyperTwistClassicCubeFace::Down,  EHyperTwistClassicCubeFace::Front}},
		{FVector(1, 0, -1),  {EHyperTwistClassicCubeFace::Down,  EHyperTwistClassicCubeFace::Right}},
		{FVector(0, -1, -1), {EHyperTwistClassicCubeFace::Down,  EHyperTwistClassicCubeFace::Back}},
		{FVector(-1, 0, -1), {EHyperTwistClassicCubeFace::Down,  EHyperTwistClassicCubeFace::Left}},
		{FVector(1, 1, 0),   {EHyperTwistClassicCubeFace::Front, EHyperTwistClassicCubeFace::Right}},
		{FVector(-1, 1, 0),  {EHyperTwistClassicCubeFace::Front, EHyperTwistClassicCubeFace::Left}},
		{FVector(1, -1, 0),  {EHyperTwistClassicCubeFace::Back,  EHyperTwistClassicCubeFace::Right}},
		{FVector(-1, -1, 0), {EHyperTwistClassicCubeFace::Back,  EHyperTwistClassicCubeFace::Left}},
		// Corners
		{FVector(1, 1, 1),    {EHyperTwistClassicCubeFace::Up,    EHyperTwistClassicCubeFace::Front, EHyperTwistClassicCubeFace::Right}},
		{FVector(-1, 1, 1),   {EHyperTwistClassicCubeFace::Up,    EHyperTwistClassicCubeFace::Front, EHyperTwistClassicCubeFace::Left}},
		{FVector(1, -1, 1),   {EHyperTwistClassicCubeFace::Up,    EHyperTwistClassicCubeFace::Back,  EHyperTwistClassicCubeFace::Right}},
		{FVector(-1, -1, 1),  {EHyperTwistClassicCubeFace::Up,    EHyperTwistClassicCubeFace::Back,  EHyperTwistClassicCubeFace::Left}},
		{FVector(1, 1, -1),   {EHyperTwistClassicCubeFace::Down,  EHyperTwistClassicCubeFace::Front, EHyperTwistClassicCubeFace::Right}},
		{FVector(-1, 1, -1),  {EHyperTwistClassicCubeFace::Down,  EHyperTwistClassicCubeFace::Front, EHyperTwistClassicCubeFace::Left}},
		{FVector(1, -1, -1),  {EHyperTwistClassicCubeFace::Down,  EHyperTwistClassicCubeFace::Back,  EHyperTwistClassicCubeFace::Right}},
		{FVector(-1, -1, -1), {EHyperTwistClassicCubeFace::Down,  EHyperTwistClassicCubeFace::Back,  EHyperTwistClassicCubeFace::Left}},
	};

	for (const FPieceDef& Def : Pieces)
	{
		CreatePiece(Def.GridPos, Def.IdentityFaces);
	}
}

void AHyperTwistClassicCubeActor::CreatePiece(const FVector& GridPos, const TArray<EHyperTwistClassicCubeFace>& IdentityFaces)
{
	UProceduralMeshComponent* Mesh = nullptr;
	if (GetWorld() != nullptr)
	{
		Mesh = NewObject<UProceduralMeshComponent>(
			this,
			NAME_None,
			RF_Transactional | RF_Transient
		);
		AddInstanceComponent(Mesh);
		Mesh->SetCanEverAffectNavigation(false);
		Mesh->RegisterComponent();
		Mesh->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepRelativeTransform);

		const FVector Center = GridPos * (CubeletSize + Gap);
		const float HalfSize = CubeletSize * 0.5f;

		for (int32 i = 0; i < 6; ++i)
		{
			const EHyperTwistClassicCubeFace Face = static_cast<EHyperTwistClassicCubeFace>(i);
			UMaterialInterface* Mat = nullptr;
			const int32 FaceIndex = static_cast<int32>(Face);
			if (IdentityFaces.Contains(Face)
				&& FaceMaterials.IsValidIndex(FaceIndex)
				&& FaceMaterials[FaceIndex] != nullptr)
			{
				Mat = FaceMaterials[FaceIndex];
			}
			else if (InternalMaterial != nullptr && !IdentityFaces.Contains(Face))
			{
				Mat = InternalMaterial;
			}

			CreateCubeletFace(Mesh, i, Center, HalfSize, Face, Mat);
		}
	}

	int32 Index = GridToIndex(GridPos);
	if (TrackedPieces.IsValidIndex(Index))
	{
		FTrackedPiece& Piece = TrackedPieces[Index];
		Piece.Mesh = Mesh;
		Piece.GridPos = GridPos;
		Piece.Orientation = FQuat::Identity;
		Piece.IdentityFaces = IdentityFaces;
		for (int32 MaterialIndex = 0; MaterialIndex < UE_ARRAY_COUNT(Piece.DynamicFaceMaterials); ++MaterialIndex)
		{
			Piece.DynamicFaceMaterials[MaterialIndex] = Mesh != nullptr
				? Cast<UMaterialInstanceDynamic>(Mesh->GetMaterial(MaterialIndex))
				: nullptr;
		}
	}
}

void AHyperTwistClassicCubeActor::CreateCubeletFace(UProceduralMeshComponent* Mesh, int32 SectionIndex,
	const FVector& Center, float HalfSize, EHyperTwistClassicCubeFace Face, UMaterialInterface* Material)
{
	if (Mesh == nullptr)
	{
		return;
	}

	TArray<FVector> Vertices;
	TArray<int32> Triangles;
	TArray<FVector> Normals;
	TArray<FVector2D> UVs;
	TArray<FColor> Colors;
	TArray<FProcMeshTangent> Tangents;

	FVector Normal = FVector::ZeroVector;
	FVector TangentX = FVector::ZeroVector;
	TArray<FVector> FaceVerts;

	switch (Face)
	{
	case EHyperTwistClassicCubeFace::Up: // +Z
		Normal = FVector(0, 0, 1);
		TangentX = FVector(1, 0, 0);
		break;

	case EHyperTwistClassicCubeFace::Down: // -Z
		Normal = FVector(0, 0, -1);
		TangentX = FVector(-1, 0, 0);
		break;

	case EHyperTwistClassicCubeFace::Front: // +Y
		Normal = FVector(0, 1, 0);
		TangentX = FVector(1, 0, 0);
		break;

	case EHyperTwistClassicCubeFace::Back: // -Y
		Normal = FVector(0, -1, 0);
		TangentX = FVector(-1, 0, 0);
		break;

	case EHyperTwistClassicCubeFace::Right: // +X
		Normal = FVector(1, 0, 0);
		TangentX = FVector(0, 1, 0);
		break;

	case EHyperTwistClassicCubeFace::Left: // -X
		Normal = FVector(-1, 0, 0);
		TangentX = FVector(0, -1, 0);
		break;
	}

	const FVector TangentY = FVector::CrossProduct(Normal, TangentX).GetSafeNormal();
	const FVector FaceCenter = Center + Normal * HalfSize;
	FaceVerts = {
		FaceCenter - TangentX * HalfSize - TangentY * HalfSize,
		FaceCenter + TangentX * HalfSize - TangentY * HalfSize,
		FaceCenter + TangentX * HalfSize + TangentY * HalfSize,
		FaceCenter - TangentX * HalfSize + TangentY * HalfSize
	};

	Vertices = FaceVerts;
	// Unreal renders clockwise procedural-mesh winding from the outward side.
	// The vertex basis above is mathematically counter-clockwise around Normal,
	// so reverse each triangle rather than exposing the cubelet interior.
	Triangles = { 0, 2, 1, 0, 3, 2 };

	for (int32 i = 0; i < 4; ++i)
	{
		Normals.Add(Normal);
		UVs.Add(FVector2D((i == 1 || i == 2) ? 1.0f : 0.0f, (i >= 2) ? 1.0f : 0.0f));
		Colors.Add(FColor::White);
		Tangents.Add(FProcMeshTangent(TangentX, false));
	}

	Mesh->CreateMeshSection(SectionIndex, Vertices, Triangles, Normals, UVs, Colors, Tangents, true);
	if (Material)
	{
		if (UMaterialInstanceDynamic* DynamicMaterial =
				Mesh->CreateDynamicMaterialInstance(SectionIndex, Material))
		{
			DynamicMaterial->SetScalarParameterValue(TEXT("GlowStrength"), BaseGlowStrength);
			DynamicMaterial->SetScalarParameterValue(TEXT("FaceOpacity"), 1.0f);
		}
		else
		{
			Mesh->SetMaterial(SectionIndex, Material);
		}
	}
}

bool AHyperTwistClassicCubeActor::IsAnimating() const
{
	return bIsAnimating;
}

bool AHyperTwistClassicCubeActor::IsSettled() const
{
	return !bIsAnimating && RotationQueue.IsEmpty();
}

int32 AHyperTwistClassicCubeActor::GetRenderablePieceCount() const
{
	int32 RenderablePieceCount = 0;
	for (const FTrackedPiece& Piece : TrackedPieces)
	{
		if (IsValid(Piece.Mesh)
			&& Piece.Mesh->IsRegistered()
			&& Piece.Mesh->GetNumSections() > 0)
		{
			++RenderablePieceCount;
		}
	}
	return RenderablePieceCount;
}

bool AHyperTwistClassicCubeActor::HasValidRenderableState() const
{
	return GetRenderableStateValidationError().IsEmpty();
}

FString AHyperTwistClassicCubeActor::GetRenderableStateValidationError() const
{
	auto IsOutwardFaceAtGridPosition = [](const EHyperTwistClassicCubeFace Face, const FVector& Position)
	{
		switch (Face)
		{
		case EHyperTwistClassicCubeFace::Up:
			return FMath::IsNearlyEqual(Position.Z, 1.0f);
		case EHyperTwistClassicCubeFace::Down:
			return FMath::IsNearlyEqual(Position.Z, -1.0f);
		case EHyperTwistClassicCubeFace::Front:
			return FMath::IsNearlyEqual(Position.Y, 1.0f);
		case EHyperTwistClassicCubeFace::Back:
			return FMath::IsNearlyEqual(Position.Y, -1.0f);
		case EHyperTwistClassicCubeFace::Left:
			return FMath::IsNearlyEqual(Position.X, -1.0f);
		case EHyperTwistClassicCubeFace::Right:
			return FMath::IsNearlyEqual(Position.X, 1.0f);
		}
		return false;
	};

	TSet<int32> OccupiedGridIndices;
	TArray<int32> StickerCountByWorldFace;
	StickerCountByWorldFace.Init(0, 6);
	int32 VisiblePieceCount = 0;
	int32 TotalStickerCount = 0;
	const bool bRequireLiveMeshes = GetWorld() != nullptr;
	const float CubeletSpacing = CubeletSize + Gap;

	for (int32 PieceIndex = 0; PieceIndex < TrackedPieces.Num(); ++PieceIndex)
	{
		const FTrackedPiece& Piece = TrackedPieces[PieceIndex];
		if (Piece.IdentityFaces.IsEmpty())
		{
			continue;
		}

		++VisiblePieceCount;
		const FVector RoundedPosition(
			FMath::RoundToFloat(Piece.GridPos.X),
			FMath::RoundToFloat(Piece.GridPos.Y),
			FMath::RoundToFloat(Piece.GridPos.Z));
		if (!Piece.GridPos.Equals(RoundedPosition, KINDA_SMALL_NUMBER)
			|| FMath::Abs(RoundedPosition.X) > 1.0f
			|| FMath::Abs(RoundedPosition.Y) > 1.0f
			|| FMath::Abs(RoundedPosition.Z) > 1.0f
			|| RoundedPosition.IsNearlyZero())
		{
			return FString::Printf(
				TEXT("piece %d occupies invalid grid position %s"),
				PieceIndex,
				*Piece.GridPos.ToCompactString());
		}

		const int32 GridIndex = GridToIndex(RoundedPosition);
		if (OccupiedGridIndices.Contains(GridIndex))
		{
			return FString::Printf(
				TEXT("piece %d duplicates occupied grid position %s"),
				PieceIndex,
				*RoundedPosition.ToCompactString());
		}
		OccupiedGridIndices.Add(GridIndex);

		const int32 ExpectedStickerCount =
			(FMath::IsNearlyEqual(FMath::Abs(RoundedPosition.X), 1.0f) ? 1 : 0)
			+ (FMath::IsNearlyEqual(FMath::Abs(RoundedPosition.Y), 1.0f) ? 1 : 0)
			+ (FMath::IsNearlyEqual(FMath::Abs(RoundedPosition.Z), 1.0f) ? 1 : 0);
		if (Piece.IdentityFaces.Num() != ExpectedStickerCount)
		{
			return FString::Printf(
				TEXT("piece %d has %d stickers at a %d-sticker grid position"),
				PieceIndex,
				Piece.IdentityFaces.Num(),
				ExpectedStickerCount);
		}

		TSet<EHyperTwistClassicCubeFace> PieceWorldFaces;
		for (const EHyperTwistClassicCubeFace IdentityFace : Piece.IdentityFaces)
		{
			EHyperTwistClassicCubeFace WorldFace = EHyperTwistClassicCubeFace::Up;
			if (!TryResolveStickerWorldFace(Piece, IdentityFace, WorldFace))
			{
				return FString::Printf(
					TEXT("piece %d has a sticker orientation that does not resolve to a cube axis"),
					PieceIndex);
			}
			if (PieceWorldFaces.Contains(WorldFace))
			{
				return FString::Printf(
					TEXT("piece %d resolves multiple stickers to world face %d"),
					PieceIndex,
					static_cast<int32>(WorldFace));
			}
			if (!IsOutwardFaceAtGridPosition(WorldFace, RoundedPosition))
			{
				return FString::Printf(
					TEXT("piece %d resolves sticker %d to inward world face %d at %s"),
					PieceIndex,
					static_cast<int32>(IdentityFace),
					static_cast<int32>(WorldFace),
					*RoundedPosition.ToCompactString());
			}

			PieceWorldFaces.Add(WorldFace);
			const int32 WorldFaceIndex = static_cast<int32>(WorldFace);
			if (!StickerCountByWorldFace.IsValidIndex(WorldFaceIndex))
			{
				return FString::Printf(
					TEXT("piece %d resolved an out-of-range world face %d"),
					PieceIndex,
					WorldFaceIndex);
			}
			++StickerCountByWorldFace[WorldFaceIndex];
			++TotalStickerCount;
		}

		if (!bRequireLiveMeshes)
		{
			continue;
		}
		if (!IsValid(Piece.Mesh) || !Piece.Mesh->IsRegistered() || Piece.Mesh->GetNumSections() != 6)
		{
			return FString::Printf(TEXT("piece %d is missing its six-section live mesh"), PieceIndex);
		}

		const FVector SolvedCenter =
			GetExpectedGridPositionFromIdentity(Piece.IdentityFaces) * CubeletSpacing;
		for (int32 SectionIndex = 0; SectionIndex < 6; ++SectionIndex)
		{
			const FProcMeshSection* Section = Piece.Mesh->GetProcMeshSection(SectionIndex);
			if (Section == nullptr
				|| !Section->bSectionVisible
				|| Section->ProcVertexBuffer.Num() != 4
				|| Section->ProcIndexBuffer.Num() != 6)
			{
				return FString::Printf(
					TEXT("piece %d section %d is not a visible four-vertex cubelet face"),
					PieceIndex,
					SectionIndex);
			}

			FVector LocalFaceCenter = FVector::ZeroVector;
			for (const FProcMeshVertex& Vertex : Section->ProcVertexBuffer)
			{
				LocalFaceCenter += Vertex.Position;
			}
			LocalFaceCenter /= static_cast<float>(Section->ProcVertexBuffer.Num());

			const EHyperTwistClassicCubeFace SectionFace =
				static_cast<EHyperTwistClassicCubeFace>(SectionIndex);
			const FVector LocalFaceNormal = GetFaceNormal(SectionFace);
			const FVector ExpectedLocalFaceCenter =
				SolvedCenter + LocalFaceNormal * (CubeletSize * 0.5f);
			if (!LocalFaceCenter.Equals(ExpectedLocalFaceCenter, 0.01f))
			{
				return FString::Printf(
					TEXT("piece %d section %d is centered at %s instead of %s"),
					PieceIndex,
					SectionIndex,
					*LocalFaceCenter.ToCompactString(),
					*ExpectedLocalFaceCenter.ToCompactString());
			}

			const uint32 IndexA = Section->ProcIndexBuffer[0];
			const uint32 IndexB = Section->ProcIndexBuffer[1];
			const uint32 IndexC = Section->ProcIndexBuffer[2];
			if (!Section->ProcVertexBuffer.IsValidIndex(static_cast<int32>(IndexA))
				|| !Section->ProcVertexBuffer.IsValidIndex(static_cast<int32>(IndexB))
				|| !Section->ProcVertexBuffer.IsValidIndex(static_cast<int32>(IndexC)))
			{
				return FString::Printf(
					TEXT("piece %d section %d contains an invalid triangle index"),
					PieceIndex,
					SectionIndex);
			}

			const FVector EdgeAB =
				Section->ProcVertexBuffer[static_cast<int32>(IndexB)].Position
				- Section->ProcVertexBuffer[static_cast<int32>(IndexA)].Position;
			const FVector EdgeAC =
				Section->ProcVertexBuffer[static_cast<int32>(IndexC)].Position
				- Section->ProcVertexBuffer[static_cast<int32>(IndexA)].Position;
			// Unreal treats clockwise procedural triangles as front-facing. Their
			// mathematical cross product therefore points opposite the authored
			// outward vertex normal.
			const FVector FrontWindingNormal =
				-FVector::CrossProduct(EdgeAB, EdgeAC).GetSafeNormal();
			if (FVector::DotProduct(FrontWindingNormal, LocalFaceNormal)
				< HyperTwistClassicCubeActorInternal::RotationDotTolerance)
			{
				return FString::Printf(
					TEXT("piece %d section %d has inward-facing triangle winding"),
					PieceIndex,
					SectionIndex);
			}
		}

		const FVector ExpectedWorldCenter = GetActorTransform().TransformPosition(
			RoundedPosition * CubeletSpacing);
		const FVector RenderedWorldCenter =
			Piece.Mesh->GetComponentTransform().TransformPosition(SolvedCenter);
		if (!RenderedWorldCenter.Equals(ExpectedWorldCenter, 0.05f))
		{
			return FString::Printf(
				TEXT("piece %d renders at %s instead of expected center %s"),
				PieceIndex,
				*RenderedWorldCenter.ToCompactString(),
				*ExpectedWorldCenter.ToCompactString());
		}

		for (const EHyperTwistClassicCubeFace IdentityFace : Piece.IdentityFaces)
		{
			EHyperTwistClassicCubeFace WorldFace = EHyperTwistClassicCubeFace::Up;
			TryResolveStickerWorldFace(Piece, IdentityFace, WorldFace);
			const FVector RenderedWorldNormal = Piece.Mesh->GetComponentQuat().RotateVector(
				GetFaceNormal(IdentityFace)).GetSafeNormal();
			const FVector ExpectedWorldNormal = GetActorQuat().RotateVector(
				GetFaceNormal(WorldFace)).GetSafeNormal();
			if (FVector::DotProduct(RenderedWorldNormal, ExpectedWorldNormal)
				< HyperTwistClassicCubeActorInternal::RotationDotTolerance)
			{
				return FString::Printf(
					TEXT("piece %d live mesh sticker normal diverges from tracked orientation"),
					PieceIndex);
			}
		}
	}

	if (VisiblePieceCount != ExpectedRenderablePieceCount
		|| OccupiedGridIndices.Num() != ExpectedRenderablePieceCount)
	{
		return FString::Printf(
			TEXT("cube has %d visible pieces across %d unique positions instead of %d"),
			VisiblePieceCount,
			OccupiedGridIndices.Num(),
			ExpectedRenderablePieceCount);
	}
	if (TotalStickerCount != 54)
	{
		return FString::Printf(TEXT("cube has %d outward stickers instead of 54"), TotalStickerCount);
	}
	for (int32 FaceIndex = 0; FaceIndex < StickerCountByWorldFace.Num(); ++FaceIndex)
	{
		if (StickerCountByWorldFace[FaceIndex] != 9)
		{
			return FString::Printf(
				TEXT("world face %d has %d stickers instead of 9"),
				FaceIndex,
				StickerCountByWorldFace[FaceIndex]);
		}
	}

	return FString();
}

bool AHyperTwistClassicCubeActor::EnsureRenderablePresentation()
{
	if (GetRenderablePieceCount() == ExpectedRenderablePieceCount)
	{
		return false;
	}

	ResetCube();
	return true;
}

bool AHyperTwistClassicCubeActor::IsSolved() const
{
	if (bIsAnimating || !RotationQueue.IsEmpty())
	{
		return false;
	}

	for (const FTrackedPiece& Piece : TrackedPieces)
	{
		if (Piece.IdentityFaces.IsEmpty())
		{
			continue;
		}

		if (!Piece.GridPos.Equals(GetExpectedGridPositionFromIdentity(Piece.IdentityFaces), KINDA_SMALL_NUMBER))
		{
			return false;
		}

		if (!IsSolvedOrientation(Piece.Orientation))
		{
			return false;
		}
	}

	return true;
}

void AHyperTwistClassicCubeActor::RotateFace(EHyperTwistClassicCubeFace Face, EHyperTwistRotationDirection Direction)
{
	QueueRotation(Face, Direction, true);
}

bool AHyperTwistClassicCubeActor::TryQueueMoveNotation(const FString& MoveNotation, const bool bGameplayMove)
{
	FHyperTwistClassicCubeMoveDescriptor MoveDescriptor;
	if (!UHyperTwistClassicCubeCommandLibrary::TryParseMoveNotation(MoveNotation, MoveDescriptor))
	{
		return false;
	}

	MoveDescriptor.bGameplayMove = bGameplayMove;
	return TryQueueMoveDescriptor(MoveDescriptor);
}

bool AHyperTwistClassicCubeActor::TryQueueMoveDescriptor(
	const FHyperTwistClassicCubeMoveDescriptor& MoveDescriptor
)
{
	if (!MoveDescriptor.IsStructurallyValid())
	{
		return false;
	}

	const int32 QuarterTurnCount = FMath::Max(MoveDescriptor.QuarterTurns, 1);
	for (int32 QuarterTurnIndex = 0; QuarterTurnIndex < QuarterTurnCount; ++QuarterTurnIndex)
	{
		QueueRotation(MoveDescriptor.Face, MoveDescriptor.Direction, MoveDescriptor.bGameplayMove);
	}

	return true;
}

bool AHyperTwistClassicCubeActor::ExecuteMoveDescriptorImmediately(
	const FHyperTwistClassicCubeMoveDescriptor& MoveDescriptor
)
{
	if (!MoveDescriptor.IsStructurallyValid())
	{
		return false;
	}

	const int32 QuarterTurnCount = FMath::Max(MoveDescriptor.QuarterTurns, 1);
	for (int32 QuarterTurnIndex = 0; QuarterTurnIndex < QuarterTurnCount; ++QuarterTurnIndex)
	{
		ApplyQuarterTurnImmediate(
			MoveDescriptor.Face,
			MoveDescriptor.Direction,
			MoveDescriptor.bGameplayMove
		);
	}

	return true;
}

int32 AHyperTwistClassicCubeActor::GetTotalCompletedMoveCount() const
{
	return TotalCompletedMoveCount;
}

int32 AHyperTwistClassicCubeActor::GetGameplayCompletedMoveCount() const
{
	return GameplayCompletedMoveCount;
}

int32 AHyperTwistClassicCubeActor::GetQueuedRotationCount() const
{
	return RotationQueue.Num();
}

FString AHyperTwistClassicCubeActor::GetCurrentScrambleNotation() const
{
	return FString::Join(CurrentScrambleMoves, TEXT(" "));
}

TArray<FHyperTwistClassicCubeMoveDescriptor> AHyperTwistClassicCubeActor::GetCompletedMoveHistory() const
{
	return CompletedMoveHistory;
}

TArray<FHyperTwistClassicCubePieceData> AHyperTwistClassicCubeActor::GetPieceDataSnapshot() const
{
	TArray<FHyperTwistClassicCubePieceData> PieceData;
	for (const FTrackedPiece& Piece : TrackedPieces)
	{
		if (Piece.IdentityFaces.IsEmpty())
		{
			continue;
		}

		FHyperTwistClassicCubePieceData Snapshot;
		Snapshot.GridPosition = Piece.GridPos;
		Snapshot.ColoredFaces = Piece.IdentityFaces;
		PieceData.Add(Snapshot);
	}

	return PieceData;
}

FString AHyperTwistClassicCubeActor::GetSolverFaceletString() const
{
	if (!IsSettled())
	{
		return FString();
	}

	auto TryBuildFromTrackedPieces = [this](FString& OutFaceletString)
	{
		TArray<TCHAR> Facelets;
		Facelets.Init(TEXT('?'), 54);

		for (const FTrackedPiece& Piece : TrackedPieces)
		{
			if (Piece.IdentityFaces.IsEmpty())
			{
				continue;
			}

			for (const EHyperTwistClassicCubeFace IdentityFace : Piece.IdentityFaces)
			{
				EHyperTwistClassicCubeFace WorldFace = EHyperTwistClassicCubeFace::Up;
				if (!TryResolveStickerWorldFace(Piece, IdentityFace, WorldFace))
				{
					return false;
				}

				const int32 FaceletIndex = GetFaceletIndexForSticker(WorldFace, Piece.GridPos);
				if (!Facelets.IsValidIndex(FaceletIndex))
				{
					return false;
				}

				Facelets[FaceletIndex] = GetSolverFaceLetter(IdentityFace);
			}
		}

		for (const TCHAR Facelet : Facelets)
		{
			if (Facelet == TEXT('?'))
			{
				return false;
			}
		}

		Facelets.Add(TEXT('\0'));
		OutFaceletString = FString(Facelets.GetData());
		return true;
	};

	FString FaceletString;
	if (TryBuildFromTrackedPieces(FaceletString))
	{
		return FaceletString;
	}

	HyperTwistClassicCubeActorInternal::EnsureSolverFaceletLibrariesInitialized();

	auto FindMoveIndex = [](const FString& MoveNotation) -> int32
	{
		const std::string EncodedNotation = TCHAR_TO_UTF8(*MoveNotation);
		for (int32 MoveIndex = 0; MoveIndex < move::COUNT; ++MoveIndex)
		{
			if (move::names[MoveIndex] == EncodedNotation)
			{
				return MoveIndex;
			}
		}

		return INDEX_NONE;
	};

	cubie::cube CurrentCube = cubie::SOLVED_CUBE;
	cubie::cube NextCube = cubie::SOLVED_CUBE;
	for (const FHyperTwistClassicCubeMoveDescriptor& MoveDescriptor : CompletedMoveHistory)
	{
		const int32 MoveIndex = FindMoveIndex(MoveDescriptor.Notation);
		if (MoveIndex == INDEX_NONE)
		{
			return FString();
		}

		cubie::mul(CurrentCube, move::cubes[MoveIndex], NextCube);
		CurrentCube = NextCube;
	}

	const std::string ReconstructedFacelets = face::from_cubie(CurrentCube);
	return FString(UTF8_TO_TCHAR(ReconstructedFacelets.c_str()));
}

void AHyperTwistClassicCubeActor::SetHintedFace(const EHyperTwistClassicCubeFace Face)
{
	bHasHintedFace = true;
	HintedFace = Face;
	UpdateHintMaterialState();
}

void AHyperTwistClassicCubeActor::ClearHintedFace()
{
	bHasHintedFace = false;
	UpdateHintMaterialState();
}

void AHyperTwistClassicCubeActor::QueueRotation(
	EHyperTwistClassicCubeFace Face,
	EHyperTwistRotationDirection Direction,
	const bool bGameplayMove
)
{
	FQueuedRotationRequest Request;
	Request.Face = Face;
	Request.Direction = Direction;
	Request.bGameplayMove = bGameplayMove;
	RotationQueue.Add(Request);
	ProcessRotationQueue();
}

void AHyperTwistClassicCubeActor::SynchronizePieceMeshTransform(FTrackedPiece& Piece)
{
	if (Piece.Mesh == nullptr || Piece.IdentityFaces.IsEmpty())
	{
		return;
	}

	const float CubeletSpacing = CubeletSize + Gap;
	const FVector SolvedCenter =
		GetExpectedGridPositionFromIdentity(Piece.IdentityFaces) * CubeletSpacing;
	const FVector CurrentCenter = Piece.GridPos * CubeletSpacing;
	const FQuat Orientation = Piece.Orientation.GetNormalized();
	const FVector RelativeLocation = CurrentCenter - Orientation.RotateVector(SolvedCenter);
	const FTransform ParentTransform =
		RootComponent != nullptr ? RootComponent->GetComponentTransform() : GetActorTransform();
	Piece.Mesh->SetWorldTransform(FTransform(Orientation, RelativeLocation) * ParentTransform);
}

void AHyperTwistClassicCubeActor::ApplyQuarterTurnImmediate(
	const EHyperTwistClassicCubeFace Face,
	const EHyperTwistRotationDirection Direction,
	const bool bGameplayMove
)
{
	const TArray<int32> Indices = GetFacePieceIndices(TrackedPieces, Face);
	if (Indices.IsEmpty())
	{
		return;
	}

	const FQuat FaceRot = GetFaceRotationQuat(Face, Direction);
	for (const int32 Idx : Indices)
	{
		FTrackedPiece& Piece = TrackedPieces[Idx];
		Piece.GridPos = RotateGridPosition(Piece.GridPos, Face, Direction);
		Piece.GridPos.X = FMath::RoundToFloat(Piece.GridPos.X);
		Piece.GridPos.Y = FMath::RoundToFloat(Piece.GridPos.Y);
		Piece.GridPos.Z = FMath::RoundToFloat(Piece.GridPos.Z);
		Piece.Orientation = (FaceRot * Piece.Orientation).GetNormalized();
		SynchronizePieceMeshTransform(Piece);
	}

	++TotalCompletedMoveCount;
	if (bGameplayMove)
	{
		++GameplayCompletedMoveCount;
	}

	FHyperTwistClassicCubeMoveDescriptor CompletedMove;
	CompletedMove.Face = Face;
	CompletedMove.Direction = Direction;
	CompletedMove.QuarterTurns = 1;
	CompletedMove.Notation = BuildQuarterTurnNotation(Face, Direction);
	CompletedMove.bGameplayMove = bGameplayMove;
	CompletedMoveHistory.Add(CompletedMove);

	UpdateHintMaterialState();
}

void AHyperTwistClassicCubeActor::ProcessRotationQueue()
{
	if (bIsAnimating || RotationQueue.IsEmpty())
	{
		return;
	}

	const FQueuedRotationRequest Next = RotationQueue[0];
	RotationQueue.RemoveAt(0);
	if (GetWorld() == nullptr)
	{
		ApplyQuarterTurnImmediate(Next.Face, Next.Direction, Next.bGameplayMove);
		ProcessRotationQueue();
		return;
	}

	StartFaceRotation(Next.Face, Next.Direction);
	ActiveRotation.bGameplayMove = Next.bGameplayMove;
}

void AHyperTwistClassicCubeActor::StartFaceRotation(EHyperTwistClassicCubeFace Face, EHyperTwistRotationDirection Direction)
{
	if (!GetWorld())
	{
		return;
	}

	TArray<int32> Indices = GetFacePieceIndices(TrackedPieces, Face);
	if (Indices.IsEmpty())
	{
		ProcessRotationQueue();
		return;
	}

	// Keep the pivot in the cube actor's local frame. A world-space pivot makes
	// a placed cube orbit the map origin during every animated turn.
	USceneComponent* Pivot = NewObject<USceneComponent>(
		this,
		NAME_None,
		RF_Transactional | RF_Transient
	);
	AddInstanceComponent(Pivot);
	Pivot->AttachToComponent(RootComponent, FAttachmentTransformRules::SnapToTargetNotIncludingScale);
	Pivot->RegisterComponent();
	Pivot->SetRelativeLocation(GetFaceCenter(Face) * (CubeletSize + Gap));
	Pivot->SetRelativeRotation(FQuat::Identity);

	// Attach face pieces to pivot (keep world transform)
	for (int32 Idx : Indices)
	{
		if (TrackedPieces[Idx].Mesh)
		{
			TrackedPieces[Idx].Mesh->AttachToComponent(Pivot, FAttachmentTransformRules::KeepWorldTransform);
		}
	}

	FQuat FaceRot = GetFaceRotationQuat(Face, Direction);
	ActiveRotation.Pivot = Pivot;
	ActiveRotation.PieceIndices = Indices;
	ActiveRotation.PivotStartRot = FQuat::Identity;
	ActiveRotation.PivotTargetRot = FaceRot;
	ActiveRotation.StartTime = FPlatformTime::Seconds();
	ActiveRotation.Duration = TurnDuration;
	ActiveRotation.Face = Face;
	ActiveRotation.Direction = Direction;
	bIsAnimating = true;
}

void AHyperTwistClassicCubeActor::FinalizeRotation()
{
	if (!IsValid(ActiveRotation.Pivot))
	{
		const EHyperTwistClassicCubeFace InterruptedFace = ActiveRotation.Face;
		const EHyperTwistRotationDirection InterruptedDirection = ActiveRotation.Direction;
		const bool bInterruptedGameplayMove = ActiveRotation.bGameplayMove;
		for (const int32 PieceIndex : ActiveRotation.PieceIndices)
		{
			if (TrackedPieces.IsValidIndex(PieceIndex)
				&& IsValid(TrackedPieces[PieceIndex].Mesh)
				&& RootComponent != nullptr)
			{
				TrackedPieces[PieceIndex].Mesh->AttachToComponent(
					RootComponent,
					FAttachmentTransformRules::KeepWorldTransform);
			}
		}
		ActiveRotation = FActiveRotation();
		bIsAnimating = false;
		ApplyQuarterTurnImmediate(
			InterruptedFace,
			InterruptedDirection,
			bInterruptedGameplayMove);
		ProcessRotationQueue();
		return;
	}

	// Detach pieces from pivot, keeping their world transforms
	for (int32 Idx : ActiveRotation.PieceIndices)
	{
		if (TrackedPieces[Idx].Mesh)
		{
			TrackedPieces[Idx].Mesh->DetachFromComponent(FDetachmentTransformRules::KeepWorldTransform);
			if (RootComponent != nullptr)
			{
				TrackedPieces[Idx].Mesh->AttachToComponent(
					RootComponent,
					FAttachmentTransformRules::KeepWorldTransform
				);
			}
		}
	}

	// Compute and apply new grid positions and orientations
	FQuat FaceRot = ActiveRotation.PivotTargetRot;
	for (int32 Idx : ActiveRotation.PieceIndices)
	{
		FTrackedPiece& Piece = TrackedPieces[Idx];
		Piece.GridPos = RotateGridPosition(Piece.GridPos, ActiveRotation.Face, ActiveRotation.Direction);
		Piece.GridPos.X = FMath::RoundToFloat(Piece.GridPos.X);
		Piece.GridPos.Y = FMath::RoundToFloat(Piece.GridPos.Y);
		Piece.GridPos.Z = FMath::RoundToFloat(Piece.GridPos.Z);
		Piece.Orientation = (FaceRot * Piece.Orientation).GetNormalized();
		SynchronizePieceMeshTransform(Piece);
	}

	++TotalCompletedMoveCount;
	if (ActiveRotation.bGameplayMove)
	{
		++GameplayCompletedMoveCount;
	}

	FHyperTwistClassicCubeMoveDescriptor CompletedMove;
	CompletedMove.Face = ActiveRotation.Face;
	CompletedMove.Direction = ActiveRotation.Direction;
	CompletedMove.QuarterTurns = 1;
	CompletedMove.Notation = BuildQuarterTurnNotation(ActiveRotation.Face, ActiveRotation.Direction);
	CompletedMove.bGameplayMove = ActiveRotation.bGameplayMove;
	CompletedMoveHistory.Add(CompletedMove);

	// Destroy pivot
	ActiveRotation.Pivot->DestroyComponent();
	ActiveRotation = FActiveRotation();
	bIsAnimating = false;
	UpdateHintMaterialState();
	ProcessRotationQueue();
}

int32 AHyperTwistClassicCubeActor::GridToIndex(const FVector& GridPos) const
{
	return (FMath::RoundToInt(GridPos.X) + 1) * 9 + (FMath::RoundToInt(GridPos.Y) + 1) * 3 + (FMath::RoundToInt(GridPos.Z) + 1);
}

FVector AHyperTwistClassicCubeActor::IndexToGrid(int32 Index) const
{
	int32 X = Index / 9 - 1;
	int32 Y = (Index % 9) / 3 - 1;
	int32 Z = Index % 3 - 1;
	return FVector((float)X, (float)Y, (float)Z);
}

FVector AHyperTwistClassicCubeActor::RotateGridPosition(const FVector& Pos, EHyperTwistClassicCubeFace Face, EHyperTwistRotationDirection Direction)
{
	return GetFaceRotationQuat(Face, Direction).RotateVector(Pos);
}

FQuat AHyperTwistClassicCubeActor::GetFaceRotationQuat(EHyperTwistClassicCubeFace Face, EHyperTwistRotationDirection Direction)
{
	const bool bCW = (Direction == EHyperTwistRotationDirection::Clockwise);
	const float QuarterTurnRadians = PI * 0.5f;
	switch (Face)
	{
	case EHyperTwistClassicCubeFace::Up:
		return FQuat(FVector::UpVector, bCW ? -QuarterTurnRadians : QuarterTurnRadians);
	case EHyperTwistClassicCubeFace::Down:
		return FQuat(FVector::UpVector, bCW ? QuarterTurnRadians : -QuarterTurnRadians);
	case EHyperTwistClassicCubeFace::Front:
		return FQuat(FVector::YAxisVector, bCW ? QuarterTurnRadians : -QuarterTurnRadians);
	case EHyperTwistClassicCubeFace::Back:
		return FQuat(FVector::YAxisVector, bCW ? -QuarterTurnRadians : QuarterTurnRadians);
	case EHyperTwistClassicCubeFace::Right:
		return FQuat(FVector::XAxisVector, bCW ? QuarterTurnRadians : -QuarterTurnRadians);
	case EHyperTwistClassicCubeFace::Left:
		return FQuat(FVector::XAxisVector, bCW ? -QuarterTurnRadians : QuarterTurnRadians);
	}
	return FQuat::Identity;
}

TArray<int32> AHyperTwistClassicCubeActor::GetFacePieceIndices(const TArray<FTrackedPiece>& Pieces, EHyperTwistClassicCubeFace Face)
{
	TArray<int32> Result;
	for (int32 i = 0; i < Pieces.Num(); ++i)
	{
		if (Pieces[i].IdentityFaces.IsEmpty())
		{
			continue;
		}
		const FVector& Pos = Pieces[i].GridPos;
		bool bOnFace = false;
		switch (Face)
		{
		case EHyperTwistClassicCubeFace::Up:    bOnFace = FMath::IsNearlyEqual(Pos.Z, 1.0f);  break;
		case EHyperTwistClassicCubeFace::Down:  bOnFace = FMath::IsNearlyEqual(Pos.Z, -1.0f); break;
		case EHyperTwistClassicCubeFace::Front: bOnFace = FMath::IsNearlyEqual(Pos.Y, 1.0f);  break;
		case EHyperTwistClassicCubeFace::Back:  bOnFace = FMath::IsNearlyEqual(Pos.Y, -1.0f); break;
		case EHyperTwistClassicCubeFace::Right: bOnFace = FMath::IsNearlyEqual(Pos.X, 1.0f);  break;
		case EHyperTwistClassicCubeFace::Left:  bOnFace = FMath::IsNearlyEqual(Pos.X, -1.0f); break;
		}
		if (bOnFace)
		{
			Result.Add(i);
		}
	}
	return Result;
}

FVector AHyperTwistClassicCubeActor::GetFaceCenter(EHyperTwistClassicCubeFace Face)
{
	switch (Face)
	{
	case EHyperTwistClassicCubeFace::Up:    return FVector(0, 0, 1);
	case EHyperTwistClassicCubeFace::Down:  return FVector(0, 0, -1);
	case EHyperTwistClassicCubeFace::Front: return FVector(0, 1, 0);
	case EHyperTwistClassicCubeFace::Back:  return FVector(0, -1, 0);
	case EHyperTwistClassicCubeFace::Right: return FVector(1, 0, 0);
	case EHyperTwistClassicCubeFace::Left:  return FVector(-1, 0, 0);
	}
	return FVector::ZeroVector;
}

int32 AHyperTwistClassicCubeActor::GetFaceAxisIndex(EHyperTwistClassicCubeFace Face)
{
	switch (Face)
	{
	case EHyperTwistClassicCubeFace::Up:
	case EHyperTwistClassicCubeFace::Down:
		return 2;
	case EHyperTwistClassicCubeFace::Front:
	case EHyperTwistClassicCubeFace::Back:
		return 1;
	case EHyperTwistClassicCubeFace::Left:
	case EHyperTwistClassicCubeFace::Right:
		return 0;
	}

	return INDEX_NONE;
}

FVector AHyperTwistClassicCubeActor::GetExpectedGridPositionFromIdentity(
	const TArray<EHyperTwistClassicCubeFace>& IdentityFaces
)
{
	FVector Result = FVector::ZeroVector;
	for (const EHyperTwistClassicCubeFace Face : IdentityFaces)
	{
		switch (Face)
		{
		case EHyperTwistClassicCubeFace::Up: Result.Z = 1.0f; break;
		case EHyperTwistClassicCubeFace::Down: Result.Z = -1.0f; break;
		case EHyperTwistClassicCubeFace::Front: Result.Y = 1.0f; break;
		case EHyperTwistClassicCubeFace::Back: Result.Y = -1.0f; break;
		case EHyperTwistClassicCubeFace::Left: Result.X = -1.0f; break;
		case EHyperTwistClassicCubeFace::Right: Result.X = 1.0f; break;
		}
	}

	return Result;
}

bool AHyperTwistClassicCubeActor::IsSolvedOrientation(const FQuat& Orientation)
{
	const FQuat Normalized = Orientation.GetNormalized();
	const double IdentityDot = FMath::Abs(Normalized.W);
	return IdentityDot >= HyperTwistClassicCubeActorInternal::RotationDotTolerance;
}

FVector AHyperTwistClassicCubeActor::GetFaceNormal(const EHyperTwistClassicCubeFace Face)
{
	switch (Face)
	{
	case EHyperTwistClassicCubeFace::Up:
		return FVector::UpVector;
	case EHyperTwistClassicCubeFace::Down:
		return -FVector::UpVector;
	case EHyperTwistClassicCubeFace::Front:
		return FVector::YAxisVector;
	case EHyperTwistClassicCubeFace::Back:
		return -FVector::YAxisVector;
	case EHyperTwistClassicCubeFace::Left:
		return -FVector::XAxisVector;
	case EHyperTwistClassicCubeFace::Right:
		return FVector::XAxisVector;
	}

	return FVector::UpVector;
}

TCHAR AHyperTwistClassicCubeActor::GetSolverFaceLetter(const EHyperTwistClassicCubeFace Face)
{
	switch (Face)
	{
	case EHyperTwistClassicCubeFace::Up:
		return TEXT('U');
	case EHyperTwistClassicCubeFace::Down:
		return TEXT('D');
	case EHyperTwistClassicCubeFace::Front:
		return TEXT('F');
	case EHyperTwistClassicCubeFace::Back:
		return TEXT('B');
	case EHyperTwistClassicCubeFace::Left:
		return TEXT('L');
	case EHyperTwistClassicCubeFace::Right:
		return TEXT('R');
	}

	return TEXT('U');
}

FString AHyperTwistClassicCubeActor::BuildQuarterTurnNotation(
	const EHyperTwistClassicCubeFace Face,
	const EHyperTwistRotationDirection Direction
)
{
	FString Notation;
	Notation.AppendChar(GetSolverFaceLetter(Face));
	if (Direction == EHyperTwistRotationDirection::CounterClockwise)
	{
		Notation.AppendChar(TEXT('\''));
	}
	return Notation;
}

bool AHyperTwistClassicCubeActor::TryResolveStickerWorldFace(
	const FTrackedPiece& Piece,
	const EHyperTwistClassicCubeFace IdentityFace,
	EHyperTwistClassicCubeFace& OutWorldFace
)
{
	const FVector RotatedNormal =
		Piece.Orientation.RotateVector(GetFaceNormal(IdentityFace)).GetSafeNormal();
	return TryResolveFaceFromImpactNormal(RotatedNormal, OutWorldFace);
}

int32 AHyperTwistClassicCubeActor::GetFaceletIndexForSticker(
	const EHyperTwistClassicCubeFace WorldFace,
	const FVector& GridPosition
)
{
	const int32 X = FMath::RoundToInt(GridPosition.X);
	const int32 Y = FMath::RoundToInt(GridPosition.Y);
	const int32 Z = FMath::RoundToInt(GridPosition.Z);

	int32 FaceBaseIndex = 0;
	int32 Row = 0;
	int32 Column = 0;

	switch (WorldFace)
	{
	case EHyperTwistClassicCubeFace::Up:
		FaceBaseIndex = 0;
		Row = Y + 1;
		Column = X + 1;
		break;
	case EHyperTwistClassicCubeFace::Right:
		FaceBaseIndex = 9;
		Row = 1 - Z;
		Column = 1 - Y;
		break;
	case EHyperTwistClassicCubeFace::Front:
		FaceBaseIndex = 18;
		Row = 1 - Z;
		Column = X + 1;
		break;
	case EHyperTwistClassicCubeFace::Down:
		FaceBaseIndex = 27;
		Row = 1 - Y;
		Column = X + 1;
		break;
	case EHyperTwistClassicCubeFace::Left:
		FaceBaseIndex = 36;
		Row = 1 - Z;
		Column = Y + 1;
		break;
	case EHyperTwistClassicCubeFace::Back:
		FaceBaseIndex = 45;
		Row = 1 - Z;
		Column = 1 - X;
		break;
	}

	return FaceBaseIndex + Row * 3 + Column;
}

void AHyperTwistClassicCubeActor::UpdateHintMaterialState()
{
	for (const FTrackedPiece& Piece : TrackedPieces)
	{
		if (Piece.Mesh == nullptr)
		{
			continue;
		}

		for (int32 FaceIndex = 0; FaceIndex < UE_ARRAY_COUNT(Piece.DynamicFaceMaterials); ++FaceIndex)
		{
			UMaterialInstanceDynamic* DynamicMaterial = Piece.DynamicFaceMaterials[FaceIndex];
			if (DynamicMaterial == nullptr)
			{
				continue;
			}

			const float GlowStrength = ResolveHintGlowStrength(
				Piece,
				static_cast<EHyperTwistClassicCubeFace>(FaceIndex)
			);
			DynamicMaterial->SetScalarParameterValue(TEXT("GlowStrength"), GlowStrength);
			DynamicMaterial->SetScalarParameterValue(
				TEXT("HintActive"),
				GlowStrength > BaseGlowStrength + KINDA_SMALL_NUMBER ? 1.0f : 0.0f
			);
		}
	}
}

float AHyperTwistClassicCubeActor::ResolveHintGlowStrength(
	const FTrackedPiece& Piece,
	const EHyperTwistClassicCubeFace IdentityFace
) const
{
	if (!bHasHintedFace || !Piece.IdentityFaces.Contains(IdentityFace))
	{
		return BaseGlowStrength;
	}

	EHyperTwistClassicCubeFace WorldFace = EHyperTwistClassicCubeFace::Up;
	if (!TryResolveStickerWorldFace(Piece, IdentityFace, WorldFace))
	{
		return BaseGlowStrength;
	}

	return WorldFace == HintedFace ? HintGlowStrength : BaseGlowStrength;
}
