// © 2025 Naked People Team. All Rights Reserved.


#include "NPCDirectorSubsystem.h"

#include "NPC.h"
#include "NPCDirectorConfig.h"
#include "NavigationSystem.h"
#include "Engine/World.h"
#include "GameFramework/Pawn.h"
#include "Kismet/GameplayStatics.h"
#include "NakedDesire/Global/NakedDesireGameInstance.h"

void UNPCDirectorSubsystem::OnWorldBeginPlay(UWorld& InWorld)
{
	Super::OnWorldBeginPlay(InWorld);

	if (!InWorld.IsGameWorld())
		return;

	const UNPCDirectorConfig* Config = GetConfig();
	if (!Config || Config->SpawnTable.Num() == 0)
		return;

	if (UTimeOfDaySubsystem* Time = InWorld.GetSubsystem<UTimeOfDaySubsystem>())
	{
		Time->OnPhaseChanged.AddUniqueDynamic(this, &UNPCDirectorSubsystem::HandlePhaseChanged);
		CachedPhase = Time->GetPhase();
	}

	PrewarmPool();

	const float Interval = FMath::Max(Config->UpdateInterval, 0.05f);
	InWorld.GetTimerManager().SetTimer(
		UpdateTimerHandle, this, &UNPCDirectorSubsystem::UpdatePopulation, Interval, true);

	UpdatePopulation();
}

void UNPCDirectorSubsystem::Deinitialize()
{
	if (const UWorld* World = GetWorld())
	{
		World->GetTimerManager().ClearTimer(UpdateTimerHandle);
		if (UTimeOfDaySubsystem* Time = World->GetSubsystem<UTimeOfDaySubsystem>())
			Time->OnPhaseChanged.RemoveDynamic(this, &UNPCDirectorSubsystem::HandlePhaseChanged);
	}

	Super::Deinitialize();
}

void UNPCDirectorSubsystem::HandlePhaseChanged(EDayPhase NewPhase)
{
	// The timer applies the new target on its next reconcile; just cache it.
	CachedPhase = NewPhase;
}

void UNPCDirectorSubsystem::PrewarmPool()
{
	UWorld* World = GetWorld();
	const UNPCDirectorConfig* Config = GetConfig();
	if (!World || !Config)
		return;

	FActorSpawnParameters Params;
	Params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;

	for (int32 i = 0; i < Config->MaxNPCs; ++i)
	{
		const ENPCType NPCType = PickWeightedClass();
		if (NPCType == ENPCType::None)
			continue;

		TSubclassOf<ANPC> SelectedNPCClass = GetRandomNPCClass(Config->NPCClasses);
		ANPC* NPC = World->SpawnActor<ANPC>(SelectedNPCClass, FVector::ZeroVector, FRotator::ZeroRotator, Params);
		NPC->Init(NPCType);
		if (!NPC)
			continue;

		NPC->DeactivateToPool();
		Pool.Add(NPC);
	}
}

void UNPCDirectorSubsystem::UpdatePopulation()
{
	const APawn* Player = GetPlayerPawn();
	const UNPCDirectorConfig* Config = GetConfig();
	if (!Player || !Config)
		return;

	const FVector PlayerLocation = Player->GetActorLocation();
	const float DespawnRadiusSq = FMath::Square(Config->DespawnRadius);

	// 1. Recycle anything that wandered (or was left) beyond the despawn radius.
	for (int32 i = Active.Num() - 1; i >= 0; --i)
	{
		ANPC* NPC = Active[i];
		if (!NPC || FVector::DistSquared(NPC->GetActorLocation(), PlayerLocation) > DespawnRadiusSq)
			ReturnToPool(NPC);
	}

	const int32 Target = CurrentTargetCount();

	// 2. Over target (e.g. day -> night drop): recycle the farthest live NPCs down to target.
	while (Active.Num() > Target)
	{
		int32 FarthestIdx = INDEX_NONE;
		float FarthestDistSq = -1.0f;
		for (int32 i = 0; i < Active.Num(); ++i)
		{
			const float DistSq = FVector::DistSquared(Active[i]->GetActorLocation(), PlayerLocation);
			if (DistSq > FarthestDistSq)
			{
				FarthestDistSq = DistSq;
				FarthestIdx = i;
			}
		}
		if (FarthestIdx == INDEX_NONE)
			break;
		ReturnToPool(Active[FarthestIdx]);
	}

	// 3. Under target: activate pooled NPCs at NavMesh points in the spawn ring.
	while (Active.Num() < Target && Pool.Num() > 0)
	{
		FVector SpawnLocation;
		if (!FindSpawnPoint(PlayerLocation, SpawnLocation))
			break; // no reachable point this tick; try again next reconcile

		ANPC* NPC = TakeFromPool();
		if (!NPC)
			break;

		// Face away from the player so a newly activated NPC reads as walking into the scene.
		const FRotator Facing = (SpawnLocation - PlayerLocation).GetSafeNormal2D().Rotation();
		NPC->ActivateFromPool(SpawnLocation, Facing);
		Active.Add(NPC);
	}
}

int32 UNPCDirectorSubsystem::CurrentTargetCount() const
{
	const UNPCDirectorConfig* Config = GetConfig();
	if (!Config)
		return 0;

	const int32 PhaseTarget = (CachedPhase == EDayPhase::Day) ? Config->TargetCountDay : Config->TargetCountNight;
	// Can never exceed what the pool can hold.
	return FMath::Clamp(PhaseTarget, 0, Config->MaxNPCs);
}

ANPC* UNPCDirectorSubsystem::TakeFromPool()
{
	while (Pool.Num() > 0)
	{
		ANPC* NPC = Pool.Pop(EAllowShrinking::No);
		if (NPC)
			return NPC;
	}
	return nullptr;
}

void UNPCDirectorSubsystem::ReturnToPool(ANPC* NPC)
{
	Active.RemoveSwap(NPC);
	if (NPC)
	{
		NPC->DeactivateToPool();
		Pool.Add(NPC);
	}
}

ENPCType UNPCDirectorSubsystem::PickWeightedClass() const
{
	const UNPCDirectorConfig* Config = GetConfig();
	if (!Config)
		return ENPCType::None;

	float TotalWeight = 0.0f;
	for (const FNPCSpawnEntry& Entry : Config->SpawnTable)
	{
		if (Entry.Type != ENPCType::None && Entry.Weight > 0.0f)
			TotalWeight += Entry.Weight;
	}
	if (TotalWeight <= 0.0f)
		return ENPCType::None;

	float Roll = FMath::FRandRange(0.0f, TotalWeight);
	for (const FNPCSpawnEntry& Entry : Config->SpawnTable)
	{
		if (Entry.Type == ENPCType::None || Entry.Weight <= 0.0f)
			continue;
		Roll -= Entry.Weight;
		if (Roll <= 0.0f)
			return Entry.Type;
	}
	return ENPCType::None;
}

bool UNPCDirectorSubsystem::FindSpawnPoint(const FVector& Around, FVector& OutLocation)
{
	UNavigationSystemV1* NavSys = UNavigationSystemV1::GetCurrent(GetWorld());
	const UNPCDirectorConfig* Config = GetConfig();
	if (!NavSys || !Config)
		return false;

	const float MinRadiusSq = FMath::Square(Config->SpawnRadiusMin);

	// Rejection-sample a reachable point in the outer radius until one lands outside the inner radius.
	for (int32 Attempt = 0; Attempt < 8; ++Attempt)
	{
		FNavLocation NavLocation;
		if (NavSys->GetRandomReachablePointInRadius(Around, Config->SpawnRadiusMax, NavLocation) &&
			FVector::DistSquared(NavLocation.Location, Around) >= MinRadiusSq)
		{
			FVector SpawnLocation = NavLocation.Location;
			CorrectSpawnLocation(SpawnLocation);
			OutLocation = SpawnLocation;
			return true;
		}
	}
	return false;
}

TSubclassOf<ANPC> UNPCDirectorSubsystem::GetRandomNPCClass(TArray<TSubclassOf<ANPC>> InNPCClasses)
{
	const int32 RandomIndex = FMath::RandRange(0, InNPCClasses.Num() - 1);
	return InNPCClasses[RandomIndex];
}

void UNPCDirectorSubsystem::CorrectSpawnLocation(FVector& OutVector)
{
	OutVector.Z += 50.0f;
}

APawn* UNPCDirectorSubsystem::GetPlayerPawn() const
{
	return UGameplayStatics::GetPlayerPawn(GetWorld(), 0);
}

UNPCDirectorConfig* UNPCDirectorSubsystem::GetConfig() const
{
	const UNakedDesireGameInstance* GameInstance =
		Cast<UNakedDesireGameInstance>(GetWorld() ? GetWorld()->GetGameInstance() : nullptr);
	return GameInstance ? GameInstance->NPCDirector : nullptr;
}