// Project Lab - NHTV Igad

#include "UnrealProject.h"
#include "DefaultGameInstance.h"
#include "MenuController.h"
#include "LobbySpawn.h"
#include "GameStateBase.h"
#include "PlayerStateBase.h"
#include "PlayerControllerBase.h"
#include "CharacterBase.h"
#include "LobbyCharacterSelect.h"
#include "MenuGameMode.h"

void ULobbyCharacterSelect::m_OnPlayerJoined(APlayerController* pc)
{
	for(auto it = m_visualCharacters.CreateIterator(); it; ++it)
	{
		(it.Value()).character->SendInitialAppearance(pc);
	}
}

void ULobbyCharacterSelect::NativeConstruct()
{
	Super::NativeConstruct();

	// Add GameMode callback
	AMenuGameMode* gm = Cast<AMenuGameMode>(GetWorld()->GetAuthGameMode());
	if(gm)
	{
		m_onPlayerJoinedDH = gm->onPlayerJoined.AddUObject(this, &ULobbyCharacterSelect::m_OnPlayerJoined);
	}

	UWorld* const world = GetWorld();
	for (TActorIterator<ALobbySpawn> iter(world); iter; ++iter)
	{
		TArray<ALobbySpawn*>* find = m_spawnPoints.Find(iter->assignedTeam);
		if (!find)
		{
			TArray<ALobbySpawn*> arr;
			arr.Add(*iter);
			m_spawnPoints.Add(iter->assignedTeam, arr);
		}
		else
			find->Add(*iter);
	}

	m_requireUpdate = false;
	m_updateInterval = 0;

	AGameStateBase* gameState = Cast<AGameStateBase>(world->GetGameState());
	check(gameState);
	gameState->OnPlayerStateChange.AddDynamic(this, &ULobbyCharacterSelect::UpdateVisualCharacters);
}
void ULobbyCharacterSelect::NativeDestruct()
{
	Super::NativeDestruct();

	// Remove GameMode Callback
	AMenuGameMode* gm = Cast<AMenuGameMode>(GetWorld()->GetAuthGameMode());
	if(gm)
	{
		gm->onPlayerJoined.Remove(m_onPlayerJoinedDH);
	}

	UWorld* const world = GetWorld();
	check(world);
	AGameStateBase* gameState = Cast<AGameStateBase>(world->GetGameState());
	check(gameState);
	gameState->OnPlayerStateChange.RemoveAll(this);
}
void ULobbyCharacterSelect::NativeTick(const FGeometry& MyGeometry, float InDeltaTime)
{
	// The updating is called once per frame
	// This is to prevent double calls when two players leave in one cycle

	// Ensure that only the server runs this
	if (!GetOwningPlayer()->HasAuthority())
		return;

	// No need to update this frame
	if (!m_requireUpdate)
		return;

	// Await the interval
	m_updateInterval -= InDeltaTime;
	if (m_updateInterval > 0)
		return;

	m_requireUpdate = false;
	m_updateInterval = 0.3f;

	UWorld* world = GetWorld();
	check(world);
	AGameStateBase* gameState = Cast<AGameStateBase>(world->GetGameState());
	check(gameState);
	TArray<int32> teamSizes = gameState->GetTeamSizes();
	TArray<TArray<APlayerStateBase*>> playersPerTeam = gameState->GetPlayersByTeam();

	// Check if players have left
	TMap<APlayerStateBase*, VisualSlot> outdatedCharacters = m_visualCharacters;
	TArray<APlayerStateBase*> players = gameState->GetPlayers();
	for (int32 i = 0; i < players.Num(); i++)
	{
		VisualSlot* find = outdatedCharacters.Find(players[i]);
		if (find)
			outdatedCharacters.Remove(players[i]);
	}
	for (auto iter = outdatedCharacters.CreateIterator(); iter; ++iter)
	{
		if (IsValid(iter->Value.character))
			iter->Value.character->Destroy();
		m_visualCharacters.Remove(iter->Key);
	}

	// Check if players switched team
	TMap<APlayerStateBase*, ACharacterBase*> newCharacters;
	TMap<APlayerStateBase*, VisualSlot> newMap;
	for (int32 i = 1; i < playersPerTeam.Num(); i++)
	{
		auto& set = playersPerTeam[i];
		for (int32 j = 0; j < set.Num(); j++)
		{
			APlayerStateBase* playerState = set[j];

			VisualSlot* find = m_visualCharacters.Find(playerState);
			const int32 team = i - 1;
			if (!find || find->team != team || find->slot != j)
			{
				// Fetch the spawn coordinates if the spawnpoint exists
				FVector position;
				FRotator rotation;
				if (team < m_spawnPoints.Num() && j < m_spawnPoints[team].Num())
				{
					position = m_spawnPoints[team][j]->GetActorLocation();
					rotation = m_spawnPoints[team][j]->GetActorRotation();
				}

				// Check if we need to create a new character, else we just repurpose the previous character (like when the player switched slot)
				ACharacterBase* character;
				if (!find || !IsValid(find->character))
				{
					FActorSpawnParameters params;
					params.bNoFail = true;
					params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
					character = GetWorld()->SpawnActor<ACharacterBase>(characterBlueprint, FTransform::Identity, params);
					if (!IsValid(character))
					{
						JERROR("Failed to spawn Actor");
						return;
					}
				}
				else
				{
					find->character->ClearEquipment();
					character = find->character;
				}

				// Set and add
				character->SetActorLocation(position);
				character->SetActorRotation(rotation.Quaternion());
				newMap.Add(playerState, VisualSlot(team, j, character));
				newCharacters.Add(playerState, character);
			}
			else
				newMap.Add(playerState, *find);
		}
	}
	m_visualCharacters = newMap;

	// Apply the customizations
	TMap<APlayerControllerBase*, APlayerStateBase*> controllers = gameState->GetPlayersByController();
	for (auto iter = controllers.CreateIterator(); iter; ++iter)
	{
		ACharacterBase** character = newCharacters.Find(iter->Value);
		if (!character || !IsValid(*character)) continue;
		(*character)->SetCustomizations(iter->Key->setupState.customizations);
		(*character)->EquipSkillsFromSkillTreeState(iter->Key->setupState.skills);

		// Set class specific equipment
		if(characterClassProperties)
		{
			FCharacterClassProperty properties = characterClassProperties->GetCharacterClass(iter->Key->setupState.characterClass);
			(*character)->EquipItems(properties.classItems);
		}
	}
}

void ULobbyCharacterSelect::Init()
{
	UWorld* const world = GetWorld();
	check(world);
	UDefaultGameInstance* const instance = Cast<UDefaultGameInstance>(world->GetGameInstance());
	check(instance);
	UCharacterSettings* const settings = instance->GetCharacterSettings();
	check(settings);

	// Fetch the characters from the save
	check(settings->characterSaves.Num() > 0);
	m_playerCharacterSaves = settings->GetValidCharacters();

	m_selection = 0;

	m_ApplyCharacter();
}

void ULobbyCharacterSelect::OnNextCharacter()
{
	if (m_selection == m_playerCharacterSaves.Num() - 1)
		return;
	m_selection++;

	m_ApplyCharacter();
}
void ULobbyCharacterSelect::OnPreviousCharacter()
{
	if (m_selection == 0)
		return;
	m_selection--;

	m_ApplyCharacter();
}

void ULobbyCharacterSelect::UpdateCharacter_Implementation(FCharacterSave save)
{
	// Blueprint implementation
}

void ULobbyCharacterSelect::m_ApplyCharacter()
{
	UpdateCharacter(m_playerCharacterSaves[m_selection]);

	// Apply the selected character to the player controller
	UWorld* const world = GetWorld();
	check(world);
	AMenuController* const controller = Cast<AMenuController>(world->GetFirstPlayerController());
	check(controller);

	FPlayerSetupState state;
	state.skills = m_playerCharacterSaves[m_selection].skillTreeState;
	state.customizations = m_playerCharacterSaves[m_selection].characterCustomization;
	state.characterClass = m_playerCharacterSaves[m_selection].characterClass;
	controller->SetSetupState(state);
}

void ULobbyCharacterSelect::UpdateVisualCharacters(APlayerControllerBase* playerController)
{
	if (!IsValid(characterBlueprint))
	{
		JERROR("Character blueprint not assigned");
		return;
	}
	if (!IsValid(playerController) && playerController != nullptr)
		return;

	// Ensure that only the server runs this
	if (!GetOwningPlayer()->HasAuthority())
		return;

	if (playerController != nullptr)
	{
		// Just update the visuals on the specific player
		APlayerStateBase* playerState = Cast<APlayerStateBase>(playerController->PlayerState);
		if (IsValid(playerState))
		{
			VisualSlot* slot = m_visualCharacters.Find(playerState);
			if (slot && IsValid(slot->character))
			{
				slot->character->SetCustomizations(playerController->setupState.customizations);
				slot->character->ClearEquipment();
				slot->character->EquipSkillsFromSkillTreeState(playerController->setupState.skills);

				// Set class specific equipment
				if(characterClassProperties)
				{
					FCharacterClassProperty properties = characterClassProperties->GetCharacterClass(playerController->setupState.characterClass);
					slot->character->EquipItems(properties.classItems);
				}
			}
			else // Failed to find the character, refresh all
				m_requireUpdate = true;
		}
	}
	else
		m_requireUpdate = true;
}