#include "HyperTwistSimulation/HyperTwistVirtual3333ProjectionLibrary.h"

#include "JsonObjectConverter.h"

namespace HyperTwistVirtual3333ProjectionLibraryInternal
{
	constexpr int32 AxisCount = 4;
	constexpr int32 MinimumOrder = 2;
	constexpr int32 MaximumOrder = 6;

	int32 GetPieceCount(const int32 Order)
	{
		return Order * Order * Order * Order;
	}

	int32 GetVisiblePieceCount(const int32 Order)
	{
		return Order * Order * Order;
	}

	int32 CoordinateFromDigit(const int32 Digit, const int32 Order)
	{
		return Order % 2 == 0
			? (Digit * 2) - (Order - 1)
			: Digit - (Order / 2);
	}

	int32 DigitFromCoordinate(const int32 Coordinate, const int32 Order)
	{
		return Order % 2 == 0
			? (Coordinate + (Order - 1)) / 2
			: Coordinate + (Order / 2);
	}

	int32 GetBoundaryCoordinateMagnitude(const int32 Order)
	{
		return Order % 2 == 0 ? Order - 1 : Order / 2;
	}

	bool IsCoordinateValueValid(const int32 Coordinate, const int32 Order)
	{
		if (Order < MinimumOrder || Order > MaximumOrder)
		{
			return false;
		}
		if (Order % 2 != 0)
		{
			const int32 Boundary = GetBoundaryCoordinateMagnitude(Order);
			return Coordinate >= -Boundary && Coordinate <= Boundary;
		}

		const int32 Shifted = Coordinate + (Order - 1);
		return Coordinate >= -(Order - 1)
			&& Coordinate <= (Order - 1)
			&& Shifted % 2 == 0;
	}

	template <typename TStruct>
	FString SerializeStructToJson(const TStruct& Value)
	{
		FString Json;
		FJsonObjectConverter::UStructToJsonObjectString(TStruct::StaticStruct(), &Value, Json, 0, 0);
		return Json;
	}

	int32 ToAxisIndex(const EHyperTwistVirtual3333Axis Axis)
	{
		return static_cast<int32>(Axis);
	}

	EHyperTwistVirtual3333Axis ToAxisEnum(const int32 AxisIndex)
	{
		switch (AxisIndex)
		{
		case 0:
			return EHyperTwistVirtual3333Axis::X;
		case 1:
			return EHyperTwistVirtual3333Axis::Y;
		case 2:
			return EHyperTwistVirtual3333Axis::Z;
		default:
			return EHyperTwistVirtual3333Axis::W;
		}
	}

	int32 GetCoordinateValue(
		const FHyperTwistVirtual3333GridCoordinate& Coordinate,
		const EHyperTwistVirtual3333Axis Axis
	)
	{
		switch (Axis)
		{
		case EHyperTwistVirtual3333Axis::X:
			return Coordinate.X;
		case EHyperTwistVirtual3333Axis::Y:
			return Coordinate.Y;
		case EHyperTwistVirtual3333Axis::Z:
			return Coordinate.Z;
		default:
			return Coordinate.W;
		}
	}

	void SetCoordinateValue(
		FHyperTwistVirtual3333GridCoordinate& Coordinate,
		const EHyperTwistVirtual3333Axis Axis,
		const int32 Value
	)
	{
		switch (Axis)
		{
		case EHyperTwistVirtual3333Axis::X:
			Coordinate.X = Value;
			break;
		case EHyperTwistVirtual3333Axis::Y:
			Coordinate.Y = Value;
			break;
		case EHyperTwistVirtual3333Axis::Z:
			Coordinate.Z = Value;
			break;
		default:
			Coordinate.W = Value;
			break;
		}
	}

	FHyperTwistVirtual3333GridCoordinate DecodeCoordinate(
		const int32 PositionIndex,
		const int32 Order
	)
	{
		FHyperTwistVirtual3333GridCoordinate Coordinate;
		int32 Remainder = PositionIndex;
		Coordinate.X = CoordinateFromDigit(Remainder % Order, Order);
		Remainder /= Order;
		Coordinate.Y = CoordinateFromDigit(Remainder % Order, Order);
		Remainder /= Order;
		Coordinate.Z = CoordinateFromDigit(Remainder % Order, Order);
		Remainder /= Order;
		Coordinate.W = CoordinateFromDigit(Remainder % Order, Order);
		return Coordinate;
	}

	int32 EncodeCoordinate(
		const FHyperTwistVirtual3333GridCoordinate& Coordinate,
		const int32 Order
	)
	{
		return (((
			DigitFromCoordinate(Coordinate.W, Order) * Order
			+ DigitFromCoordinate(Coordinate.Z, Order)) * Order
			+ DigitFromCoordinate(Coordinate.Y, Order)) * Order
			+ DigitFromCoordinate(Coordinate.X, Order));
	}

	FString GetAxisLabel(const EHyperTwistVirtual3333Axis Axis)
	{
		switch (Axis)
		{
		case EHyperTwistVirtual3333Axis::X:
			return TEXT("X");
		case EHyperTwistVirtual3333Axis::Y:
			return TEXT("Y");
		case EHyperTwistVirtual3333Axis::Z:
			return TEXT("Z");
		default:
			return TEXT("W");
		}
	}

	FHyperTwistPuzzleDefinitionRef BuildDefinition(const int32 Order)
	{
		FHyperTwistPuzzleDefinitionRef Definition;
		Definition.PuzzleId = FString::Printf(
			TEXT("hypercube/%dx%dx%dx%d"),
			Order,
			Order,
			Order,
			Order);
		Definition.PuzzleFamily = EHyperTwistPuzzleFamily::Hypercube;
		Definition.Dimension = 4;
		Definition.DefinitionVersion = TEXT("2026.07");
		Definition.NotationProfile =
			FString::Printf(TEXT("virtual-%dx%dx%dx%d-slice-v1"), Order, Order, Order, Order);
		Definition.SizeVector = {Order, Order, Order, Order};
		Definition.Variant = TEXT("virtual-slice-projection");
		return Definition;
	}

	bool IsSupportedDefinition(const FHyperTwistPuzzleDefinitionRef& Definition)
	{
		if (!Definition.IsStructurallyValid()
			|| Definition.PuzzleFamily != EHyperTwistPuzzleFamily::Hypercube
			|| Definition.Dimension != 4
			|| Definition.SizeVector.Num() != 4)
		{
			return false;
		}

		const int32 Order = Definition.SizeVector[0];
		if (Order < MinimumOrder || Order > MaximumOrder)
		{
			return false;
		}
		for (const int32 Size : Definition.SizeVector)
		{
			if (Size != Order)
			{
				return false;
			}
		}
		return Definition.PuzzleId == FString::Printf(
			TEXT("hypercube/%dx%dx%dx%d"),
			Order,
			Order,
			Order,
			Order);
	}

	FHyperTwistVirtual3333SignedAxis MakeSignedAxis(
		const EHyperTwistVirtual3333Axis Axis,
		const bool bPositiveDirection
	)
	{
		FHyperTwistVirtual3333SignedAxis SignedAxis;
		SignedAxis.Axis = Axis;
		SignedAxis.bPositiveDirection = bPositiveDirection;
		return SignedAxis;
	}

	FHyperTwistVirtual3333PieceOrientation BuildIdentityOrientation()
	{
		FHyperTwistVirtual3333PieceOrientation Orientation;
		Orientation.Basis = {
			MakeSignedAxis(EHyperTwistVirtual3333Axis::X, true),
			MakeSignedAxis(EHyperTwistVirtual3333Axis::Y, true),
			MakeSignedAxis(EHyperTwistVirtual3333Axis::Z, true),
			MakeSignedAxis(EHyperTwistVirtual3333Axis::W, true)
		};
		return Orientation;
	}

	bool IsIdentityOrientation(const FHyperTwistVirtual3333PieceOrientation& Orientation)
	{
		if (Orientation.Basis.Num() != AxisCount)
		{
			return false;
		}

		for (int32 AxisIndex = 0; AxisIndex < AxisCount; ++AxisIndex)
		{
			if (Orientation.Basis[AxisIndex].Axis != ToAxisEnum(AxisIndex)
				|| !Orientation.Basis[AxisIndex].bPositiveDirection)
			{
				return false;
			}
		}

		return true;
	}

	bool IsRuntimeStateShapeValid(const FHyperTwistVirtual3333RuntimeState& State)
	{
		const int32 Order = State.GetOrder();
		const int32 PieceCount = State.GetPieceCount();
		const FString ExpectedStateProfile = FString::Printf(
			TEXT("hypercube-%dx%dx%dx%d-runtime-v1"),
			Order,
			Order,
			Order,
			Order);
		if (State.StateProfile != ExpectedStateProfile
			|| !State.Definition.IsStructurallyValid()
			|| !IsSupportedDefinition(State.Definition)
			|| State.Definition.PuzzleFamily != EHyperTwistPuzzleFamily::Hypercube
			|| State.Definition.Dimension != 4
			|| Order < MinimumOrder
			|| Order > MaximumOrder
			|| State.PositionToPiece.Num() != PieceCount
			|| State.PieceOrientations.Num() != PieceCount)
		{
			return false;
		}

		TBitArray<> SeenPieces(false, PieceCount);
		for (const int32 PieceId : State.PositionToPiece)
		{
			if (PieceId < 0 || PieceId >= PieceCount || SeenPieces[PieceId])
			{
				return false;
			}
			SeenPieces[PieceId] = true;
		}

		for (const FHyperTwistVirtual3333PieceOrientation& Orientation :
			 State.PieceOrientations)
		{
			if (!Orientation.IsStructurallyValid())
			{
				return false;
			}
		}
		return true;
	}

	bool IsSolvedRuntimeState(const FHyperTwistVirtual3333RuntimeState& State)
	{
		if (!IsRuntimeStateShapeValid(State))
		{
			return false;
		}

		for (int32 PositionIndex = 0; PositionIndex < State.GetPieceCount(); ++PositionIndex)
		{
			if (State.PositionToPiece[PositionIndex] != PositionIndex
				|| !IsIdentityOrientation(State.PieceOrientations[PositionIndex]))
			{
				return false;
			}
		}

		return true;
	}

	TArray<EHyperTwistVirtual3333Axis> GetAxesExcluding(
		const EHyperTwistVirtual3333Axis ExcludedA,
		const EHyperTwistVirtual3333Axis ExcludedB = EHyperTwistVirtual3333Axis::W,
		const bool bUseSecondExclusion = false
	)
	{
		TArray<EHyperTwistVirtual3333Axis> Axes;
		Axes.Reserve(3);
		for (int32 AxisIndex = 0; AxisIndex < AxisCount; ++AxisIndex)
		{
			const EHyperTwistVirtual3333Axis Axis = ToAxisEnum(AxisIndex);
			if (Axis == ExcludedA || (bUseSecondExclusion && Axis == ExcludedB))
			{
				continue;
			}

			Axes.Add(Axis);
		}

		return Axes;
	}

	void GetRotationPlaneAxes(
		const EHyperTwistVirtual3333Axis SliceAxis,
		const EHyperTwistVirtual3333Axis RotationAxis,
		EHyperTwistVirtual3333Axis& OutAxisA,
		EHyperTwistVirtual3333Axis& OutAxisB
	)
	{
		const TArray<EHyperTwistVirtual3333Axis> PlaneAxes =
			GetAxesExcluding(SliceAxis, RotationAxis, true);
		OutAxisA = PlaneAxes[0];
		OutAxisB = PlaneAxes[1];
	}

	FHyperTwistVirtual3333GridCoordinate RotateCoordinateInPlane(
		const FHyperTwistVirtual3333GridCoordinate& Coordinate,
		const EHyperTwistVirtual3333Axis AxisA,
		const EHyperTwistVirtual3333Axis AxisB,
		const EHyperTwistVirtual3333TurnDirection Direction
	)
	{
		FHyperTwistVirtual3333GridCoordinate Rotated = Coordinate;
		const int32 ValueA = GetCoordinateValue(Coordinate, AxisA);
		const int32 ValueB = GetCoordinateValue(Coordinate, AxisB);
		if (Direction == EHyperTwistVirtual3333TurnDirection::Clockwise)
		{
			SetCoordinateValue(Rotated, AxisA, -ValueB);
			SetCoordinateValue(Rotated, AxisB, ValueA);
		}
		else
		{
			SetCoordinateValue(Rotated, AxisA, ValueB);
			SetCoordinateValue(Rotated, AxisB, -ValueA);
		}

		return Rotated;
	}

	FHyperTwistVirtual3333SignedAxis RotateSignedAxisInPlane(
		const FHyperTwistVirtual3333SignedAxis& SignedAxis,
		const EHyperTwistVirtual3333Axis AxisA,
		const EHyperTwistVirtual3333Axis AxisB,
		const EHyperTwistVirtual3333TurnDirection Direction
	)
	{
		if (SignedAxis.Axis != AxisA && SignedAxis.Axis != AxisB)
		{
			return SignedAxis;
		}

		const int32 WorldSign = SignedAxis.bPositiveDirection ? 1 : -1;
		FHyperTwistVirtual3333GridCoordinate UnitVector;
		SetCoordinateValue(UnitVector, SignedAxis.Axis, WorldSign);
		const FHyperTwistVirtual3333GridCoordinate Rotated =
			RotateCoordinateInPlane(UnitVector, AxisA, AxisB, Direction);

		for (int32 AxisIndex = 0; AxisIndex < AxisCount; ++AxisIndex)
		{
			const EHyperTwistVirtual3333Axis Axis = ToAxisEnum(AxisIndex);
			const int32 Value = GetCoordinateValue(Rotated, Axis);
			if (Value != 0)
			{
				return MakeSignedAxis(Axis, Value > 0);
			}
		}

		return SignedAxis;
	}

	FHyperTwistVirtual3333PieceOrientation RotateOrientationInPlane(
		const FHyperTwistVirtual3333PieceOrientation& Orientation,
		const EHyperTwistVirtual3333Axis AxisA,
		const EHyperTwistVirtual3333Axis AxisB,
		const EHyperTwistVirtual3333TurnDirection Direction
	)
	{
		FHyperTwistVirtual3333PieceOrientation Rotated = Orientation;
		for (FHyperTwistVirtual3333SignedAxis& BasisAxis : Rotated.Basis)
		{
			BasisAxis = RotateSignedAxisInPlane(BasisAxis, AxisA, AxisB, Direction);
		}

		return Rotated;
	}

	EHyperTwistMelindaStickerColor ResolveLocalStickerColor(
		const EHyperTwistVirtual3333Axis Axis,
		const int32 Sign
	)
	{
		switch (Axis)
		{
		case EHyperTwistVirtual3333Axis::X:
			return Sign > 0
				? EHyperTwistMelindaStickerColor::XPositive
				: EHyperTwistMelindaStickerColor::XNegative;
		case EHyperTwistVirtual3333Axis::Y:
			return Sign > 0
				? EHyperTwistMelindaStickerColor::YPositive
				: EHyperTwistMelindaStickerColor::YNegative;
		case EHyperTwistVirtual3333Axis::Z:
			return Sign > 0
				? EHyperTwistMelindaStickerColor::ZPositive
				: EHyperTwistMelindaStickerColor::ZNegative;
		default:
			return Sign > 0
				? EHyperTwistMelindaStickerColor::WPositive
				: EHyperTwistMelindaStickerColor::WNegative;
		}
	}

	FString BuildCoordinateLabel(const FHyperTwistVirtual3333GridCoordinate& Coordinate)
	{
		return FString::Printf(
			TEXT("X%+d_Y%+d_Z%+d_W%+d"),
			Coordinate.X,
			Coordinate.Y,
			Coordinate.Z,
			Coordinate.W
		);
	}

	FString BuildNotation(
		const FHyperTwistVirtual3333SliceTurnRequest& Request,
		const EHyperTwistVirtual3333Axis PlaneAxisA,
		const EHyperTwistVirtual3333Axis PlaneAxisB
	)
	{
		return FString::Printf(
			TEXT("%s[%+d]-hold%s-plane%s%s-%s"),
			*GetAxisLabel(Request.SliceAxis),
			Request.SliceCoordinate,
			*GetAxisLabel(Request.RotationAxis),
			*GetAxisLabel(PlaneAxisA),
			*GetAxisLabel(PlaneAxisB),
			Request.Direction == EHyperTwistVirtual3333TurnDirection::Clockwise
				? TEXT("cw")
				: TEXT("ccw")
		);
	}

	FHyperTwistVirtual3333ProjectedCell BuildProjectedCell(
		const FHyperTwistVirtual3333GridCoordinate& PieceHomeCoordinate,
		const FHyperTwistVirtual3333PieceOrientation& Orientation,
		const EHyperTwistVirtual3333Axis WorldAxis,
		const int32 Order
	)
	{
		FHyperTwistVirtual3333ProjectedCell Cell;
		Cell.Axis = WorldAxis;
		Cell.bPositiveSide = true;

		for (int32 LocalAxisIndex = 0; LocalAxisIndex < AxisCount; ++LocalAxisIndex)
		{
			const EHyperTwistVirtual3333Axis LocalAxis = ToAxisEnum(LocalAxisIndex);
			const FHyperTwistVirtual3333SignedAxis& BasisAxis = Orientation.Basis[LocalAxisIndex];
			if (BasisAxis.Axis != WorldAxis)
			{
				continue;
			}

			const int32 LocalStickerSign = GetCoordinateValue(PieceHomeCoordinate, LocalAxis);
			if (FMath::Abs(LocalStickerSign) != GetBoundaryCoordinateMagnitude(Order))
			{
				return Cell;
			}

			const int32 VisibleWorldSign = 1;
			const int32 CorrespondingLocalSide = BasisAxis.bPositiveDirection == (VisibleWorldSign > 0)
				? 1
				: -1;
			if (FMath::Sign(LocalStickerSign) == CorrespondingLocalSide)
			{
				Cell.bHasSticker = true;
				Cell.Color = ResolveLocalStickerColor(LocalAxis, LocalStickerSign);
			}
			return Cell;
		}

		return Cell;
	}
}

bool FHyperTwistVirtual3333GridCoordinate::IsValidForOrder(const int32 Order) const
{
	using namespace HyperTwistVirtual3333ProjectionLibraryInternal;
	return Order >= MinimumOrder
		&& Order <= MaximumOrder
		&& IsCoordinateValueValid(X, Order)
		&& IsCoordinateValueValid(Y, Order)
		&& IsCoordinateValueValid(Z, Order)
		&& IsCoordinateValueValid(W, Order);
}

int32 FHyperTwistVirtual3333RuntimeState::GetOrder() const
{
	if (Definition.SizeVector.Num() != 4)
	{
		return 0;
	}
	const int32 Order = Definition.SizeVector[0];
	for (const int32 Size : Definition.SizeVector)
	{
		if (Size != Order)
		{
			return 0;
		}
	}
	return Order;
}

int32 FHyperTwistVirtual3333RuntimeState::GetPieceCount() const
{
	const int32 Order = GetOrder();
	return UHyperTwistVirtual3333ProjectionLibrary::IsSupportedOrder(Order)
		? HyperTwistVirtual3333ProjectionLibraryInternal::GetPieceCount(Order)
		: 0;
}

bool FHyperTwistVirtual3333RuntimeState::IsStructurallyValid() const
{
	using namespace HyperTwistVirtual3333ProjectionLibraryInternal;
	if (!IsRuntimeStateShapeValid(*this))
	{
		return false;
	}

	return bIsSolved == IsSolvedRuntimeState(*this);
}

bool FHyperTwistVirtual3333ProjectedTesseract::IsStructurallyValid() const
{
	if (!UHyperTwistVirtual3333ProjectionLibrary::IsSupportedOrder(PuzzleOrder))
	{
		return false;
	}

	const int32 PieceCount =
		HyperTwistVirtual3333ProjectionLibraryInternal::GetPieceCount(PuzzleOrder);
	if (PositionIndex < 0
		|| PositionIndex >= PieceCount
		|| PieceId < 0
		|| PieceId >= PieceCount
		|| !HyperTwistVirtual3333ProjectionLibraryInternal::IsCoordinateValueValid(
			LocalGridCoordinate.X,
			PuzzleOrder)
		|| !HyperTwistVirtual3333ProjectionLibraryInternal::IsCoordinateValueValid(
			LocalGridCoordinate.Y,
			PuzzleOrder)
		|| !HyperTwistVirtual3333ProjectionLibraryInternal::IsCoordinateValueValid(
			LocalGridCoordinate.Z,
			PuzzleOrder)
		|| SlotLabel.IsEmpty()
		|| PieceLabel.IsEmpty()
		|| bPieceInSolvedPosition != (PositionIndex == PieceId)
		|| VisibleCells.Num() != 3)
	{
		return false;
	}

	bool bSeenAxes[4] = {false, false, false, false};
	for (const FHyperTwistVirtual3333ProjectedCell& Cell : VisibleCells)
	{
		const int32 AxisIndex = static_cast<int32>(Cell.Axis);
		if (!Cell.IsStructurallyValid()
			|| AxisIndex < 0
			|| AxisIndex >= 4
			|| bSeenAxes[AxisIndex])
		{
			return false;
		}

		bSeenAxes[AxisIndex] = true;
	}

	return true;
}

bool FHyperTwistVirtual3333VisibleProjection::IsStructurallyValid() const
{
	if (Definition.SizeVector.Num() != 4)
	{
		return false;
	}
	const int32 Order = Definition.SizeVector[0];
	const int32 PieceCount =
		HyperTwistVirtual3333ProjectionLibraryInternal::GetPieceCount(Order);
	const int32 VisiblePieceCount =
		HyperTwistVirtual3333ProjectionLibraryInternal::GetVisiblePieceCount(Order);
	if (ProjectionProfile.IsEmpty()
		|| !Definition.IsStructurallyValid()
		|| !HyperTwistVirtual3333ProjectionLibraryInternal::IsSupportedDefinition(Definition)
		|| !UHyperTwistVirtual3333ProjectionLibrary::IsSupportedOrder(Order)
		|| !HyperTwistVirtual3333ProjectionLibraryInternal::IsCoordinateValueValid(
			VisibleSliceCoordinate,
			Order)
		|| DisplayAxes.Num() != 3
		|| Tesseracts.Num() != VisiblePieceCount)
	{
		return false;
	}

	bool bSeenDisplayAxes[4] = {false, false, false, false};
	for (const EHyperTwistVirtual3333Axis Axis : DisplayAxes)
	{
		const int32 AxisIndex = static_cast<int32>(Axis);
		if (Axis == VisibleSliceAxis
			|| AxisIndex < 0
			|| AxisIndex >= 4
			|| bSeenDisplayAxes[AxisIndex])
		{
			return false;
		}

		bSeenDisplayAxes[AxisIndex] = true;
	}

	TBitArray<> SeenPositions(false, PieceCount);
	int32 VisibleCellCount = 0;
	for (const FHyperTwistVirtual3333ProjectedTesseract& Tesseract : Tesseracts)
	{
		if (Tesseract.PuzzleOrder != Order
			|| !Tesseract.IsStructurallyValid()
			|| SeenPositions[Tesseract.PositionIndex])
		{
			return false;
		}

		SeenPositions[Tesseract.PositionIndex] = true;
		VisibleCellCount += Tesseract.VisibleCells.Num();
	}

	return VisibleCellCount == VisiblePieceCount * 3;
}


FHyperTwistPuzzleDefinitionRef UHyperTwistVirtual3333ProjectionLibrary::MakePuzzleDefinition(
	const int32 Order
)
{
	const int32 SafeOrder = IsSupportedOrder(Order) ? Order : 3;
	return HyperTwistVirtual3333ProjectionLibraryInternal::BuildDefinition(SafeOrder);
}

FHyperTwistVirtual3333RuntimeState UHyperTwistVirtual3333ProjectionLibrary::BuildSolvedState(
	const int32 Order
)
{
	using namespace HyperTwistVirtual3333ProjectionLibraryInternal;
	const int32 SafeOrder = IsSupportedOrder(Order) ? Order : 3;
	const int32 PieceCount =
		HyperTwistVirtual3333ProjectionLibraryInternal::GetPieceCount(SafeOrder);
	FHyperTwistVirtual3333RuntimeState State;
	State.StateProfile = FString::Printf(
		TEXT("hypercube-%dx%dx%dx%d-runtime-v1"),
		SafeOrder,
		SafeOrder,
		SafeOrder,
		SafeOrder);
	State.Definition = MakePuzzleDefinition(SafeOrder);
	State.PositionToPiece.Reserve(PieceCount);
	State.PieceOrientations.Reserve(PieceCount);
	for (int32 PieceId = 0; PieceId < PieceCount; ++PieceId)
	{
		State.PositionToPiece.Add(PieceId);
		State.PieceOrientations.Add(BuildIdentityOrientation());
	}
	State.bIsSolved = true;
	return State;
}

FHyperTwistPuzzleState UHyperTwistVirtual3333ProjectionLibrary::BuildPuzzleStateEnvelope(
	const FHyperTwistVirtual3333RuntimeState& RuntimeState
)
{
	const FHyperTwistVirtual3333RuntimeState SafeRuntimeState =
		RuntimeState.IsStructurallyValid()
			? RuntimeState
			: BuildSolvedState(3);
	FHyperTwistPuzzleState PuzzleState;
	PuzzleState.Definition = SafeRuntimeState.Definition;
	PuzzleState.StateEncodingKind = EHyperTwistStateEncodingKind::FamilySpecific;
	PuzzleState.StateEncoding.EncodingProfile = SafeRuntimeState.StateProfile;
	PuzzleState.StateEncoding.PayloadJson =
		SerializeRuntimeStateToJson(SafeRuntimeState);
	const int32 Order = SafeRuntimeState.GetOrder();
	PuzzleState.OrientationFrame.Reference = FString::Printf(
		TEXT("hypercube-%dx%dx%dx%d-canonical-v1"),
		Order,
		Order,
		Order,
		Order);
	PuzzleState.bIsSolved = SafeRuntimeState.bIsSolved;
	PuzzleState.Source = EHyperTwistStateSource::Runtime;
	return PuzzleState;
}

FString UHyperTwistVirtual3333ProjectionLibrary::SerializeRuntimeStateToJson(
	const FHyperTwistVirtual3333RuntimeState& RuntimeState
)
{
	return HyperTwistVirtual3333ProjectionLibraryInternal::SerializeStructToJson(RuntimeState);
}

FString UHyperTwistVirtual3333ProjectionLibrary::GetAxisLabel(
	const EHyperTwistVirtual3333Axis Axis
)
{
	return HyperTwistVirtual3333ProjectionLibraryInternal::GetAxisLabel(Axis);
}

TArray<EHyperTwistVirtual3333Axis>
UHyperTwistVirtual3333ProjectionLibrary::GetAvailableRotationAxesForSliceAxis(
	const EHyperTwistVirtual3333Axis SliceAxis
)
{
	return HyperTwistVirtual3333ProjectionLibraryInternal::GetAxesExcluding(SliceAxis);
}

TArray<int32> UHyperTwistVirtual3333ProjectionLibrary::GetSliceCoordinatesForOrder(
	const int32 Order
)
{
	TArray<int32> Coordinates;
	if (!IsSupportedOrder(Order))
	{
		return Coordinates;
	}
	Coordinates.Reserve(Order);
	for (int32 LayerIndex = 0; LayerIndex < Order; ++LayerIndex)
	{
		Coordinates.Add(
			HyperTwistVirtual3333ProjectionLibraryInternal::CoordinateFromDigit(
				LayerIndex,
				Order));
	}
	return Coordinates;
}

int32 UHyperTwistVirtual3333ProjectionLibrary::GetSliceCoordinateForLayer(
	const int32 Order,
	const int32 OneBasedLayer
)
{
	if (!IsSupportedOrder(Order))
	{
		return 0;
	}
	const int32 SafeLayer = FMath::Clamp(OneBasedLayer, 1, Order);
	return HyperTwistVirtual3333ProjectionLibraryInternal::CoordinateFromDigit(
		SafeLayer - 1,
		Order);
}

bool UHyperTwistVirtual3333ProjectionLibrary::IsSupportedOrder(const int32 Order)
{
	return Order >= HyperTwistVirtual3333ProjectionLibraryInternal::MinimumOrder
		&& Order <= HyperTwistVirtual3333ProjectionLibraryInternal::MaximumOrder;
}

bool UHyperTwistVirtual3333ProjectionLibrary::IsRotationAxisAvailable(
	const EHyperTwistVirtual3333Axis SliceAxis,
	const EHyperTwistVirtual3333Axis RotationAxis
)
{
	return RotationAxis != SliceAxis;
}

FHyperTwistVirtual3333SliceTurnResult UHyperTwistVirtual3333ProjectionLibrary::ApplySliceTurn(
	const FHyperTwistVirtual3333RuntimeState& State,
	const FHyperTwistVirtual3333SliceTurnRequest& Request
)
{
	using namespace HyperTwistVirtual3333ProjectionLibraryInternal;

	FHyperTwistVirtual3333SliceTurnResult Result;
	Result.State = State;

	if (!State.IsStructurallyValid())
	{
		Result.Warnings.Add(TEXT("invalid-virtual-nxnxnxn-state"));
		return Result;
	}

	if (!IsSupportedDefinition(State.Definition))
	{
		Result.Warnings.Add(TEXT("unsupported-virtual-nxnxnxn-definition"));
		return Result;
	}

	const int32 Order = State.GetOrder();
	const int32 PieceCount = State.GetPieceCount();
	if (!Request.IsStructurallyValid()
		|| !IsCoordinateValueValid(Request.SliceCoordinate, Order))
	{
		Result.Warnings.Add(TEXT("invalid-virtual-nxnxnxn-turn-request"));
		return Result;
	}

	EHyperTwistVirtual3333Axis PlaneAxisA = EHyperTwistVirtual3333Axis::Y;
	EHyperTwistVirtual3333Axis PlaneAxisB = EHyperTwistVirtual3333Axis::Z;
	GetRotationPlaneAxes(Request.SliceAxis, Request.RotationAxis, PlaneAxisA, PlaneAxisB);

	Result.State.PositionToPiece = State.PositionToPiece;
	Result.State.PieceOrientations = State.PieceOrientations;
	for (int32 PositionIndex = 0; PositionIndex < PieceCount; ++PositionIndex)
	{
		const FHyperTwistVirtual3333GridCoordinate Coordinate =
			DecodeCoordinate(PositionIndex, Order);
		if (GetCoordinateValue(Coordinate, Request.SliceAxis) != Request.SliceCoordinate)
		{
			continue;
		}

		const int32 PieceId = State.PositionToPiece[PositionIndex];
		const FHyperTwistVirtual3333GridCoordinate RotatedCoordinate =
			RotateCoordinateInPlane(Coordinate, PlaneAxisA, PlaneAxisB, Request.Direction);
		const int32 NewPositionIndex = EncodeCoordinate(RotatedCoordinate, Order);
		Result.State.PositionToPiece[NewPositionIndex] = PieceId;
		Result.State.PieceOrientations[PieceId] = RotateOrientationInPlane(
			State.PieceOrientations[PieceId],
			PlaneAxisA,
			PlaneAxisB,
			Request.Direction
		);
	}

	Result.State.bIsSolved = IsSolvedRuntimeState(Result.State);
	Result.AppliedNotation = BuildNotation(Request, PlaneAxisA, PlaneAxisB);
	Result.bApplied = true;
	Result.bExactStateUpdate = Result.State.IsStructurallyValid();
	if (!Result.bExactStateUpdate)
	{
		Result.Warnings.Add(TEXT("virtual-nxnxnxn-state-became-invalid"));
	}

	return Result;
}

FHyperTwistVirtual3333ScrambleResult
UHyperTwistVirtual3333ProjectionLibrary::GenerateScramble(
	const int32 Order,
	const int32 MoveCount,
	const int32 RandomSeed
)
{
	FHyperTwistVirtual3333ScrambleResult Result;
	if (!IsSupportedOrder(Order) || MoveCount < 1 || MoveCount > 500)
	{
		Result.Warnings.Add(TEXT("virtual-nxnxnxn-scramble-request-invalid"));
		return Result;
	}

	const TArray<int32> SliceCoordinates = GetSliceCoordinatesForOrder(Order);
	if (SliceCoordinates.Num() != Order)
	{
		Result.Warnings.Add(TEXT("virtual-nxnxnxn-scramble-layers-unavailable"));
		return Result;
	}

	FRandomStream Random(RandomSeed);
	Result.State = BuildSolvedState(Order);
	Result.AppliedMoves.Reserve(MoveCount);
	FHyperTwistVirtual3333SliceTurnRequest PreviousRequest;
	bool bHasPreviousRequest = false;
	for (int32 MoveIndex = 0; MoveIndex < MoveCount; ++MoveIndex)
	{
		FHyperTwistVirtual3333SliceTurnRequest Request;
		bool bDistinctFromPrevious = false;
		for (int32 Attempt = 0; Attempt < 16 && !bDistinctFromPrevious; ++Attempt)
		{
			Request.SliceAxis = static_cast<EHyperTwistVirtual3333Axis>(
				Random.RandRange(0, 3));
			Request.SliceCoordinate =
				SliceCoordinates[Random.RandRange(0, SliceCoordinates.Num() - 1)];
			const TArray<EHyperTwistVirtual3333Axis> RotationAxes =
				GetAvailableRotationAxesForSliceAxis(Request.SliceAxis);
			Request.RotationAxis =
				RotationAxes[Random.RandRange(0, RotationAxes.Num() - 1)];
			Request.Direction = Random.RandRange(0, 1) == 0
				? EHyperTwistVirtual3333TurnDirection::Clockwise
				: EHyperTwistVirtual3333TurnDirection::CounterClockwise;
			bDistinctFromPrevious = !bHasPreviousRequest
				|| Request.SliceAxis != PreviousRequest.SliceAxis
				|| Request.SliceCoordinate != PreviousRequest.SliceCoordinate
				|| Request.RotationAxis != PreviousRequest.RotationAxis;
		}

		const FHyperTwistVirtual3333SliceTurnResult TurnResult =
			ApplySliceTurn(Result.State, Request);
		if (!TurnResult.bApplied || !TurnResult.bExactStateUpdate)
		{
			Result.Warnings = TurnResult.Warnings;
			Result.Warnings.Add(TEXT("virtual-nxnxnxn-scramble-turn-failed"));
			Result.State = FHyperTwistVirtual3333RuntimeState();
			Result.AppliedMoves.Reset();
			return Result;
		}

		Result.State = TurnResult.State;
		Result.AppliedMoves.Add(TurnResult.AppliedNotation);
		PreviousRequest = Request;
		bHasPreviousRequest = true;
	}

	Result.FinalSelection = PreviousRequest;
	Result.bExactStateUpdate = Result.State.IsStructurallyValid();
	Result.bGenerated = Result.bExactStateUpdate
		&& Result.AppliedMoves.Num() == MoveCount;
	if (!Result.bGenerated)
	{
		Result.Warnings.Add(TEXT("virtual-nxnxnxn-scramble-state-invalid"));
	}
	return Result;
}

FHyperTwistVirtual3333ProjectionBuildResult
UHyperTwistVirtual3333ProjectionLibrary::BuildVisibleProjection(
	const FHyperTwistVirtual3333RuntimeState& State,
	const EHyperTwistVirtual3333Axis VisibleSliceAxis,
	const int32 VisibleSliceCoordinate
)
{
	using namespace HyperTwistVirtual3333ProjectionLibraryInternal;

	FHyperTwistVirtual3333ProjectionBuildResult Result;
	Result.Projection.Definition = State.Definition.IsStructurallyValid()
		? State.Definition
		: MakePuzzleDefinition(3);
	Result.Projection.VisibleSliceAxis = VisibleSliceAxis;
	Result.Projection.VisibleSliceCoordinate = VisibleSliceCoordinate;
	Result.Projection.DisplayAxes = GetAxesExcluding(VisibleSliceAxis);

	if (!State.IsStructurallyValid())
	{
		Result.Warnings.Add(TEXT("invalid-virtual-nxnxnxn-state"));
		return Result;
	}

	if (!IsSupportedDefinition(State.Definition))
	{
		Result.Warnings.Add(TEXT("unsupported-virtual-nxnxnxn-definition"));
		return Result;
	}

	const int32 Order = State.GetOrder();
	const int32 PieceCount = State.GetPieceCount();
	const int32 VisiblePieceCount = GetVisiblePieceCount(Order);
	Result.Projection.ProjectionProfile = FString::Printf(
		TEXT("hypercube-%dx%dx%dx%d-visible-slice-projection-v1"),
		Order,
		Order,
		Order,
		Order);
	if (!IsCoordinateValueValid(VisibleSliceCoordinate, Order))
	{
		Result.Warnings.Add(TEXT("invalid-visible-slice-coordinate"));
		return Result;
	}

	Result.Projection.Tesseracts.Reserve(VisiblePieceCount);
	for (int32 PositionIndex = 0; PositionIndex < PieceCount; ++PositionIndex)
	{
		const FHyperTwistVirtual3333GridCoordinate Coordinate =
			DecodeCoordinate(PositionIndex, Order);
		if (GetCoordinateValue(Coordinate, VisibleSliceAxis) != VisibleSliceCoordinate)
		{
			continue;
		}

		const int32 PieceId = State.PositionToPiece[PositionIndex];
		const FHyperTwistVirtual3333GridCoordinate PieceHomeCoordinate =
			DecodeCoordinate(PieceId, Order);
		const FHyperTwistVirtual3333PieceOrientation& Orientation =
			State.PieceOrientations[PieceId];

		FHyperTwistVirtual3333ProjectedTesseract Tesseract;
		Tesseract.PositionIndex = PositionIndex;
		Tesseract.PieceId = PieceId;
		Tesseract.PuzzleOrder = Order;
		Tesseract.LocalGridCoordinate = FIntVector(
			GetCoordinateValue(Coordinate, Result.Projection.DisplayAxes[0]),
			GetCoordinateValue(Coordinate, Result.Projection.DisplayAxes[1]),
			GetCoordinateValue(Coordinate, Result.Projection.DisplayAxes[2])
		);
		Tesseract.SlotLabel = BuildCoordinateLabel(Coordinate);
		Tesseract.PieceLabel = BuildCoordinateLabel(PieceHomeCoordinate);
		Tesseract.bPieceInSolvedPosition = PositionIndex == PieceId;
		Tesseract.VisibleCells.Reserve(3);
		for (const EHyperTwistVirtual3333Axis DisplayAxis : Result.Projection.DisplayAxes)
		{
			Tesseract.VisibleCells.Add(
				BuildProjectedCell(PieceHomeCoordinate, Orientation, DisplayAxis, Order)
			);
		}

		Result.Projection.Tesseracts.Add(Tesseract);
	}

	Result.bProjected = Result.Projection.IsStructurallyValid();
	Result.bExactProjection = Result.bProjected;
	if (!Result.bProjected)
	{
		Result.Warnings.Add(TEXT("virtual-nxnxnxn-projection-invalid"));
	}

	return Result;
}
