#include "VAServerLauncher.h"

#include <string>

#include "Misc/FileHelper.h"
#include "SocketSubsystem.h"
#include "GeneralProjectSettings.h"

#include "Utility/VirtualRealityUtilities.h"

#include "VAUtils.h"
#include "VASettings.h"
#include "VAPlugin.h"


bool FVAServerLauncher::RemoteStartVAServer(const FString& Host, const int Port, const FString& VersionName)
{
	if (!UVirtualRealityUtilities::IsMaster())
	{
		return false;
	}

	if (VAServerLauncherSocket != nullptr)
	{
		return true;
	}

	FVAUtils::LogStuff("[FVAServerLauncher::RemoteStartVAServer()]: Try to remotely start the VAServer at address " + 
		Host + ":" + FString::FromInt(Port) + " for version: " + VersionName, false);


	//Connect
	const FString SocketName(TEXT("VAServerStarterConnection"));
	VAServerLauncherSocket = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->CreateSocket(
		NAME_Stream, SocketName, false);

	TSharedPtr<FInternetAddr> InternetAddress = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->CreateInternetAddr();

	bool bValidIP;
	InternetAddress->SetIp(*Host, bValidIP);
	InternetAddress->SetPort(Port);

	if (!bValidIP)
	{
		FVAUtils::LogStuff("[FVAServerLauncher::RemoteStartVAServer()]: The Ip cannot be parsed!", true);
		return false;
	}

	if (VAServerLauncherSocket == nullptr || !VAServerLauncherSocket->Connect(*InternetAddress))
	{
		FVAUtils::LogStuff("[FVAServerLauncher::RemoteStartVAServer()]: Cannot connect to Launcher!", true);
		return false;
	}
	FVAUtils::LogStuff("[FVAServerLauncher::RemoteStartVAServer()]: Successfully connected to Launcher", false);

	//Send requested version
	TArray<uint8> RequestData = ConvertString(VersionName);	
	int BytesSend = 0;
	VAServerLauncherSocket->Send(RequestData.GetData(), RequestData.Num(), BytesSend);
	FVAUtils::LogStuff("[FVAServerLauncher::RemoteStartVAServer()]: Send " + FString::FromInt(BytesSend) + 
		" bytes to the VAServer Launcher, with version name: " + VersionName + " Waiting for answer.", false);

	//Receive response
	const int32 BufferSize = 16;
	int32 BytesRead = 0;
	uint8 Response[16];
	if (VAServerLauncherSocket->Recv(Response, BufferSize, BytesRead) && BytesRead == 1)
	{
		switch (Response[0])
		{
		case 'g':
			FVAUtils::LogStuff("[FVAServerLauncher::RemoteStartVAServer()]: Received go from launcher, VAServer seems to be correctly started.", false);
			break;
		case 'n':
			FVAUtils::OpenMessageBox("[FVAServerLauncher::RemoteStartVAServer()]: VAServer cannot be launched, invalid VAServer binary file or cannot be found",
			                        true);
			VAServerLauncherSocket = nullptr;
			return false;
		case 'i':
			FVAUtils::OpenMessageBox("[FVAServerLauncher::RemoteStartVAServer()]: VAServer cannot be launched, invalid file entry in the config", true);
			VAServerLauncherSocket = nullptr;
			return false;
		case 'a':
			FVAUtils::OpenMessageBox("[FVAServerLauncher::RemoteStartVAServer()]: VAServer was aborted", true);
			VAServerLauncherSocket = nullptr;
			return false;
		case 'f':
			FVAUtils::OpenMessageBox("[FVAServerLauncher::RemoteStartVAServer()]: VAServer cannot be launched, requested version \"" + 
				VersionName + "\" is not available/specified", true);
			VAServerLauncherSocket = nullptr;
			return false;
		default:
			FVAUtils::OpenMessageBox("[FVAServerLauncher::RemoteStartVAServer()]: Unexpected response from VAServer Launcher: " + 
				FString(reinterpret_cast<char*>(&Response[0])), true);
			VAServerLauncherSocket = nullptr;
			return false;
		}
	}
	else
	{
		FVAUtils::LogStuff("[FVAServerLauncher::RemoteStartVAServer()]: Error while receiving response from VAServer Launcher", true);
		VAServerLauncherSocket = nullptr;
		return false;
	}
	return true;
}

bool FVAServerLauncher::StartVAServerLauncher()
{
  //check whether we can also start the VSServer Launcher python script.

	if (!UVirtualRealityUtilities::IsMaster())
	{
		return false;
	}

  const UVASettings* Settings = GetDefault<UVASettings>();
  FString LauncherScriptDir = Settings->VALauncherPath;

  if(FPaths::IsRelative(LauncherScriptDir))
  {
    FString ProjectDir = FPaths::ProjectDir();
    ProjectDir = IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*ProjectDir);
    LauncherScriptDir = FPaths::ConvertRelativePathToFull(ProjectDir, LauncherScriptDir);
  }

  LauncherScriptDir = FPaths::Combine(LauncherScriptDir, TEXT("LaunchScript"));
  FString LauncherScript = TEXT("VirtualAcousticsStarterServer.py");
  if (FPaths::FileExists(FPaths::Combine(LauncherScriptDir, LauncherScript)))
  {
		FString CMDCommand = "cd/d " + LauncherScriptDir + " & ";

		//check whether py or python exist
		auto DoesCommandExist = [&](FString Command)
		{
			//this checks whether a given command returns a result
			FString TmpCmdResultFile = "tmpPyVersion.txt";
			TmpCmdResultFile = FPaths::Combine(LauncherScriptDir, TmpCmdResultFile);
			Command = Command + " >> " + TmpCmdResultFile;
			system(TCHAR_TO_ANSI(*Command));
			FString Result;
			FFileHelper::LoadFileToString(Result, *TmpCmdResultFile);
			IFileManager::Get().Delete(*TmpCmdResultFile);
			return !Result.IsEmpty();
		};

		bool bPyExists = DoesCommandExist("py --version");
		bool bPythonExists = DoesCommandExist("python --version");

		if(bPythonExists || bPyExists)
		{
			FString Command = CMDCommand + "start " + (bPythonExists?"python":"py") + " " + LauncherScript;
			system(TCHAR_TO_ANSI(*Command));
			return true;
		}
		else
		{
			FVAUtils::OpenMessageBox("VA Launcher cannot be started since neither \"py\" nor \"python\" can be found. If it is installed add it to PATH (and restart Visual Studio)", true);
			return false;
		}
  }
  else
  {
    FVAUtils::LogStuff("[FVAServerLauncher::StartVAServerLauncher] Unable to automatically start the launcher script, looked for "+LauncherScript+" at "+LauncherScriptDir+". If you want to use this convenience function change the VALauncher Path in the Engine/Virtual Acoustics(VA) section of the project settings. However, nothing bad will happen without.");
  }
  return false;
}

bool FVAServerLauncher::SendFileToVAServer(const FString& RelativeFilename)
{

	if (!UVirtualRealityUtilities::IsMaster())
	{
		return false;
	}
	
	if(VAServerLauncherSocket==nullptr)
	{
		FVAUtils::LogStuff("[FVAServerLauncher::SendFileToVAServer()]: No connection to VAServer Starter, so no files can be send to VAServer!", true);
		return false;
	}

	if(!GetDefault<UVASettings>()->VALauncherCopyFiles)
	{
		FVAUtils::LogStuff("[FVAServerLauncher::SendFileToVAServer()]: Setting to not send files over the network to VAServer is set, so not sending anything!", false);
		return false;
	}

	if(!FPaths::FileExists(FPaths::Combine(FPaths::ProjectContentDir(),RelativeFilename)))
	{
		FVAUtils::LogStuff("[FVAServerLauncher::SendFileToVAServer()]: File to send("+RelativeFilename+") could not be found and therefore not send!", true);
		return false;
	}

	TArray<uint8> FileBinaryArray;
   FFileHelper::LoadFileToArray(FileBinaryArray, *FPaths::Combine(FPaths::ProjectContentDir(),RelativeFilename));

	const FString ProjectName = GetDefault<UGeneralProjectSettings>()->ProjectName;
	FString MetaInfo = "file:"+RelativeFilename+":"+FString::FromInt(FileBinaryArray.Num())+":"+ProjectName;
	TArray<uint8> MetaInfoBinary = ConvertString(MetaInfo);

	int32 BytesSend;
	VAServerLauncherSocket->Send(MetaInfoBinary.GetData(), MetaInfoBinary.Num(), BytesSend);
	//Receive response
	const int32 BufferSize = 16;
	int32 BytesRead = 0;
	uint8 Response[16];
	if (VAServerLauncherSocket->Recv(Response, BufferSize, BytesRead))
	{
		FString ResponseString = ByteArrayToString(Response, BytesRead);
		if(ResponseString=="ack"){
			//VAServer waits for file
			int32 BytesAlreadySend = 0;
			while(BytesAlreadySend<FileBinaryArray.Num())
			{
				//send 1024 byte packages
				int32 BytesToSend = (FileBinaryArray.Num()-BytesAlreadySend>1024?1024:FileBinaryArray.Num()-BytesAlreadySend);
				VAServerLauncherSocket->Send(&FileBinaryArray[BytesAlreadySend],BytesToSend, BytesSend);
				BytesAlreadySend += BytesSend;
			}
			FVAUtils::LogStuff("[FVAServerLauncher::SendFileToVAServer()]: Entire file ("+RelativeFilename+") send!", false);
			VAServerLauncherSocket->Recv(Response, BufferSize, BytesRead);
			if(BytesRead==3 && Response[0]=='a' && Response[1]=='c' && Response[2]=='k')
			{
				FVAUtils::LogStuff("[FVAServerLauncher::SendFileToVAServer()]: File was received by VAServerLauncher successfully!", false);
				//the search path is added potenitally multiple times, but can only be added once the folder is created (which the above guarantees)
				const std::string SearchPath = "../tmp/" + std::string(TCHAR_TO_UTF8(*GetDefault<UGeneralProjectSettings>()->ProjectName));
				FVAPlugin::AddVAServerSearchPath(SearchPath);
			}
			else
			{
				FVAUtils::LogStuff("[FVAServerLauncher::SendFileToVAServer()]: File was NOT received by VAServerLauncher!", true);
				return false;
			}
		}
		else if (ResponseString=="exists"){
			FVAUtils::LogStuff("[FVAServerLauncher::SendFileToVAServer()]: File already exists with same size, no need ro re-send!", false);
		}
		else
		{
			FVAUtils::LogStuff("[FVAServerLauncher::SendFileToVAServer()]: Server Launcher does not want to receive a file, answer: "+ResponseString, true);
			return false;	
		}
	}
	else
	{
		FVAUtils::LogStuff("[FVAServerLauncher::SendFileToVAServer()]: Server Launcher does not want to receive a file, no answer!", true);
		return false;	
	}

	//the search path is added in any case once after connecting to a new server

	return true;
}

void FVAServerLauncher::ReleaseVAServerLauncherConnection()
{
	if (!UVirtualRealityUtilities::IsMaster())
	{
		return;
	}

	if(VAServerLauncherSocket!=nullptr){
		VAServerLauncherSocket->Close();
		VAServerLauncherSocket = nullptr;
	}
}

bool FVAServerLauncher::IsVAServerLauncherConnected()
{
	return VAServerLauncherSocket!=nullptr;
}

TArray<uint8> FVAServerLauncher::ConvertString(const FString& String)
{
	TArray<uint8> RequestData;
	for (TCHAR Character : String.GetCharArray())
	{
		const uint8 InByte = static_cast<uint8>(Character);
		if (InByte != 0)
		{
			RequestData.Add(static_cast<uint8>(Character));
		}
	}
	return RequestData;
}

FString FVAServerLauncher::ByteArrayToString(const uint8* In, int32 Count)
{
	FString Result;
	Result.Empty(Count);

	while (Count)
	{
		// Put the byte into an int16
		int16 Value = *In;

		Result += TCHAR(Value);

		++In;
		Count--;
	}
	return Result;
}