// Project Lab - NHTV Igad

#include "UnrealProject.h"
#include "NetworkSwitchComponent.h"
#include "CreatureSpawnComponent.h"
#include "DefaultGameMode.h"
#include "SpawnerBase.h"
#include "NPCBase.h"
#include "NetworkCharacter.h"
#include "NetworkPlayer.h"
#include "EnemyBase.h"
#include "GeneralEnemy.h"
#include "MiniBossCreature.h"


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

	USceneComponent* spawnerRoot = CreateDefaultSubobject<USceneComponent>("Spawner Root");
	RootComponent = spawnerRoot;

	displayMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
	displayMesh->bHiddenInGame = true;
	displayMesh->bGenerateOverlapEvents = false;
	displayMesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);
	displayMesh->AttachTo(RootComponent);

	// Kill particle effect
	killParticle = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("Kill Particle1"));
	killParticle->AttachTo(RootComponent);

	displayArrow = CreateDefaultSubobject<UArrowComponent>(TEXT("Arrow"));
	displayArrow->AttachTo(displayMesh);

	displayDoors = CreateDefaultSubobject<UNetworkSwitchComponent>(TEXT("Visualizer"));
	displayDoors->AttachTo(displayMesh);

	visualizerComponent = CreateDefaultSubobject<UCreatureSpawnComponent>(TEXT("Control points visualizer"));

	visualizerComponent->AttachTo(displayMesh);

	team = NPCTeam::Team1;
	aggroRadius = 800;
	deaggroRadius = aggroRadius;
	m_mobCount = 0;
	formationScale = 1.0f;
	formationDistance = 100;
	formationRadius = 100;
	resetTimer = 30;
	dropKeyFragmentIndex = 0;
	hasDroppedKey = false;

	bReplicates = true;
	bAlwaysRelevant = true;
}

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

	TArray<AActor*> test;
	GetAttachedActors(test);
	for (int i = 0; i < test.Num(); i++)
	{

		if (Cast<ANPCBase>(test[i]))
		{
			spawns.Add(test[i]->GetClass());
			formationPoints.Add(FVector2D(test[i]->GetActorLocation() - GetActorLocation()));
			formationRotation.Add(test[i]->GetActorRotation().Yaw);
			//RPRINT("mobs" + spawns[i]->GetName());
			if (Role == ROLE_Authority)
			{
				Cast<ANPCBase>(test[i])->UnsetSpawn();
			}
			(test[i])->Destroy();
			(test[i]) = nullptr;
		}
		else
		{
			SpawnLocation.Add(test[i]);

		}

	}
	for (int32 i = 0; i < spawns.Num(); i++)
		m_mobs.Add(nullptr);

	m_threatMap.SetNum(m_mobs.Num());
	for (int i = 0; i < spawns.Num(); i++)
	{
		if (i != 0 && spawns[i]->IsChildOf(AGeneralEnemy::StaticClass()))
		{
			Swap(spawns[i], spawns[0]);
			Swap(formationPoints[i], formationPoints[0]);
			Swap(formationRotation[i], formationRotation[0]);
		}
	}
	TArray<FHitResult> hitResult;
	GetWorld()->LineTraceMultiByChannel(hitResult, GetActorLocation(), GetActorLocation() + GetActorUpVector()*-10000.0f, ECollisionChannel::ECC_WorldStatic, FCollisionQueryParams(FName(L"spawnerPlacementTrace"),true));
	bool hit = false;
	float distance = BIG_NUMBER;
	FVector impactLocation;
	for (int i = 0; i < hitResult.Num(); i++)
	{
		if (FVector::Dist(FVector(hitResult[i].ImpactPoint), GetActorLocation()) < distance&&FVector(hitResult[i].ImpactPoint)!= GetActorLocation())
		{
			distance = FVector::Dist(FVector(hitResult[i].ImpactPoint), GetActorLocation());
			impactLocation = FVector(hitResult[i].ImpactPoint);
			hit = true;
		}
	}

	// Correct spawner position when it can move to the ground below it
	if (hit)
	{
		this->SetActorLocation(FVector(GetActorLocation().X, GetActorLocation().Y, impactLocation.Z));
	}

	for (int i = 0; i < formationPoints.Num(); i++)
	{
		formationPoints[i] = FVector2D(GetActorRotation().RotateVector(FVector(formationPoints[i].X, formationPoints[i].Y, 0)));
		formationPoints[i] *= formationScale;
	}
	if (spawns.Num() > formationPoints.Num())
	{
		int difference = spawns.Num() - formationPoints.Num();
		for (int i = 0; i < difference; i++)
		{
			formationPoints.Add(FVector2D());
		}
	}
	if (spawns.Num() > formationRotation.Num())
	{
		int difference = spawns.Num() - formationRotation.Num();
		for (int i = 0; i < difference; i++)
		{
			formationRotation.Add(0);
		}
	}

}

void ASpawnerBase::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
	Super::EndPlay(EndPlayReason);

	if (Role != ROLE_Authority)
		return;

	for (int32 i = 0; i < m_mobs.Num(); i++)
	{
		if (m_mobs[i])
		{
			m_mobs[i]->UnsetSpawn();
			m_mobs[i] = nullptr;
		}
	}
	formationEnemies.Empty();
	m_nearbyPlayers.clear();
}

void ASpawnerBase::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
//	if (formationEnemies.Num() > spawns.Num())
//		formationEnemies.Empty();
	bool reset = false;
	m_resetTimer -= DeltaTime;
	if (m_resetTimer <= 0)
	{
		reset = true;
	}
	for (int i = 0; i < m_mobs.Num(); i++)
	{
		if (m_mobs[i] == nullptr)
			continue;
		if (reset)
		{
			ForceResetTarget(m_mobs[i]);
		}
		if (m_mobs[i]->IsA(AEnemyBase::StaticClass()))
		{
			Cast<AEnemyBase>(m_mobs[i])->hasGeneral = hasGeneral;
		}
		if (m_mobs[i]->IsA(AGeneralEnemy::StaticClass()))
		{
			AGeneralEnemy* asGeneral = Cast<AGeneralEnemy>(m_mobs[i]);
			if (formationEnemies != asGeneral->formationEnemies)
			{
				asGeneral->formationEnemies = formationEnemies;
			}
		}
	}
}

int32 ASpawnerBase::OnMobDie(class ANPCBase* mob)
{
	check(m_mobCount > 0);
	check(Role == ROLE_Authority);

	int32 idx = -1;
	for (int32 i = 0; i < spawns.Num(); i++)
	{
		if (m_mobs[i] == mob)
		{
			if (m_mobs[i]->IsA(AGeneralEnemy::StaticClass()))
			{
				formationEnemies.Empty();
			}
			if (m_mobs[i]->IsA(AEnemyBase::StaticClass()))
			{
				AEnemyBase* asEnemyBase = Cast<AEnemyBase>(m_mobs[i]);
				if (asEnemyBase->hasGeneral)
					formationEnemies.Remove(asEnemyBase);
			}

			m_mobCount--;

			if(m_mobCount == 0)
			{
				OnCampCleared();
			}

			m_mobs[i] = nullptr;

			idx = i;

			m_threatMap[i].clear();
		
			break;
		}
	}

	check(idx >= 0);
	return idx;
}

void ASpawnerBase::OnCampCleared_Implementation()
{
	if(killParticle->Template)
	{
		killParticle->ResetParticles();
		killParticle->ActivateSystem();
	}
	m_OnCampCleared();
}

int32 ASpawnerBase::GetAliveMobCount()
{
	return m_mobCount;
}

FVector ASpawnerBase::SpawnResetPosition()
{
	return GetActorLocation();
}

void ASpawnerBase::m_OnPlayerEnterOverlap(class ANetworkCharacter& player)
{
	auto find = m_nearbyPlayers.find(&player);
	if (find != m_nearbyPlayers.end())
		return;

	ANetworkPlayer* playerCast = Cast<ANetworkPlayer>(&player);
	if (IsValid(playerCast))
	{
		playerCast->AddThreatLevel_Client(threatLevel);
	}

	// Player was succesfully added
	m_nearbyPlayers.emplace(&player);

	//adding the threat to all mobs

/*	for (int32 i = 0; i < m_mobs.Num(); i++)
	{
		if (m_mobs[i] == nullptr)
			continue;
	//	auto ret = m_threatMap[i].emplace(&player, 0);
		if (!ret.second)
		{
			FPRINT("failed to add player to creaturemap ");
		}
	}*/
}
void ASpawnerBase::m_OnPlayerExitOverlap(class ANetworkCharacter& player)
{
	auto find = m_nearbyPlayers.find(&player);
	if (find == m_nearbyPlayers.end())
	{
		FERROR("leaving player was not found in the nearbyPlayers");
		return;
	}

	ANetworkPlayer* playerCast = Cast<ANetworkPlayer>(&player);
	if (IsValid(playerCast))
	{
		playerCast->RemoveThreatLevel_Client(threatLevel);
	}

	// Player was succesfully removed
	m_nearbyPlayers.erase(find);
}

class ANetworkPlayer* ASpawnerBase::m_GetBiggestThreat(int32 index)
{
	ANetworkPlayer* ret = nullptr;
	int32 biggestThreat = -1;
	for (auto cpair : m_threatMap[index])
	{
		if (cpair.second > biggestThreat)
		{
			biggestThreat = cpair.second;
			ret = cpair.first;
		}
	}
	return ret;
}
void ASpawnerBase::m_OnMobSpawn(int32 index)
{
	ANPCBase* creature = m_mobs[index];
	if (creature == nullptr)
	{
		FWARNING("creature is null at spawn");
		return;
	}
	if (creature->IsA(AGeneralEnemy::StaticClass()))
	{
		AGeneralEnemy* asGeneral = Cast<AGeneralEnemy>(creature);
		asGeneral->formationPoints = formationPoints;
	}
	else
	{
		if (creature->IsA(AEnemyBase::StaticClass()))
			formationEnemies.Add(Cast<AEnemyBase>(creature));
	}
	m_threatMap[index].clear();

/*	for (ANetworkPlayer* player : m_nearbyPlayers)
	{
		auto ret = m_threatMap[index].insert(std::pair<ANetworkPlayer*, int32>(player, 0));
		if (!ret.second)
		{
			FPRINT("adding failed");
		}
	}*/

	if (creature->IsA(AMiniBossCreature::StaticClass()) && dropKeyFragmentIndex >= keyFragmentMin && dropKeyFragmentIndex <= keyFragmentMax)
		Cast<AMiniBossCreature>(creature)->keyDropped = PlayerKeyType(dropKeyFragmentIndex);
}

class ANetworkPlayer* ASpawnerBase::GetClosestPlayer()
{
	if (m_nearbyPlayers.empty())
		return nullptr;
	UWorld* const world = GetWorld();
	if (!world)
		return nullptr;
	ADefaultGameMode* const mode = Cast<ADefaultGameMode>(world->GetAuthGameMode());
	if (!mode)
		return nullptr;

	// Find the closest player (in aggroRadius)
	ANetworkPlayer* closest = nullptr;
	float closestDist = BIG_NUMBER;
	const float aggroSqr = aggroRadius * aggroRadius;
	for (ANetworkCharacter* character :m_nearbyPlayers)
	{
		
		ANetworkPlayer *player = Cast<ANetworkPlayer>(character);
		if (player)
		{

		//	ANetworkPlayer* player = Cast<ANetworkPlayer>(*iter);
			const float distSqr = FVector2D::DistSquared(FVector2D(this->GetActorLocation()), FVector2D(player->GetActorLocation()));
			if (distSqr < closestDist && distSqr <= aggroSqr)
			{
				closest = player;
				closestDist = distSqr;
			}
		}
	}

	return closest;
}

void ASpawnerBase::AddThreat(class ANPCBase* creature, class ANetworkPlayer* character, int32 threat)
{
	if (creature == nullptr || creature->IsPendingKill() || character == nullptr || character->IsPendingKill())
		return;
	//find the creaturemap

	int32 index = 0;
	bool found = false;
	for (ANPCBase* mob : m_mobs)
	{
		if (creature == mob)
		{
			found = true;
			break;
		}
		index++;
	}
	if (!found)
	{
		FERROR("creature not part of threatMap");
		return;
	}

	//find the player
	map<class ANetworkPlayer*, int32>::iterator it = m_threatMap[index].find(character);

	if (it == m_threatMap[index].end())
	{
		FERROR("character not part of creatureMap");
		return;
	}
	it->second += threat;
}

void ASpawnerBase::GetNewTarget(class ANPCBase* mob)
{
	return;
}
void ASpawnerBase::ForceResetTarget(class ANPCBase* mob)
{
	mob->target = nullptr;
}
void ASpawnerBase::ForceSetTarget(class ANPCBase* mob,class ANetworkPlayer* target)
{
	mob->target = target;
}
void ASpawnerBase::SetTeam(int newTeam)
{
	team = (NPCTeam)newTeam;

	for (int i = 0; i < m_mobs.Num(); i++)
	{
		if (!IsValid(m_mobs[i]))
			continue;

		m_mobs[i]->SetTeam(newTeam);

		if (!m_mobs[i]->target)
			continue;
		if(m_mobs[i]->target->GetTeam() == newTeam)
			m_mobs[i]->target = nullptr;
	}
}
void ASpawnerBase::CaptureCamp(int team)
{
	SetTeam(team);
}

TArray<ANetworkPlayer*> ASpawnerBase::GetNearbyPlayers()
{
	TArray<ANetworkPlayer*> players;
	for(ANetworkCharacter* character : m_nearbyPlayers)
	{
		ANetworkPlayer* player = Cast<ANetworkPlayer>(character);
		if(player)
		{
			players.Add(player);
		}
	}
	return players;
}

void ASpawnerBase::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);

	DOREPLIFETIME(ASpawnerBase, team);
}