// Copyright ExByte Studios. Item Interaction Ecosystem.

#pragma once

#include "CoreMinimal.h"
#include "GameplayTagContainer.h"
#include "ItemRuntimeProperties.generated.h"

/**
 * Tag->value pairs (section 4.4).
 *
 * The TЗ sketched these as TMaps, but UHT forbids TMap inside replicated structs,
 * and runtime properties must replicate (they live in FItemInstanceData). So they
 * are stored as small replicable arrays instead; the typed accessors below hide
 * that and behave like a map. N is tiny per item, so linear lookup is fine.
 */
USTRUCT(BlueprintType)
struct FItemFloatProperty
{
	GENERATED_BODY()

	UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Item|Runtime")
	FGameplayTag Tag;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Item|Runtime")
	float Value = 0.f;

	bool operator==(const FItemFloatProperty& O) const { return Tag == O.Tag && Value == O.Value; }
};

USTRUCT(BlueprintType)
struct FItemIntProperty
{
	GENERATED_BODY()

	UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Item|Runtime")
	FGameplayTag Tag;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Item|Runtime")
	int32 Value = 0;

	bool operator==(const FItemIntProperty& O) const { return Tag == O.Tag && Value == O.Value; }
};

USTRUCT(BlueprintType)
struct FItemBoolProperty
{
	GENERATED_BODY()

	UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Item|Runtime")
	FGameplayTag Tag;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Item|Runtime")
	bool Value = false;

	bool operator==(const FItemBoolProperty& O) const { return Tag == O.Tag && Value == O.Value; }
};

USTRUCT(BlueprintType)
struct FItemStringProperty
{
	GENERATED_BODY()

	UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Item|Runtime")
	FGameplayTag Tag;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Item|Runtime")
	FString Value;

	bool operator==(const FItemStringProperty& O) const { return Tag == O.Tag && Value == O.Value; }
};

/**
 * Dynamic, per-instance properties keyed by gameplay tag (section 4.4).
 * Only data differing from the definition defaults should be stored, to keep
 * replication/save payloads small. Typed accessors fall back when a key is absent.
 */
USTRUCT(BlueprintType)
struct ITEMCORE_API FItemRuntimeProperties
{
	GENERATED_BODY()

	UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Item|Runtime")
	TArray<FItemFloatProperty> FloatProperties;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Item|Runtime")
	TArray<FItemIntProperty> IntProperties;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Item|Runtime")
	TArray<FItemBoolProperty> BoolProperties;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Item|Runtime")
	TArray<FItemStringProperty> StringProperties;

	bool IsEmpty() const
	{
		return FloatProperties.Num() == 0 && IntProperties.Num() == 0
			&& BoolProperties.Num() == 0 && StringProperties.Num() == 0;
	}

	// --- Float ---
	float GetFloat(const FGameplayTag& Tag, float Fallback = 0.f) const
	{
		for (const FItemFloatProperty& P : FloatProperties) { if (P.Tag == Tag) { return P.Value; } }
		return Fallback;
	}
	bool HasFloat(const FGameplayTag& Tag) const
	{
		for (const FItemFloatProperty& P : FloatProperties) { if (P.Tag == Tag) { return true; } }
		return false;
	}
	void SetFloat(const FGameplayTag& Tag, float Value)
	{
		for (FItemFloatProperty& P : FloatProperties) { if (P.Tag == Tag) { P.Value = Value; return; } }
		FloatProperties.Add({ Tag, Value });
	}
	void RemoveFloat(const FGameplayTag& Tag)
	{
		FloatProperties.RemoveAll([&Tag](const FItemFloatProperty& P) { return P.Tag == Tag; });
	}

	// --- Int ---
	int32 GetInt(const FGameplayTag& Tag, int32 Fallback = 0) const
	{
		for (const FItemIntProperty& P : IntProperties) { if (P.Tag == Tag) { return P.Value; } }
		return Fallback;
	}
	bool HasInt(const FGameplayTag& Tag) const
	{
		for (const FItemIntProperty& P : IntProperties) { if (P.Tag == Tag) { return true; } }
		return false;
	}
	void SetInt(const FGameplayTag& Tag, int32 Value)
	{
		for (FItemIntProperty& P : IntProperties) { if (P.Tag == Tag) { P.Value = Value; return; } }
		IntProperties.Add({ Tag, Value });
	}

	// --- Bool ---
	bool GetBool(const FGameplayTag& Tag, bool Fallback = false) const
	{
		for (const FItemBoolProperty& P : BoolProperties) { if (P.Tag == Tag) { return P.Value; } }
		return Fallback;
	}
	bool HasBool(const FGameplayTag& Tag) const
	{
		for (const FItemBoolProperty& P : BoolProperties) { if (P.Tag == Tag) { return true; } }
		return false;
	}
	void SetBool(const FGameplayTag& Tag, bool Value)
	{
		for (FItemBoolProperty& P : BoolProperties) { if (P.Tag == Tag) { P.Value = Value; return; } }
		BoolProperties.Add({ Tag, Value });
	}

	// --- String ---
	FString GetString(const FGameplayTag& Tag, const FString& Fallback = FString()) const
	{
		for (const FItemStringProperty& P : StringProperties) { if (P.Tag == Tag) { return P.Value; } }
		return Fallback;
	}
	bool HasString(const FGameplayTag& Tag) const
	{
		for (const FItemStringProperty& P : StringProperties) { if (P.Tag == Tag) { return true; } }
		return false;
	}
	void SetString(const FGameplayTag& Tag, const FString& Value)
	{
		for (FItemStringProperty& P : StringProperties) { if (P.Tag == Tag) { P.Value = Value; return; } }
		StringProperties.Add({ Tag, Value });
	}

	bool operator==(const FItemRuntimeProperties& Other) const
	{
		return ArrayEqual(FloatProperties, Other.FloatProperties)
			&& ArrayEqual(IntProperties, Other.IntProperties)
			&& ArrayEqual(BoolProperties, Other.BoolProperties)
			&& ArrayEqual(StringProperties, Other.StringProperties);
	}
	bool operator!=(const FItemRuntimeProperties& Other) const { return !(*this == Other); }

private:
	template<typename T>
	static bool ArrayEqual(const TArray<T>& A, const TArray<T>& B)
	{
		if (A.Num() != B.Num()) { return false; }
		for (const T& Elem : A) { if (!B.Contains(Elem)) { return false; } }
		return true;
	}
};
