#include "HyperTwistIPC/HyperTwistSolverOracleLibrary.h"
#include "HyperTwistSolverLibrary.h"
#include "ThirdParty/rob-twophase/cubie.h"
#include "ThirdParty/rob-twophase/face.h"
#include "ThirdParty/rob-twophase/move.h"
#include "Misc/Paths.h"
#include "Misc/FileHelper.h"
#include "HAL/PlatformFilemanager.h"
#include "UObject/Package.h"

namespace HyperTwistSolverOracleLibraryInternal
{
	FString GetPythonExecutableName()
	{
#if PLATFORM_WINDOWS
		return TEXT("python.exe");
#else
		return TEXT("python3");
#endif
	}

	bool TryResolveRobTwophaseMoveIndex(const FString& MoveString, int32& OutMoveIndex)
	{
		const FString TrimmedMove = MoveString.TrimStartAndEnd();
		for (int32 MoveIndex = 0; MoveIndex < move::COUNT; ++MoveIndex)
		{
			if (TrimmedMove.Equals(UTF8_TO_TCHAR(move::names[MoveIndex].c_str()), ESearchCase::IgnoreCase))
			{
				OutMoveIndex = MoveIndex;
				return true;
			}
		}

		return false;
	}

	bool TryReplayMoveSequence(
		const FString& FaceletString,
		const TArray<FString>& Moves,
		cubie::cube& OutCube
	)
	{
		if (!UHyperTwistSolverLibrary::InitializeSolver() || !UHyperTwistSolverLibrary::VerifyFaceletString(FaceletString))
		{
			return false;
		}

		std::string Facelets(TCHAR_TO_UTF8(*FaceletString));
		if (face::to_cubie(Facelets, OutCube) != 0 || cubie::check_cube(OutCube) != 0)
		{
			return false;
		}

		for (const FString& MoveString : Moves)
		{
			int32 MoveIndex = INDEX_NONE;
			if (!TryResolveRobTwophaseMoveIndex(MoveString, MoveIndex))
			{
				return false;
			}

			cubie::cube NextCube;
			cubie::mul(OutCube, move::cubes[MoveIndex], NextCube);
			OutCube = NextCube;
		}

		return cubie::check_cube(OutCube) == 0;
	}
}

FString UHyperTwistSolverOracleLibrary::GetBrownanSolverPath()
{
    return FPaths::ConvertRelativePathToFull(
        FPaths::ProjectDir() / TEXT("Binaries/Win64/Solvers/brownan-solver.exe"));
}

FString UHyperTwistSolverOracleLibrary::GetBrownanWrapperPath()
{
    return FPaths::ConvertRelativePathToFull(
        FPaths::ProjectDir() / TEXT("Binaries/Win64/Solvers/brownan_solver_wrapper.py"));
}

FString UHyperTwistSolverOracleLibrary::GetCahidenesWrapperPath()
{
    return FPaths::ConvertRelativePathToFull(
        FPaths::ProjectDir() / TEXT("Binaries/Win64/Solvers/cahidenes_solver_wrapper.py"));
}

bool UHyperTwistSolverOracleLibrary::AreSolverOraclesAvailable()
{
    return FPaths::FileExists(GetBrownanSolverPath())
        && FPaths::FileExists(GetBrownanWrapperPath())
        && FPaths::FileExists(GetCahidenesWrapperPath());
}

TArray<FString> UHyperTwistSolverOracleLibrary::SolveWithBrownanOracle(const FString& FaceletString)
{
    TArray<FString> EmptyResult;
    FString WrapperPath = GetBrownanWrapperPath();
    if (!FPaths::FileExists(WrapperPath))
    {
        UE_LOG(LogTemp, Warning, TEXT("Brownan wrapper not found at %s"), *WrapperPath);
        return EmptyResult;
    }

    UHyperTwistIPCProcessManager* IPC = UHyperTwistIPCProcessManager::CreateIPCManager(
        GetTransientPackage());

    FHyperTwistIPCProcessOptions Options;
    Options.ExecutablePath = HyperTwistSolverOracleLibraryInternal::GetPythonExecutableName();
    Options.Arguments.Add(WrapperPath);
    Options.Arguments.Add(FaceletString);
    Options.WorkingDirectory = FPaths::GetPath(WrapperPath);
    Options.TimeoutSeconds = 60.0f;

    FHyperTwistIPCResult Result = IPC->RunProcess(Options);
    if (!Result.bSuccess)
    {
        const FString FailureDetails = Result.ErrorMessage.IsEmpty() ? Result.StdOut : Result.ErrorMessage;
        UE_LOG(LogTemp, Warning, TEXT("Brownan solver failed: %s"), *FailureDetails);
        return EmptyResult;
    }

    FString Output = Result.StdOut.TrimStartAndEnd();
    if (Output.IsEmpty())
    {
        UE_LOG(LogTemp, Warning, TEXT("Brownan solver returned no parseable solution output for state '%s'."), *FaceletString);
        return EmptyResult;
    }

    TArray<FString> Moves;
    Output.ParseIntoArrayWS(Moves);
    return Moves;
}

bool UHyperTwistSolverOracleLibrary::VerifySolutionWithBrownanOracle(const FString& CubeState, const TArray<FString>& Moves)
{
    cubie::cube FinalCube;
    return HyperTwistSolverOracleLibraryInternal::TryReplayMoveSequence(CubeState, Moves, FinalCube)
        && FinalCube == cubie::SOLVED_CUBE;
}

TArray<FString> UHyperTwistSolverOracleLibrary::SolveWithCahidenesOracle(const FString& FaceletString)
{
    TArray<FString> EmptyResult;
    FString WrapperPath = GetCahidenesWrapperPath();
    if (!FPaths::FileExists(WrapperPath))
    {
        UE_LOG(LogTemp, Warning, TEXT("Cahidenes wrapper not found at %s"), *WrapperPath);
        return EmptyResult;
    }

    UHyperTwistIPCProcessManager* IPC = UHyperTwistIPCProcessManager::CreateIPCManager(
        GetTransientPackage());

    FHyperTwistIPCProcessOptions Options;
    Options.ExecutablePath = HyperTwistSolverOracleLibraryInternal::GetPythonExecutableName();
    Options.Arguments.Add(WrapperPath);
    Options.Arguments.Add(FaceletString);
    Options.WorkingDirectory = FPaths::GetPath(WrapperPath);
    Options.TimeoutSeconds = 30.0f;

    FHyperTwistIPCResult Result = IPC->RunProcess(Options);
    if (!Result.bSuccess)
    {
        const FString FailureDetails = Result.ErrorMessage.IsEmpty() ? Result.StdOut : Result.ErrorMessage;
        UE_LOG(LogTemp, Warning, TEXT("Cahidenes solver failed: %s"), *FailureDetails);
        return EmptyResult;
    }

    FString Output = Result.StdOut.TrimStartAndEnd();
    if (Output.IsEmpty())
    {
        UE_LOG(LogTemp, Warning, TEXT("Cahidenes solver returned no parseable solution output for state '%s'."), *FaceletString);
        return EmptyResult;
    }

    TArray<FString> Moves;
    Output.ParseIntoArrayWS(Moves);
    return Moves;
}

bool UHyperTwistSolverOracleLibrary::CompareSolvers(const FString& FaceletString)
{
    if (!UHyperTwistSolverLibrary::VerifyFaceletString(FaceletString))
    {
        return false;
    }

    // rob-twophase result
    TArray<FString> RobTwophaseMoves = UHyperTwistSolverLibrary::SolveClassicState(FaceletString, 5000, 25, 1);

    // cahidenes result
    TArray<FString> CahidenesMoves = SolveWithCahidenesOracle(FaceletString);

    // brownan result
    TArray<FString> BrownanMoves = SolveWithBrownanOracle(FaceletString);

    const int32 RobCount = RobTwophaseMoves.Num();
    const int32 CahCount = CahidenesMoves.Num();
    const int32 BrownCount = BrownanMoves.Num();

    if (RobCount <= 0 || CahCount <= 0 || BrownCount <= 0)
    {
        return false;
    }

    if (!VerifySolutionWithBrownanOracle(FaceletString, RobTwophaseMoves)
        || !VerifySolutionWithBrownanOracle(FaceletString, CahidenesMoves)
        || !VerifySolutionWithBrownanOracle(FaceletString, BrownanMoves))
    {
        return false;
    }

    return RobCount == CahCount && CahCount == BrownCount;
}
