// Copyright 2023, CodeSpartan

#include "BuffsSubsystem.h"
#include "Kismet/GameplayStatics.h"
#include "Engine/EngineTypes.h"
#include "TimerManager.h"
#include "Engine/World.h"

void UBuffsSubsystem::RegisterBuff(const FGuid& Guid, float Duration, FOnBuffEndedSignature BuffEndDelegate)
{
	if (Duration > 0.0f) {
		if (bUseHeapInsteadOfTimers) {
			BuffsHeap.HeapPush(FBuffHandle(GetOuter(), Guid, Duration, BuffEndDelegate));
		}
		else
		{
			FTimerHandle UniqueHandle;
			FTimerDelegate TimerDelegate = FTimerDelegate::CreateLambda([this, Guid, BuffEndDelegate]()
			{
					BuffEndDelegate.ExecuteIfBound(Guid);
					UnregisterBuff(Guid);
			});

			GetWorld()->GetTimerManager().SetTimer(UniqueHandle, TimerDelegate, Duration, false, -1.f);
			BuffExpirationHandles.Add(Guid, UniqueHandle);
		}
	}
	else if (Duration == 0.0f)
	{
		BuffEndDelegate.ExecuteIfBound(Guid);
	}
}

void UBuffsSubsystem::UnregisterBuff(const FGuid& Guid)
{
	if (BuffExpirationHandles.Contains(Guid))
	{
		GetWorld()->GetTimerManager().ClearTimer(BuffExpirationHandles[Guid]);
		BuffExpirationHandles.Remove(Guid);
	}
}

void UBuffsSubsystem::StartTickingBuff(const FGuid& Guid, float InitialDelay, float TickFrequency, FOnBuffTickSignature TickDelegate)
{
	FTimerHandle UniqueHandle;
	FTimerDelegate TimerDelegate = FTimerDelegate::CreateLambda([Guid, TickDelegate]()
	{
		TickDelegate.ExecuteIfBound(Guid);
	});

	GetWorld()->GetTimerManager().SetTimer(UniqueHandle, TimerDelegate, TickFrequency, true, InitialDelay);
	BuffTickHandles.Add(Guid, UniqueHandle);
}

void UBuffsSubsystem::StopTickingBuff(const FGuid& Guid)
{
	if (BuffTickHandles.Contains(Guid))
	{
		GetWorld()->GetTimerManager().ClearTimer(BuffTickHandles[Guid]);
		BuffTickHandles.Remove(Guid);
	}
}

void UBuffsSubsystem::Tick(float DeltaTime)
{
	checkf(IsInitialized(), TEXT("Ticking should have been disabled for an uninitialized subsystem : remember to call IsInitialized in the subsystem's IsTickable, IsTickableInEditor and/or IsTickableWhenPaused implementation"));

	while (true)
	{
		if (BuffsHeap.IsEmpty())
			break;
		const FBuffHandle Buff = BuffsHeap.HeapTop();
		if (Buff.ExpirationDate <= UGameplayStatics::GetTimeSeconds(GetOuter())) {
			Buff.BuffEndDelegate.ExecuteIfBound(Buff.Guid);
			StopTickingBuff(Buff.Guid);
			BuffsHeap.HeapPopDiscard();
		}
		else {
			break; // Exit the loop if the top node is not expired
		}
	}
}

UBuffsSubsystem::UBuffsSubsystem()
	: UGameInstanceSubsystem()
{
	BuffsHeap.Heapify(); // probably unnecessary for an empty array
}

// Boilerplate below ----------------------------------------------------------------------------------
// Copied from WorldSystem.h to make this Subsystem tickable

UWorld& UBuffsSubsystem::GetWorldRef() const
{
	return *GetOuter()->GetWorld();
}

UWorld* UBuffsSubsystem::GetWorld() const
{
	return GetOuter()->GetWorld();
}

bool UBuffsSubsystem::ShouldCreateSubsystem(UObject* Outer) const
{
	if (!Super::ShouldCreateSubsystem(Outer))
	{
		return false;
	}

	UWorld* World = Outer->GetWorld();
	check(World);
	return DoesSupportWorldType(World->WorldType) && World->GetNetMode() == NM_DedicatedServer;
}
bool UBuffsSubsystem::DoesSupportWorldType(const EWorldType::Type WorldType) const
{
	return WorldType == EWorldType::Game || WorldType == EWorldType::PIE;

}
void UBuffsSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
	check(!bInitialized);
	bInitialized = true;
}

void UBuffsSubsystem::Deinitialize()
{
	check(bInitialized);
	bInitialized = false;
}

ETickableTickType UBuffsSubsystem::GetTickableTickType() const
{
	// By default (if the child class doesn't override GetTickableTickType), don't let CDOs ever tick: 
	return IsTemplate() ? ETickableTickType::Never : FTickableGameObject::GetTickableTickType();
}

bool UBuffsSubsystem::IsAllowedToTick() const
{
	// No matter what IsTickable says, don't let CDOs or uninitialized world subsystems tick :
	// Note: even if GetTickableTickType was overridden by the child class and returns something else than ETickableTickType::Never for CDOs, 
	//  it's probably a mistake, so by default, don't allow ticking. If the child class really intends its CDO to tick, it can always override IsAllowedToTick...
	return !IsTemplate() && bInitialized;
}

TStatId UBuffsSubsystem::GetStatId() const
{
	RETURN_QUICK_DECLARE_CYCLE_STAT(UBuffsSubsystem, STATGROUP_Tickables);
}