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


#include "StatsManager.h"

#include "NakedDesire/Player/NakedDesireCharacter.h"

namespace
{
	// Max raw exposure one observer can read at once: Boobs + Ass + Genitals fully exposed, equal
	// weights (GDD §7.1 — per-part weighting is uniform for now). Normalizes per-observer exposure
	// to [0,1] so EmbarrassmentGainRate reads as "gain/sec at full exposure, single close observer".
	constexpr float MaxObservedExposure = 3.0f;
}

UStatsManager::UStatsManager()
{
	PrimaryComponentTick.bCanEverTick = true;
	PrimaryComponentTick.TickInterval = 1.0f;
}

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

	OwnerCharacter = Cast<ANakedDesireCharacter>(GetOwner());

	EmbarrassmentUpdate.Broadcast(Embarrassment, MaxEmbarrassment);
	StaminaUpdate.Broadcast(Stamina, MaxStamina);
	EnergyUpdate.Broadcast(Energy, MaxEnergy);
}

void UStatsManager::TickComponent(float DeltaTime, enum ELevelTick TickType,
	FActorComponentTickFunction* ThisTickFunction)
{
	DecreaseEnergy(0.9f);

	const float ExposureRate = ComputeObservedExposureRate();
	if (ExposureRate > 0.0f)
		IncreaseEmbarrassment(EmbarrassmentGainRate * ExposureRate * DeltaTime);
	else
		DecreaseEmbarrassment(EmbarrassmentDecayRate * DeltaTime);
}

float UStatsManager::ComputeObservedExposureRate()
{
	if (!OwnerCharacter)
		return 0.0f;

	// Sum each active observer's normalized exposure, re-traced live so redressing / exposing
	// updates the rate immediately even while the observer set is unchanged.
	float SumNormalizedExposure = 0.0f;
	int32 ActiveObservers = 0;

	for (int32 i = Observers.Num() - 1; i >= 0; --i)
	{
		AActor* Observer = Observers[i].Actor.Get();
		if (!Observer)
		{
			Observers.RemoveAtSwap(i);
			continue;
		}

		const float Exposure = OwnerCharacter->GetObservedExposureFrom(Observer->GetActorLocation(), Observer);
		if (Exposure > 0.0f)
		{
			// Weight by NPC type (§10.2): a Stalker's stare contributes more than a Walker's glance.
			SumNormalizedExposure += (Exposure / MaxObservedExposure) * Observers[i].Weight;
			++ActiveObservers;
		}
	}

	if (ActiveObservers == 0)
		return 0.0f;

	// Mean per-observer exposure, then a saturating crowd multiplier (diminishing returns).
	const float MeanExposure = SumNormalizedExposure / ActiveObservers;
	const float DensityMultiplier = 1.0f + ObserverDensityScale * FMath::Loge(static_cast<float>(ActiveObservers));
	return MeanExposure * DensityMultiplier;
}

void UStatsManager::Init(UClothingManager* InClothingManager)
{
	ClothingManager = InClothingManager;
}

void UStatsManager::SetObserved(const bool bObserved, AActor* Observer, const float Weight)
{
	if (!Observer)
		return;

	const int32 ExistingIndex = Observers.IndexOfByPredicate(
		[Observer](const FObserverEntry& Entry) { return Entry.Actor == Observer; });

	bool bChanged = false;
	if (bObserved)
	{
		if (ExistingIndex == INDEX_NONE)
		{
			Observers.Add({ Observer, Weight });
			bChanged = true;
		}
	}
	else if (ExistingIndex != INDEX_NONE)
	{
		Observers.RemoveAtSwap(ExistingIndex);
		bChanged = true;
	}

	if (bChanged)
		OnObserversChanged.Broadcast();
}

int32 UStatsManager::GetObserverCount() const
{
	int32 Count = 0;
	for (const FObserverEntry& Observer : Observers)
	{
		if (Observer.Actor.IsValid())
			++Count;
	}
	return Count;
}

void UStatsManager::IncreaseEmbarrassment(const float Amount)
{
	Embarrassment = FMath::Clamp(Embarrassment + Amount, 0, MaxEmbarrassment);
	// Embarrassment-max session loss is handled by USessionManagerSubsystem, which
	// subscribes to EmbarrassmentUpdate (GDD §4.4). Broadcast is the integration point.
	EmbarrassmentUpdate.Broadcast(Embarrassment, MaxEmbarrassment);
}

void UStatsManager::DecreaseEmbarrassment(const float Amount)
{
	Embarrassment = FMath::Clamp(Embarrassment - Amount, 0, MaxEmbarrassment);
	EmbarrassmentUpdate.Broadcast(Embarrassment, MaxEmbarrassment);
}

void UStatsManager::DecreaseStamina(const float Amount)
{
	Stamina = FMath::Clamp(Stamina - Amount, 0, MaxStamina);
	StaminaUpdate.Broadcast(Stamina, MaxStamina);
}

void UStatsManager::IncreaseStamina(const float Amount)
{
	Stamina = FMath::Clamp(Stamina + Amount, 0, MaxStamina);
	StaminaUpdate.Broadcast(Stamina, MaxStamina);
}

void UStatsManager::DecreaseEnergy(const float Amount)
{
	Energy = FMath::Clamp(Energy - Amount, 0, MaxEnergy);
	EnergyUpdate.Broadcast(Energy, MaxEnergy);
}

void UStatsManager::RestoreEnergy()
{
	Energy = MaxEnergy;
	EnergyUpdate.Broadcast(Energy, MaxEnergy);
}

void UStatsManager::ResetStats()
{
	Embarrassment = 0.0f;
	Energy = MaxEnergy;
	Stamina = MaxStamina;

	EmbarrassmentUpdate.Broadcast(Embarrassment, MaxEmbarrassment);
	StaminaUpdate.Broadcast(Stamina, MaxStamina);
	EnergyUpdate.Broadcast(Energy, MaxEnergy);
}

