// Copyright 2023, CodeSpartan

#pragma once

#include "CoreMinimal.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "Engine/EngineTypes.h"
#include "Tickable.h"
#include "Kismet/GameplayStatics.h"
#include "BuffsSubsystem.generated.h"

DECLARE_DYNAMIC_DELEGATE_OneParam(FOnBuffEndedSignature, const FGuid&, Guid);
DECLARE_DYNAMIC_DELEGATE_OneParam(FOnBuffTickSignature, const FGuid&, Guid);

USTRUCT()
struct FBuffHandle
{
	GENERATED_BODY()

	//UPROPERTY() -- UE constructs the struct on startup, twice, and checks it against the other, so it fails because the two created instances have different GUIDs
	FGuid Guid;

	/* The ExpirationDate is in seconds: TimeSinceStart (in seconds) at the creation of this struct + Buff's Duration (in seconds) */
	UPROPERTY()
	float ExpirationDate;

	UPROPERTY()
	FOnBuffEndedSignature BuffEndDelegate;

	FBuffHandle() : Guid(FGuid::NewGuid()), ExpirationDate (0.0f) {}

	FBuffHandle(const UObject* WorldContextObject, FGuid InGuid, float Duration, FOnBuffEndedSignature InDelegate) : Guid(InGuid), BuffEndDelegate(InDelegate)
	{
		ExpirationDate = UGameplayStatics::GetRealTimeSeconds(WorldContextObject) + Duration;
	}

	bool operator==(const FBuffHandle& Other) const
	{
		return Other.Guid == Guid;
	}

	bool operator<(const FBuffHandle& Other) const
	{
		return ExpirationDate < Other.ExpirationDate;
	}
};

/**
 * Buffs Subsystem exists only on Dedicater Server
 * and manages Buffs expiration
 */
UCLASS()
class MMOKITCODE_API UBuffsSubsystem : public UGameInstanceSubsystem, public FTickableGameObject
{
	GENERATED_BODY()

public:

	// Copied from WorldSubsystem.h
	UBuffsSubsystem();
	virtual UWorld* GetWorld() const override final;
	UWorld& GetWorldRef() const;
	virtual bool ShouldCreateSubsystem(UObject* Outer) const override;
	bool IsInitialized() const { return bInitialized; }

	// FTickableGameObject implementation Begin
	virtual UWorld* GetTickableGameObjectWorld() const override { return GetWorld(); }
	virtual ETickableTickType GetTickableTickType() const override;
	virtual bool IsAllowedToTick() const override final;
	virtual void Tick(float DeltaTime) override;
	virtual TStatId GetStatId() const override;
	// FTickableGameObject implementation End

	// USubsystem implementation Begin
	virtual void Initialize(FSubsystemCollectionBase & Collection) override;
	virtual void Deinitialize() override;
	// USubsystem implementation End

	UFUNCTION(BlueprintCallable, Category = "BuffsSubsystem")
	void RegisterBuff(const FGuid& Guid, float Duration, FOnBuffEndedSignature BuffEndDelegate);

	/* Unregisters buff from BuffEndDelegate, only works with bUseHeapInsteadOfTimers as false
	 * If bUseHeapInsteadOfTimers is true, non-existing buffs are allowed to expire, but this does nothing */
	UFUNCTION(BlueprintCallable, Category = "BuffsSubsystem")
	void UnregisterBuff(const FGuid& Guid);

	UFUNCTION(BlueprintCallable, Category = "BuffsSubsystem")
	void StartTickingBuff(const FGuid& Guid, float InitialDelay, float TickFrequency, FOnBuffTickSignature TickDelegate);

	UFUNCTION(BlueprintCallable, Category = "BuffsSubsystem")
	void StopTickingBuff(const FGuid& Guid);

	UPROPERTY()
	TArray<FBuffHandle> BuffsHeap;

	UPROPERTY()
	TMap<FGuid, FTimerHandle> BuffTickHandles;

	/* If true, uses Heap to find buffs' expiration time. If false, uses Timers.
	 * Can be changed at runtime to affect how new buffs will be handled
	 */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "BuffsSubsystem")
	bool bUseHeapInsteadOfTimers = true;

	// Used if bUseHeapInsteadOfTimers == false
	UPROPERTY()
	TMap<FGuid, FTimerHandle> BuffExpirationHandles;

private:
	bool bInitialized = false;

protected:
	virtual bool DoesSupportWorldType(const EWorldType::Type WorldType) const;
};
