#include "HyperTwistSimulation/HyperTwistMagicCube5DRuntimeLibrary.h"

#include "JsonObjectConverter.h"

namespace HyperTwistMagicCube5DRuntimeLibraryInternal
{
	constexpr int32 AxisCount = 5;
	constexpr int32 MinimumOrder = 2;
	constexpr int32 MaximumOrder = 6;
	constexpr int32 ColorCount = AxisCount * 2;

	int32 IntegerPower(const int32 Base, const int32 Exponent)
	{
		int32 Result = 1;
		for (int32 Index = 0; Index < Exponent; ++Index)
		{
			Result *= Base;
		}
		return Result;
	}

	int32 GetPieceCount(const int32 Order)
	{
		return IntegerPower(Order, AxisCount);
	}

	int32 GetFaceletCount(const int32 Order)
	{
		return ColorCount * IntegerPower(Order, AxisCount - 1);
	}

	int32 GetBoundaryCubieCount(const int32 Order)
	{
		const int32 InteriorOrder = FMath::Max(Order - 2, 0);
		return GetPieceCount(Order) - IntegerPower(InteriorOrder, AxisCount);
	}

	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;
		}

		const int32 Boundary = GetBoundaryCoordinateMagnitude(Order);
		if (Coordinate < -Boundary || Coordinate > Boundary)
		{
			return false;
		}

		return Order % 2 != 0 || (Coordinate + Boundary) % 2 == 0;
	}

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

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

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

	int32 GetCoordinateValue(
		const FHyperTwistMagicCube5DGridCoordinate& Coordinate,
		const EHyperTwistMagicCube5DAxis Axis
	)
	{
		const int32 AxisIndex = ToAxisIndex(Axis);
		return Coordinate.Components.IsValidIndex(AxisIndex)
			? Coordinate.Components[AxisIndex]
			: 0;
	}

	void SetCoordinateValue(
		FHyperTwistMagicCube5DGridCoordinate& Coordinate,
		const EHyperTwistMagicCube5DAxis Axis,
		const int32 Value
	)
	{
		const int32 AxisIndex = ToAxisIndex(Axis);
		if (Coordinate.Components.Num() != AxisCount)
		{
			Coordinate.Components.Init(0, AxisCount);
		}
		Coordinate.Components[AxisIndex] = Value;
	}

	FHyperTwistMagicCube5DGridCoordinate DecodeCoordinate(
		const int32 PositionIndex,
		const int32 Order
	)
	{
		FHyperTwistMagicCube5DGridCoordinate Coordinate;
		Coordinate.Components.Init(0, AxisCount);
		int32 Remainder = PositionIndex;
		for (int32 AxisIndex = 0; AxisIndex < AxisCount; ++AxisIndex)
		{
			Coordinate.Components[AxisIndex] =
				CoordinateFromDigit(Remainder % Order, Order);
			Remainder /= Order;
		}
		return Coordinate;
	}

	int32 EncodeCoordinate(
		const FHyperTwistMagicCube5DGridCoordinate& Coordinate,
		const int32 Order
	)
	{
		int32 PositionIndex = 0;
		int32 PlaceValue = 1;
		for (int32 AxisIndex = 0; AxisIndex < AxisCount; ++AxisIndex)
		{
			PositionIndex +=
				DigitFromCoordinate(Coordinate.Components[AxisIndex], Order) * PlaceValue;
			PlaceValue *= Order;
		}
		return PositionIndex;
	}

	FHyperTwistMagicCube5DSignedAxis MakeSignedAxis(
		const EHyperTwistMagicCube5DAxis Axis,
		const bool bPositiveDirection
	)
	{
		FHyperTwistMagicCube5DSignedAxis SignedAxis;
		SignedAxis.Axis = Axis;
		SignedAxis.bPositiveDirection = bPositiveDirection;
		return SignedAxis;
	}

	FHyperTwistMagicCube5DPieceOrientation BuildIdentityOrientation()
	{
		FHyperTwistMagicCube5DPieceOrientation Orientation;
		Orientation.Basis.Reserve(AxisCount);
		for (int32 AxisIndex = 0; AxisIndex < AxisCount; ++AxisIndex)
		{
			Orientation.Basis.Add(MakeSignedAxis(ToAxisEnum(AxisIndex), true));
		}
		return Orientation;
	}

	bool IsIdentityOrientation(
		const FHyperTwistMagicCube5DPieceOrientation& 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;
	}

	FHyperTwistPuzzleDefinitionRef BuildDefinition(const int32 Order)
	{
		FHyperTwistPuzzleDefinitionRef Definition;
		Definition.PuzzleId =
			FString::Printf(TEXT("magiccube5d/%d-order"), Order);
		Definition.PuzzleFamily = EHyperTwistPuzzleFamily::Hypercube;
		Definition.Dimension = AxisCount;
		Definition.DefinitionVersion = TEXT("2026.07");
		Definition.NotationProfile = TEXT("magiccube5d-signed-face-plane-v1");
		Definition.SizeVector.Init(Order, AxisCount);
		Definition.Variant = TEXT("family-owned-exact-piece-orientation");
		return Definition;
	}

	bool IsSupportedDefinition(const FHyperTwistPuzzleDefinitionRef& Definition)
	{
		if (!Definition.IsStructurallyValid()
			|| Definition.PuzzleFamily != EHyperTwistPuzzleFamily::Hypercube
			|| Definition.Dimension != AxisCount
			|| Definition.SizeVector.Num() != AxisCount)
		{
			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("magiccube5d/%d-order"), Order);
	}

	bool IsRuntimeStateShapeValid(const FHyperTwistMagicCube5DRuntimeState& State)
	{
		const int32 PieceCount = GetPieceCount(State.Order);
		if (State.StateProfile != FString::Printf(
				TEXT("magiccube5d-order-%d-piece-orientation-v1"),
				State.Order)
			|| !IsSupportedDefinition(State.Definition)
			|| State.Definition.SizeVector[0] != State.Order
			|| State.PositionToPiece.Num() != PieceCount
			|| State.PieceOrientations.Num() != PieceCount
			|| State.AppliedMoveCount < 0)
		{
			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 FHyperTwistMagicCube5DPieceOrientation& Orientation :
			 State.PieceOrientations)
		{
			if (!Orientation.IsStructurallyValid())
			{
				return false;
			}
		}
		return true;
	}

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

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

	FHyperTwistMagicCube5DGridCoordinate RotateCoordinateInPlane(
		const FHyperTwistMagicCube5DGridCoordinate& Coordinate,
		const EHyperTwistMagicCube5DAxis AxisA,
		const EHyperTwistMagicCube5DAxis AxisB,
		const EHyperTwistMagicCube5DTurnDirection Direction
	)
	{
		FHyperTwistMagicCube5DGridCoordinate Rotated = Coordinate;
		const int32 ValueA = GetCoordinateValue(Coordinate, AxisA);
		const int32 ValueB = GetCoordinateValue(Coordinate, AxisB);
		if (Direction == EHyperTwistMagicCube5DTurnDirection::PositiveQuarterTurn)
		{
			SetCoordinateValue(Rotated, AxisA, -ValueB);
			SetCoordinateValue(Rotated, AxisB, ValueA);
		}
		else
		{
			SetCoordinateValue(Rotated, AxisA, ValueB);
			SetCoordinateValue(Rotated, AxisB, -ValueA);
		}
		return Rotated;
	}

	FHyperTwistMagicCube5DSignedAxis RotateSignedAxisInPlane(
		const FHyperTwistMagicCube5DSignedAxis& SignedAxis,
		const EHyperTwistMagicCube5DAxis AxisA,
		const EHyperTwistMagicCube5DAxis AxisB,
		const EHyperTwistMagicCube5DTurnDirection Direction
	)
	{
		if (SignedAxis.Axis != AxisA && SignedAxis.Axis != AxisB)
		{
			return SignedAxis;
		}

		FHyperTwistMagicCube5DGridCoordinate UnitVector;
		UnitVector.Components.Init(0, AxisCount);
		SetCoordinateValue(
			UnitVector,
			SignedAxis.Axis,
			SignedAxis.bPositiveDirection ? 1 : -1);
		const FHyperTwistMagicCube5DGridCoordinate Rotated =
			RotateCoordinateInPlane(UnitVector, AxisA, AxisB, Direction);
		for (int32 AxisIndex = 0; AxisIndex < AxisCount; ++AxisIndex)
		{
			const int32 Value = Rotated.Components[AxisIndex];
			if (Value != 0)
			{
				return MakeSignedAxis(ToAxisEnum(AxisIndex), Value > 0);
			}
		}
		return SignedAxis;
	}

	FHyperTwistMagicCube5DPieceOrientation RotateOrientationInPlane(
		const FHyperTwistMagicCube5DPieceOrientation& Orientation,
		const EHyperTwistMagicCube5DAxis AxisA,
		const EHyperTwistMagicCube5DAxis AxisB,
		const EHyperTwistMagicCube5DTurnDirection Direction
	)
	{
		FHyperTwistMagicCube5DPieceOrientation Rotated = Orientation;
		for (FHyperTwistMagicCube5DSignedAxis& BasisAxis : Rotated.Basis)
		{
			BasisAxis = RotateSignedAxisInPlane(BasisAxis, AxisA, AxisB, Direction);
		}
		return Rotated;
	}

	TSet<int32> GetSelectedSliceCoordinates(
		const FHyperTwistMagicCube5DTurnRequest& Request,
		const int32 Order
	)
	{
		TSet<int32> Coordinates;
		for (int32 LayerIndex = 0; LayerIndex < Order; ++LayerIndex)
		{
			if ((Request.SliceMask & (1 << LayerIndex)) == 0)
			{
				continue;
			}
			const int32 Digit = Request.bPositiveFace
				? Order - 1 - LayerIndex
				: LayerIndex;
			Coordinates.Add(CoordinateFromDigit(Digit, Order));
		}
		return Coordinates;
	}

	int32 ResolveColorForWorldSide(
		const FHyperTwistMagicCube5DGridCoordinate& PieceHomeCoordinate,
		const FHyperTwistMagicCube5DPieceOrientation& Orientation,
		const EHyperTwistMagicCube5DAxis WorldAxis,
		const bool bPositiveWorldSide,
		const int32 Order
	)
	{
		const int32 Boundary = GetBoundaryCoordinateMagnitude(Order);
		for (int32 LocalAxisIndex = 0; LocalAxisIndex < AxisCount; ++LocalAxisIndex)
		{
			const FHyperTwistMagicCube5DSignedAxis& BasisAxis =
				Orientation.Basis[LocalAxisIndex];
			if (BasisAxis.Axis != WorldAxis)
			{
				continue;
			}

			const bool bPositiveLocalSide =
				BasisAxis.bPositiveDirection == bPositiveWorldSide;
			const int32 RequiredLocalCoordinate =
				bPositiveLocalSide ? Boundary : -Boundary;
			if (PieceHomeCoordinate.Components[LocalAxisIndex]
				!= RequiredLocalCoordinate)
			{
				return INDEX_NONE;
			}
			return LocalAxisIndex * 2 + (bPositiveLocalSide ? 1 : 0);
		}
		return INDEX_NONE;
	}

	FString BuildNotation(const FHyperTwistMagicCube5DTurnRequest& Request)
	{
		return FString::Printf(
			TEXT("%s%s{%02X}:%s%s:%s"),
			Request.bPositiveFace ? TEXT("+") : TEXT("-"),
			*GetAxisLabel(Request.FaceAxis),
			Request.SliceMask,
			*GetAxisLabel(Request.RotationAxisA),
			*GetAxisLabel(Request.RotationAxisB),
			Request.Direction ==
				EHyperTwistMagicCube5DTurnDirection::PositiveQuarterTurn
					? TEXT("+90")
					: TEXT("-90"));
	}
}

bool FHyperTwistMagicCube5DPieceOrientation::IsStructurallyValid() const
{
	using namespace HyperTwistMagicCube5DRuntimeLibraryInternal;
	if (Basis.Num() != AxisCount)
	{
		return false;
	}

	bool bSeenAxes[AxisCount] = {false, false, false, false, false};
	int32 NegativeDirectionCount = 0;
	int32 PermutationInversionCount = 0;
	for (const FHyperTwistMagicCube5DSignedAxis& SignedAxis : Basis)
	{
		const int32 AxisIndex = ToAxisIndex(SignedAxis.Axis);
		if (AxisIndex < 0 || AxisIndex >= AxisCount || bSeenAxes[AxisIndex])
		{
			return false;
		}
		bSeenAxes[AxisIndex] = true;
		NegativeDirectionCount += SignedAxis.bPositiveDirection ? 0 : 1;
	}

	for (int32 LeftIndex = 0; LeftIndex < Basis.Num(); ++LeftIndex)
	{
		for (int32 RightIndex = LeftIndex + 1;
			 RightIndex < Basis.Num();
			 ++RightIndex)
		{
			if (ToAxisIndex(Basis[LeftIndex].Axis)
				> ToAxisIndex(Basis[RightIndex].Axis))
			{
				++PermutationInversionCount;
			}
		}
	}

	// A legal 5D quarter turn is a proper signed permutation, never a reflection.
	return (NegativeDirectionCount + PermutationInversionCount) % 2 == 0;
}

bool FHyperTwistMagicCube5DGridCoordinate::IsValidForOrder(const int32 Order) const
{
	using namespace HyperTwistMagicCube5DRuntimeLibraryInternal;
	if (Components.Num() != AxisCount)
	{
		return false;
	}
	for (const int32 Component : Components)
	{
		if (!IsCoordinateValueValid(Component, Order))
		{
			return false;
		}
	}
	return true;
}

int32 FHyperTwistMagicCube5DRuntimeState::GetPieceCount() const
{
	return UHyperTwistMagicCube5DRuntimeLibrary::IsSupportedOrder(Order)
		? HyperTwistMagicCube5DRuntimeLibraryInternal::GetPieceCount(Order)
		: 0;
}

bool FHyperTwistMagicCube5DRuntimeState::IsStructurallyValid() const
{
	using namespace HyperTwistMagicCube5DRuntimeLibraryInternal;
	return IsRuntimeStateShapeValid(*this)
		&& bIsSolved == IsSolvedRuntimeState(*this);
}

bool FHyperTwistMagicCube5DTurnRequest::IsValidForOrder(const int32 Order) const
{
	using namespace HyperTwistMagicCube5DRuntimeLibraryInternal;
	const int32 FaceAxisIndex = ToAxisIndex(FaceAxis);
	const int32 RotationAxisAIndex = ToAxisIndex(RotationAxisA);
	const int32 RotationAxisBIndex = ToAxisIndex(RotationAxisB);
	return Order >= MinimumOrder
		&& Order <= MaximumOrder
		&& SliceMask > 0
		&& SliceMask < (1 << Order)
		&& FaceAxisIndex >= 0
		&& FaceAxisIndex < AxisCount
		&& RotationAxisAIndex >= 0
		&& RotationAxisAIndex < AxisCount
		&& RotationAxisBIndex >= 0
		&& RotationAxisBIndex < AxisCount
		&& RotationAxisA != RotationAxisB
		&& RotationAxisA != FaceAxis
		&& RotationAxisB != FaceAxis;
}

bool FHyperTwistMagicCube5DProjectedCubie::IsStructurallyValid(
	const int32 Order
) const
{
	const int32 PieceCount =
		HyperTwistMagicCube5DRuntimeLibraryInternal::GetPieceCount(Order);
	return PositionIndex >= 0
		&& PositionIndex < PieceCount
		&& PieceId >= 0
		&& PieceId < PieceCount
		&& Position.IsValidForOrder(Order)
		&& RepresentativeColorIndex >= 0
		&& RepresentativeColorIndex <
			HyperTwistMagicCube5DRuntimeLibraryInternal::ColorCount
		&& bPieceInSolvedPosition == (PositionIndex == PieceId);
}

bool FHyperTwistMagicCube5DProjectedFacelet::IsStructurallyValid(
	const int32 PieceCount
) const
{
	const int32 AxisIndex =
		HyperTwistMagicCube5DRuntimeLibraryInternal::ToAxisIndex(WorldAxis);
	return PositionIndex >= 0
		&& PositionIndex < PieceCount
		&& PieceId >= 0
		&& PieceId < PieceCount
		&& AxisIndex >= 0
		&& AxisIndex <
			HyperTwistMagicCube5DRuntimeLibraryInternal::AxisCount
		&& ColorIndex >= 0
		&& ColorIndex <
			HyperTwistMagicCube5DRuntimeLibraryInternal::ColorCount;
}

bool FHyperTwistMagicCube5DProjection::IsStructurallyValid() const
{
	using namespace HyperTwistMagicCube5DRuntimeLibraryInternal;
	if (ProjectionProfile != TEXT("magiccube5d-boundary-facelet-projection-v1")
		|| !UHyperTwistMagicCube5DRuntimeLibrary::IsSupportedOrder(Order))
	{
		return false;
	}

	const int32 PieceCount = GetPieceCount(Order);
	const int32 ExpectedCubieCount = GetBoundaryCubieCount(Order);
	if (Cubies.Num() != ExpectedCubieCount
		|| Facelets.Num() != GetFaceletCount(Order))
	{
		return false;
	}

	TBitArray<> SeenCubiePositions(false, PieceCount);
	for (const FHyperTwistMagicCube5DProjectedCubie& Cubie : Cubies)
	{
		if (!Cubie.IsStructurallyValid(Order)
			|| SeenCubiePositions[Cubie.PositionIndex])
		{
			return false;
		}
		SeenCubiePositions[Cubie.PositionIndex] = true;
	}

	TSet<int64> SeenFacelets;
	SeenFacelets.Reserve(Facelets.Num());
	for (const FHyperTwistMagicCube5DProjectedFacelet& Facelet : Facelets)
	{
		if (!Facelet.IsStructurallyValid(PieceCount))
		{
			return false;
		}
		const int64 Key =
			(static_cast<int64>(Facelet.PositionIndex) * ColorCount)
			+ (ToAxisIndex(Facelet.WorldAxis) * 2)
			+ (Facelet.bPositiveWorldSide ? 1 : 0);
		if (SeenFacelets.Contains(Key))
		{
			return false;
		}
		SeenFacelets.Add(Key);
	}
	return true;
}

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

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

FHyperTwistMagicCube5DRuntimeState
UHyperTwistMagicCube5DRuntimeLibrary::BuildSolvedState(const int32 Order)
{
	using namespace HyperTwistMagicCube5DRuntimeLibraryInternal;
	const int32 SafeOrder = IsSupportedOrder(Order) ? Order : 3;
	const int32 PieceCount = GetPieceCount(SafeOrder);
	FHyperTwistMagicCube5DRuntimeState State;
	State.StateProfile = FString::Printf(
		TEXT("magiccube5d-order-%d-piece-orientation-v1"),
		SafeOrder);
	State.Definition = MakePuzzleDefinition(SafeOrder);
	State.Order = 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.AppliedMoveCount = 0;
	State.bIsSolved = true;
	return State;
}

FHyperTwistPuzzleState
UHyperTwistMagicCube5DRuntimeLibrary::BuildPuzzleStateEnvelope(
	const FHyperTwistMagicCube5DRuntimeState& RuntimeState
)
{
	const FHyperTwistMagicCube5DRuntimeState SafeState =
		RuntimeState.IsStructurallyValid()
			? RuntimeState
			: BuildSolvedState(3);
	FHyperTwistPuzzleState PuzzleState;
	PuzzleState.Definition = SafeState.Definition;
	PuzzleState.StateEncodingKind = EHyperTwistStateEncodingKind::FamilySpecific;
	PuzzleState.StateEncoding.EncodingProfile = SafeState.StateProfile;
	PuzzleState.StateEncoding.PayloadJson =
		SerializeRuntimeStateToJson(SafeState);
	PuzzleState.OrientationFrame.Reference =
		TEXT("magiccube5d-canonical-signed-axis-frame-v1");
	PuzzleState.bIsSolved = SafeState.bIsSolved;
	PuzzleState.Source = EHyperTwistStateSource::Runtime;
	return PuzzleState;
}

FHyperTwistMagicCube5DTurnResult
UHyperTwistMagicCube5DRuntimeLibrary::ApplyTurn(
	const FHyperTwistMagicCube5DRuntimeState& State,
	const FHyperTwistMagicCube5DTurnRequest& Request
)
{
	using namespace HyperTwistMagicCube5DRuntimeLibraryInternal;
	FHyperTwistMagicCube5DTurnResult Result;
	Result.State = State;
	if (!State.IsStructurallyValid())
	{
		Result.Warnings.Add(TEXT("invalid-magiccube5d-runtime-state"));
		return Result;
	}
	if (!Request.IsValidForOrder(State.Order))
	{
		Result.Warnings.Add(TEXT("invalid-magiccube5d-turn-request"));
		return Result;
	}

	const TSet<int32> SelectedCoordinates =
		GetSelectedSliceCoordinates(Request, State.Order);
	if (SelectedCoordinates.IsEmpty())
	{
		Result.Warnings.Add(TEXT("magiccube5d-turn-selects-no-layers"));
		return Result;
	}

	Result.State.PositionToPiece = State.PositionToPiece;
	Result.State.PieceOrientations = State.PieceOrientations;
	for (int32 PositionIndex = 0;
		 PositionIndex < State.PositionToPiece.Num();
		 ++PositionIndex)
	{
		const FHyperTwistMagicCube5DGridCoordinate Coordinate =
			DecodeCoordinate(PositionIndex, State.Order);
		if (!SelectedCoordinates.Contains(
				GetCoordinateValue(Coordinate, Request.FaceAxis)))
		{
			continue;
		}

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

	Result.State.AppliedMoveCount = State.AppliedMoveCount + 1;
	Result.State.bIsSolved = IsSolvedRuntimeState(Result.State);
	Result.AppliedNotation = BuildNotation(Request);
	Result.bApplied = true;
	Result.bExactStateUpdate = Result.State.IsStructurallyValid();
	if (!Result.bExactStateUpdate)
	{
		Result.Warnings.Add(TEXT("magiccube5d-state-became-invalid"));
	}
	return Result;
}

FHyperTwistMagicCube5DScrambleResult
UHyperTwistMagicCube5DRuntimeLibrary::GenerateScramble(
	const int32 Order,
	const int32 MoveCount,
	const int32 RandomSeed
)
{
	using namespace HyperTwistMagicCube5DRuntimeLibraryInternal;
	FHyperTwistMagicCube5DScrambleResult Result;
	if (!IsSupportedOrder(Order) || MoveCount < 1 || MoveCount > 500)
	{
		Result.Warnings.Add(TEXT("invalid-magiccube5d-scramble-request"));
		return Result;
	}

	FRandomStream Random(RandomSeed);
	Result.State = BuildSolvedState(Order);
	Result.Moves.Reserve(MoveCount);
	Result.AppliedNotation.Reserve(MoveCount);
	FHyperTwistMagicCube5DTurnRequest PreviousRequest;
	bool bHasPreviousRequest = false;
	for (int32 MoveIndex = 0; MoveIndex < MoveCount; ++MoveIndex)
	{
		FHyperTwistMagicCube5DTurnRequest Request;
		bool bDistinct = false;
		for (int32 Attempt = 0; Attempt < 32 && !bDistinct; ++Attempt)
		{
			Request.FaceAxis =
				ToAxisEnum(Random.RandRange(0, AxisCount - 1));
			Request.bPositiveFace = Random.RandRange(0, 1) == 1;
			Request.SliceMask = 1 << Random.RandRange(0, Order - 1);

			TArray<EHyperTwistMagicCube5DAxis> OrthogonalAxes;
			OrthogonalAxes.Reserve(AxisCount - 1);
			for (int32 AxisIndex = 0; AxisIndex < AxisCount; ++AxisIndex)
			{
				const EHyperTwistMagicCube5DAxis Axis = ToAxisEnum(AxisIndex);
				if (Axis != Request.FaceAxis)
				{
					OrthogonalAxes.Add(Axis);
				}
			}
			const int32 AxisAIndex =
				Random.RandRange(0, OrthogonalAxes.Num() - 1);
			int32 AxisBIndex =
				Random.RandRange(0, OrthogonalAxes.Num() - 2);
			if (AxisBIndex >= AxisAIndex)
			{
				++AxisBIndex;
			}
			Request.RotationAxisA = OrthogonalAxes[AxisAIndex];
			Request.RotationAxisB = OrthogonalAxes[AxisBIndex];
			Request.Direction = Random.RandRange(0, 1) == 0
				? EHyperTwistMagicCube5DTurnDirection::PositiveQuarterTurn
				: EHyperTwistMagicCube5DTurnDirection::NegativeQuarterTurn;
			bDistinct = !bHasPreviousRequest
				|| Request.FaceAxis != PreviousRequest.FaceAxis
				|| Request.bPositiveFace != PreviousRequest.bPositiveFace
				|| Request.SliceMask != PreviousRequest.SliceMask
				|| Request.RotationAxisA != PreviousRequest.RotationAxisA
				|| Request.RotationAxisB != PreviousRequest.RotationAxisB;
		}

		const FHyperTwistMagicCube5DTurnResult TurnResult =
			ApplyTurn(Result.State, Request);
		if (!TurnResult.bApplied || !TurnResult.bExactStateUpdate)
		{
			Result.Warnings = TurnResult.Warnings;
			Result.Warnings.Add(TEXT("magiccube5d-scramble-turn-failed"));
			Result.State = FHyperTwistMagicCube5DRuntimeState();
			Result.Moves.Reset();
			Result.AppliedNotation.Reset();
			return Result;
		}

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

	Result.bExactStateUpdate = Result.State.IsStructurallyValid();
	Result.bGenerated = Result.bExactStateUpdate
		&& Result.Moves.Num() == MoveCount
		&& !Result.State.bIsSolved;
	if (!Result.bGenerated)
	{
		Result.Warnings.Add(TEXT("magiccube5d-scramble-result-invalid"));
	}
	return Result;
}

FHyperTwistMagicCube5DProjectionBuildResult
UHyperTwistMagicCube5DRuntimeLibrary::BuildProjection(
	const FHyperTwistMagicCube5DRuntimeState& State
)
{
	using namespace HyperTwistMagicCube5DRuntimeLibraryInternal;
	FHyperTwistMagicCube5DProjectionBuildResult Result;
	Result.Projection.Order = State.Order;
	if (!State.IsStructurallyValid())
	{
		Result.Warnings.Add(TEXT("invalid-magiccube5d-runtime-state"));
		return Result;
	}

	const int32 PieceCount = State.GetPieceCount();
	const int32 Boundary = GetBoundaryCoordinateMagnitude(State.Order);
	Result.Projection.Cubies.Reserve(GetBoundaryCubieCount(State.Order));
	Result.Projection.Facelets.Reserve(GetFaceletCount(State.Order));
	for (int32 PositionIndex = 0; PositionIndex < PieceCount; ++PositionIndex)
	{
		const FHyperTwistMagicCube5DGridCoordinate Position =
			DecodeCoordinate(PositionIndex, State.Order);
		const int32 PieceId = State.PositionToPiece[PositionIndex];
		const FHyperTwistMagicCube5DGridCoordinate PieceHomeCoordinate =
			DecodeCoordinate(PieceId, State.Order);
		const FHyperTwistMagicCube5DPieceOrientation& Orientation =
			State.PieceOrientations[PieceId];

		int32 RepresentativeColorIndex = INDEX_NONE;
		for (int32 AxisIndex = 0; AxisIndex < AxisCount; ++AxisIndex)
		{
			const int32 Coordinate = Position.Components[AxisIndex];
			if (FMath::Abs(Coordinate) != Boundary)
			{
				continue;
			}

			const bool bPositiveWorldSide = Coordinate > 0;
			const int32 ColorIndex = ResolveColorForWorldSide(
				PieceHomeCoordinate,
				Orientation,
				ToAxisEnum(AxisIndex),
				bPositiveWorldSide,
				State.Order);
			if (ColorIndex == INDEX_NONE)
			{
				Result.Warnings.Add(FString::Printf(
					TEXT("missing-facelet-at-position-%d-axis-%d"),
					PositionIndex,
					AxisIndex));
				return Result;
			}

			FHyperTwistMagicCube5DProjectedFacelet Facelet;
			Facelet.PositionIndex = PositionIndex;
			Facelet.PieceId = PieceId;
			Facelet.WorldAxis = ToAxisEnum(AxisIndex);
			Facelet.bPositiveWorldSide = bPositiveWorldSide;
			Facelet.ColorIndex = ColorIndex;
			Result.Projection.Facelets.Add(Facelet);
			if (RepresentativeColorIndex == INDEX_NONE)
			{
				RepresentativeColorIndex = ColorIndex;
			}
		}

		if (RepresentativeColorIndex == INDEX_NONE)
		{
			continue;
		}

		FHyperTwistMagicCube5DProjectedCubie Cubie;
		Cubie.PositionIndex = PositionIndex;
		Cubie.PieceId = PieceId;
		Cubie.Position = Position;
		Cubie.RepresentativeColorIndex = RepresentativeColorIndex;
		Cubie.bPieceInSolvedPosition = PositionIndex == PieceId;
		Result.Projection.Cubies.Add(Cubie);
	}

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

FString UHyperTwistMagicCube5DRuntimeLibrary::SerializeRuntimeStateToJson(
	const FHyperTwistMagicCube5DRuntimeState& RuntimeState
)
{
	FString Json;
	FJsonObjectConverter::UStructToJsonObjectString(
		FHyperTwistMagicCube5DRuntimeState::StaticStruct(),
		&RuntimeState,
		Json,
		0,
		0);
	return Json;
}

bool UHyperTwistMagicCube5DRuntimeLibrary::TryDeserializeRuntimeStateFromJson(
	const FString& Json,
	FHyperTwistMagicCube5DRuntimeState& OutRuntimeState
)
{
	FHyperTwistMagicCube5DRuntimeState ParsedState;
	if (Json.IsEmpty()
		|| !FJsonObjectConverter::JsonObjectStringToUStruct(
			Json,
			&ParsedState,
			0,
			0)
		|| !ParsedState.IsStructurallyValid())
	{
		return false;
	}
	OutRuntimeState = MoveTemp(ParsedState);
	return true;
}

FString UHyperTwistMagicCube5DRuntimeLibrary::GetAxisLabel(
	const EHyperTwistMagicCube5DAxis Axis
)
{
	return HyperTwistMagicCube5DRuntimeLibraryInternal::GetAxisLabel(Axis);
}
