#include "HyperTwistSimulation/HyperTwistMelindaProjectionActor.h"

#include "Components/SceneComponent.h"
#include "Engine/World.h"
#include "HyperTwistBootstrap/HyperTwistContractLibrary.h"
#include "HyperTwistCore/HyperTwistCoreLibrary.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"

namespace HyperTwistMelindaProjectionActorInternal
{
	constexpr float ClickTraceDistance = 20000.0f;

	FHyperTwistPuzzleDefinitionRef ResolveDefinition(const FHyperTwistPuzzleState& State)
	{
		return State.Definition.IsStructurallyValid()
			? State.Definition
			: UHyperTwistContractLibrary::MakeSampleHyperPuzzleDefinition();
	}
}

AHyperTwistMelindaProjectionActor::AHyperTwistMelindaProjectionActor()
{
	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 AHyperTwistMelindaProjectionActor::OnConstruction(const FTransform& Transform)
{
	Super::OnConstruction(Transform);

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

	if (bUsePreviewRandomState)
	{
		if (!GenerateRandomState(PreviewRandomSeed))
		{
			ResetToSolvedState();
		}
		return;
	}

	ResetToSolvedState();
}

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

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

	if (bUsePreviewRandomState)
	{
		if (!GenerateRandomState(PreviewRandomSeed))
		{
			ResetToSolvedState();
		}
		return;
	}

	ResetToSolvedState();
}

void AHyperTwistMelindaProjectionActor::ResetToSolvedState()
{
	CurrentState = UHyperTwistContractLibrary::MakeSampleHyperPuzzleState();
	LastAppliedNotation = TEXT("solved");
	LastWarnings.Reset();
	RefreshProjection();
}

bool AHyperTwistMelindaProjectionActor::GenerateRandomState(const int32 RandomSeed)
{
	const FHyperTwistRandomStateGenerationResult GeneratedState =
		UHyperTwistCoreLibrary::GenerateRandomPuzzleState(
			HyperTwistMelindaProjectionActorInternal::ResolveDefinition(CurrentState),
			RandomSeed
		);

	LastWarnings = GeneratedState.Warnings;
	if (!GeneratedState.bGenerated || !GeneratedState.Validation.bIsSolvable)
	{
		if (!LastWarnings.Contains(TEXT("melinda-random-state-generation-failed")))
		{
			LastWarnings.Add(TEXT("melinda-random-state-generation-failed"));
		}
		return false;
	}

	CurrentState = GeneratedState.State;
	LastAppliedNotation = FString::Printf(TEXT("random/%d"), GeneratedState.SeedUsed);
	return RefreshProjection();
}

bool AHyperTwistMelindaProjectionActor::ApplyTurnRequest(
	const FHyperTwistMelindaCellTurnRequest& Request
)
{
	const FHyperTwistMelindaCellTurnTransformBuildResult TransformResult =
		UHyperTwistMelindaProjectionLibrary::BuildCellTurnTransformation(
			HyperTwistMelindaProjectionActorInternal::ResolveDefinition(CurrentState),
			Request
		);

	LastWarnings = TransformResult.Warnings;
	if (!TransformResult.bBuilt || !TransformResult.bExactTransform)
	{
		if (!LastWarnings.Contains(TEXT("melinda-cell-turn-build-failed")))
		{
			LastWarnings.Add(TEXT("melinda-cell-turn-build-failed"));
		}
		return false;
	}

	const FHyperTwistApplyTransformationResult AppliedResult =
		UHyperTwistCoreLibrary::ApplyTransformation(CurrentState, TransformResult.Transformation);
	LastWarnings.Append(AppliedResult.Warnings);
	if (!AppliedResult.bApplied || !AppliedResult.bExactStateUpdate)
	{
		if (!LastWarnings.Contains(TEXT("melinda-cell-turn-apply-failed")))
		{
			LastWarnings.Add(TEXT("melinda-cell-turn-apply-failed"));
		}
		return false;
	}

	CurrentState = AppliedResult.State;
	LastAppliedNotation = AppliedResult.AppliedNotation;
	return RefreshProjection();
}

bool AHyperTwistMelindaProjectionActor::ProcessClick(
	const FVector& RayOrigin,
	const FVector& RayDirection,
	const bool bCounterClockwise
)
{
	if (GetWorld() == nullptr || RayDirection.IsNearlyZero())
	{
		return false;
	}

	FHitResult Hit;
	FCollisionQueryParams QueryParams(SCENE_QUERY_STAT(MelindaProjectionClick), false);
	const FVector TraceEnd = RayOrigin + (RayDirection.GetSafeNormal() * HyperTwistMelindaProjectionActorInternal::ClickTraceDistance);
	if (!GetWorld()->LineTraceSingleByChannel(
			Hit,
			RayOrigin,
			TraceEnd,
			ECC_Visibility,
			QueryParams
		))
	{
		return false;
	}

	UProceduralMeshComponent* Mesh = Cast<UProceduralMeshComponent>(Hit.GetComponent());
	const FDisplayedCubieMetadata* Metadata = Mesh != nullptr ? DisplayedCubieMetadata.Find(Mesh) : nullptr;
	if (Metadata == nullptr)
	{
		return false;
	}

	const FVector LocalImpactNormal =
		Mesh->GetComponentTransform().InverseTransformVectorNoScale(Hit.ImpactNormal);
	int32 LocalAxisIndex = INDEX_NONE;
	if (!TryResolveLocalAxisFromImpactNormal(LocalImpactNormal, LocalAxisIndex)
		|| !Metadata->LocalAxes.IsValidIndex(LocalAxisIndex))
	{
		return false;
	}

	FHyperTwistMelindaCellTurnRequest Request;
	Request.Cell = Metadata->Cell;
	Request.RotationAxis = Metadata->LocalAxes[LocalAxisIndex];
	Request.Direction = bCounterClockwise
		? EHyperTwistMelindaTurnDirection::CounterClockwise
		: EHyperTwistMelindaTurnDirection::Clockwise;
	return ApplyTurnRequest(Request);
}

bool AHyperTwistMelindaProjectionActor::RefreshProjection()
{
	const FHyperTwistMelindaCellFirstProjectionBuildResult ProjectionResult =
		UHyperTwistMelindaProjectionLibrary::BuildCellFirstProjection(CurrentState, CellCenterSpacing);

	LastWarnings.Append(ProjectionResult.Warnings);
	if (!ProjectionResult.bProjected || !ProjectionResult.Projection.IsStructurallyValid())
	{
		CurrentProjection = FHyperTwistMelindaCellFirstProjection();
		ClearRenderedCells();
		if (!LastWarnings.Contains(TEXT("melinda-cell-first-projection-failed")))
		{
			LastWarnings.Add(TEXT("melinda-cell-first-projection-failed"));
		}
		return false;
	}

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

bool AHyperTwistMelindaProjectionActor::SaveRuntimeState()
{
	const FHyperTwistMelindaCellFirstProjectionBuildResult ProjectionResult =
		UHyperTwistMelindaProjectionLibrary::BuildCellFirstProjection(
			CurrentState,
			CellCenterSpacing);
	if (!CurrentState.IsStructurallyValid()
		|| !ProjectionResult.bProjected
		|| !ProjectionResult.bExactProjection
		|| !ProjectionResult.Projection.IsStructurallyValid())
	{
		LastPersistenceStatus = TEXT("save rejected: active 2x 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 = 2;
	SaveGame->bUsesCellFirstState = true;
	SaveGame->CellFirstState = CurrentState;
	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 = TEXT("saved 2x2x2x2 session");
	return true;
}

bool AHyperTwistMelindaProjectionActor::LoadRuntimeState()
{
	const FString SlotName = GetRuntimeSaveSlotName();
	if (!UGameplayStatics::DoesSaveGameExist(SlotName, 0))
	{
		LastPersistenceStatus = TEXT("no saved 2x2x2x2 session");
		return false;
	}

	const UHyperTwistFourDimensionalSaveGame* SaveGame =
		Cast<UHyperTwistFourDimensionalSaveGame>(
			UGameplayStatics::LoadGameFromSlot(SlotName, 0));
	if (SaveGame == nullptr
		|| SaveGame->SchemaVersion != 1
		|| SaveGame->PuzzleOrder != 2
		|| !SaveGame->bUsesCellFirstState
		|| !SaveGame->CellFirstState.IsStructurallyValid())
	{
		LastPersistenceStatus = TEXT("load rejected: invalid 2x2x2x2 save");
		return false;
	}

	const FHyperTwistMelindaCellFirstProjectionBuildResult ProjectionResult =
		UHyperTwistMelindaProjectionLibrary::BuildCellFirstProjection(
			SaveGame->CellFirstState,
			CellCenterSpacing);
	if (!ProjectionResult.bProjected
		|| !ProjectionResult.bExactProjection
		|| !ProjectionResult.Projection.IsStructurallyValid())
	{
		LastPersistenceStatus = TEXT("load rejected: invalid 2x projection payload");
		return false;
	}

	const FHyperTwistPuzzleState PreviousState = CurrentState;
	const FString PreviousNotation = LastAppliedNotation;
	CurrentState = SaveGame->CellFirstState;
	LastAppliedNotation = SaveGame->LastAppliedNotation;
	LastWarnings.Reset();
	if (!RefreshProjection())
	{
		CurrentState = PreviousState;
		LastAppliedNotation = PreviousNotation;
		RefreshProjection();
		LastPersistenceStatus = TEXT("load rejected: projection rebuild failed");
		return false;
	}

	LastPersistenceStatus = TEXT("loaded 2x2x2x2 session");
	return true;
}

FString AHyperTwistMelindaProjectionActor::GetRuntimeSaveSlotName()
{
	return TEXT("HyperTwist_4D_2x2x2x2_v1");
}

void AHyperTwistMelindaProjectionActor::ClearRenderedCells()
{
	DisplayedCubieMetadata.Empty();

	for (UProceduralMeshComponent* Mesh : SpawnedCubieMeshes)
	{
		if (IsValid(Mesh))
		{
			RemoveInstanceComponent(Mesh);
			Mesh->DestroyComponent();
		}
	}
	SpawnedCubieMeshes.Reset();

	for (USceneComponent* CellRoot : SpawnedCellRoots)
	{
		if (IsValid(CellRoot))
		{
			RemoveInstanceComponent(CellRoot);
			CellRoot->DestroyComponent();
		}
	}
	SpawnedCellRoots.Reset();
}

void AHyperTwistMelindaProjectionActor::BuildRenderedCells()
{
	ClearRenderedCells();
	if (!CurrentProjection.IsStructurallyValid() || SceneRoot == nullptr)
	{
		return;
	}

	for (const FHyperTwistMelindaProjectedCell& CellProjection : CurrentProjection.Cells)
	{
		BuildProjectedCellRoot(CellProjection);
	}
}

void AHyperTwistMelindaProjectionActor::BuildProjectedCellRoot(
	const FHyperTwistMelindaProjectedCell& CellProjection
)
{
	if (SceneRoot == nullptr)
	{
		return;
	}

	const FName ComponentName = MakeUniqueObjectName(
		this,
		USceneComponent::StaticClass(),
		*FString::Printf(TEXT("MelindaCell_%s"), *CellProjection.CellLabel)
	);
	USceneComponent* CellRoot = NewObject<USceneComponent>(this, ComponentName, RF_Transactional);
	if (CellRoot == nullptr)
	{
		return;
	}

	AddInstanceComponent(CellRoot);
	CellRoot->RegisterComponent();
	CellRoot->AttachToComponent(SceneRoot, FAttachmentTransformRules::KeepRelativeTransform);
	CellRoot->SetRelativeLocation(CellProjection.DisplayCenter);
	SpawnedCellRoots.Add(CellRoot);

	for (const FHyperTwistMelindaProjectedCubie& CubieProjection : CellProjection.Cubies)
	{
		BuildProjectedCubie(CellRoot, CellProjection, CubieProjection);
	}
}

void AHyperTwistMelindaProjectionActor::BuildProjectedCubie(
	USceneComponent* CellRoot,
	const FHyperTwistMelindaProjectedCell& CellProjection,
	const FHyperTwistMelindaProjectedCubie& CubieProjection
)
{
	if (CellRoot == nullptr || CubieProjection.Faces.Num() != 3)
	{
		return;
	}

	const FName ComponentName = MakeUniqueObjectName(
		this,
		UProceduralMeshComponent::StaticClass(),
		*FString::Printf(
			TEXT("MelindaCubie_%s_%02d"),
			*CellProjection.CellLabel,
			CubieProjection.PositionIndex
		)
	);
	UProceduralMeshComponent* Mesh = NewObject<UProceduralMeshComponent>(
		this,
		ComponentName,
		RF_Transactional
	);
	if (Mesh == nullptr)
	{
		return;
	}

	AddInstanceComponent(Mesh);
	Mesh->RegisterComponent();
	Mesh->AttachToComponent(CellRoot, FAttachmentTransformRules::KeepRelativeTransform);
	Mesh->SetMobility(EComponentMobility::Movable);
	Mesh->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
	Mesh->SetCollisionObjectType(ECC_WorldDynamic);
	Mesh->SetCollisionResponseToAllChannels(ECR_Block);
	Mesh->bUseComplexAsSimpleCollision = true;

	const FVector Center = FVector(
		static_cast<float>(CubieProjection.LocalGridCoordinate.X),
		static_cast<float>(CubieProjection.LocalGridCoordinate.Y),
		static_cast<float>(CubieProjection.LocalGridCoordinate.Z)
	) * (CubieSize + CubieGap);
	const float HalfSize = CubieSize * 0.5f;

	FDisplayedCubieMetadata Metadata;
	Metadata.Cell = CellProjection.Cell;
	Metadata.LocalAxes.Reserve(3);
	for (int32 FaceIndex = 0; FaceIndex < CubieProjection.Faces.Num(); ++FaceIndex)
	{
		const FHyperTwistMelindaProjectedCubieFace& FaceProjection =
			CubieProjection.Faces[FaceIndex];
		Metadata.LocalAxes.Add(FaceProjection.Axis);
		CreateCubieFace(
			Mesh,
			FaceIndex,
			Center,
			HalfSize,
			FaceIndex,
			FaceProjection.bPositiveSide,
			ResolveStickerColor(FaceProjection.Color)
		);
	}

	DisplayedCubieMetadata.Add(Mesh, Metadata);
	SpawnedCubieMeshes.Add(Mesh);
}

void AHyperTwistMelindaProjectionActor::CreateCubieFace(
	UProceduralMeshComponent* Mesh,
	const int32 SectionIndex,
	const FVector& Center,
	const float HalfSize,
	const int32 LocalAxisIndex,
	const bool bPositiveSide,
	const FLinearColor& FaceColor
)
{
	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 (LocalAxisIndex)
	{
	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,
		true
	);

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

bool AHyperTwistMelindaProjectionActor::TryResolveLocalAxisFromImpactNormal(
	const FVector& ImpactNormal,
	int32& OutLocalAxisIndex
)
{
	const FVector Normal = ImpactNormal.GetSafeNormal();
	if (Normal.IsNearlyZero())
	{
		return false;
	}

	const FVector AbsNormal = Normal.GetAbs();
	if (AbsNormal.X >= AbsNormal.Y && AbsNormal.X >= AbsNormal.Z)
	{
		OutLocalAxisIndex = 0;
	}
	else if (AbsNormal.Y >= AbsNormal.Z)
	{
		OutLocalAxisIndex = 1;
	}
	else
	{
		OutLocalAxisIndex = 2;
	}

	return true;
}

FLinearColor AHyperTwistMelindaProjectionActor::ResolveStickerColor(
	const EHyperTwistMelindaStickerColor StickerColor
)
{
	switch (StickerColor)
	{
	case EHyperTwistMelindaStickerColor::XPositive:
		return FLinearColor(0.88f, 0.16f, 0.12f);
	case EHyperTwistMelindaStickerColor::XNegative:
		return FLinearColor(1.0f, 0.48f, 0.0f);
	case EHyperTwistMelindaStickerColor::YPositive:
		return FLinearColor(0.1f, 0.62f, 0.22f);
	case EHyperTwistMelindaStickerColor::YNegative:
		return FLinearColor(0.09f, 0.31f, 0.8f);
	case EHyperTwistMelindaStickerColor::ZPositive:
		return FLinearColor(0.95f, 0.95f, 0.95f);
	case EHyperTwistMelindaStickerColor::ZNegative:
		return FLinearColor(0.95f, 0.82f, 0.14f);
	case EHyperTwistMelindaStickerColor::WPositive:
		return FLinearColor(0.0f, 0.72f, 0.78f);
	case EHyperTwistMelindaStickerColor::WNegative:
		return FLinearColor(0.72f, 0.14f, 0.62f);
	default:
		return FLinearColor(0.12f, 0.12f, 0.12f);
	}
}
