// Fill out your copyright notice in the Description page of Project Settings.


#include "Components/StatForgeComponent.h"
#include "Config/StatForgeDeveloperSettings.h"
#include "Data/GameplayStatEffect.h"


FActiveGameplayStatEffect FActiveGameplayStatEffect::CreateNew(UObject* SourceObject, UGameplayStatEffect* Effect, int32 Stacks)
{
	if (!Effect || !SourceObject) return FActiveGameplayStatEffect();

	FActiveGameplayStatEffect ActiveEffect;
	ActiveEffect.SourceObject = SourceObject;
	ActiveEffect.GameplayEffectClass = Effect->GetClass();
	ActiveEffect.StackCount = Stacks;
	ActiveEffect.GameplayEffectInstance = Effect;
	ActiveEffect.RemainingDuration = Effect->Duration;
	ActiveEffect.NextTickDuration = Effect->TickPeriod;
	ActiveEffect.bIsTicking = ActiveEffect.NextTickDuration > 0.f;
	ActiveEffect.Handle = FGuid::NewGuid();
	
	return ActiveEffect;
}

UStatForgeComponent::UStatForgeComponent()
{
	PrimaryComponentTick.bCanEverTick = true;
}

float UStatForgeComponent::GetStatValue(const FName StatName) const
{
	for (const FStatRuntimeDef& Def : StatArray)
	{
		if (Def.StatDef.StatName == StatName)
		{
			return Def.CurrentValue;
		}
	}
	
	return 0.f;
}

void UStatForgeComponent::BeginPlay()
{
	Super::BeginPlay();
	
	TMap<FName, float> DataValueMap;

	if (const UStatForgeDeveloperSettings* DeveloperSettings = GetDefault<UStatForgeDeveloperSettings>())
	{
		for (const auto& StatPair : InitialStatValueArray)
		{
			FStatDef Def = DeveloperSettings->GetStatByName(StatPair.Key);
			if (Def.IsValid())
			{
				StatArray.Add(FStatRuntimeDef(Def));
				FGameplayStatEffectMod Mod;
				Mod.AffectedStat = StatPair.Key;
				Mod.ModOperation = EModOperation::Override;
				Mod.CalculationType = EGameplayStatEffectModCalculation::Flat;
				Mod.FlatValue = StatPair.Value;
				ApplyModToStat(Mod, DataValueMap);
			}
		}	
	}
	
	for (const TSubclassOf<UGameplayStatEffect>& Effect : InitialEffectArray)
	{
		ApplyGameplayStatEffect(Effect, DataValueMap);
	}
}

void UStatForgeComponent::TickComponent(float DeltaTime, enum ELevelTick TickType,
	FActorComponentTickFunction* ThisTickFunction)
{
	Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
	TickEffects(DeltaTime);
}

void UStatForgeComponent::IsStat(FName StatName, FName CheckedName, EIsStatResult& Result)
{
	Result = StatName.IsEqual(CheckedName)? EIsStatResult::Yes : EIsStatResult::No;
}

void UStatForgeComponent::Internal_ApplyGameplayStatEffect(UGameplayStatEffect* Effect)
{
	if (!CanApplyEffect(Effect)) return;
	
	PreGameplayEffectExecute(Effect);
	for (const FGameplayStatEffectMod& Modifier : Effect->Modifiers)
	{
		ApplyModToStat(Modifier, Effect->GetDataValueMap());
	}
	PostGameplayEffectExecute(Effect);
}

void UStatForgeComponent::ApplyModToStat(const FGameplayStatEffectMod& Mod, const TMap<FName, float> DataValueMap)
{
	FStatRuntimeDef* StatDef = GetStatRef(Mod.AffectedStat);
	if (!StatDef) return;
	
	PreStatModify(StatDef->StatDef.StatName);
	const float OldValue = StatDef->BaseValue;
	const float ModValue = Mod.GetModValue(this, DataValueMap);
	switch (Mod.ModOperation) 
	{
		case EModOperation::Add:
			StatDef->BaseValue += ModValue;
			break;
		case EModOperation::MultiplyAdditive:
		case EModOperation::MultiplyCompound:
			StatDef->BaseValue *= ModValue;
			break;
		case EModOperation::Override:
			StatDef->BaseValue = ModValue;
			break;
	}
	RecalculateStatCurrentValue(Mod.AffectedStat, OldValue);
}

void UStatForgeComponent::RecalculateStatCurrentValue(const FName StatName, const float OldValue)
{
	FStatRuntimeDef* StatDef = GetStatRef(StatName);
	if (!StatDef) return;
	
	const float BaseValue = StatDef->BaseValue;
	float AdditiveSum = 0.f;
	float MultiAddSum = 0.f;
	float MultiCompound = 1.f;
	bool bHasOverride = false;
	float OverrideValue = 0.f;
	for (FActiveGameplayStatEffect& Effect : ActiveEffectsArray)
	{
		if (Effect.bIsTicking) continue;
		
		for (auto Mod : Effect.GameplayEffectInstance->Modifiers)
		{
			if (Mod.AffectedStat != StatName) continue;
			
			const float StackCount = Effect.StackCount;
			const float ModValue = Mod.GetModValue(this, Effect.GameplayEffectInstance->GetDataValueMap());
			switch (Mod.ModOperation) {
			case EModOperation::Add:
				{
					AdditiveSum += (ModValue * StackCount);
					break;
				}
			case EModOperation::MultiplyAdditive:
				{
					MultiAddSum += (ModValue * StackCount);
					break;
				}
			case EModOperation::MultiplyCompound:
				{
					MultiCompound *= (ModValue * StackCount);
					break;
				}
			case EModOperation::Override:
				{
					bHasOverride = true;
					OverrideValue = ModValue;
					break;
				}
			}
		}
			
		if (bHasOverride)
		{
			break;
		}
	}
		
	float Result = (BaseValue + AdditiveSum) * (1.f + MultiAddSum) * MultiCompound;
	if (bHasOverride)
	{
		Result = OverrideValue;
	}
	
	StatDef->CurrentValue = Result;
	PostStatModify(StatName);
	OnStatChanged.Broadcast(StatName, OldValue, StatDef->CurrentValue);
}

FGuid UStatForgeComponent::ApplyGameplayStatEffect(const TSubclassOf<UGameplayStatEffect> EffectClass, TMap<FName, float> DataValueMap, int32 Stacks)
{
	if (!EffectClass) return FGuid();
	
	UGameplayStatEffect* Effect = NewObject<UGameplayStatEffect>(this, EffectClass);
	check(Effect);
	
	Effect->AppendDataValueMap(DataValueMap);
	FGuid GuidToReturn;
	
	if (Effect->Duration == GameplayStatEffectDuration_Instant)
	{
		Internal_ApplyGameplayStatEffect(Effect);
	}
	else
	{
		FActiveGameplayStatEffect ActiveEffect = FActiveGameplayStatEffect::CreateNew(this, Effect, Stacks);
		GuidToReturn = ActiveEffect.Handle;
		ActiveEffectsArray.Add(ActiveEffect);
		AddGameplayTags(Effect->CharacterTags);

		for (const FName StatName : GetAffectedStatsFromEffect(ActiveEffect.GameplayEffectInstance))
		{
			const float OldValue = GetStatValue(StatName);
			RecalculateStatCurrentValue(StatName, OldValue);
		}
	}
	
	return GuidToReturn;
}

bool UStatForgeComponent::RemoveGameplayStatEffect(FGuid EffectId)
{
	for (int32 i = ActiveEffectsArray.Num() - 1; i >= 0; --i)
	{
		if (ActiveEffectsArray[i].Handle == EffectId)
		{
			RemoveGameplayTags(ActiveEffectsArray[i].GameplayEffectInstance->CharacterTags);
			TArray<FName> AffectedStats = GetAffectedStatsFromEffect(ActiveEffectsArray[i].GameplayEffectInstance.Get());
			ActiveEffectsArray.RemoveAt(i);

			for (const FName& Stat : AffectedStats)
			{
				const float OldValue = GetStatValue(Stat);
				RecalculateStatCurrentValue(Stat, OldValue);
			}
			
			return true;
		}
	}
	
	return false;
}

bool UStatForgeComponent::HasStat(const FName& StatName) const
{
	for (const FStatRuntimeDef& Stat : StatArray)
	{
		if (Stat.StatDef.StatName == StatName)
		{
			return true;
		}
	}
	
	return false;
}

void UStatForgeComponent::AddGameplayTag(const FGameplayTag& NewTag)
{
	if (NewTag.IsValid())
	{
		CharacterGameplayTags.AddTag(NewTag);
		NotifyGameplayTagsChanged();
	}
}

void UStatForgeComponent::AddGameplayTags(const FGameplayTagContainer& NewTags)
{
	if(NewTags.Num() > 0)
	{
		CharacterGameplayTags.AppendTags(NewTags);
		NotifyGameplayTagsChanged();
	}
}

bool UStatForgeComponent::RemoveGameplayTag(const FGameplayTag& NewTag)
{
	if (NewTag.IsValid())
	{
		CharacterGameplayTags.RemoveTag(NewTag);
		NotifyGameplayTagsChanged();
		return true;
	}
	return false;
}

void UStatForgeComponent::RemoveGameplayTags(const FGameplayTagContainer& NewTags)
{
	if(NewTags.Num() > 0)
	{
		CharacterGameplayTags.RemoveTags(NewTags);
		NotifyGameplayTagsChanged();
	}
}

void UStatForgeComponent::NotifyGameplayTagsChanged() const
{
	OnTagsChanged.Broadcast();
}

TArray<FName> UStatForgeComponent::GetAffectedStatsFromEffect(UGameplayStatEffect* Effect)
{
	TArray<FName> ChangedStats;
	for (auto Mod : Effect->Modifiers)
	{
		ChangedStats.AddUnique(Mod.AffectedStat);
	}
	
	return ChangedStats;
}

FStatRuntimeDef* UStatForgeComponent::GetStatRef(const FName StatName)
{
	for (FStatRuntimeDef& Def : StatArray)
	{
		if (Def.StatDef.StatName == StatName)
		{
			return &Def;
		}
	}
	
	return nullptr;
}

void UStatForgeComponent::TickEffects(float DeltaTime)
{
	TArray<FName> StatNames;
	for (int32 i = ActiveEffectsArray.Num() - 1; i >= 0; --i)
	{
		StatNames.Append(GetAffectedStatsFromEffect(ActiveEffectsArray[i].GameplayEffectInstance));

		if (!CanTickEffect(ActiveEffectsArray[i].GameplayEffectInstance.Get()))
		{
			ActiveEffectsArray[i].RemainingDuration = 0;
		}
		else
		{
			ActiveEffectsArray[i].RemainingDuration -= DeltaTime;
		}
		
		if (ActiveEffectsArray[i].RemainingDuration <= 0.f)
		{
			RemoveGameplayStatEffect(ActiveEffectsArray[i].Handle);
			
			continue;
		}
		
		if (ActiveEffectsArray[i].GameplayEffectInstance->TickPeriod > 0.f)
		{
			ActiveEffectsArray[i].NextTickDuration -= DeltaTime;
			if (ActiveEffectsArray[i].NextTickDuration <= 0)
			{
				ActiveEffectsArray[i].NextTickDuration = ActiveEffectsArray[i].GameplayEffectInstance->TickPeriod;
				Internal_ApplyGameplayStatEffect(ActiveEffectsArray[i].GameplayEffectInstance);
			}	
		}
	}
	
	for (const FName StatName : StatNames)
	{
		const float OldValue = GetStatValue(StatName);
		RecalculateStatCurrentValue(StatName, OldValue);
	}
	
#if WITH_EDITOR
	for (const auto& Stat : StatArray)
	{
		StatValuesPreview.Add(Stat.StatDef.StatName, GetStatValue(Stat.StatDef.StatName));
	}
#endif
}

bool UStatForgeComponent::CanApplyEffect(const UGameplayStatEffect* StatEffect) const
{
	if (!StatEffect) return false;
	
	return GetGameplayTags().HasAll(StatEffect->TagsRequiredToApply);
}

bool UStatForgeComponent::CanTickEffect(const UGameplayStatEffect* StatEffect) const
{
	if (!StatEffect) return false;
	
	return GetGameplayTags().HasAll(StatEffect->TagsRequiredToTick);
}


