#include "HyperTwistCore/HyperTwistCoreLibrary.h"

#include "HyperTwistAlgorithm/HyperTwistAlgorithmLibrary.h"
#include "JsonObjectConverter.h"

namespace HyperTwistCoreLibraryInternal
{
	struct FMelindaOrientationLookup
	{
		TArray<int32> ParityByOrientationIndex;
		TArray<int32> TwistByOrientationIndex;
		TArray<int32> OrientationIndicesByParityAndTwist[2][3];
	};

	template <typename TStruct>
	bool DeserializePayload(const FHyperTwistSerializedPayload& Payload, TStruct& OutValue)
	{
		if (Payload.PayloadJson.IsEmpty())
		{
			return false;
		}

		return FJsonObjectConverter::JsonObjectStringToUStruct(Payload.PayloadJson, &OutValue, 0, 0);
	}

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

	bool IsIdentityPermutation(const TArray<int32>& Values)
	{
		for (int32 Index = 0; Index < Values.Num(); ++Index)
		{
			if (Values[Index] != Index)
			{
				return false;
			}
		}

		return true;
	}

	bool IsZeroOrientation(const TArray<int32>& Values)
	{
		for (const int32 Value : Values)
		{
			if (Value != 0)
			{
				return false;
			}
		}

		return true;
	}

	bool IsUniformFace(const TArray<FString>& Facelets)
	{
		if (Facelets.Num() == 0)
		{
			return false;
		}

		const FString& Reference = Facelets[0];
		for (const FString& Facelet : Facelets)
		{
			if (Facelet != Reference)
			{
				return false;
			}
		}

		return true;
	}

	bool IsCanonicalLabelSequence(const TArray<FString>& Labels, const FString& Prefix)
	{
		for (int32 Index = 0; Index < Labels.Num(); ++Index)
		{
			if (Labels[Index] != FString::Printf(TEXT("%s%d"), *Prefix, Index))
			{
				return false;
			}
		}

		return Labels.Num() > 0;
	}

	const TArray<TArray<int32>>& GetMelindaOrientationPermutations()
	{
		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;
	}

	bool IsValidMelindaOrientationIndex(const int32 OrientationIndex)
	{
		return OrientationIndex >= 0 && OrientationIndex < GetMelindaOrientationPermutations().Num();
	}

	int32 FindMelindaOrientationIndex(const TArray<int32>& Permutation)
	{
		const TArray<TArray<int32>>& KnownPermutations = GetMelindaOrientationPermutations();
		for (int32 Index = 0; Index < KnownPermutations.Num(); ++Index)
		{
			if (KnownPermutations[Index] == Permutation)
			{
				return Index;
			}
		}

		return INDEX_NONE;
	}

	int32 ComposeMelindaOrientationIndices(const int32 DeltaIndex, const int32 CurrentIndex)
	{
		if (!IsValidMelindaOrientationIndex(DeltaIndex) || !IsValidMelindaOrientationIndex(CurrentIndex))
		{
			return INDEX_NONE;
		}

		const TArray<TArray<int32>>& KnownPermutations = GetMelindaOrientationPermutations();
		const TArray<int32>& DeltaPermutation = KnownPermutations[DeltaIndex];
		const TArray<int32>& CurrentPermutation = KnownPermutations[CurrentIndex];

		TArray<int32> ComposedPermutation;
		ComposedPermutation.SetNum(4);

		for (int32 AxisIndex = 0; AxisIndex < 4; ++AxisIndex)
		{
			ComposedPermutation[AxisIndex] = CurrentPermutation[DeltaPermutation[AxisIndex]];
		}

		return FindMelindaOrientationIndex(ComposedPermutation);
	}

	int32 InvertMelindaOrientationIndex(const int32 OrientationIndex)
	{
		if (!IsValidMelindaOrientationIndex(OrientationIndex))
		{
			return INDEX_NONE;
		}

		const TArray<TArray<int32>>& KnownPermutations = GetMelindaOrientationPermutations();
		const TArray<int32>& OrientationPermutation = KnownPermutations[OrientationIndex];

		TArray<int32> InversePermutation;
		InversePermutation.SetNum(4);

		for (int32 AxisIndex = 0; AxisIndex < 4; ++AxisIndex)
		{
			InversePermutation[OrientationPermutation[AxisIndex]] = AxisIndex;
		}

		return FindMelindaOrientationIndex(InversePermutation);
	}

	int32 CountPermutationInversions(const TArray<int32>& Values)
	{
		int32 Inversions = 0;

		for (int32 LeftIndex = 0; LeftIndex < Values.Num(); ++LeftIndex)
		{
			for (int32 RightIndex = LeftIndex + 1; RightIndex < Values.Num(); ++RightIndex)
			{
				if (Values[LeftIndex] > Values[RightIndex])
				{
					++Inversions;
				}
			}
		}

		return Inversions;
	}

	bool IsEvenPermutation(const TArray<int32>& Values)
	{
		return (CountPermutationInversions(Values) % 2) == 0;
	}

	bool HasExpectedMelindaSizeVector(const TArray<int32>& SizeVector)
	{
		return SizeVector.Num() == 0
			|| (SizeVector.Num() == 4
				&& SizeVector[0] == 2
				&& SizeVector[1] == 2
				&& SizeVector[2] == 2
				&& SizeVector[3] == 2);
	}

	bool IsSupportedMelindaDefinition(const FHyperTwistPuzzleDefinitionRef& Definition)
	{
		return Definition.IsStructurallyValid()
			&& Definition.PuzzleId == TEXT("hypercube/2x2x2x2")
			&& Definition.PuzzleFamily == EHyperTwistPuzzleFamily::Hypercube
			&& Definition.Dimension == 4
			&& HasExpectedMelindaSizeVector(Definition.SizeVector);
	}

	bool DoMelindaDefinitionsMatch(
		const FHyperTwistPuzzleDefinitionRef& LeftDefinition,
		const FHyperTwistPuzzleDefinitionRef& RightDefinition
	)
	{
		return LeftDefinition.PuzzleId == RightDefinition.PuzzleId
			&& LeftDefinition.PuzzleFamily == RightDefinition.PuzzleFamily
			&& LeftDefinition.Dimension == RightDefinition.Dimension
			&& LeftDefinition.DefinitionVersion == RightDefinition.DefinitionVersion
			&& LeftDefinition.NotationProfile == RightDefinition.NotationProfile
			&& LeftDefinition.SizeVector == RightDefinition.SizeVector
			&& LeftDefinition.Variant == RightDefinition.Variant;
	}

	bool IsSupportedMelindaState(const FHyperTwistPuzzleState& State)
	{
		return State.IsStructurallyValid()
			&& State.StateEncoding.EncodingProfile == TEXT("melinda-2x2x2x2-state-v1")
			&& IsSupportedMelindaDefinition(State.Definition);
	}

	bool IsSupportedMelindaTransformation(const FHyperTwistTransformation& Transformation)
	{
		return Transformation.IsStructurallyValid()
			&& Transformation.TransformEncoding.EncodingProfile == TEXT("melinda-2x2x2x2-transform-v1")
			&& IsSupportedMelindaDefinition(Transformation.Definition);
	}

	int32 GetMelindaHandednessBit(const int32 SignatureId)
	{
		int32 WorkingValue = SignatureId & 0xF;
		int32 BitParity = 0;

		while (WorkingValue != 0)
		{
			BitParity ^= (WorkingValue & 1);
			WorkingValue >>= 1;
		}

		return BitParity;
	}

	int32 NormalizeMelindaTwistBalance(const int32 RawTwistSum)
	{
		int32 ModuloBalance = RawTwistSum % 3;
		if (ModuloBalance < 0)
		{
			ModuloBalance += 3;
		}

		switch (ModuloBalance)
		{
		case 0:
			return 0;
		case 1:
			return 1;
		default:
			return -1;
		}
	}

	int32 MakeMelindaAxisPairKey(int32 AxisA, int32 AxisB)
	{
		if (AxisA > AxisB)
		{
			Swap(AxisA, AxisB);
		}

		return (AxisA * 4) + AxisB;
	}

	int32 DetermineMelindaTwistValue(const TArray<int32>& Permutation)
	{
		// Twist class is defined by which solved axis-pairing the orientation induces:
		// (01)(23), (02)(13), or (03)(12).
		int32 PairKeyA = MakeMelindaAxisPairKey(Permutation[0], Permutation[1]);
		int32 PairKeyB = MakeMelindaAxisPairKey(Permutation[2], Permutation[3]);

		if (PairKeyA > PairKeyB)
		{
			Swap(PairKeyA, PairKeyB);
		}

		if (PairKeyA == MakeMelindaAxisPairKey(0, 1) && PairKeyB == MakeMelindaAxisPairKey(2, 3))
		{
			return 0;
		}

		if (PairKeyA == MakeMelindaAxisPairKey(0, 2) && PairKeyB == MakeMelindaAxisPairKey(1, 3))
		{
			return 1;
		}

		return -1;
	}

	const FMelindaOrientationLookup& GetMelindaOrientationLookup()
	{
		static FMelindaOrientationLookup Lookup;

		if (Lookup.ParityByOrientationIndex.Num() == 0)
		{
			const TArray<TArray<int32>>& KnownPermutations = GetMelindaOrientationPermutations();
			Lookup.ParityByOrientationIndex.SetNum(KnownPermutations.Num());
			Lookup.TwistByOrientationIndex.SetNum(KnownPermutations.Num());

			TArray<int32> OrientationIndicesByParity[2];

			for (int32 OrientationIndex = 0; OrientationIndex < KnownPermutations.Num(); ++OrientationIndex)
			{
				const int32 OrientationParity = CountPermutationInversions(KnownPermutations[OrientationIndex]) % 2;
				const int32 TwistValue = DetermineMelindaTwistValue(KnownPermutations[OrientationIndex]);
				Lookup.ParityByOrientationIndex[OrientationIndex] = OrientationParity;
				Lookup.TwistByOrientationIndex[OrientationIndex] = TwistValue;
				Lookup.OrientationIndicesByParityAndTwist[OrientationParity][TwistValue + 1].Add(OrientationIndex);
			}
		}

		return Lookup;
	}

	bool IsSolvedMelindaState(const FHyperTwistMelinda2x2x2x2StateEncoding& MelindaState)
	{
		return MelindaState.IsStructurallyValid()
			&& IsIdentityPermutation(MelindaState.PositionToPiece)
			&& IsZeroOrientation(MelindaState.PieceOrientation);
	}

	FHyperTwistMelinda2x2x2x2StateEncoding MakeSolvedMelindaStateEncoding()
	{
		FHyperTwistMelinda2x2x2x2StateEncoding MelindaState;
		MelindaState.PositionToPiece.Reserve(16);
		for (int32 PieceId = 0; PieceId < 16; ++PieceId)
		{
			MelindaState.PositionToPiece.Add(PieceId);
		}

		MelindaState.PieceOrientation.Init(0, 16);
		return MelindaState;
	}

	FHyperTwistPuzzleState MakeMelindaPuzzleState(
		const FHyperTwistPuzzleDefinitionRef& Definition,
		const FHyperTwistMelinda2x2x2x2StateEncoding& MelindaState
	)
	{
		FHyperTwistPuzzleState State;
		State.Definition = Definition;
		State.StateEncodingKind = EHyperTwistStateEncodingKind::FamilySpecific;
		State.StateEncoding.EncodingProfile = MelindaState.EncodingProfile;
		State.StateEncoding.PayloadJson = SerializePayload(MelindaState);
		State.OrientationFrame.Reference = MelindaState.FrameProfile;
		State.bIsSolved = IsSolvedMelindaState(MelindaState);
		State.Source = EHyperTwistStateSource::Runtime;
		return State;
	}

	FHyperTwistTransformation MakeMelindaTransformation(
		const FHyperTwistPuzzleDefinitionRef& Definition,
		const FHyperTwistMelinda2x2x2x2TransformEncoding& MelindaTransform,
		const EHyperTwistTransformKind TransformKind,
		const FString& Notation,
		const FString& OriginalNotation
	)
	{
		FHyperTwistTransformation Transformation;
		Transformation.Definition = Definition;
		Transformation.TransformKind = TransformKind;
		Transformation.Notation = Notation;
		Transformation.TransformEncoding.EncodingProfile = MelindaTransform.EncodingProfile;
		Transformation.TransformEncoding.PayloadJson = SerializePayload(MelindaTransform);
		Transformation.bInvertible = true;
		Transformation.OriginalNotation = OriginalNotation.IsEmpty() ? Notation : OriginalNotation;
		return Transformation;
	}

	FHyperTwistTransformation MakeIdentityMelindaTransformation(const FHyperTwistPuzzleDefinitionRef& Definition)
	{
		FHyperTwistMelinda2x2x2x2TransformEncoding IdentityTransform;
		IdentityTransform.MoveId = TEXT("identity");
		IdentityTransform.PositionPullMap.Reserve(16);
		for (int32 PositionIndex = 0; PositionIndex < 16; ++PositionIndex)
		{
			IdentityTransform.PositionPullMap.Add(PositionIndex);
		}

		IdentityTransform.OrientationDeltaPerNewPosition.Init(0, 16);
		return MakeMelindaTransformation(
			Definition,
			IdentityTransform,
			EHyperTwistTransformKind::SingleMove,
			TEXT("identity"),
			TEXT("identity")
		);
	}

	int32 GetMelindaSignatureBit(const int32 SignatureId, const int32 AxisIndex)
	{
		if (AxisIndex < 0 || AxisIndex >= 4)
		{
			return 0;
		}

		return (SignatureId >> AxisIndex) & 1;
	}

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

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

		const bool bPositiveColor = GetMelindaSignatureBit(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 MakeMelindaSignatureLabel(const int32 SignatureId)
	{
		return FString::Printf(
			TEXT("%cX%cY%cZ%cW"),
			GetMelindaSignatureBit(SignatureId, 0) != 0 ? TEXT('+') : TEXT('-'),
			GetMelindaSignatureBit(SignatureId, 1) != 0 ? TEXT('+') : TEXT('-'),
			GetMelindaSignatureBit(SignatureId, 2) != 0 ? TEXT('+') : TEXT('-'),
			GetMelindaSignatureBit(SignatureId, 3) != 0 ? TEXT('+') : TEXT('-')
		);
	}

	int32 GetMelindaProjectionGridRow(const int32 PositionIndex)
	{
		return GetMelindaSignatureBit(PositionIndex, 2);
	}

	int32 GetMelindaProjectionGridColumn(const int32 PositionIndex)
	{
		return (GetMelindaSignatureBit(PositionIndex, 1) * 2) + GetMelindaSignatureBit(PositionIndex, 0);
	}

	FHyperTwistMelindaStickerProjection MakeMelindaStickerProjection(
		const FHyperTwistMelinda2x2x2x2StateEncoding& MelindaState,
		const int32 PositionIndex,
		const int32 PieceId,
		const int32 WorldAxisIndex
	)
	{
		FHyperTwistMelindaStickerProjection Projection;
		Projection.Axis = GetMelindaAxisEnum(WorldAxisIndex);
		Projection.bPositiveSide = GetMelindaSignatureBit(PositionIndex, WorldAxisIndex) != 0;

		if (PieceId < 0
			|| PieceId >= MelindaState.PieceOrientation.Num()
			|| !IsValidMelindaOrientationIndex(MelindaState.PieceOrientation[PieceId]))
		{
			return Projection;
		}

		const TArray<int32>& OrientationPermutation =
			GetMelindaOrientationPermutations()[MelindaState.PieceOrientation[PieceId]];

		const int32 LocalAxisIndex = OrientationPermutation[WorldAxisIndex];
		Projection.SourcePieceAxisIndex = LocalAxisIndex;
		Projection.Color = GetMelindaStickerColor(PieceId, LocalAxisIndex);
		return Projection;
	}

	FHyperTwistMelindaFlatTileProjection MakeMelindaFlatTileProjection(
		const FHyperTwistMelinda2x2x2x2StateEncoding& MelindaState,
		const int32 PositionIndex,
		const FHyperTwistMelindaFlatProjectionOptions& Options
	)
	{
		FHyperTwistMelindaFlatTileProjection Tile;
		Tile.Panel = GetMelindaSignatureBit(PositionIndex, 3) != 0
			? EHyperTwistMelindaProjectionPanel::Back
			: EHyperTwistMelindaProjectionPanel::Front;
		Tile.PositionIndex = PositionIndex;
		Tile.PieceId = MelindaState.PositionToPiece[PositionIndex];
		Tile.GridRow = GetMelindaProjectionGridRow(PositionIndex);
		Tile.GridColumn = GetMelindaProjectionGridColumn(PositionIndex);
		Tile.SlotLabel = MakeMelindaSignatureLabel(PositionIndex);
		Tile.PieceLabel = Options.bIncludePieceLabels ? MakeMelindaSignatureLabel(Tile.PieceId) : FString();
		Tile.bPieceInSolvedPosition = Tile.PieceId == PositionIndex;
		Tile.PanelSticker = MakeMelindaStickerProjection(MelindaState, PositionIndex, Tile.PieceId, 3);

		if (Options.bIncludeStickerPeeks)
		{
			Tile.PeekStickers.Reserve(3);
			for (int32 AxisIndex = 0; AxisIndex < 3; ++AxisIndex)
			{
				Tile.PeekStickers.Add(
					MakeMelindaStickerProjection(MelindaState, PositionIndex, Tile.PieceId, AxisIndex)
				);
			}
		}

		return Tile;
	}

	bool TryDeserializeMelindaState(
		const FHyperTwistPuzzleState& State,
		FHyperTwistMelinda2x2x2x2StateEncoding& OutMelindaState,
		TArray<FString>& OutWarnings
	)
	{
		if (!IsSupportedMelindaState(State))
		{
			OutWarnings.Add(TEXT("unsupported-melinda-state"));
			return false;
		}

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

		return true;
	}

	bool TryDeserializeMelindaTransform(
		const FHyperTwistTransformation& Transformation,
		FHyperTwistMelinda2x2x2x2TransformEncoding& OutMelindaTransform,
		TArray<FString>& OutWarnings
	)
	{
		if (!IsSupportedMelindaTransformation(Transformation))
		{
			OutWarnings.Add(TEXT("unsupported-melinda-transformation"));
			return false;
		}

		if (!DeserializePayload(Transformation.TransformEncoding, OutMelindaTransform)
			|| !OutMelindaTransform.IsStructurallyValid())
		{
			OutWarnings.Add(TEXT("invalid-melinda-transform-payload"));
			return false;
		}

		return true;
	}

	FString MakeMelindaInverseLabel(const FString& BaseLabel)
	{
		return FString::Printf(TEXT("inverse(%s)"), BaseLabel.IsEmpty() ? TEXT("transform") : *BaseLabel);
	}

	FString MakeMelindaCompositionLabel(const FString& FirstLabel, const FString& SecondLabel)
	{
		return FString::Printf(
			TEXT("compose(%s,%s)"),
			FirstLabel.IsEmpty() ? TEXT("first") : *FirstLabel,
			SecondLabel.IsEmpty() ? TEXT("second") : *SecondLabel
		);
	}

	bool TryApplyMelindaTransform(
		const FHyperTwistPuzzleState& State,
		const FHyperTwistTransformation& Transformation,
		FHyperTwistApplyTransformationResult& Result
	)
	{
		if (State.StateEncoding.EncodingProfile != TEXT("melinda-2x2x2x2-state-v1")
			|| Transformation.TransformEncoding.EncodingProfile != TEXT("melinda-2x2x2x2-transform-v1"))
		{
			return false;
		}

		if (!DoMelindaDefinitionsMatch(State.Definition, Transformation.Definition))
		{
			Result.Warnings.Add(TEXT("melinda-definition-mismatch"));
			return true;
		}

		FHyperTwistMelinda2x2x2x2StateEncoding MelindaState;
		if (!DeserializePayload(State.StateEncoding, MelindaState) || !MelindaState.IsStructurallyValid())
		{
			Result.Warnings.Add(TEXT("invalid-melinda-state-payload"));
			return true;
		}

		FHyperTwistMelinda2x2x2x2TransformEncoding MelindaTransform;
		if (!DeserializePayload(Transformation.TransformEncoding, MelindaTransform) || !MelindaTransform.IsStructurallyValid())
		{
			Result.Warnings.Add(TEXT("invalid-melinda-transform-payload"));
			return true;
		}

		if (MelindaState.FrameProfile != MelindaTransform.FrameProfile
			|| MelindaState.TopologyVersion != MelindaTransform.TopologyVersion)
		{
			Result.Warnings.Add(TEXT("melinda-frame-or-topology-mismatch"));
			return true;
		}

		FHyperTwistMelinda2x2x2x2StateEncoding NextState = MelindaState;
		TArray<int32> NextPositionToPiece;
		NextPositionToPiece.SetNum(16);
		TArray<int32> NextPieceOrientation = MelindaState.PieceOrientation;

		for (int32 NewPosition = 0; NewPosition < 16; ++NewPosition)
		{
			const int32 OldPosition = MelindaTransform.PositionPullMap[NewPosition];
			const int32 PieceId = MelindaState.PositionToPiece[OldPosition];
			const int32 DeltaIndex = MelindaTransform.OrientationDeltaPerNewPosition[NewPosition];
			const int32 CurrentOrientationIndex = MelindaState.PieceOrientation[PieceId];
			const int32 NextOrientationIndex = ComposeMelindaOrientationIndices(DeltaIndex, CurrentOrientationIndex);

			if (NextOrientationIndex == INDEX_NONE)
			{
				Result.Warnings.Add(TEXT("melinda-orientation-composition-failed"));
				return true;
			}

			NextPositionToPiece[NewPosition] = PieceId;
			NextPieceOrientation[PieceId] = NextOrientationIndex;
		}

		NextState.PositionToPiece = MoveTemp(NextPositionToPiece);
		NextState.PieceOrientation = MoveTemp(NextPieceOrientation);

		if (!NextState.IsStructurallyValid())
		{
			Result.Warnings.Add(TEXT("invalid-melinda-next-state"));
			return true;
		}

		Result.State.StateEncodingKind = EHyperTwistStateEncodingKind::FamilySpecific;
		Result.State.StateEncoding.EncodingProfile = NextState.EncodingProfile;
		Result.State.StateEncoding.PayloadJson = SerializePayload(NextState);
		Result.State.OrientationFrame.Reference = NextState.FrameProfile;
		Result.State.bIsSolved = IsSolvedMelindaState(NextState);
		Result.State.Source = EHyperTwistStateSource::Runtime;
		Result.State.Notes = FString();
		Result.bExactStateUpdate = true;
		return true;
	}
}

FHyperTwistNotationNormalizationResult UHyperTwistCoreLibrary::NormalizeNotation(const FString& RawNotation)
{
	FHyperTwistNotationNormalizationResult Result;
	Result.InputNotation = RawNotation;

	FString Working = RawNotation;
	Working.TrimStartAndEndInline();
	Working.ReplaceInline(TEXT("\r"), TEXT(" "));
	Working.ReplaceInline(TEXT("\n"), TEXT(" "));
	Working.ReplaceInline(TEXT("\t"), TEXT(" "));
	Working.ReplaceInline(TEXT("’"), TEXT("'"));
	Working.ReplaceInline(TEXT("`"), TEXT("'"));

	while (Working.Contains(TEXT("  ")))
	{
		Working.ReplaceInline(TEXT("  "), TEXT(" "));
	}

	Result.NormalizedNotation = Working;
	Result.bIsValid = !Working.IsEmpty();
	Result.bWasModified = Working != RawNotation;

	if (Result.bWasModified)
	{
		Result.Warnings.Add(TEXT("notation-normalized"));
	}

	if (!Result.bIsValid)
	{
		Result.Warnings.Add(TEXT("notation-empty-after-normalization"));
	}

	return Result;
}

bool UHyperTwistCoreLibrary::TryCanonicalizeAlgorithmNotation(
	const FString& RawNotation,
	FString& OutCanonicalNotation,
	FString& OutErrorMessage,
	int32& OutErrorPosition
)
{
	return UHyperTwistAlgorithmLibrary::TryCanonicalizeAlgorithmText(
		RawNotation,
		OutCanonicalNotation,
		OutErrorMessage,
		OutErrorPosition
	);
}

bool UHyperTwistCoreLibrary::IsSolved(const FHyperTwistPuzzleState& State)
{
	if (!State.IsStructurallyValid())
	{
		return false;
	}

	if (State.StateEncoding.EncodingProfile == TEXT("melinda-2x2x2x2-state-v1"))
	{
		FHyperTwistMelinda2x2x2x2StateEncoding MelindaState;
		if (!HyperTwistCoreLibraryInternal::DeserializePayload(State.StateEncoding, MelindaState))
		{
			return false;
		}

		return HyperTwistCoreLibraryInternal::IsSolvedMelindaState(MelindaState);
	}

	if (State.bIsSolved)
	{
		return true;
	}

	if (State.StateEncoding.EncodingProfile == TEXT("classic-piece-orbit-v1"))
	{
		FHyperTwistClassicPieceOrbitState OrbitState;
		if (!HyperTwistCoreLibraryInternal::DeserializePayload(State.StateEncoding, OrbitState))
		{
			return false;
		}

		return HyperTwistCoreLibraryInternal::IsIdentityPermutation(OrbitState.Corners.Permutation)
			&& HyperTwistCoreLibraryInternal::IsZeroOrientation(OrbitState.Corners.Orientation)
			&& HyperTwistCoreLibraryInternal::IsIdentityPermutation(OrbitState.Edges.Permutation)
			&& HyperTwistCoreLibraryInternal::IsZeroOrientation(OrbitState.Edges.Orientation)
			&& HyperTwistCoreLibraryInternal::IsIdentityPermutation(OrbitState.Centers.Permutation)
			&& HyperTwistCoreLibraryInternal::IsZeroOrientation(OrbitState.Centers.Orientation);
	}

	if (State.StateEncoding.EncodingProfile == TEXT("classic-facelet-snapshot-v1"))
	{
		FHyperTwistClassicFaceletSnapshotState FaceletState;
		if (!HyperTwistCoreLibraryInternal::DeserializePayload(State.StateEncoding, FaceletState))
		{
			return false;
		}

		return HyperTwistCoreLibraryInternal::IsUniformFace(FaceletState.U)
			&& HyperTwistCoreLibraryInternal::IsUniformFace(FaceletState.R)
			&& HyperTwistCoreLibraryInternal::IsUniformFace(FaceletState.F)
			&& HyperTwistCoreLibraryInternal::IsUniformFace(FaceletState.D)
			&& HyperTwistCoreLibraryInternal::IsUniformFace(FaceletState.L)
			&& HyperTwistCoreLibraryInternal::IsUniformFace(FaceletState.B);
	}

	if (State.StateEncoding.EncodingProfile == TEXT("hypercube-piece-map-v1"))
	{
		FHyperTwistHypercubePieceMapState HyperState;
		if (!HyperTwistCoreLibraryInternal::DeserializePayload(State.StateEncoding, HyperState))
		{
			return false;
		}

		return HyperTwistCoreLibraryInternal::IsCanonicalLabelSequence(HyperState.Corners, TEXT("c"))
			&& HyperTwistCoreLibraryInternal::IsCanonicalLabelSequence(HyperState.Edges, TEXT("e"))
			&& HyperTwistCoreLibraryInternal::IsCanonicalLabelSequence(HyperState.Faces, TEXT("f"))
			&& HyperTwistCoreLibraryInternal::IsCanonicalLabelSequence(HyperState.Cells, TEXT("cell"));
	}

	return false;
}

FHyperTwistPuzzleStateValidationResult UHyperTwistCoreLibrary::ValidatePuzzleState(const FHyperTwistPuzzleState& State)
{
	FHyperTwistPuzzleStateValidationResult Result;
	Result.bStructureValid = State.IsStructurallyValid();

	if (!Result.bStructureValid)
	{
		Result.Errors.Add(TEXT("invalid-puzzle-state-structure"));
		return Result;
	}

	if (State.StateEncoding.EncodingProfile != TEXT("melinda-2x2x2x2-state-v1")
		|| !HyperTwistCoreLibraryInternal::IsSupportedMelindaDefinition(State.Definition))
	{
		Result.Errors.Add(TEXT("unsupported-melinda-state"));
		return Result;
	}

	Result.bStateSupported = true;

	FHyperTwistMelinda2x2x2x2StateEncoding MelindaState;
	if (!HyperTwistCoreLibraryInternal::DeserializePayload(State.StateEncoding, MelindaState)
		|| !MelindaState.IsStructurallyValid())
	{
		Result.Errors.Add(TEXT("invalid-melinda-state-payload"));
		return Result;
	}

	if (State.StateEncodingKind != EHyperTwistStateEncodingKind::FamilySpecific)
	{
		Result.Warnings.Add(TEXT("melinda-state-encoding-kind-mismatch"));
	}

	if (State.OrientationFrame.Reference != MelindaState.FrameProfile)
	{
		Result.Warnings.Add(TEXT("melinda-frame-reference-mismatch"));
	}

	const HyperTwistCoreLibraryInternal::FMelindaOrientationLookup& OrientationLookup =
		HyperTwistCoreLibraryInternal::GetMelindaOrientationLookup();

	Result.bPermutationParityEven = HyperTwistCoreLibraryInternal::IsEvenPermutation(MelindaState.PositionToPiece);
	if (!Result.bPermutationParityEven)
	{
		Result.Errors.Add(TEXT("melinda-permutation-parity-odd"));
	}

	int32 RawTwistSum = 0;

	for (int32 PositionIndex = 0; PositionIndex < MelindaState.PositionToPiece.Num(); ++PositionIndex)
	{
		const int32 PieceId = MelindaState.PositionToPiece[PositionIndex];
		const int32 OrientationIndex = MelindaState.PieceOrientation[PieceId];
		const int32 ExpectedParity =
			HyperTwistCoreLibraryInternal::GetMelindaHandednessBit(PieceId)
			^ HyperTwistCoreLibraryInternal::GetMelindaHandednessBit(PositionIndex);
		const int32 OrientationParity = OrientationLookup.ParityByOrientationIndex[OrientationIndex];

		if (OrientationParity != ExpectedParity)
		{
			++Result.OrientationParityMismatchCount;
		}

		RawTwistSum += OrientationLookup.TwistByOrientationIndex[OrientationIndex];
	}

	Result.bOrientationParityCompatible = Result.OrientationParityMismatchCount == 0;
	if (!Result.bOrientationParityCompatible)
	{
		Result.Errors.Add(TEXT("melinda-orientation-parity-mismatch"));
	}

	Result.TwistBalance = HyperTwistCoreLibraryInternal::NormalizeMelindaTwistBalance(RawTwistSum);
	Result.bTwistConserved = Result.TwistBalance == 0;
	if (!Result.bTwistConserved)
	{
		Result.Errors.Add(TEXT("melinda-twist-balance-mismatch"));
	}

	Result.bIsSolvable = Result.bStateSupported
		&& Result.bStructureValid
		&& Result.bPermutationParityEven
		&& Result.bOrientationParityCompatible
		&& Result.bTwistConserved;
	return Result;
}

FHyperTwistRandomStateGenerationResult UHyperTwistCoreLibrary::GenerateRandomPuzzleState(
	const FHyperTwistPuzzleDefinitionRef& Definition,
	int32 RandomSeed
)
{
	FHyperTwistRandomStateGenerationResult Result;
	Result.SeedUsed = RandomSeed;

	if (!Definition.IsStructurallyValid())
	{
		Result.Warnings.Add(TEXT("invalid-puzzle-definition"));
		return Result;
	}

	if (!HyperTwistCoreLibraryInternal::IsSupportedMelindaDefinition(Definition))
	{
		Result.Warnings.Add(TEXT("unsupported-random-state-definition"));
		return Result;
	}

	const HyperTwistCoreLibraryInternal::FMelindaOrientationLookup& OrientationLookup =
		HyperTwistCoreLibraryInternal::GetMelindaOrientationLookup();
	FRandomStream RandomStream(RandomSeed);

	FHyperTwistMelinda2x2x2x2StateEncoding MelindaState;
	MelindaState.PositionToPiece.Reserve(16);
	for (int32 PieceId = 0; PieceId < 16; ++PieceId)
	{
		MelindaState.PositionToPiece.Add(PieceId);
	}

	for (int32 PositionIndex = MelindaState.PositionToPiece.Num() - 1; PositionIndex > 0; --PositionIndex)
	{
		const int32 SwapIndex = RandomStream.RandRange(0, PositionIndex);
		if (SwapIndex != PositionIndex)
		{
			Swap(MelindaState.PositionToPiece[PositionIndex], MelindaState.PositionToPiece[SwapIndex]);
		}
	}

	if (!HyperTwistCoreLibraryInternal::IsEvenPermutation(MelindaState.PositionToPiece))
	{
		Swap(MelindaState.PositionToPiece[14], MelindaState.PositionToPiece[15]);
	}

	MelindaState.PieceOrientation.Init(0, 16);

	int32 RawTwistSum = 0;
	const int32 AnchorPosition = 15;

	for (int32 PositionIndex = 0; PositionIndex < AnchorPosition; ++PositionIndex)
	{
		const int32 PieceId = MelindaState.PositionToPiece[PositionIndex];
		const int32 RequiredParity =
			HyperTwistCoreLibraryInternal::GetMelindaHandednessBit(PieceId)
			^ HyperTwistCoreLibraryInternal::GetMelindaHandednessBit(PositionIndex);
		const int32 TargetTwist = RandomStream.RandRange(-1, 1);
		const TArray<int32>& OrientationBucket =
			OrientationLookup.OrientationIndicesByParityAndTwist[RequiredParity][TargetTwist + 1];

		if (OrientationBucket.Num() == 0)
		{
			Result.Warnings.Add(TEXT("melinda-orientation-bucket-empty"));
			return Result;
		}

		const int32 ChosenOrientation =
			OrientationBucket[RandomStream.RandRange(0, OrientationBucket.Num() - 1)];
		MelindaState.PieceOrientation[PieceId] = ChosenOrientation;
		RawTwistSum += OrientationLookup.TwistByOrientationIndex[ChosenOrientation];
	}

	const int32 AnchorPieceId = MelindaState.PositionToPiece[AnchorPosition];
	const int32 AnchorRequiredParity =
		HyperTwistCoreLibraryInternal::GetMelindaHandednessBit(AnchorPieceId)
		^ HyperTwistCoreLibraryInternal::GetMelindaHandednessBit(AnchorPosition);
	const int32 AnchorTwist = -HyperTwistCoreLibraryInternal::NormalizeMelindaTwistBalance(RawTwistSum);
	const TArray<int32>& AnchorBucket =
		OrientationLookup.OrientationIndicesByParityAndTwist[AnchorRequiredParity][AnchorTwist + 1];

	if (AnchorBucket.Num() == 0)
	{
		Result.Warnings.Add(TEXT("melinda-anchor-orientation-bucket-empty"));
		return Result;
	}

	MelindaState.PieceOrientation[AnchorPieceId] =
		AnchorBucket[RandomStream.RandRange(0, AnchorBucket.Num() - 1)];

	Result.State = HyperTwistCoreLibraryInternal::MakeMelindaPuzzleState(Definition, MelindaState);
	Result.AttemptsUsed = 1;
	Result.Validation = ValidatePuzzleState(Result.State);
	Result.bGenerated = Result.Validation.bIsSolvable;

	if (!Result.bGenerated)
	{
		Result.Warnings.Add(TEXT("generated-melinda-state-failed-validation"));
	}

	return Result;
}

FHyperTwistTransformationInversionResult UHyperTwistCoreLibrary::InvertTransformation(
	const FHyperTwistTransformation& Transformation
)
{
	FHyperTwistTransformationInversionResult Result;

	if (!Transformation.IsStructurallyValid())
	{
		Result.Warnings.Add(TEXT("invalid-transformation"));
		return Result;
	}

	FHyperTwistMelinda2x2x2x2TransformEncoding MelindaTransform;
	if (!HyperTwistCoreLibraryInternal::TryDeserializeMelindaTransform(
		Transformation,
		MelindaTransform,
		Result.Warnings
	))
	{
		return Result;
	}

	FHyperTwistMelinda2x2x2x2TransformEncoding InverseTransform = MelindaTransform;
	InverseTransform.MoveId = HyperTwistCoreLibraryInternal::MakeMelindaInverseLabel(MelindaTransform.MoveId);
	InverseTransform.PositionPullMap.Init(0, 16);
	InverseTransform.OrientationDeltaPerNewPosition.Init(0, 16);

	for (int32 NewPosition = 0; NewPosition < 16; ++NewPosition)
	{
		const int32 OldPosition = MelindaTransform.PositionPullMap[NewPosition];
		const int32 InverseOrientation =
			HyperTwistCoreLibraryInternal::InvertMelindaOrientationIndex(
				MelindaTransform.OrientationDeltaPerNewPosition[NewPosition]
			);

		if (InverseOrientation == INDEX_NONE)
		{
			Result.Warnings.Add(TEXT("melinda-orientation-inverse-failed"));
			return Result;
		}

		InverseTransform.PositionPullMap[OldPosition] = NewPosition;
		InverseTransform.OrientationDeltaPerNewPosition[OldPosition] = InverseOrientation;
	}

	Result.Transformation = HyperTwistCoreLibraryInternal::MakeMelindaTransformation(
		Transformation.Definition,
		InverseTransform,
		Transformation.TransformKind,
		HyperTwistCoreLibraryInternal::MakeMelindaInverseLabel(
			MelindaTransform.MoveId.IsEmpty() ? Transformation.Notation : MelindaTransform.MoveId
		),
		HyperTwistCoreLibraryInternal::MakeMelindaInverseLabel(Transformation.Notation)
	);
	Result.bInverted = true;
	Result.bExactInverse = true;
	return Result;
}

FHyperTwistTransformationCompositionResult UHyperTwistCoreLibrary::ComposeTransformations(
	const FHyperTwistTransformation& FirstTransformation,
	const FHyperTwistTransformation& SecondTransformation
)
{
	FHyperTwistTransformationCompositionResult Result;

	if (!FirstTransformation.IsStructurallyValid() || !SecondTransformation.IsStructurallyValid())
	{
		Result.Warnings.Add(TEXT("invalid-transformation"));
		return Result;
	}

	if (!HyperTwistCoreLibraryInternal::DoMelindaDefinitionsMatch(
		FirstTransformation.Definition,
		SecondTransformation.Definition
	))
	{
		Result.Warnings.Add(TEXT("melinda-definition-mismatch"));
		return Result;
	}

	FHyperTwistMelinda2x2x2x2TransformEncoding FirstMelindaTransform;
	if (!HyperTwistCoreLibraryInternal::TryDeserializeMelindaTransform(
		FirstTransformation,
		FirstMelindaTransform,
		Result.Warnings
	))
	{
		return Result;
	}

	FHyperTwistMelinda2x2x2x2TransformEncoding SecondMelindaTransform;
	if (!HyperTwistCoreLibraryInternal::TryDeserializeMelindaTransform(
		SecondTransformation,
		SecondMelindaTransform,
		Result.Warnings
	))
	{
		return Result;
	}

	if (FirstMelindaTransform.FrameProfile != SecondMelindaTransform.FrameProfile
		|| FirstMelindaTransform.TopologyVersion != SecondMelindaTransform.TopologyVersion)
	{
		Result.Warnings.Add(TEXT("melinda-frame-or-topology-mismatch"));
		return Result;
	}

	FHyperTwistMelinda2x2x2x2TransformEncoding ComposedTransform;
	ComposedTransform.MoveId = HyperTwistCoreLibraryInternal::MakeMelindaCompositionLabel(
		FirstMelindaTransform.MoveId,
		SecondMelindaTransform.MoveId
	);
	ComposedTransform.FrameProfile = FirstMelindaTransform.FrameProfile;
	ComposedTransform.TopologyVersion = FirstMelindaTransform.TopologyVersion;
	ComposedTransform.PositionPullMap.Init(0, 16);
	ComposedTransform.OrientationDeltaPerNewPosition.Init(0, 16);

	for (int32 NewPosition = 0; NewPosition < 16; ++NewPosition)
	{
		const int32 IntermediatePosition = SecondMelindaTransform.PositionPullMap[NewPosition];
		ComposedTransform.PositionPullMap[NewPosition] = FirstMelindaTransform.PositionPullMap[IntermediatePosition];

		const int32 ComposedOrientation = HyperTwistCoreLibraryInternal::ComposeMelindaOrientationIndices(
			SecondMelindaTransform.OrientationDeltaPerNewPosition[NewPosition],
			FirstMelindaTransform.OrientationDeltaPerNewPosition[IntermediatePosition]
		);

		if (ComposedOrientation == INDEX_NONE)
		{
			Result.Warnings.Add(TEXT("melinda-orientation-composition-failed"));
			return Result;
		}

		ComposedTransform.OrientationDeltaPerNewPosition[NewPosition] = ComposedOrientation;
	}

	Result.Transformation = HyperTwistCoreLibraryInternal::MakeMelindaTransformation(
		FirstTransformation.Definition,
		ComposedTransform,
		EHyperTwistTransformKind::Composite,
		HyperTwistCoreLibraryInternal::MakeMelindaCompositionLabel(
			FirstMelindaTransform.MoveId.IsEmpty() ? FirstTransformation.Notation : FirstMelindaTransform.MoveId,
			SecondMelindaTransform.MoveId.IsEmpty() ? SecondTransformation.Notation : SecondMelindaTransform.MoveId
		),
		HyperTwistCoreLibraryInternal::MakeMelindaCompositionLabel(
			FirstTransformation.Notation,
			SecondTransformation.Notation
		)
	);
	Result.bComposed = true;
	Result.bExactComposition = true;
	return Result;
}

FHyperTwistMelindaScramblePacketBuildResult UHyperTwistCoreLibrary::BuildMelindaScramblePacketFromGeneratedState(
	const FHyperTwistRandomStateGenerationResult& GeneratedStateResult
)
{
	FHyperTwistMelindaScramblePacketBuildResult Result;
	Result.Warnings.Append(GeneratedStateResult.Warnings);

	if (GeneratedStateResult.State.IsStructurallyValid())
	{
		const bool bHasUsableValidation =
			GeneratedStateResult.Validation.bStateSupported
			|| GeneratedStateResult.Validation.bStructureValid
			|| GeneratedStateResult.Validation.Errors.Num() > 0
			|| GeneratedStateResult.Validation.Warnings.Num() > 0;
		Result.TargetStateValidation = bHasUsableValidation
			? GeneratedStateResult.Validation
			: ValidatePuzzleState(GeneratedStateResult.State);
	}

	if (!GeneratedStateResult.bGenerated)
	{
		Result.Warnings.Add(TEXT("generated-state-result-missing-solvable-state"));
		return Result;
	}

	FHyperTwistMelinda2x2x2x2StateEncoding GeneratedMelindaState;
	if (!HyperTwistCoreLibraryInternal::TryDeserializeMelindaState(
		GeneratedStateResult.State,
		GeneratedMelindaState,
		Result.Warnings
	))
	{
		return Result;
	}

	if (!Result.TargetStateValidation.bIsSolvable)
	{
		Result.Warnings.Add(TEXT("generated-state-result-failed-validation"));
		return Result;
	}

	Result.Packet.Definition = GeneratedStateResult.State.Definition;
	Result.Packet.PacketSource = EHyperTwistMelindaScramblePacketSource::GeneratedState;
	Result.Packet.TargetState = GeneratedStateResult.State;
	Result.Packet.RandomSeed = GeneratedStateResult.SeedUsed;
	Result.Packet.bCarriesTargetState = true;
	Result.Packet.bCarriesExactTransformSequence = false;
	Result.bBuilt = Result.Packet.IsStructurallyValid();

	if (!Result.bBuilt)
	{
		Result.Warnings.Add(TEXT("invalid-melinda-scramble-packet"));
	}

	return Result;
}

FHyperTwistMelindaScramblePacketBuildResult UHyperTwistCoreLibrary::BuildMelindaScramblePacketFromTransformSequence(
	const FHyperTwistPuzzleDefinitionRef& Definition,
	const TArray<FHyperTwistTransformation>& TransformSequence
)
{
	FHyperTwistMelindaScramblePacketBuildResult Result;

	if (!HyperTwistCoreLibraryInternal::IsSupportedMelindaDefinition(Definition))
	{
		Result.Warnings.Add(TEXT("unsupported-scramble-packet-definition"));
		return Result;
	}

	FHyperTwistPuzzleState CurrentState = HyperTwistCoreLibraryInternal::MakeMelindaPuzzleState(
		Definition,
		HyperTwistCoreLibraryInternal::MakeSolvedMelindaStateEncoding()
	);
	FHyperTwistTransformation NetTransform =
		HyperTwistCoreLibraryInternal::MakeIdentityMelindaTransformation(Definition);

	for (const FHyperTwistTransformation& Transformation : TransformSequence)
	{
		if (!HyperTwistCoreLibraryInternal::DoMelindaDefinitionsMatch(Definition, Transformation.Definition))
		{
			Result.Warnings.Add(TEXT("melinda-definition-mismatch"));
			return Result;
		}

		const FHyperTwistApplyTransformationResult ApplyResult = ApplyTransformation(CurrentState, Transformation);
		Result.Warnings.Append(ApplyResult.Warnings);
		if (!ApplyResult.bApplied || !ApplyResult.bExactStateUpdate)
		{
			Result.Warnings.Add(TEXT("melinda-transform-sequence-apply-failed"));
			return Result;
		}

		const FHyperTwistTransformationCompositionResult CompositionResult =
			ComposeTransformations(NetTransform, Transformation);
		Result.Warnings.Append(CompositionResult.Warnings);
		if (!CompositionResult.bComposed || !CompositionResult.bExactComposition)
		{
			Result.Warnings.Add(TEXT("melinda-transform-sequence-compose-failed"));
			return Result;
		}

		CurrentState = ApplyResult.State;
		NetTransform = CompositionResult.Transformation;
	}

	Result.TargetStateValidation = ValidatePuzzleState(CurrentState);
	if (!Result.TargetStateValidation.bIsSolvable)
	{
		Result.Warnings.Add(TEXT("melinda-transform-sequence-target-state-invalid"));
		return Result;
	}

	Result.Packet.Definition = Definition;
	Result.Packet.PacketSource = EHyperTwistMelindaScramblePacketSource::TransformSequence;
	Result.Packet.TargetState = CurrentState;
	Result.Packet.TransformSequence = TransformSequence;
	Result.Packet.NetTransform = NetTransform;
	Result.Packet.bCarriesTargetState = true;
	Result.Packet.bCarriesExactTransformSequence = true;
	Result.bBuilt = Result.Packet.IsStructurallyValid();

	if (!Result.bBuilt)
	{
		Result.Warnings.Add(TEXT("invalid-melinda-scramble-packet"));
	}

	return Result;
}

FHyperTwistMelindaFlatProjectionBuildResult UHyperTwistCoreLibrary::BuildMelindaFlatProjection(
	const FHyperTwistPuzzleState& State,
	const FHyperTwistMelindaFlatProjectionOptions& Options
)
{
	FHyperTwistMelindaFlatProjectionBuildResult Result;
	Result.SourceStateValidation = ValidatePuzzleState(State);

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

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

	if (!Result.SourceStateValidation.bIsSolvable)
	{
		Result.Warnings.Add(TEXT("melinda-state-not-solvable-for-projection"));
	}

	Result.Projection.Definition = State.Definition;
	Result.Projection.Options = Options;
	Result.Projection.FrontTiles.Reserve(8);
	Result.Projection.BackTiles.Reserve(8);

	for (int32 PositionIndex = 0; PositionIndex < MelindaState.PositionToPiece.Num(); ++PositionIndex)
	{
		const FHyperTwistMelindaFlatTileProjection Tile =
			HyperTwistCoreLibraryInternal::MakeMelindaFlatTileProjection(
				MelindaState,
				PositionIndex,
				Options
			);

		if (Tile.Panel == EHyperTwistMelindaProjectionPanel::Front)
		{
			Result.Projection.FrontTiles.Add(Tile);
		}
		else
		{
			Result.Projection.BackTiles.Add(Tile);
		}
	}

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

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

FHyperTwistApplyTransformationResult UHyperTwistCoreLibrary::ApplyTransformation(const FHyperTwistPuzzleState& State, const FHyperTwistTransformation& Transformation)
{
	FHyperTwistApplyTransformationResult Result;
	Result.State = State;

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

	if (!Transformation.IsStructurallyValid())
	{
		Result.Warnings.Add(TEXT("invalid-transformation"));
		return Result;
	}

	const FHyperTwistNotationNormalizationResult Normalization = NormalizeNotation(Transformation.Notation);
	Result.AppliedNotation = Normalization.NormalizedNotation;
	Result.Warnings.Append(Normalization.Warnings);

	if (!Normalization.bIsValid)
	{
		return Result;
	}

	Result.bApplied = true;

	if (HyperTwistCoreLibraryInternal::TryApplyMelindaTransform(State, Transformation, Result))
	{
		if (!Result.bExactStateUpdate)
		{
			Result.bApplied = false;
		}

		return Result;
	}

	if (Normalization.NormalizedNotation.Equals(TEXT("I"), ESearchCase::IgnoreCase)
		|| Normalization.NormalizedNotation.Equals(TEXT("identity"), ESearchCase::IgnoreCase))
	{
		Result.bExactStateUpdate = true;
		Result.State.bIsSolved = IsSolved(State);
		return Result;
	}

	Result.bExactStateUpdate = false;
	Result.State.bIsSolved = false;
	Result.State.Source = EHyperTwistStateSource::Runtime;
	Result.State.Notes = FString::Printf(
		TEXT("placeholder-transform-application:%s"),
		*Normalization.NormalizedNotation
	);
	Result.Warnings.Add(TEXT("exact-piece-update-pending"));
	return Result;
}
