#include "HyperTwistSimulation/HyperTwistMelindaProjectionLibrary.h"

#include "JsonObjectConverter.h"

namespace HyperTwistMelindaProjectionLibraryInternal
{
	struct FCellDescriptor
	{
		EHyperTwistMelindaCell Cell = EHyperTwistMelindaCell::Outer;
		EHyperTwistMelindaCellAxis FixedAxis = EHyperTwistMelindaCellAxis::W;
		bool bPositiveSide = true;
		const TCHAR* Label = TEXT("O");
	};

	template <typename TStruct>
	bool DeserializePayload(const FHyperTwistSerializedPayload& Payload, TStruct& OutValue)
	{
		return !Payload.PayloadJson.IsEmpty()
			&& FJsonObjectConverter::JsonObjectStringToUStruct(Payload.PayloadJson, &OutValue, 0, 0);
	}

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

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

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

	FCellDescriptor GetCellDescriptor(const EHyperTwistMelindaCell Cell)
	{
		switch (Cell)
		{
		case EHyperTwistMelindaCell::Left:
			return {Cell, EHyperTwistMelindaCellAxis::X, false, TEXT("L")};
		case EHyperTwistMelindaCell::Right:
			return {Cell, EHyperTwistMelindaCellAxis::X, true, TEXT("R")};
		case EHyperTwistMelindaCell::Back:
			return {Cell, EHyperTwistMelindaCellAxis::Y, false, TEXT("B")};
		case EHyperTwistMelindaCell::Front:
			return {Cell, EHyperTwistMelindaCellAxis::Y, true, TEXT("F")};
		case EHyperTwistMelindaCell::Down:
			return {Cell, EHyperTwistMelindaCellAxis::Z, false, TEXT("D")};
		case EHyperTwistMelindaCell::Up:
			return {Cell, EHyperTwistMelindaCellAxis::Z, true, TEXT("U")};
		case EHyperTwistMelindaCell::Inner:
			return {Cell, EHyperTwistMelindaCellAxis::W, false, TEXT("I")};
		default:
			return {Cell, EHyperTwistMelindaCellAxis::W, true, TEXT("O")};
		}
	}

	int32 GetSignatureBit(const int32 SignatureId, const int32 AxisIndex)
	{
		return AxisIndex >= 0 && AxisIndex < 4 ? ((SignatureId >> AxisIndex) & 1) : 0;
	}

	TArray<int32> GetRemainingAxes(const int32 FixedAxisIndex)
	{
		TArray<int32> RemainingAxes;
		RemainingAxes.Reserve(3);
		for (int32 AxisIndex = 0; AxisIndex < 4; ++AxisIndex)
		{
			if (AxisIndex != FixedAxisIndex)
			{
				RemainingAxes.Add(AxisIndex);
			}
		}

		return RemainingAxes;
	}

	TArray<int32> BuildIdentityPullMap()
	{
		TArray<int32> PullMap;
		PullMap.Reserve(16);
		for (int32 PositionIndex = 0; PositionIndex < 16; ++PositionIndex)
		{
			PullMap.Add(PositionIndex);
		}

		return PullMap;
	}

	TArray<int32> BuildIdentityOrientationDeltas()
	{
		TArray<int32> Deltas;
		Deltas.Init(0, 16);
		return Deltas;
	}

	FHyperTwistPuzzleState BuildSolvedMelindaState(const FHyperTwistPuzzleDefinitionRef& Definition)
	{
		FHyperTwistMelinda2x2x2x2StateEncoding StateEncoding;
		StateEncoding.PositionToPiece.Reserve(16);
		for (int32 PieceId = 0; PieceId < 16; ++PieceId)
		{
			StateEncoding.PositionToPiece.Add(PieceId);
		}

		StateEncoding.PieceOrientation.Init(0, 16);

		FHyperTwistPuzzleState PuzzleState;
		PuzzleState.Definition = Definition;
		PuzzleState.StateEncodingKind = EHyperTwistStateEncodingKind::FamilySpecific;
		PuzzleState.StateEncoding.EncodingProfile = StateEncoding.EncodingProfile;
		PuzzleState.StateEncoding.PayloadJson = SerializeStructToJson(StateEncoding);
		PuzzleState.OrientationFrame.Reference = StateEncoding.FrameProfile;
		PuzzleState.bIsSolved = true;
		PuzzleState.Source = EHyperTwistStateSource::Runtime;
		return PuzzleState;
	}

	bool IsSupportedDefinition(const FHyperTwistPuzzleDefinitionRef& Definition)
	{
		return Definition.IsStructurallyValid()
			&& Definition.PuzzleId == TEXT("hypercube/2x2x2x2")
			&& Definition.PuzzleFamily == EHyperTwistPuzzleFamily::Hypercube
			&& Definition.Dimension == 4
			&& Definition.SizeVector == TArray<int32>({2, 2, 2, 2});
	}

	bool TryDeserializeMelindaState(
		const FHyperTwistPuzzleState& State,
		FHyperTwistMelinda2x2x2x2StateEncoding& OutState,
		TArray<FString>& OutWarnings
	)
	{
		if (State.StateEncoding.EncodingProfile != TEXT("melinda-2x2x2x2-state-v1")
			|| !IsSupportedDefinition(State.Definition))
		{
			OutWarnings.Add(TEXT("unsupported-melinda-state"));
			return false;
		}

		if (!DeserializePayload(State.StateEncoding, OutState) || !OutState.IsStructurallyValid())
		{
			OutWarnings.Add(TEXT("invalid-melinda-state-payload"));
			return false;
		}

		return true;
	}

	const TArray<TArray<int32>>& GetOrientationPermutations()
	{
		static TArray<TArray<int32>> Permutations;
		if (Permutations.Num() == 0)
		{
			for (int32 A = 0; A < 4; ++A)
			{
				for (int32 B = 0; B < 4; ++B)
				{
					if (B == A)
					{
						continue;
					}

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

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

							Permutations.Add({A, B, C, D});
						}
					}
				}
			}
		}

		return Permutations;
	}

	EHyperTwistMelindaStickerColor GetStickerColor(const int32 PieceId, const int32 LocalAxisIndex)
	{
		if (LocalAxisIndex < 0 || LocalAxisIndex >= 4)
		{
			return EHyperTwistMelindaStickerColor::Unknown;
		}

		const bool bPositiveColor = GetSignatureBit(PieceId, LocalAxisIndex) != 0;
		switch (LocalAxisIndex)
		{
		case 0:
			return bPositiveColor
				? EHyperTwistMelindaStickerColor::XPositive
				: EHyperTwistMelindaStickerColor::XNegative;
		case 1:
			return bPositiveColor
				? EHyperTwistMelindaStickerColor::YPositive
				: EHyperTwistMelindaStickerColor::YNegative;
		case 2:
			return bPositiveColor
				? EHyperTwistMelindaStickerColor::ZPositive
				: EHyperTwistMelindaStickerColor::ZNegative;
		default:
			return bPositiveColor
				? EHyperTwistMelindaStickerColor::WPositive
				: EHyperTwistMelindaStickerColor::WNegative;
		}
	}

	FString GetCellLetterForAxisAndSign(const int32 AxisIndex, const bool bPositiveSide)
	{
		switch (AxisIndex)
		{
		case 0:
			return bPositiveSide ? TEXT("R") : TEXT("L");
		case 1:
			return bPositiveSide ? TEXT("F") : TEXT("B");
		case 2:
			return bPositiveSide ? TEXT("U") : TEXT("D");
		default:
			return bPositiveSide ? TEXT("O") : TEXT("I");
		}
	}

	FString BuildTouchLabel(const int32 SignatureId, const int32 ExcludedAxisIndex)
	{
		static const int32 AxisPriority[] = {3, 2, 1, 0};

		FString Label;
		for (const int32 AxisIndex : AxisPriority)
		{
			if (AxisIndex == ExcludedAxisIndex)
			{
				continue;
			}

			Label += GetCellLetterForAxisAndSign(
				AxisIndex,
				GetSignatureBit(SignatureId, AxisIndex) != 0
			);
		}

		return Label;
	}

	FVector GetProjectionBasis(const int32 AxisIndex)
	{
		switch (AxisIndex)
		{
		case 0:
			return FVector(1.0f, 0.0f, 0.0f);
		case 1:
			return FVector(0.0f, 1.0f, 0.0f);
		case 2:
			return FVector(0.0f, 0.0f, 1.0f);
		default:
			return FVector(0.7f, -0.55f, 0.8f).GetSafeNormal();
		}
	}

	int32 GetOrientationDeltaIndexForRotationAxis(const int32 RotationAxisIndex)
	{
		// These two odd, twist-preserving orientation deltas keep the move family
		// closed under repeated application while still distinguishing the two
		// principal axis pairings in the owned Phase 6A interaction surface.
		return (RotationAxisIndex == 0 || RotationAxisIndex == 1) ? 6 : 1;
	}

	void DecodePositionSigns(const int32 PositionIndex, int32 OutSigns[4])
	{
		for (int32 AxisIndex = 0; AxisIndex < 4; ++AxisIndex)
		{
			OutSigns[AxisIndex] = GetSignatureBit(PositionIndex, AxisIndex) != 0 ? 1 : -1;
		}
	}

	int32 EncodePositionSigns(const int32 Signs[4])
	{
		int32 PositionIndex = 0;
		for (int32 AxisIndex = 0; AxisIndex < 4; ++AxisIndex)
		{
			if (Signs[AxisIndex] > 0)
			{
				PositionIndex |= (1 << AxisIndex);
			}
		}

		return PositionIndex;
	}
}

bool FHyperTwistMelindaProjectedCubie::IsStructurallyValid() const
{
	if (PositionIndex < 0
		|| PositionIndex >= 16
		|| PieceId < 0
		|| PieceId >= 16
		|| LocalGridCoordinate.X < -1
		|| LocalGridCoordinate.X > 1
		|| LocalGridCoordinate.X == 0
		|| LocalGridCoordinate.Y < -1
		|| LocalGridCoordinate.Y > 1
		|| LocalGridCoordinate.Y == 0
		|| LocalGridCoordinate.Z < -1
		|| LocalGridCoordinate.Z > 1
		|| LocalGridCoordinate.Z == 0
		|| SlotLabel.IsEmpty()
		|| PieceLabel.IsEmpty()
		|| bPieceInSolvedPosition != (PositionIndex == PieceId)
		|| Faces.Num() != 3)
	{
		return false;
	}

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

		bSeenAxes[AxisIndex] = true;
	}

	return true;
}

bool FHyperTwistMelindaProjectedCell::IsStructurallyValid() const
{
	if (CellLabel.IsEmpty()
		|| DisplayCenter.ContainsNaN()
		|| AvailableAxes.Num() != 3
		|| Cubies.Num() != 8)
	{
		return false;
	}

	bool bSeenAvailableAxes[4] = {false, false, false, false};
	for (const EHyperTwistMelindaCellAxis Axis : AvailableAxes)
	{
		const int32 AxisIndex = static_cast<int32>(Axis);
		if (AxisIndex < 0
			|| AxisIndex >= 4
			|| Axis == FixedAxis
			|| bSeenAvailableAxes[AxisIndex])
		{
			return false;
		}

		bSeenAvailableAxes[AxisIndex] = true;
	}

	bool bSeenPositions[16] = {
		false, false, false, false,
		false, false, false, false,
		false, false, false, false,
		false, false, false, false
	};
	for (const FHyperTwistMelindaProjectedCubie& Cubie : Cubies)
	{
		if (!Cubie.IsStructurallyValid()
			|| bSeenPositions[Cubie.PositionIndex])
		{
			return false;
		}

		bSeenPositions[Cubie.PositionIndex] = true;
		for (const FHyperTwistMelindaProjectedCubieFace& Face : Cubie.Faces)
		{
			if (!AvailableAxes.Contains(Face.Axis))
			{
				return false;
			}
		}
	}

	return true;
}

bool FHyperTwistMelindaCellFirstProjection::IsStructurallyValid() const
{
	if (ProjectionProfile.IsEmpty()
		|| !Definition.IsStructurallyValid()
		|| Cells.Num() != 8)
	{
		return false;
	}

	bool bSeenCells[8] = {
		false, false, false, false,
		false, false, false, false
	};
	for (const FHyperTwistMelindaProjectedCell& Cell : Cells)
	{
		const int32 CellIndex = static_cast<int32>(Cell.Cell);
		if (!Cell.IsStructurallyValid()
			|| CellIndex < 0
			|| CellIndex >= 8
			|| bSeenCells[CellIndex])
		{
			return false;
		}

		bSeenCells[CellIndex] = true;
	}

	return true;
}


FString UHyperTwistMelindaProjectionLibrary::GetCellLabel(const EHyperTwistMelindaCell Cell)
{
	return HyperTwistMelindaProjectionLibraryInternal::GetCellDescriptor(Cell).Label;
}

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

bool UHyperTwistMelindaProjectionLibrary::IsAxisAvailableForCell(
	const EHyperTwistMelindaCell Cell,
	const EHyperTwistMelindaCellAxis Axis
)
{
	return HyperTwistMelindaProjectionLibraryInternal::GetCellDescriptor(Cell).FixedAxis != Axis;
}

TArray<EHyperTwistMelindaCellAxis> UHyperTwistMelindaProjectionLibrary::GetAvailableAxesForCell(
	const EHyperTwistMelindaCell Cell
)
{
	const int32 FixedAxisIndex = HyperTwistMelindaProjectionLibraryInternal::ToAxisIndex(
		HyperTwistMelindaProjectionLibraryInternal::GetCellDescriptor(Cell).FixedAxis
	);

	TArray<EHyperTwistMelindaCellAxis> Result;
	Result.Reserve(3);
	for (const int32 AxisIndex : HyperTwistMelindaProjectionLibraryInternal::GetRemainingAxes(FixedAxisIndex))
	{
		Result.Add(HyperTwistMelindaProjectionLibraryInternal::ToAxisEnum(AxisIndex));
	}

	return Result;
}

FHyperTwistMelindaCellTurnTransformBuildResult UHyperTwistMelindaProjectionLibrary::BuildCellTurnTransformation(
	const FHyperTwistPuzzleDefinitionRef& Definition,
	const FHyperTwistMelindaCellTurnRequest& Request
)
{
	FHyperTwistMelindaCellTurnTransformBuildResult Result;
	if (!HyperTwistMelindaProjectionLibraryInternal::IsSupportedDefinition(Definition))
	{
		Result.Warnings.Add(TEXT("unsupported-melinda-definition"));
		return Result;
	}

	const HyperTwistMelindaProjectionLibraryInternal::FCellDescriptor CellDescriptor =
		HyperTwistMelindaProjectionLibraryInternal::GetCellDescriptor(Request.Cell);
	const int32 FixedAxisIndex =
		HyperTwistMelindaProjectionLibraryInternal::ToAxisIndex(CellDescriptor.FixedAxis);
	const int32 RotationAxisIndex =
		HyperTwistMelindaProjectionLibraryInternal::ToAxisIndex(Request.RotationAxis);
	if (FixedAxisIndex == RotationAxisIndex)
	{
		Result.Warnings.Add(TEXT("rotation-axis-matches-fixed-cell-axis"));
		return Result;
	}

	const TArray<int32> RemainingAxes =
		HyperTwistMelindaProjectionLibraryInternal::GetRemainingAxes(FixedAxisIndex);
	int32 PlaneAxisA = INDEX_NONE;
	int32 PlaneAxisB = INDEX_NONE;
	for (const int32 AxisIndex : RemainingAxes)
	{
		if (AxisIndex == RotationAxisIndex)
		{
			continue;
		}

		if (PlaneAxisA == INDEX_NONE)
		{
			PlaneAxisA = AxisIndex;
		}
		else
		{
			PlaneAxisB = AxisIndex;
		}
	}

	if (PlaneAxisA == INDEX_NONE || PlaneAxisB == INDEX_NONE)
	{
		Result.Warnings.Add(TEXT("unable-to-resolve-turn-plane"));
		return Result;
	}

	FHyperTwistMelinda2x2x2x2TransformEncoding TransformEncoding;
	TransformEncoding.MoveId = FString::Printf(
		TEXT("melinda-cell-turn.%s.%s.%s"),
		*GetCellLabel(Request.Cell).ToLower(),
		*GetAxisLabel(Request.RotationAxis).ToLower(),
		Request.Direction == EHyperTwistMelindaTurnDirection::Clockwise ? TEXT("cw") : TEXT("ccw")
	);
	TransformEncoding.PositionPullMap =
		HyperTwistMelindaProjectionLibraryInternal::BuildIdentityPullMap();
	TransformEncoding.OrientationDeltaPerNewPosition =
		HyperTwistMelindaProjectionLibraryInternal::BuildIdentityOrientationDeltas();

	const int32 OrientationDeltaIndex =
		HyperTwistMelindaProjectionLibraryInternal::GetOrientationDeltaIndexForRotationAxis(
			RotationAxisIndex
		);

	const int32 FixedAxisSign = CellDescriptor.bPositiveSide ? 1 : -1;
	for (int32 NewPosition = 0; NewPosition < 16; ++NewPosition)
	{
		int32 NewSigns[4];
		HyperTwistMelindaProjectionLibraryInternal::DecodePositionSigns(NewPosition, NewSigns);
		if (NewSigns[FixedAxisIndex] != FixedAxisSign)
		{
			continue;
		}

		int32 OldSigns[4] = {NewSigns[0], NewSigns[1], NewSigns[2], NewSigns[3]};
		if (Request.Direction == EHyperTwistMelindaTurnDirection::Clockwise)
		{
			OldSigns[PlaneAxisA] = -NewSigns[PlaneAxisB];
			OldSigns[PlaneAxisB] = NewSigns[PlaneAxisA];
		}
		else
		{
			OldSigns[PlaneAxisA] = NewSigns[PlaneAxisB];
			OldSigns[PlaneAxisB] = -NewSigns[PlaneAxisA];
		}

		TransformEncoding.PositionPullMap[NewPosition] =
			HyperTwistMelindaProjectionLibraryInternal::EncodePositionSigns(OldSigns);
		TransformEncoding.OrientationDeltaPerNewPosition[NewPosition] =
			OrientationDeltaIndex;
	}

	Result.Transformation.Definition = Definition;
	Result.Transformation.TransformKind = EHyperTwistTransformKind::SingleMove;
	Result.Transformation.Notation = FString::Printf(
		TEXT("%s%s%s"),
		*GetCellLabel(Request.Cell),
		*GetAxisLabel(Request.RotationAxis),
		Request.Direction == EHyperTwistMelindaTurnDirection::Clockwise ? TEXT("") : TEXT("'")
	);
	Result.Transformation.TransformEncoding.EncodingProfile = TransformEncoding.EncodingProfile;
	Result.Transformation.TransformEncoding.PayloadJson =
		HyperTwistMelindaProjectionLibraryInternal::SerializeStructToJson(TransformEncoding);
	Result.Transformation.bInvertible = true;
	Result.Transformation.OriginalNotation = Result.Transformation.Notation;

	if (!Result.Transformation.IsStructurallyValid())
	{
		Result.Warnings.Add(TEXT("invalid-melinda-cell-turn-transform"));
		return Result;
	}

	const FHyperTwistPuzzleState SolvedState =
		HyperTwistMelindaProjectionLibraryInternal::BuildSolvedMelindaState(Definition);
	const FHyperTwistApplyTransformationResult ApplyResult =
		UHyperTwistCoreLibrary::ApplyTransformation(SolvedState, Result.Transformation);
	Result.Warnings.Append(ApplyResult.Warnings);
	Result.bBuilt = ApplyResult.bApplied && ApplyResult.bExactStateUpdate;
	if (!Result.bBuilt)
	{
		Result.Warnings.Add(TEXT("melinda-cell-turn-application-failed"));
		return Result;
	}

	const FHyperTwistPuzzleStateValidationResult Validation =
		UHyperTwistCoreLibrary::ValidatePuzzleState(ApplyResult.State);
	Result.bExactTransform = Validation.bIsSolvable;
	if (!Result.bExactTransform)
	{
		Result.Warnings.Add(TEXT("melinda-cell-turn-result-not-solvable"));
	}

	return Result;
}

FHyperTwistMelindaCellFirstProjectionBuildResult UHyperTwistMelindaProjectionLibrary::BuildCellFirstProjection(
	const FHyperTwistPuzzleState& State,
	const float CellCenterSpacing
)
{
	FHyperTwistMelindaCellFirstProjectionBuildResult Result;
	Result.SourceStateValidation = UHyperTwistCoreLibrary::ValidatePuzzleState(State);

	FHyperTwistMelinda2x2x2x2StateEncoding MelindaState;
	if (!HyperTwistMelindaProjectionLibraryInternal::TryDeserializeMelindaState(
		State,
		MelindaState,
		Result.Warnings
	))
	{
		return Result;
	}

	if (!Result.SourceStateValidation.bStateSupported || !Result.SourceStateValidation.bStructureValid)
	{
		Result.Warnings.Add(TEXT("melinda-state-not-projectable"));
		return Result;
	}

	const TArray<TArray<int32>>& OrientationPermutations =
		HyperTwistMelindaProjectionLibraryInternal::GetOrientationPermutations();

	Result.Projection.Definition = State.Definition;
	Result.Projection.Cells.Reserve(8);

	static const EHyperTwistMelindaCell CellsInProjectionOrder[] = {
		EHyperTwistMelindaCell::Left,
		EHyperTwistMelindaCell::Right,
		EHyperTwistMelindaCell::Back,
		EHyperTwistMelindaCell::Front,
		EHyperTwistMelindaCell::Down,
		EHyperTwistMelindaCell::Up,
		EHyperTwistMelindaCell::Inner,
		EHyperTwistMelindaCell::Outer
	};

	for (const EHyperTwistMelindaCell Cell : CellsInProjectionOrder)
	{
		const HyperTwistMelindaProjectionLibraryInternal::FCellDescriptor CellDescriptor =
			HyperTwistMelindaProjectionLibraryInternal::GetCellDescriptor(Cell);
		const int32 FixedAxisIndex =
			HyperTwistMelindaProjectionLibraryInternal::ToAxisIndex(CellDescriptor.FixedAxis);
		const TArray<int32> RemainingAxes =
			HyperTwistMelindaProjectionLibraryInternal::GetRemainingAxes(FixedAxisIndex);

		FHyperTwistMelindaProjectedCell ProjectedCell;
		ProjectedCell.Cell = Cell;
		ProjectedCell.FixedAxis = CellDescriptor.FixedAxis;
		ProjectedCell.bPositiveSide = CellDescriptor.bPositiveSide;
		ProjectedCell.CellLabel = CellDescriptor.Label;
		ProjectedCell.DisplayCenter =
			HyperTwistMelindaProjectionLibraryInternal::GetProjectionBasis(FixedAxisIndex)
			* (CellDescriptor.bPositiveSide ? CellCenterSpacing : -CellCenterSpacing);
		ProjectedCell.Cubies.Reserve(8);
		for (const int32 AxisIndex : RemainingAxes)
		{
			ProjectedCell.AvailableAxes.Add(
				HyperTwistMelindaProjectionLibraryInternal::ToAxisEnum(AxisIndex)
			);
		}

		for (int32 PositionIndex = 0; PositionIndex < MelindaState.PositionToPiece.Num(); ++PositionIndex)
		{
			if (HyperTwistMelindaProjectionLibraryInternal::GetSignatureBit(PositionIndex, FixedAxisIndex)
				!= (CellDescriptor.bPositiveSide ? 1 : 0))
			{
				continue;
			}

			const int32 PieceId = MelindaState.PositionToPiece[PositionIndex];
			if (!MelindaState.PieceOrientation.IsValidIndex(PieceId)
				|| !OrientationPermutations.IsValidIndex(MelindaState.PieceOrientation[PieceId]))
			{
				Result.Warnings.Add(TEXT("invalid-melinda-piece-orientation"));
				return Result;
			}

			const TArray<int32>& OrientationPermutation =
				OrientationPermutations[MelindaState.PieceOrientation[PieceId]];

			FHyperTwistMelindaProjectedCubie ProjectedCubie;
			ProjectedCubie.PositionIndex = PositionIndex;
			ProjectedCubie.PieceId = PieceId;
			ProjectedCubie.LocalGridCoordinate = FIntVector(
				HyperTwistMelindaProjectionLibraryInternal::GetSignatureBit(
					PositionIndex,
					RemainingAxes[0]
				) != 0 ? 1 : -1,
				HyperTwistMelindaProjectionLibraryInternal::GetSignatureBit(
					PositionIndex,
					RemainingAxes[1]
				) != 0 ? 1 : -1,
				HyperTwistMelindaProjectionLibraryInternal::GetSignatureBit(
					PositionIndex,
					RemainingAxes[2]
				) != 0 ? 1 : -1
			);
			ProjectedCubie.SlotLabel =
				HyperTwistMelindaProjectionLibraryInternal::BuildTouchLabel(
					PositionIndex,
					FixedAxisIndex
				);
			ProjectedCubie.PieceLabel =
				HyperTwistMelindaProjectionLibraryInternal::BuildTouchLabel(
					PieceId,
					FixedAxisIndex
				);
			ProjectedCubie.bPieceInSolvedPosition = PositionIndex == PieceId;
			ProjectedCubie.Faces.Reserve(3);

			for (const int32 AxisIndex : RemainingAxes)
			{
				FHyperTwistMelindaProjectedCubieFace Face;
				Face.Axis = HyperTwistMelindaProjectionLibraryInternal::ToAxisEnum(AxisIndex);
				Face.bPositiveSide =
					HyperTwistMelindaProjectionLibraryInternal::GetSignatureBit(
						PositionIndex,
						AxisIndex
					) != 0;
				Face.Color =
					HyperTwistMelindaProjectionLibraryInternal::GetStickerColor(
						PieceId,
						OrientationPermutation[AxisIndex]
					);
				ProjectedCubie.Faces.Add(Face);
			}

			ProjectedCell.Cubies.Add(ProjectedCubie);
		}

		if (!ProjectedCell.IsStructurallyValid())
		{
			Result.Warnings.Add(TEXT("invalid-melinda-projected-cell"));
			return Result;
		}

		Result.Projection.Cells.Add(ProjectedCell);
	}

	if (!Result.Projection.IsStructurallyValid())
	{
		Result.Warnings.Add(TEXT("invalid-melinda-cell-first-projection"));
		return Result;
	}

	Result.bProjected = true;
	Result.bExactProjection = true;
	return Result;
}
