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


#include "InventoryComponent.h"

#include "ItemDataAsset.h"

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

	// Necessaire pour que InitializeComponent() soit appele.
	bWantsInitializeComponent = true;
}

void UInventoryComponent::InitializeComponent()
{
	Super::InitializeComponent();

	EnsureSlotCount();

	SelectedHotbarIndex = FMath::Clamp(SelectedHotbarIndex, 0, FMath::Max(0, GetHotbarSlotCount() - 1));
}

void UInventoryComponent::SelectHotbarSlot(int32 Index)
{
	const int32 Count = GetHotbarSlotCount();
	if (Index < 0 || Index >= Count || Index == SelectedHotbarIndex)
	{
		return;
	}

	SelectedHotbarIndex = Index;
	OnSelectedHotbarSlotChanged.Broadcast(SelectedHotbarIndex);
}

void UInventoryComponent::CycleHotbarSelection(int32 Delta)
{
	const int32 Count = GetHotbarSlotCount();
	if (Count <= 0 || Delta == 0)
	{
		return;
	}

	// Modulo positif : en C++, -1 % 6 vaut -1 et non 5. Sans le double
	// modulo, reculer depuis le premier slot donnerait un index negatif.
	const int32 NewIndex = ((SelectedHotbarIndex + Delta) % Count + Count) % Count;
	SelectHotbarSlot(NewIndex);
}

FInventorySlot UInventoryComponent::GetSelectedSlot() const
{
	return Slots.IsValidIndex(SelectedHotbarIndex) ? Slots[SelectedHotbarIndex] : FInventorySlot();
}

void UInventoryComponent::EnsureSlotCount()
{
	const int32 Target = GetTotalSlotCount();
	if (Slots.Num() != Target)
	{
		Slots.SetNum(Target);
	}
}

int32 UInventoryComponent::AddItem(UItemDataAsset* Item, int32 Quantity, int32 RemainingUses)
{
	if (!Item || Quantity <= 0)
	{
		return Quantity;
	}

	EnsureSlotCount();

	const int32 MaxStack = Item->GetEffectiveMaxStack();
	const int32 MaxUses = FMath::Max(1, Item->MaxUses);
	// -1 = "plein" : un objet neuf, ou un ajout qui ne precise pas d'etat.
	const int32 StartingUses = (RemainingUses < 0) ? MaxUses : FMath::Clamp(RemainingUses, 0, MaxUses);

	int32 Remaining = Quantity;

	// Premiere passe : completer les piles deja entamees. On le fait avant
	// d'ouvrir un nouveau slot, sinon on gaspillerait des emplacements en
	// laissant des piles a moitie vides un peu partout.
	for (FInventorySlot& Slot : Slots)
	{
		if (Remaining <= 0)
		{
			break;
		}

		if (Slot.Item == Item && Slot.Quantity < MaxStack)
		{
			const int32 Added = FMath::Min(Remaining, MaxStack - Slot.Quantity);
			Slot.Quantity += Added;
			Remaining -= Added;
		}
	}

	// Deuxieme passe : ouvrir de nouveaux slots. La BARRE RAPIDE est servie en
	// premier, pour qu'un objet ramasse soit immediatement utilisable sans
	// passer par l'inventaire.
	auto FillEmptyRange = [&](int32 First, int32 Last)
	{
		for (int32 Index = First; Index < Last && Remaining > 0; ++Index)
		{
			FInventorySlot& Slot = Slots[Index];
			if (Slot.IsEmpty())
			{
				const int32 Added = FMath::Min(Remaining, MaxStack);
				Slot.Item = Item;
				Slot.Quantity = Added;
				Slot.RemainingUses = StartingUses;
				Remaining -= Added;
			}
		}
	};

	// Inverse ces deux lignes si tu veux un jour servir l'inventaire d'abord.
	const int32 BackpackStart = GetBackpackStartIndex();
	FillEmptyRange(0, BackpackStart);
	// Barre pleine : on deborde dans l'inventaire.
	FillEmptyRange(BackpackStart, Slots.Num());

	if (Remaining < Quantity)
	{
		OnInventoryChanged.Broadcast();
	}

	return Remaining;
}

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

	int32 Remaining = Quantity;

	// On vide les plus petites piles d'abord : ca libere des slots au plus
	// vite au lieu de laisser trainer plusieurs piles a moitie pleines.
	while (Remaining > 0)
	{
		FInventorySlot* Best = nullptr;
		for (FInventorySlot& Slot : Slots)
		{
			if (Slot.Item == Item && Slot.Quantity > 0)
			{
				if (!Best || Slot.Quantity < Best->Quantity)
				{
					Best = &Slot;
				}
			}
		}

		if (!Best)
		{
			break;
		}

		const int32 Taken = FMath::Min(Remaining, Best->Quantity);
		Best->Quantity -= Taken;
		Remaining -= Taken;

		if (Best->Quantity <= 0)
		{
			Best->Clear();
		}
	}

	const int32 Removed = Quantity - Remaining;
	if (Removed > 0)
	{
		OnInventoryChanged.Broadcast();
	}

	return Removed;
}

bool UInventoryComponent::ConsumeUse(int32 SlotIndex)
{
	if (!Slots.IsValidIndex(SlotIndex))
	{
		return false;
	}

	FInventorySlot& Slot = Slots[SlotIndex];
	if (Slot.IsEmpty())
	{
		return false;
	}

	const int32 MaxUses = FMath::Max(1, Slot.Item->MaxUses);

	// Objet a usage unique : un exemplaire disparait, point.
	if (MaxUses <= 1)
	{
		Slot.Quantity -= 1;
		if (Slot.Quantity <= 0)
		{
			Slot.Clear();
		}

		OnInventoryChanged.Broadcast();
		return true;
	}

	Slot.RemainingUses -= 1;
	if (Slot.RemainingUses <= 0)
	{
		Slot.Quantity -= 1;
		if (Slot.Quantity <= 0)
		{
			Slot.Clear();
		}
		else
		{
			// Cas theorique : un objet a charges ne s'empile pas. On recharge
			// quand meme l'exemplaire suivant, au cas ou une pile serait
			// construite autrement un jour.
			Slot.RemainingUses = MaxUses;
		}
	}

	OnInventoryChanged.Broadcast();
	return true;
}

int32 UInventoryComponent::GetItemCount(const UItemDataAsset* Item) const
{
	if (!Item)
	{
		return 0;
	}

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

	return Total;
}

int32 UInventoryComponent::GetRoomFor(const UItemDataAsset* Item) const
{
	if (!Item)
	{
		return 0;
	}

	const int32 MaxStack = Item->GetEffectiveMaxStack();
	int32 Room = 0;

	for (const FInventorySlot& Slot : Slots)
	{
		if (Slot.IsEmpty())
		{
			Room += MaxStack;
		}
		else if (Slot.Item == Item)
		{
			Room += FMath::Max(0, MaxStack - Slot.Quantity);
		}
	}

	return Room;
}

int32 UInventoryComponent::GetUsedSlotCount() const
{
	int32 Used = 0;
	for (const FInventorySlot& Slot : Slots)
	{
		if (!Slot.IsEmpty())
		{
			++Used;
		}
	}

	return Used;
}

int32 UInventoryComponent::FindFirstEmptySlot() const
{
	for (int32 Index = 0; Index < Slots.Num(); ++Index)
	{
		if (Slots[Index].IsEmpty())
		{
			return Index;
		}
	}

	return INDEX_NONE;
}

bool UInventoryComponent::TakeFromSlot(int32 SlotIndex, int32 Quantity, FInventorySlot& OutTaken)
{
	OutTaken.Clear();

	if (Quantity <= 0 || !Slots.IsValidIndex(SlotIndex))
	{
		return false;
	}

	FInventorySlot& Slot = Slots[SlotIndex];
	if (Slot.IsEmpty())
	{
		return false;
	}

	const int32 Taken = FMath::Min(Quantity, Slot.Quantity);

	OutTaken.Item = Slot.Item;
	OutTaken.Quantity = Taken;
	// L'etat d'usure part avec l'objet : jeter une canette entamee puis la
	// reprendre ne doit pas la remplir.
	OutTaken.RemainingUses = Slot.RemainingUses;

	Slot.Quantity -= Taken;
	if (Slot.Quantity <= 0)
	{
		Slot.Clear();
	}

	OnInventoryChanged.Broadcast();
	return true;
}

bool UInventoryComponent::MoveItemQuantity(int32 FromIndex, int32 ToIndex, int32 Quantity)
{
	if (Quantity <= 0 || FromIndex == ToIndex || !Slots.IsValidIndex(FromIndex) || !Slots.IsValidIndex(ToIndex))
	{
		return false;
	}

	FInventorySlot& From = Slots[FromIndex];
	if (From.IsEmpty())
	{
		return false;
	}

	// Deplacement total : on retombe sur le comportement complet, echange compris.
	if (Quantity >= From.Quantity)
	{
		return MoveItem(FromIndex, ToIndex);
	}

	FInventorySlot& To = Slots[ToIndex];
	const int32 MaxStack = From.Item->GetEffectiveMaxStack();

	if (To.IsEmpty())
	{
		const int32 Transferred = FMath::Min(Quantity, MaxStack);
		To.Item = From.Item;
		To.Quantity = Transferred;
		To.RemainingUses = From.RemainingUses;
		From.Quantity -= Transferred;

		OnInventoryChanged.Broadcast();
		return true;
	}

	if (To.Item == From.Item)
	{
		const int32 Transferable = FMath::Min(Quantity, MaxStack - To.Quantity);
		if (Transferable <= 0)
		{
			return false;
		}

		To.Quantity += Transferable;
		From.Quantity -= Transferable;

		OnInventoryChanged.Broadcast();
		return true;
	}

	// Objet different et transfert partiel : aucune interpretation raisonnable.
	// On refuse plutot que d'inventer un comportement que le joueur ne
	// comprendrait pas.
	return false;
}

bool UInventoryComponent::MoveItem(int32 FromIndex, int32 ToIndex)
{
	if (FromIndex == ToIndex || !Slots.IsValidIndex(FromIndex) || !Slots.IsValidIndex(ToIndex))
	{
		return false;
	}

	FInventorySlot& From = Slots[FromIndex];
	if (From.IsEmpty())
	{
		return false;
	}

	FInventorySlot& To = Slots[ToIndex];

	// Cible vide : la pile entiere part, quelle que soit sa taille.
	if (To.IsEmpty())
	{
		To = From;
		From.Clear();
		OnInventoryChanged.Broadcast();
		return true;
	}

	// Meme objet : on fusionne dans la limite de la pile. Pour un objet a
	// charges, GetEffectiveMaxStack vaut 1, donc on tombe toujours sur
	// l'echange -- deux canettes entamees ne fusionnent jamais.
	if (To.Item == From.Item)
	{
		const int32 MaxStack = From.Item->GetEffectiveMaxStack();
		const int32 Transferable = FMath::Min(From.Quantity, MaxStack - To.Quantity);

		// Cible deja pleine : on echange plutot que de ne rien faire, sinon le
		// geste du joueur resterait sans effet et passerait pour un bug.
		if (Transferable <= 0)
		{
			Swap(From, To);
			OnInventoryChanged.Broadcast();
			return true;
		}

		To.Quantity += Transferable;
		From.Quantity -= Transferable;
		if (From.Quantity <= 0)
		{
			From.Clear();
		}

		OnInventoryChanged.Broadcast();
		return true;
	}

	// Objets differents : simple echange.
	Swap(From, To);
	OnInventoryChanged.Broadcast();
	return true;
}

bool UInventoryComponent::SetBonusSlotCount(int32 NewBonusSlots)
{
	const int32 NewTotal = FMath::Max(0, HotbarSlotCount + BackpackSlotCount + FMath::Max(0, NewBonusSlots));

	// On refuse plutot que de detruire du butin en silence. C'est a l'appelant
	// (le code qui deséquipe le sac) de decider quoi faire : bloquer le retrait,
	// ou vider le surplus au sol avant de reessayer.
	if (GetUsedSlotCount() > NewTotal)
	{
		return false;
	}

	BonusSlotCount = FMath::Max(0, NewBonusSlots);

	// Compacter avant de tronquer : sans ca, un objet range dans un slot de
	// fin serait supprime alors qu'il reste de la place au debut.
	CompactSlots();
	Slots.SetNum(NewTotal);

	OnInventoryChanged.Broadcast();
	return true;
}

void UInventoryComponent::CompactSlots()
{
	const int32 Start = GetBackpackStartIndex();
	const int32 Count = Slots.Num() - Start;
	if (Count <= 0)
	{
		return;
	}

	// On ne compacte QUE la portion sac. Compacter tout le tableau ferait
	// remonter des objets du sac dans la barre rapide sans que le joueur
	// l'ait demande -- et la troncature ne retire de toute facon que la fin.
	TArray<FInventorySlot> Backpack;
	Backpack.Append(Slots.GetData() + Start, Count);

	Backpack.StableSort([](const FInventorySlot& A, const FInventorySlot& B)
	{
		return !A.IsEmpty() && B.IsEmpty();
	});

	for (int32 Index = 0; Index < Count; ++Index)
	{
		Slots[Start + Index] = Backpack[Index];
	}
}
