// Copyright Epic Games, Inc. All Rights Reserved.
// UE5 Modular Game Framework — BPC_InventorySystem Implementation

#include "Inventory/BPC_InventorySystem.h"
#include "Inventory/DA_ItemData.h"
#include "Algo/Sort.h"

DEFINE_LOG_CATEGORY_STATIC(LogInventory, Log, All);

UBPC_InventorySystem::UBPC_InventorySystem()
{
	PrimaryComponentTick.bCanEverTick = false;

	GridWidth = 8;
	GridHeight = 5;
	MaxWeight = 50.0f;
}

// ============================================================================
// Overrides
// ============================================================================

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

	// Initialize grid with empty slots.
	const int32 TotalSlots = GridWidth * GridHeight;
	Slots.SetNum(TotalSlots);

	for (int32 i = 0; i < TotalSlots; ++i)
	{
		Slots[i].GridX = i % GridWidth;
		Slots[i].GridY = i / GridWidth;
	}

	RecalculateWeight();

	UE_LOG(LogInventory, Log, TEXT("BPC_InventorySystem::BeginPlay — %d slots (%dx%d), MaxWeight: %.1f"),
		TotalSlots, GridWidth, GridHeight, MaxWeight);
}

// ============================================================================
// Core Operations
// ============================================================================

int32 UBPC_InventorySystem::AddItem(UDA_ItemData* Item, int32 Quantity)
{
	if (!Item || Quantity <= 0)
	{
		return 0;
	}

	if (!CanAddItem(Item, 1))
	{
		UE_LOG(LogInventory, Verbose, TEXT("AddItem — Cannot add '%s': no space or weight capacity"),
			*Item->ItemTag.GetTagName().ToString());
		return 0;
	}

	int32 Remaining = Quantity;
	int32 Added = 0;

	// Step 1: Try to stack onto existing partial stacks.
	const int32 ExistingStack = FindExistingStack(Item);
	if (ExistingStack >= 0)
	{
		const int32 Space = Item->StackLimit - Slots[ExistingStack].Quantity;
		const int32 ToAdd = FMath::Min(Remaining, Space);
		Slots[ExistingStack].Quantity += ToAdd;
		Remaining -= ToAdd;
		Added += ToAdd;
	}

	// Step 2: Fill empty slots for remaining items.
	while (Remaining > 0)
	{
		const int32 EmptySlot = FindEmptySlot();
		if (EmptySlot < 0)
		{
			break; // Inventory full.
		}

		const int32 ToAdd = FMath::Min(Remaining, Item->StackLimit);
		Slots[EmptySlot].Item = Item;
		Slots[EmptySlot].Quantity = ToAdd;
		Remaining -= ToAdd;
		Added += ToAdd;
	}

	if (Added > 0)
	{
		RecalculateWeight();
		MarkDirty();
		OnItemAdded.Broadcast(Item, Added);

		UE_LOG(LogInventory, Log, TEXT("AddItem — '%s' x%d added (requested %d)"),
			*Item->ItemTag.GetTagName().ToString(), Added, Quantity);
	}

	return Added;
}

int32 UBPC_InventorySystem::RemoveItem(UDA_ItemData* Item, int32 Quantity)
{
	if (!Item || Quantity <= 0)
	{
		return 0;
	}

	int32 Remaining = Quantity;
	int32 Removed = 0;

	// Remove from all stacks of this item (starting from the end to avoid index shifting).
	for (int32 i = Slots.Num() - 1; i >= 0 && Remaining > 0; --i)
	{
		if (Slots[i].Item == Item)
		{
			const int32 ToRemove = FMath::Min(Remaining, Slots[i].Quantity);
			Slots[i].Quantity -= ToRemove;
			Remaining -= ToRemove;
			Removed += ToRemove;

			if (Slots[i].Quantity <= 0)
			{
				Slots[i].Clear();
			}
		}
	}

	if (Removed > 0)
	{
		RecalculateWeight();
		MarkDirty();
		OnItemRemoved.Broadcast(Item, Removed);
	}

	return Removed;
}

int32 UBPC_InventorySystem::RemoveItemFromSlot(int32 SlotIndex, int32 Quantity)
{
	if (!Slots.IsValidIndex(SlotIndex) || Slots[SlotIndex].IsEmpty() || Quantity <= 0)
	{
		return 0;
	}

	UDA_ItemData* Item = Slots[SlotIndex].Item;
	const int32 ToRemove = FMath::Min(Quantity, Slots[SlotIndex].Quantity);
	Slots[SlotIndex].Quantity -= ToRemove;

	if (Slots[SlotIndex].Quantity <= 0)
	{
		Slots[SlotIndex].Clear();
	}

	RecalculateWeight();
	MarkDirty();
	OnItemRemoved.Broadcast(Item, ToRemove);

	return ToRemove;
}

bool UBPC_InventorySystem::CanAddItem(UDA_ItemData* Item, int32 Quantity) const
{
	if (!Item || Quantity <= 0)
	{
		return false;
	}

	// Check weight capacity (for at least one unit).
	const float ItemWeight = Item->Weight;
	if (CurrentWeight + ItemWeight > MaxWeight)
	{
		return false;
	}

	// Check if there's space.
	const bool bHasExistingStack = FindExistingStack(Item) >= 0;
	const bool bHasEmptySlot = FindEmptySlot() >= 0;

	return bHasExistingStack || bHasEmptySlot;
}

// ============================================================================
// Query
// ============================================================================

int32 UBPC_InventorySystem::GetItemCount(UDA_ItemData* Item) const
{
	if (!Item)
	{
		return 0;
	}

	int32 Count = 0;
	for (const FInventorySlot& Slot : Slots)
	{
		if (Slot.Item == Item)
		{
			Count += Slot.Quantity;
		}
	}

	return Count;
}

bool UBPC_InventorySystem::HasItem(UDA_ItemData* Item, int32 Quantity) const
{
	return GetItemCount(Item) >= Quantity;
}

int32 UBPC_InventorySystem::FindItemSlot(UDA_ItemData* Item) const
{
	if (!Item)
	{
		return -1;
	}

	for (int32 i = 0; i < Slots.Num(); ++i)
	{
		if (Slots[i].Item == Item)
		{
			return i;
		}
	}

	return -1;
}

TArray<UDA_ItemData*> UBPC_InventorySystem::GetAllItems() const
{
	TSet<UDA_ItemData*> UniqueItems;

	for (const FInventorySlot& Slot : Slots)
	{
		if (!Slot.IsEmpty())
		{
			UniqueItems.Add(Slot.Item);
		}
	}

	return UniqueItems.Array();
}

int32 UBPC_InventorySystem::GetEmptySlotCount() const
{
	int32 Count = 0;
	for (const FInventorySlot& Slot : Slots)
	{
		if (Slot.IsEmpty())
		{
			++Count;
		}
	}
	return Count;
}

float UBPC_InventorySystem::GetRemainingWeight() const
{
	return FMath::Max(MaxWeight - CurrentWeight, 0.0f);
}

// ============================================================================
// Organization
// ============================================================================

void UBPC_InventorySystem::SortInventory()
{
	// Separate empty slots from filled ones.
	TArray<FInventorySlot> FilledSlots;
	TArray<FInventorySlot> EmptySlots;

	for (const FInventorySlot& Slot : Slots)
	{
		if (Slot.IsEmpty())
		{
			EmptySlots.Add(Slot);
		}
		else
		{
			FilledSlots.Add(Slot);
		}
	}

	// Sort filled slots by ItemType, then DisplayName.
	Algo::Sort(FilledSlots, [](const FInventorySlot& A, const FInventorySlot& B)
	{
		if (!A.Item || !B.Item) return A.Item != nullptr;

		if (A.Item->ItemType != B.Item->ItemType)
		{
			return static_cast<uint8>(A.Item->ItemType) < static_cast<uint8>(B.Item->ItemType);
		}

		return A.Item->DisplayName.ToString() < B.Item->DisplayName.ToString();
	});

	// Rebuild slots array: sorted filled + empties.
	Slots.Empty();
	Slots.Append(FilledSlots);
	Slots.Append(EmptySlots);

	// Update grid positions.
	for (int32 i = 0; i < Slots.Num(); ++i)
	{
		Slots[i].GridX = i % GridWidth;
		Slots[i].GridY = i / GridWidth;
	}

	MarkDirty();
	UE_LOG(LogInventory, Log, TEXT("SortInventory — %d filled slots sorted"), FilledSlots.Num());
}

void UBPC_InventorySystem::ConsolidateStacks()
{
	// For each unique item type, merge partial stacks.
	TArray<UDA_ItemData*> Items = GetAllItems();

	for (UDA_ItemData* Item : Items)
	{
		if (!Item || Item->StackLimit <= 1)
		{
			continue;
		}

		// Gather all slots of this item.
		TArray<int32> SlotsWithItem;
		int32 TotalQuantity = 0;

		for (int32 i = 0; i < Slots.Num(); ++i)
		{
			if (Slots[i].Item == Item && Slots[i].Quantity < Item->StackLimit)
			{
				SlotsWithItem.Add(i);
				TotalQuantity += Slots[i].Quantity;
			}
		}

		if (SlotsWithItem.Num() <= 1)
		{
			continue; // Nothing to consolidate.
		}

		// Clear all partial stacks.
		for (int32 SlotIdx : SlotsWithItem)
		{
			Slots[SlotIdx].Clear();
		}

		// Re-add as consolidated stacks.
		int32 Remaining = TotalQuantity;
		for (int32& SlotIdx : SlotsWithItem)
		{
			if (Remaining <= 0) break;

			const int32 StackSize = FMath::Min(Remaining, Item->StackLimit);
			Slots[SlotIdx].Item = Item;
			Slots[SlotIdx].Quantity = StackSize;
			Remaining -= StackSize;
		}
	}

	MarkDirty();
	UE_LOG(LogInventory, Log, TEXT("ConsolidateStacks — Complete"));
}

// ============================================================================
// Internal
// ============================================================================

void UBPC_InventorySystem::RecalculateWeight()
{
	float Total = 0.0f;
	for (const FInventorySlot& Slot : Slots)
	{
		if (!Slot.IsEmpty() && Slot.Item)
		{
			Total += Slot.Item->Weight * Slot.Quantity;
		}
	}

	if (!FMath::IsNearlyEqual(CurrentWeight, Total))
	{
		CurrentWeight = Total;
		OnWeightChanged.Broadcast(CurrentWeight, MaxWeight);
	}
}

int32 UBPC_InventorySystem::FindExistingStack(UDA_ItemData* Item) const
{
	if (!Item)
	{
		return -1;
	}

	for (int32 i = 0; i < Slots.Num(); ++i)
	{
		if (Slots[i].Item == Item && Slots[i].Quantity < Item->StackLimit)
		{
			return i;
		}
	}

	return -1;
}

int32 UBPC_InventorySystem::FindEmptySlot() const
{
	for (int32 i = 0; i < Slots.Num(); ++i)
	{
		if (Slots[i].IsEmpty())
		{
			return i;
		}
	}

	return -1;
}

void UBPC_InventorySystem::MarkDirty()
{
	bDirty = true;
	OnInventoryChanged.Broadcast();
}
