// Project Lab - NHTV Igad

#include "UnrealProject.h"
#include "DefaultGameMode.h"
#include "DefaultGameState.h"
#include "DefaultGameInstance.h"
#include "PlayerSpawn.h"
#include "NetworkPlayer.h"
#include "CreatureSpawn.h"
#include "DefaultPlayer.h"
#include "DefaultPlayerController.h"
#include "DefaultPlayerState.h"
#include "GameStateBase.h"
#include "SessionManager.h"

#if PLATFORM_SPECIFIC_WIN == 0
#include "HeatMapMetrics.h"
#endif

#if UE_INCLUDE_METRICS
#include "MiniMapVolume.h"
#include "IngameHUD.h"
#include "MiniMapWidget.h"
#endif

ADefaultGameMode::ADefaultGameMode()
{
	PrimaryActorTick.bCanEverTick = true;

	// Lobby system starts the match
	bDelayedStart = true;

	PlayerControllerClass = ADefaultPlayerController::StaticClass();
	DefaultPawnClass = ConstructorHelpers::FClassFinder<APawn>(TEXT("/Game/Assets/Blueprints/BP_DefaultPlayer")).Class;
	SpectatorClass = ConstructorHelpers::FClassFinder<ASpectatorPawn>(TEXT("/Game/Assets/Blueprints/BP_SpectatorPawn")).Class;

	GameStateClass = ADefaultGameState::StaticClass();
	PlayerStateClass = ADefaultPlayerState::StaticClass();
}

void ADefaultGameMode::PreInitializeComponents()
{
	Super::PreInitializeComponents();

	ADefaultGameState* gameState = GetGameState<ADefaultGameState>();
	check(gameState);
	m_playerSpawns.AddZeroed(4);
	//FString str = UGameplayStatics::ParseOption(OptionsString, FString("teamCount"));
	//int32 teamCountSetting = FCString::Atoi(*str);
	//if(teamCountSetting > 1)
	//{
	//	gameState->m_mapTeamCount = teamCountSetting;
	//	GWPRINT(L"Set map team count to " + gameState->m_mapTeamCount);
	//}
}

void ADefaultGameMode::InitGame(const FString& MapName, const FString& Options, FString& ErrorMessage)
{
	Super::InitGame(MapName, Options, ErrorMessage);

	// Start game immediately in editor since we can't use seamless travel to obtain the players
	UWorld* world = GetWorld();
	if(world->WorldType == EWorldType::PIE)
	{
		m_StartGameStartTimer();
	}

	// Find lobby state blueprint 
	for(TActorIterator<ALobbyState> it(world); it; ++it)
	{
		m_lobbyState = *it;
	}

	if(m_lobbyState)
	{
		GPRINT("Received traveled lobby state " + m_lobbyState->GetName());
		m_totalPlayerCount = m_lobbyState->players.Num();
	}
	else
	{
		m_totalPlayerCount = 1;
	}
}
void ADefaultGameMode::BeginPlay()
{
	Super::BeginPlay();

#if PLATFORM_SPECIFIC_WIN == 0
	HeatMapMetrics::ResetMap();
#endif
}
void ADefaultGameMode::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
#if UE_INCLUDE_METRICS
	if (Metrics::EndSession())
		JPRINT("Metrics session ended");
	else
		JERROR("Failed to close metrics session");
#endif

	Super::EndPlay(EndPlayReason);

#if PLATFORM_SPECIFIC_WIN == 0
	HeatMapMetrics::ExportToFile();
#endif
}

void ADefaultGameMode::PostSeamlessTravel()
{
	GPRINT("PostSeamlessTravel");
	Super::PostSeamlessTravel();
}

void ADefaultGameMode::HandleSeamlessTravelPlayer(AController*& C)
{
	Super::HandleSeamlessTravelPlayer(C);
	
	UDefaultGameInstance* inst = Cast<UDefaultGameInstance>(GetGameInstance());

	// Register new player
	ADefaultPlayerController* newPC = Cast<ADefaultPlayerController>(C);
	if(!newPC)
	{
		GERROR("Invalid player genereted by HandleSeamlessTravelPlayer");
	}
	else
	{
		GPRINT("Transfering logged in player " + C->GetName());
		AGameStateBase* gameState = Cast<AGameStateBase>(GameState);
		gameState->RegisterPlayer(newPC);
	}

	TSharedPtr<const FUniqueNetId> netID = newPC->PlayerState->UniqueId.GetUniqueNetId();
	if(m_lobbyState)
	{
		m_lobbyState->players.RemoveAll([netID](FLobbyStatePlayer& item) 
		{
			return item.netID.IsValid() && netID.IsValid() && (*item.netID == *netID);
		});
	}

	m_numTravelledPlayers++;
	if(m_numTravelledPlayers >= m_totalPlayerCount)
	{
		// Set a timer to start the game
		m_StartGameStartTimer();
	}
	else
	{
		int32 left = m_totalPlayerCount - m_numTravelledPlayers;
		GPRINT("Still waiting for " + left + " more players");
		for(auto& p : m_lobbyState->players)
		{
			FString name = inst->sessionManager->GetPlayerName(*p.netID);
			GPRINT("--" + name + " [" + p.netID->ToDebugString() + "]");
		}
	}
}

void ADefaultGameMode::PreLogin(const FString& Options, const FString& Address, const TSharedPtr<const FUniqueNetId>& UniqueId, FString& ErrorMessage)
{
	Super::PreLogin(Options, Address, UniqueId, ErrorMessage);
	FString username = "Invalid";

	if(GameState->GetMatchState() == MatchState::InProgress)
	{
		// Check if player is reconnecting but was previously connected
		FString uniqueIDString = UniqueId->ToString();
		if (m_connectedPlayersNetID.Contains(uniqueIDString))
		{
			GWPRINT(L"Reconnecting player " + username + " / " + uniqueIDString);
		}
		else 
		{
			ErrorMessage = "Game has already started";
		}
	}
}
APlayerController* ADefaultGameMode::Login(class UPlayer* NewPlayer, ENetRole InRemoteRole, const FString& Portal, const FString& Options, const TSharedPtr<const FUniqueNetId>& UniqueId, FString& ErrorMessage)
{
	// Register players that ever connected to the game so they can reconnect
	APlayerController* controller = Super::Login(NewPlayer, InRemoteRole, Portal, Options, UniqueId, ErrorMessage);
	if (controller)
	{
		FString uniqueIDString = UniqueId->ToString();
		m_connectedPlayersNetID.Add(uniqueIDString);
		GWPRINT(L"Registering connected player ID " + uniqueIDString);
	}

	// Register logged on player
	// Note: this should only happen when running a PIE instance or standalone with a startup level
	//		normal login procedure happens in the menu and players are then transfered to the game using seamless travel
	ADefaultPlayerController* defPC = Cast<ADefaultPlayerController>(controller);
	if(!defPC)
	{
		GERROR("Invalid player genereted by HandleSeamlessTravelPlayer");
	}
	else
	{
		GPRINT("Logging in new player " + defPC->GetName());
		AGameStateBase* gameState = Cast<AGameStateBase>(GameState);
		gameState->RegisterPlayer(defPC);
	}

	return controller;
}

void ADefaultGameMode::PostLogin(APlayerController* NewPlayer)
{
	Super::PostLogin(NewPlayer);

	// Auto assign team when in PIE
	UWorld* world = GetWorld();
	if(world->WorldType == EWorldType::PIE)
	{
		AGameStateBase* gameState = Cast<AGameStateBase>(GameState);
		APlayerStateBase* state = Cast<APlayerStateBase>(NewPlayer->PlayerState);
		state->AutoAssignTeam();
	}
}
void ADefaultGameMode::Logout(AController* Exiting)
{
	GWPRINT(L"Player left the game");

	// Unregister players if not playing
	if (GameState->GetMatchState() != MatchState::InProgress)
	{
		if (Exiting->PlayerState)
		{
			FString uniqueIDString = Exiting->PlayerState->UniqueId->ToString();
			GWPRINT(L"Removing connected player ID " + uniqueIDString);
			m_connectedPlayersNetID.Remove(uniqueIDString);
		}
	}
	Super::Logout(Exiting);
}

void ADefaultGameMode::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
	m_gameTimer += DeltaTime;
}

void ADefaultGameMode::HandleMatchHasStarted()
{
	GWPRINT(L"Match Started");
	GetGameState<ADefaultGameState>()->gameEnded = false;

	// Assign team mates
	ADefaultGameState* gameState = GetWorld()->GetGameState<ADefaultGameState>();
	TArray<TArray<ADefaultPlayerState*>> playersByTeam = gameState->GetPlayersByTeam<ADefaultPlayerState>();

	for (int32 i = 0; i < playersByTeam.Num(); i++)
	{
		auto& arr = playersByTeam[i];
		if (arr.Num() == 2)
		{
			arr[0]->teamMate = arr[1];
			arr[1]->teamMate = arr[0];
		}
	}

	Super::HandleMatchHasStarted();
}
void ADefaultGameMode::HandleMatchHasEnded()
{
	GWPRINT(L"Match Ended");
	GetGameState<ADefaultGameState>()->gameEnded = true;
	Super::HandleMatchHasStarted();
}

void ADefaultGameMode::RegisterPlayerSpawn(APlayerSpawn& spawn)
{
	if (spawn.assignedTeam > m_playerSpawns.Num() - 1)
		m_playerSpawns.SetNumZeroed(spawn.assignedTeam + 1);

	m_playerSpawns[spawn.assignedTeam] = &spawn;
}

void ADefaultGameMode::RegisterPlayer(class ANetworkPlayer& player)
{
	if(m_players.Contains(&player))
	{
		GWARNING("Player already registered with game mode " + player.GetName());
		return;
	}

	m_players.Add(&player);
}
void ADefaultGameMode::UnregisterPlayer(class ANetworkPlayer& player)
{
	m_players.Remove(&player);
}

void ADefaultGameMode::StartGame()
{
#if UE_INCLUDE_METRICS
	// Start a new metrics session, and output the file in the Metrics folder
	FString filePath = FPaths::GameDir() + FString(L"Metrics/");
	if (Metrics::StartSession(filePath.GetCharArray().GetData()))
	{
		// Retrieve the active map size
		for (TActorIterator<AMiniMapVolume> iter(GetWorld()); iter; ++iter)
		{
			AMiniMapVolume* const volume = *iter;
			volume->SetActorRotation(FRotator(0, 0, 0));
			FVector area = volume->area->GetScaledBoxExtent();
			const FVector pos = volume->GetActorLocation();
			FVector2D min, max;
			min.X = -area.X + pos.X;
			min.Y = -area.Y + pos.Y;
			max.X = area.X + pos.X;
			max.Y = area.Y + pos.Y;
			Metrics::SetMapData(min.X, max.X, min.Y, max.Y);
			break;
		}
		JPRINT("New metrics session started");
	}
	else
		JERROR("Failed to open new metrics session");
#endif

	GPRINT("Starting Game");
	SetMatchState(MatchState::InProgress);
	m_gameTimer = 0;
}

APlayerSpawn* ADefaultGameMode::GetOptimalSpawn(int32 team)
{
	check(team > 0);

	if (m_playerSpawns.Num() == 0)
		return nullptr;
	if (m_playerSpawns.Num() == 1)
		return m_playerSpawns[0];

	if (team - 1 < m_playerSpawns.Num())
		return m_playerSpawns[team - 1];
	
	// Either no players yet, or something went wrong
	// Pick random spawn
	return m_playerSpawns[FMath::Rand() % m_playerSpawns.Num()];
}

TArray<class ANetworkPlayer*> ADefaultGameMode::GetPlayers()
{
	return m_players;
}

void ADefaultGameMode::BossHasBeenKilled(int32 team)
{
	TArray<class ANetworkCharacter*> players(m_players);

	// Set winning team in gamestate (replicated)
	GetGameState<ADefaultGameState>()->gameWinningTeam = team;

	EndMatch();

	// Destroy all player pawns so that they respawn as spectators
	for (int32 i = 0; i < players.Num(); i++)
	{
		players[i]->Destroy(true);
	}

	TPRINT("Boss was killed. (ADefaultGameMode) the team = " + team);
}

float ADefaultGameMode::GetGameTime()
{
	return m_gameTimer;
}

void ADefaultGameMode::m_StartGameStartTimer()
{
	GPRINT("Starting game in 2 seconds");
	GetWorld()->GetTimerManager().SetTimer(m_gameStartTimer, this, &ADefaultGameMode::m_GameStart, 2.0f, false);
}
void ADefaultGameMode::m_GameStart()
{
	StartGame();
}
