#include "HyperTwistSimulation/HyperTwistVirtual3333ProjectionActor.h"

#include "Components/SceneComponent.h"
#include "HyperTwistSimulation/HyperTwistFourDimensionalSaveGame.h"
#include "Kismet/GameplayStatics.h"
#include "Materials/MaterialInterface.h"
#include "Misc/DateTime.h"
#include "ProceduralMeshComponent/Public/ProceduralMeshComponent.h"
#include "UObject/ConstructorHelpers.h"

AHyperTwistVirtual3333ProjectionActor::AHyperTwistVirtual3333ProjectionActor()
{
	PrimaryActorTick.bCanEverTick = false;

	SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot"));
	RootComponent = SceneRoot;

	static ConstructorHelpers::FObjectFinder<UMaterialInterface> VertexColorMaterialFinder(
		TEXT("/Game/HyperTwistTraining/Materials/M_HT_ProjectionVertexColor.M_HT_ProjectionVertexColor"));
	if (VertexColorMaterialFinder.Succeeded())
	{
		VertexColorMaterial = VertexColorMaterialFinder.Object;
	}
}

void AHyperTwistVirtual3333ProjectionActor::OnConstruction(const FTransform& Transform)
{
	Super::OnConstruction(Transform);

	if (!bGenerateOnConstruction)
	{
		if (CurrentState.IsStructurallyValid())
		{
			LastWarnings.Reset();
			EnsureSelectionIsValid();
			RefreshProjection();
		}
		return;
	}

	ResetToSolvedState();
}

void AHyperTwistVirtual3333ProjectionActor::BeginPlay()
{
	Super::BeginPlay();

	if (bGenerateOnConstruction && CurrentState.IsStructurallyValid())
	{
		return;
	}

	ResetToSolvedState();
}

void AHyperTwistVirtual3333ProjectionActor::ResetToSolvedState()
{
	PuzzleOrder = UHyperTwistVirtual3333ProjectionLibrary::IsSupportedOrder(PuzzleOrder)
		? PuzzleOrder
		: 3;
	CurrentState = UHyperTwistVirtual3333ProjectionLibrary::BuildSolvedState(PuzzleOrder);
	CurrentSliceCoordinate =
		UHyperTwistVirtual3333ProjectionLibrary::GetSliceCoordinateForLayer(
			PuzzleOrder,
			PuzzleOrder);
	LastAppliedNotation = TEXT("solved");
	LastScramble.Reset();
	LastWarnings.Reset();
	EnsureSelectionIsValid();
	RefreshProjection();
}

bool AHyperTwistVirtual3333ProjectionActor::GenerateScramble(
	const int32 MoveCount,
	const int32 RandomSeed
)
{
	const FHyperTwistVirtual3333ScrambleResult ScrambleResult =
		UHyperTwistVirtual3333ProjectionLibrary::GenerateScramble(
			PuzzleOrder,
			MoveCount,
			RandomSeed);
	LastWarnings = ScrambleResult.Warnings;
	if (!ScrambleResult.bGenerated || !ScrambleResult.bExactStateUpdate)
	{
		return false;
	}

	CurrentState = ScrambleResult.State;
	CurrentSliceAxis = ScrambleResult.FinalSelection.SliceAxis;
	CurrentSliceCoordinate = ScrambleResult.FinalSelection.SliceCoordinate;
	CurrentRotationAxis = ScrambleResult.FinalSelection.RotationAxis;
	LastScramble = ScrambleResult.AppliedMoves;
	LastAppliedNotation = FString::Printf(
		TEXT("scrambled %d moves (seed %d)"),
		MoveCount,
		RandomSeed);
	return RefreshProjection();
}

bool AHyperTwistVirtual3333ProjectionActor::SetPuzzleOrder(const int32 NewOrder)
{
	if (!UHyperTwistVirtual3333ProjectionLibrary::IsSupportedOrder(NewOrder))
	{
		LastWarnings = {TEXT("virtual-nxnxnxn-order-unsupported")};
		return false;
	}
	if (PuzzleOrder == NewOrder
		&& CurrentState.IsStructurallyValid()
		&& CurrentState.GetOrder() == NewOrder)
	{
		return true;
	}

	PuzzleOrder = NewOrder;
	ResetToSolvedState();
	return CurrentState.IsStructurallyValid() && CurrentProjection.IsStructurallyValid();
}

bool AHyperTwistVirtual3333ProjectionActor::ApplyTurnRequest(
	const FHyperTwistVirtual3333SliceTurnRequest& Request
)
{
	const FHyperTwistVirtual3333SliceTurnResult TurnResult =
		UHyperTwistVirtual3333ProjectionLibrary::ApplySliceTurn(CurrentState, Request);
	LastWarnings = TurnResult.Warnings;
	if (!TurnResult.bApplied || !TurnResult.bExactStateUpdate)
	{
		if (!LastWarnings.Contains(TEXT("virtual-nxnxnxn-turn-apply-failed")))
		{
			LastWarnings.Add(TEXT("virtual-nxnxnxn-turn-apply-failed"));
		}
		return false;
	}

	CurrentState = TurnResult.State;
	CurrentSliceAxis = Request.SliceAxis;
	CurrentSliceCoordinate = Request.SliceCoordinate;
	CurrentRotationAxis = Request.RotationAxis;
	LastAppliedNotation = TurnResult.AppliedNotation;
	return RefreshProjection();
}

bool AHyperTwistVirtual3333ProjectionActor::ApplyCurrentSelectionClockwise()
{
	FHyperTwistVirtual3333SliceTurnRequest Request;
	Request.SliceAxis = CurrentSliceAxis;
	Request.SliceCoordinate = CurrentSliceCoordinate;
	Request.RotationAxis = CurrentRotationAxis;
	Request.Direction = EHyperTwistVirtual3333TurnDirection::Clockwise;
	return ApplyTurnRequest(Request);
}

bool AHyperTwistVirtual3333ProjectionActor::ApplyCurrentSelectionCounterClockwise()
{
	FHyperTwistVirtual3333SliceTurnRequest Request;
	Request.SliceAxis = CurrentSliceAxis;
	Request.SliceCoordinate = CurrentSliceCoordinate;
	Request.RotationAxis = CurrentRotationAxis;
	Request.Direction = EHyperTwistVirtual3333TurnDirection::CounterClockwise;
	return ApplyTurnRequest(Request);
}

bool AHyperTwistVirtual3333ProjectionActor::SetSliceAxis(
	const EHyperTwistVirtual3333Axis SliceAxis
)
{
	CurrentSliceAxis = SliceAxis;
	EnsureSelectionIsValid();
	LastWarnings.Reset();
	return CurrentState.IsStructurallyValid() ? RefreshProjection() : true;
}

bool AHyperTwistVirtual3333ProjectionActor::SetSliceCoordinate(const int32 SliceCoordinate)
{
	const TArray<int32> Coordinates =
		UHyperTwistVirtual3333ProjectionLibrary::GetSliceCoordinatesForOrder(PuzzleOrder);
	if (!Coordinates.Contains(SliceCoordinate))
	{
		LastWarnings = {TEXT("virtual-nxnxnxn-slice-coordinate-invalid")};
		return false;
	}
	CurrentSliceCoordinate = SliceCoordinate;
	EnsureSelectionIsValid();
	LastWarnings.Reset();
	return CurrentState.IsStructurallyValid() ? RefreshProjection() : true;
}

bool AHyperTwistVirtual3333ProjectionActor::SetSliceLayer(const int32 OneBasedLayer)
{
	if (OneBasedLayer < 1 || OneBasedLayer > PuzzleOrder)
	{
		LastWarnings = {TEXT("virtual-nxnxnxn-slice-layer-invalid")};
		return false;
	}
	return SetSliceCoordinate(
		UHyperTwistVirtual3333ProjectionLibrary::GetSliceCoordinateForLayer(
			PuzzleOrder,
			OneBasedLayer));
}

bool AHyperTwistVirtual3333ProjectionActor::CycleSliceCoordinate(
	const int32 DirectionStep
)
{
	const TArray<int32> Coordinates =
		UHyperTwistVirtual3333ProjectionLibrary::GetSliceCoordinatesForOrder(PuzzleOrder);
	if (Coordinates.IsEmpty())
	{
		LastWarnings = {TEXT("virtual-nxnxnxn-no-slice-layers")};
		return false;
	}

	int32 CurrentIndex = Coordinates.IndexOfByKey(CurrentSliceCoordinate);
	if (CurrentIndex == INDEX_NONE)
	{
		CurrentIndex = Coordinates.Num() - 1;
	}
	const int32 Step = DirectionStep >= 0
		? DirectionStep % Coordinates.Num()
		: -((-DirectionStep) % Coordinates.Num());
	CurrentIndex = (CurrentIndex + Step + Coordinates.Num()) % Coordinates.Num();
	return SetSliceCoordinate(Coordinates[CurrentIndex]);
}

bool AHyperTwistVirtual3333ProjectionActor::SetRotationAxis(
	const EHyperTwistVirtual3333Axis RotationAxis
)
{
	if (!UHyperTwistVirtual3333ProjectionLibrary::IsRotationAxisAvailable(
			CurrentSliceAxis,
			RotationAxis
		))
	{
		LastWarnings = {TEXT("virtual-3x3x3x3-rotation-axis-unavailable")};
		return false;
	}

	CurrentRotationAxis = RotationAxis;
	EnsureSelectionIsValid();
	LastWarnings.Reset();
	return CurrentState.IsStructurallyValid() ? RefreshProjection() : true;
}

bool AHyperTwistVirtual3333ProjectionActor::CycleRotationAxis(const int32 DirectionStep)
{
	const TArray<EHyperTwistVirtual3333Axis> AvailableAxes =
		UHyperTwistVirtual3333ProjectionLibrary::GetAvailableRotationAxesForSliceAxis(
			CurrentSliceAxis
		);
	if (AvailableAxes.Num() == 0)
	{
		LastWarnings = {TEXT("virtual-3x3x3x3-no-rotation-axes")};
		return false;
	}

	int32 CurrentIndex = AvailableAxes.IndexOfByKey(CurrentRotationAxis);
	if (CurrentIndex == INDEX_NONE)
	{
		CurrentIndex = 0;
	}

	const int32 WrappedStep = DirectionStep >= 0 ? DirectionStep % AvailableAxes.Num() : -((-DirectionStep) % AvailableAxes.Num());
	CurrentIndex = (CurrentIndex + WrappedStep + AvailableAxes.Num()) % AvailableAxes.Num();
	CurrentRotationAxis = AvailableAxes[CurrentIndex];
	LastWarnings.Reset();
	return CurrentState.IsStructurallyValid() ? RefreshProjection() : true;
}

void AHyperTwistVirtual3333ProjectionActor::ToggleCellShellRendering()
{
	bRenderCellShells = !bRenderCellShells;
	LastWarnings.Reset();
	RefreshProjection();
}

bool AHyperTwistVirtual3333ProjectionActor::RefreshProjection()
{
	const FHyperTwistVirtual3333ProjectionBuildResult ProjectionResult =
		UHyperTwistVirtual3333ProjectionLibrary::BuildVisibleProjection(
			CurrentState,
			CurrentSliceAxis,
			CurrentSliceCoordinate
		);

	LastWarnings.Append(ProjectionResult.Warnings);
	if (!ProjectionResult.bProjected || !ProjectionResult.Projection.IsStructurallyValid())
	{
		CurrentProjection = FHyperTwistVirtual3333VisibleProjection();
		ClearRenderedTesseracts();
		if (!LastWarnings.Contains(TEXT("virtual-nxnxnxn-projection-failed")))
		{
			LastWarnings.Add(TEXT("virtual-nxnxnxn-projection-failed"));
		}
		return false;
	}

	CurrentProjection = ProjectionResult.Projection;
	BuildRenderedTesseracts();
	return ProjectionResult.bExactProjection;
}

bool AHyperTwistVirtual3333ProjectionActor::SaveRuntimeState()
{
	if (!CurrentState.IsStructurallyValid()
		|| CurrentState.GetOrder() != PuzzleOrder
		|| !CurrentProjection.IsStructurallyValid())
	{
		LastPersistenceStatus = TEXT("save rejected: active 4D state is invalid");
		return false;
	}

	UHyperTwistFourDimensionalSaveGame* SaveGame =
		Cast<UHyperTwistFourDimensionalSaveGame>(
			UGameplayStatics::CreateSaveGameObject(
				UHyperTwistFourDimensionalSaveGame::StaticClass()));
	if (SaveGame == nullptr)
	{
		LastPersistenceStatus = TEXT("save failed: save object unavailable");
		return false;
	}

	SaveGame->PuzzleOrder = PuzzleOrder;
	SaveGame->bUsesCellFirstState = false;
	SaveGame->VisibleSliceState = CurrentState;
	SaveGame->SliceAxis = CurrentSliceAxis;
	SaveGame->SliceCoordinate = CurrentSliceCoordinate;
	SaveGame->RotationAxis = CurrentRotationAxis;
	SaveGame->bRenderCellShells = bRenderCellShells;
	SaveGame->LastAppliedNotation = LastAppliedNotation;
	SaveGame->SavedAtUtc = FDateTime::UtcNow().ToIso8601();
	if (!UGameplayStatics::SaveGameToSlot(
			SaveGame,
			GetRuntimeSaveSlotName(),
			0))
	{
		LastPersistenceStatus = TEXT("save failed: slot could not be written");
		return false;
	}

	LastPersistenceStatus = FString::Printf(
		TEXT("saved %dx%dx%dx%d session"),
		PuzzleOrder,
		PuzzleOrder,
		PuzzleOrder,
		PuzzleOrder);
	return true;
}

bool AHyperTwistVirtual3333ProjectionActor::LoadRuntimeState()
{
	const FString SlotName = GetRuntimeSaveSlotName();
	if (!UGameplayStatics::DoesSaveGameExist(SlotName, 0))
	{
		LastPersistenceStatus = FString::Printf(
			TEXT("no saved %dx%dx%dx%d session"),
			PuzzleOrder,
			PuzzleOrder,
			PuzzleOrder,
			PuzzleOrder);
		return false;
	}

	const UHyperTwistFourDimensionalSaveGame* SaveGame =
		Cast<UHyperTwistFourDimensionalSaveGame>(
			UGameplayStatics::LoadGameFromSlot(SlotName, 0));
	const TArray<int32> ValidCoordinates =
		UHyperTwistVirtual3333ProjectionLibrary::GetSliceCoordinatesForOrder(
			PuzzleOrder);
	if (SaveGame == nullptr
		|| SaveGame->SchemaVersion != 1
		|| SaveGame->PuzzleOrder != PuzzleOrder
		|| SaveGame->bUsesCellFirstState
		|| !SaveGame->VisibleSliceState.IsStructurallyValid()
		|| SaveGame->VisibleSliceState.GetOrder() != PuzzleOrder
		|| !ValidCoordinates.Contains(SaveGame->SliceCoordinate)
		|| !UHyperTwistVirtual3333ProjectionLibrary::IsRotationAxisAvailable(
			SaveGame->SliceAxis,
			SaveGame->RotationAxis))
	{
		LastPersistenceStatus = TEXT("load rejected: invalid or cross-order 4D save");
		return false;
	}

	const FHyperTwistVirtual3333ProjectionBuildResult ProjectionResult =
		UHyperTwistVirtual3333ProjectionLibrary::BuildVisibleProjection(
			SaveGame->VisibleSliceState,
			SaveGame->SliceAxis,
			SaveGame->SliceCoordinate);
	if (!ProjectionResult.bProjected
		|| !ProjectionResult.bExactProjection
		|| !ProjectionResult.Projection.IsStructurallyValid())
	{
		LastPersistenceStatus = TEXT("load rejected: invalid 4D projection payload");
		return false;
	}

	const FHyperTwistVirtual3333RuntimeState PreviousState = CurrentState;
	const EHyperTwistVirtual3333Axis PreviousSliceAxis = CurrentSliceAxis;
	const int32 PreviousSliceCoordinate = CurrentSliceCoordinate;
	const EHyperTwistVirtual3333Axis PreviousRotationAxis = CurrentRotationAxis;
	const bool bPreviousRenderCellShells = bRenderCellShells;
	const FString PreviousNotation = LastAppliedNotation;

	CurrentState = SaveGame->VisibleSliceState;
	CurrentSliceAxis = SaveGame->SliceAxis;
	CurrentSliceCoordinate = SaveGame->SliceCoordinate;
	CurrentRotationAxis = SaveGame->RotationAxis;
	bRenderCellShells = SaveGame->bRenderCellShells;
	LastAppliedNotation = SaveGame->LastAppliedNotation;
	LastWarnings.Reset();
	if (!RefreshProjection())
	{
		CurrentState = PreviousState;
		CurrentSliceAxis = PreviousSliceAxis;
		CurrentSliceCoordinate = PreviousSliceCoordinate;
		CurrentRotationAxis = PreviousRotationAxis;
		bRenderCellShells = bPreviousRenderCellShells;
		LastAppliedNotation = PreviousNotation;
		RefreshProjection();
		LastPersistenceStatus = TEXT("load rejected: projection rebuild failed");
		return false;
	}

	LastPersistenceStatus = FString::Printf(
		TEXT("loaded %dx%dx%dx%d session"),
		PuzzleOrder,
		PuzzleOrder,
		PuzzleOrder,
		PuzzleOrder);
	return true;
}

FString AHyperTwistVirtual3333ProjectionActor::GetRuntimeSaveSlotName() const
{
	return FString::Printf(
		TEXT("HyperTwist_4D_%dx%dx%dx%d_v1"),
		PuzzleOrder,
		PuzzleOrder,
		PuzzleOrder,
		PuzzleOrder);
}

FString AHyperTwistVirtual3333ProjectionActor::GetSelectionLabel() const
{
	return FString::Printf(
		TEXT("%dx%dx%dx%d | slice %s[%+d], hold %s"),
		PuzzleOrder,
		PuzzleOrder,
		PuzzleOrder,
		PuzzleOrder,
		*UHyperTwistVirtual3333ProjectionLibrary::GetAxisLabel(CurrentSliceAxis),
		CurrentSliceCoordinate,
		*UHyperTwistVirtual3333ProjectionLibrary::GetAxisLabel(CurrentRotationAxis)
	);
}

void AHyperTwistVirtual3333ProjectionActor::EnsureSelectionIsValid()
{
	PuzzleOrder = UHyperTwistVirtual3333ProjectionLibrary::IsSupportedOrder(PuzzleOrder)
		? PuzzleOrder
		: 3;
	const TArray<int32> Coordinates =
		UHyperTwistVirtual3333ProjectionLibrary::GetSliceCoordinatesForOrder(PuzzleOrder);
	if (!Coordinates.Contains(CurrentSliceCoordinate))
	{
		CurrentSliceCoordinate = Coordinates.IsEmpty() ? 0 : Coordinates.Last();
	}

	const TArray<EHyperTwistVirtual3333Axis> AvailableAxes =
		UHyperTwistVirtual3333ProjectionLibrary::GetAvailableRotationAxesForSliceAxis(
			CurrentSliceAxis
		);
	if (!AvailableAxes.Contains(CurrentRotationAxis))
	{
		CurrentRotationAxis = AvailableAxes.Num() > 0
			? AvailableAxes[0]
			: EHyperTwistVirtual3333Axis::X;
	}
}

void AHyperTwistVirtual3333ProjectionActor::ClearRenderedTesseracts()
{
	for (UProceduralMeshComponent* Mesh : SpawnedTesseractMeshes)
	{
		if (IsValid(Mesh))
		{
			RemoveInstanceComponent(Mesh);
			Mesh->DestroyComponent();
		}
	}

	SpawnedTesseractMeshes.Reset();
}

void AHyperTwistVirtual3333ProjectionActor::BuildRenderedTesseracts()
{
	ClearRenderedTesseracts();
	if (!CurrentProjection.IsStructurallyValid() || SceneRoot == nullptr)
	{
		return;
	}

	for (const FHyperTwistVirtual3333ProjectedTesseract& TesseractProjection :
		 CurrentProjection.Tesseracts)
	{
		BuildProjectedTesseract(TesseractProjection);
	}
}

void AHyperTwistVirtual3333ProjectionActor::BuildProjectedTesseract(
	const FHyperTwistVirtual3333ProjectedTesseract& TesseractProjection
)
{
	if (SceneRoot == nullptr || TesseractProjection.VisibleCells.Num() != 3)
	{
		return;
	}

	const FName ComponentName = MakeUniqueObjectName(
		this,
		UProceduralMeshComponent::StaticClass(),
		*FString::Printf(TEXT("VirtualNTesseract_%04d"), TesseractProjection.PositionIndex)
	);
	UProceduralMeshComponent* Mesh = NewObject<UProceduralMeshComponent>(
		this,
		ComponentName,
		RF_Transactional
	);
	if (Mesh == nullptr)
	{
		return;
	}

	AddInstanceComponent(Mesh);
	Mesh->RegisterComponent();
	Mesh->AttachToComponent(SceneRoot, FAttachmentTransformRules::KeepRelativeTransform);
	Mesh->SetMobility(EComponentMobility::Movable);
	Mesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);
	Mesh->SetCollisionResponseToAllChannels(ECR_Ignore);
	Mesh->bUseComplexAsSimpleCollision = false;
	Mesh->SetCastShadow(false);

	// Even orders use doubled half-step lattice coordinates to preserve exact turns.
	// Convert them back to one visual cell step so 4x and 6x do not render at 2x spacing.
	const float DisplayCoordinateScale = PuzzleOrder % 2 == 0 ? 0.5f : 1.0f;
	const FVector Center = FVector(
		static_cast<float>(TesseractProjection.LocalGridCoordinate.X),
		static_cast<float>(TesseractProjection.LocalGridCoordinate.Y),
		static_cast<float>(TesseractProjection.LocalGridCoordinate.Z)
	) * DisplayCoordinateScale * (TesseractSize + TesseractGap);
	const float HalfSize = TesseractSize * 0.5f;

	for (int32 FaceIndex = 0; FaceIndex < TesseractProjection.VisibleCells.Num(); ++FaceIndex)
	{
		const FHyperTwistVirtual3333ProjectedCell& Cell = TesseractProjection.VisibleCells[FaceIndex];
		CreateTesseractFace(
			Mesh,
			FaceIndex,
			Center,
			HalfSize,
			FaceIndex,
			true,
			ResolveStickerColor(Cell.Color, Cell.bHasSticker, FaceOpacity, EmptyFaceOpacity)
		);
	}

	if (bRenderCellShells)
	{
		for (int32 FaceIndex = 0; FaceIndex < 3; ++FaceIndex)
		{
			CreateTesseractFace(
				Mesh,
				FaceIndex + 3,
				Center,
				HalfSize,
				FaceIndex,
				false,
				ResolveShellColor(ShellOpacity)
			);
		}
	}

	SpawnedTesseractMeshes.Add(Mesh);
}

void AHyperTwistVirtual3333ProjectionActor::CreateTesseractFace(
	UProceduralMeshComponent* Mesh,
	const int32 SectionIndex,
	const FVector& Center,
	const float HalfSize,
	const int32 DisplayAxisIndex,
	const bool bPositiveSide,
	const FLinearColor& FaceColor
) const
{
	if (Mesh == nullptr)
	{
		return;
	}

	TArray<FVector> Vertices;
	TArray<int32> Triangles;
	TArray<FVector> Normals;
	TArray<FVector2D> UVs;
	TArray<FLinearColor> VertexColors;
	TArray<FProcMeshTangent> Tangents;

	FVector Normal = FVector::ZeroVector;
	FVector TangentX = FVector::ForwardVector;
	switch (DisplayAxisIndex)
	{
	case 0:
		Normal = FVector(bPositiveSide ? 1.0f : -1.0f, 0.0f, 0.0f);
		TangentX = FVector(0.0f, 1.0f, 0.0f);
		Vertices = {
			Center + FVector(bPositiveSide ? HalfSize : -HalfSize, -HalfSize, -HalfSize),
			Center + FVector(bPositiveSide ? HalfSize : -HalfSize, +HalfSize, -HalfSize),
			Center + FVector(bPositiveSide ? HalfSize : -HalfSize, +HalfSize, +HalfSize),
			Center + FVector(bPositiveSide ? HalfSize : -HalfSize, -HalfSize, +HalfSize)
		};
		if (!bPositiveSide)
		{
			Swap(Vertices[1], Vertices[3]);
		}
		break;

	case 1:
		Normal = FVector(0.0f, bPositiveSide ? 1.0f : -1.0f, 0.0f);
		TangentX = FVector(1.0f, 0.0f, 0.0f);
		Vertices = {
			Center + FVector(-HalfSize, bPositiveSide ? HalfSize : -HalfSize, -HalfSize),
			Center + FVector(+HalfSize, bPositiveSide ? HalfSize : -HalfSize, -HalfSize),
			Center + FVector(+HalfSize, bPositiveSide ? HalfSize : -HalfSize, +HalfSize),
			Center + FVector(-HalfSize, bPositiveSide ? HalfSize : -HalfSize, +HalfSize)
		};
		if (bPositiveSide)
		{
			Swap(Vertices[1], Vertices[3]);
		}
		break;

	default:
		Normal = FVector(0.0f, 0.0f, bPositiveSide ? 1.0f : -1.0f);
		TangentX = FVector(1.0f, 0.0f, 0.0f);
		Vertices = {
			Center + FVector(-HalfSize, -HalfSize, bPositiveSide ? HalfSize : -HalfSize),
			Center + FVector(+HalfSize, -HalfSize, bPositiveSide ? HalfSize : -HalfSize),
			Center + FVector(+HalfSize, +HalfSize, bPositiveSide ? HalfSize : -HalfSize),
			Center + FVector(-HalfSize, +HalfSize, bPositiveSide ? HalfSize : -HalfSize)
		};
		if (!bPositiveSide)
		{
			Swap(Vertices[1], Vertices[3]);
		}
		break;
	}

	Triangles = {0, 1, 2, 0, 2, 3};
	Normals.Init(Normal, 4);
	UVs = {
		FVector2D(0.0f, 0.0f),
		FVector2D(1.0f, 0.0f),
		FVector2D(1.0f, 1.0f),
		FVector2D(0.0f, 1.0f)
	};
	VertexColors.Init(FaceColor, 4);
	Tangents.Init(FProcMeshTangent(TangentX, false), 4);

	Mesh->CreateMeshSection_LinearColor(
		SectionIndex,
		Vertices,
		Triangles,
		Normals,
		UVs,
		VertexColors,
		Tangents,
		false
	);

	if (VertexColorMaterial != nullptr)
	{
		Mesh->SetMaterial(SectionIndex, VertexColorMaterial);
	}
}

FLinearColor AHyperTwistVirtual3333ProjectionActor::ResolveStickerColor(
	const EHyperTwistMelindaStickerColor StickerColor,
	const bool bHasSticker,
	const float StickerOpacity,
	const float InEmptyFaceOpacity
)
{
	if (!bHasSticker)
	{
		return FLinearColor(0.12f, 0.14f, 0.16f, InEmptyFaceOpacity);
	}

	FLinearColor Color;
	switch (StickerColor)
	{
	case EHyperTwistMelindaStickerColor::XPositive:
		Color = FLinearColor(0.88f, 0.16f, 0.12f, StickerOpacity);
		break;
	case EHyperTwistMelindaStickerColor::XNegative:
		Color = FLinearColor(1.0f, 0.48f, 0.0f, StickerOpacity);
		break;
	case EHyperTwistMelindaStickerColor::YPositive:
		Color = FLinearColor(0.1f, 0.62f, 0.22f, StickerOpacity);
		break;
	case EHyperTwistMelindaStickerColor::YNegative:
		Color = FLinearColor(0.09f, 0.31f, 0.8f, StickerOpacity);
		break;
	case EHyperTwistMelindaStickerColor::ZPositive:
		Color = FLinearColor(0.95f, 0.95f, 0.95f, StickerOpacity);
		break;
	case EHyperTwistMelindaStickerColor::ZNegative:
		Color = FLinearColor(0.95f, 0.82f, 0.14f, StickerOpacity);
		break;
	case EHyperTwistMelindaStickerColor::WPositive:
		Color = FLinearColor(0.0f, 0.72f, 0.78f, StickerOpacity);
		break;
	case EHyperTwistMelindaStickerColor::WNegative:
		Color = FLinearColor(0.72f, 0.14f, 0.62f, StickerOpacity);
		break;
	default:
		Color = FLinearColor(0.18f, 0.18f, 0.18f, InEmptyFaceOpacity);
		break;
	}

	return Color;
}

FLinearColor AHyperTwistVirtual3333ProjectionActor::ResolveShellColor(const float Opacity)
{
	return FLinearColor(0.64f, 0.71f, 0.78f, Opacity);
}
