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

#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "InventoryComponent.generated.h"

class UItemDataAsset;

/**
 * Contenu d'un emplacement. Item nul = slot vide.
 * Le tableau de slots a toujours exactement la taille de l'inventaire :
 * l'index dans le tableau EST la position dans la grille, ce qui rend
 * l'affichage et le futur drag & drop triviaux.
 */
USTRUCT(BlueprintType)
struct FInventorySlot
{
	GENERATED_BODY()

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventaire")
	TObjectPtr<UItemDataAsset> Item = nullptr;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventaire")
	int32 Quantity = 0;

	/**
	 * Charges restantes de l'exemplaire courant, pour les objets a usages
	 * multiples. Etat par instance, d'ou sa place ici et pas sur le DataAsset :
	 * une canette entamee ne doit pas entamer toutes les canettes du jeu.
	 */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventaire")
	int32 RemainingUses = 0;

	bool IsEmpty() const { return Item == nullptr || Quantity <= 0; }

	void Clear()
	{
		Item = nullptr;
		Quantity = 0;
		RemainingUses = 0;
	}
};

/** Diffuse a chaque modification du contenu ou de la taille. L'UI s'y abonne. */
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnInventoryChanged);

/** Diffuse quand le joueur change de slot actif dans la barre rapide. */
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnSelectedHotbarSlotChanged, int32, NewIndex);

/**
 * Inventaire en grille a slots, facon Raft / Valheim.
 *
 * Le nombre de slots est dynamique : BaseSlotCount fixe le minimum, et
 * BonusSlotCount permet a un sac equipe de l'agrandir. Retirer le sac
 * refuse si les objets ne rentrent plus, plutot que de les faire
 * disparaitre en silence.
 */
UCLASS(ClassGroup = (Survival), meta = (BlueprintSpawnableComponent))
class EMBERWILD_API UInventoryComponent : public UActorComponent
{
	GENERATED_BODY()

public:
	UInventoryComponent();

	virtual void InitializeComponent() override;

	virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;

	/**
	 * Ajoute des objets, en remplissant d'abord les piles entamees.
	 * @param RemainingUses charges de l'exemplaire ajoute ; -1 signifie "plein".
	 *        Sert a rendre son etat a un objet ramasse au sol.
	 * @return le nombre d'exemplaires qui N'ONT PAS pu rentrer (0 = tout est passe).
	 */
	UFUNCTION(BlueprintCallable, Category = "Inventaire")
	int32 AddItem(UItemDataAsset* Item, int32 Quantity = 1, int32 RemainingUses = -1);

	/**
	 * Consomme une utilisation du slot.
	 * Retire un exemplaire quand il ne reste plus de charge.
	 * @return true si quelque chose a ete consomme.
	 */
	UFUNCTION(BlueprintCallable, Category = "Inventaire")
	bool ConsumeUse(int32 SlotIndex);

	/**
	 * Retire des objets, en vidant d'abord les piles les plus petites.
	 * @return le nombre d'exemplaires reellement retires.
	 */
	UFUNCTION(BlueprintCallable, Category = "Inventaire")
	int32 RemoveItem(UItemDataAsset* Item, int32 Quantity = 1);

	/** Combien d'exemplaires de cet objet, toutes piles confondues. */
	UFUNCTION(BlueprintPure, Category = "Inventaire")
	int32 GetItemCount(const UItemDataAsset* Item) const;

	/** Combien d'exemplaires pourraient encore rentrer, sans rien modifier. */
	UFUNCTION(BlueprintPure, Category = "Inventaire")
	int32 GetRoomFor(const UItemDataAsset* Item) const;

	/**
	 * Un seul tableau, deux conteneurs distincts :
	 *   index 0 .. HotbarSlotCount-1   -> barre rapide
	 *   index HotbarSlotCount .. fin   -> grille d'inventaire (+ bonus de sac)
	 *
	 * Un objet range dans la barre n'apparait PAS dans la grille, et
	 * inversement. Ils partagent le meme tableau uniquement pour que le
	 * glisser-deposer entre les deux ne demande aucune conversion d'index.
	 */
	UFUNCTION(BlueprintPure, Category = "Inventaire")
	int32 GetTotalSlotCount() const { return FMath::Max(0, HotbarSlotCount + BackpackSlotCount + BonusSlotCount); }

	/** Premier index affiche par la grille, juste apres la barre rapide. */
	UFUNCTION(BlueprintPure, Category = "Inventaire")
	int32 GetBackpackStartIndex() const { return GetHotbarSlotCount(); }

	UFUNCTION(BlueprintPure, Category = "Inventaire")
	int32 GetUsedSlotCount() const;

	UFUNCTION(BlueprintPure, Category = "Inventaire")
	const TArray<FInventorySlot>& GetSlots() const { return Slots; }

	/**
	 * Deplace la pile entiere d'un slot vers un autre.
	 * - cible vide      -> la pile entiere part
	 * - meme objet      -> fusion jusqu'a MaxStackSize, le reste demeure a la source
	 * - objet different -> echange des deux slots
	 * @return true si quelque chose a change.
	 */
	UFUNCTION(BlueprintCallable, Category = "Inventaire")
	bool MoveItem(int32 FromIndex, int32 ToIndex);

	/**
	 * Deplace seulement une partie d'une pile.
	 *
	 * Un transfert partiel vers un slot occupe par un objet DIFFERENT est
	 * refuse : il n'existe aucune facon sensee d'echanger la moitie d'une pile
	 * contre autre chose. Seul un deplacement total peut provoquer un echange.
	 *
	 * @return true si quelque chose a change.
	 */
	UFUNCTION(BlueprintCallable, Category = "Inventaire")
	bool MoveItemQuantity(int32 FromIndex, int32 ToIndex, int32 Quantity);

	// ------------------------------------------------------------------
	// Transferts entre deux inventaires (coffres)
	// ------------------------------------------------------------------

	/**
	 * Deplace le contenu d'un slot vers un autre, dans le MEME inventaire ou
	 * dans un autre. C'est desormais l'unique implementation du deplacement :
	 * MoveItem() et MoveItemQuantity() ne sont que des appels avec From == To.
	 *
	 * Statique parce qu'elle n'appartient ni a la source ni a la destination --
	 * elle arbitre entre les deux, et doit diffuser OnInventoryChanged sur les
	 * DEUX. Oublier la seconde diffusion laisse la grille d'en face afficher un
	 * etat perime jusqu'au prochain evenement, ce qui se lit comme un objet
	 * qui disparait.
	 *
	 * @param Quantity nombre d'exemplaires ; MAX_int32 pour la pile entiere.
	 * @return true si quelque chose a change.
	 */
	UFUNCTION(BlueprintCallable, Category = "Inventaire")
	static bool TransferSlot(UInventoryComponent* From, int32 FromIndex, UInventoryComponent* To, int32 ToIndex, int32 Quantity);

	/**
	 * Envoie le contenu d'un slot vers un autre inventaire sans designer de
	 * case d'arrivee : c'est le clic droit de l'ecran de coffre.
	 *
	 * Passe par AddItem(), donc exactement comme un ramassage -- piles entamees
	 * d'abord, puis barre rapide, puis grille. Ce qui ne rentre pas reste sur
	 * place : un transfert rapide ne perd jamais rien.
	 *
	 * @return le nombre d'exemplaires reellement partis.
	 */
	UFUNCTION(BlueprintCallable, Category = "Inventaire")
	static int32 QuickTransferSlot(UInventoryComponent* From, int32 FromIndex, UInventoryComponent* To);

	/**
	 * Deverse tout ce qui peut entrer vers un autre inventaire : les boutons
	 * "Tout ranger" et "Tout prendre".
	 *
	 * @param FirstIndex premier slot considere. Le joueur passe
	 *        GetBackpackStartIndex() pour que "Tout ranger" laisse sa barre
	 *        rapide tranquille -- ranger ses outils dans le coffre a chaque
	 *        retour d'expedition serait une punition, pas un confort.
	 * @return le nombre d'exemplaires reellement partis.
	 */
	UFUNCTION(BlueprintCallable, Category = "Inventaire")
	int32 TransferAllTo(UInventoryComponent* To, int32 FirstIndex = 0);

	/**
	 * Transforme ce composant en contenant du monde : NewSlotCount cases de
	 * grille et AUCUNE barre rapide, pour que GetBackpackStartIndex() vaille 0
	 * et que la grille affiche la totalite du coffre.
	 *
	 * A appeler avant que le composant contienne quoi que ce soit -- typiquement
	 * depuis PreInitializeComponents() de l'acteur porteur. Reduire la taille
	 * d'un conteneur deja rempli fait disparaitre ce qui depasse.
	 */
	UFUNCTION(BlueprintCallable, Category = "Inventaire")
	void ConfigureAsContainer(int32 NewSlotCount);

	/** Index du premier slot libre, ou INDEX_NONE si l'inventaire est plein. */
	UFUNCTION(BlueprintPure, Category = "Inventaire")
	int32 FindFirstEmptySlot() const;

	/**
	 * Retire jusqu'a Quantity exemplaires d'un slot precis et renvoie ce qui a
	 * ete retire. Sert au drop au sol et, plus tard, au transfert vers un coffre.
	 *
	 * Different de RemoveItem() qui pioche par type sans se soucier du slot :
	 * ici le joueur a designe une case, on ne touche qu'a celle-la.
	 */
	UFUNCTION(BlueprintCallable, Category = "Inventaire")
	bool TakeFromSlot(int32 SlotIndex, int32 Quantity, FInventorySlot& OutTaken);

	/**
	 * Change le bonus de slots apporte par l'equipement (sac).
	 * @return false si reduire la taille laisserait des objets dehors ;
	 *         dans ce cas rien n'est modifie et le sac ne doit pas etre retire.
	 */
	UFUNCTION(BlueprintCallable, Category = "Inventaire")
	bool SetBonusSlotCount(int32 NewBonusSlots);

	UPROPERTY(BlueprintAssignable, Category = "Inventaire")
	FOnInventoryChanged OnInventoryChanged;

	// ------------------------------------------------------------------
	// Barre rapide
	// ------------------------------------------------------------------
	//
	// La barre rapide n'est PAS un jeu de slots separe : ce sont les
	// HotbarSlotCount premiers slots de l'inventaire, simplement affiches
	// deux fois. Consequence directe : le glisser-deposer entre la barre et
	// la grille fonctionne sans une ligne de code de plus, puisque ce sont
	// les memes index dans le meme tableau.

	/** Change le slot actif. Un index hors de la barre est ignore. */
	UFUNCTION(BlueprintCallable, Category = "Inventaire|Barre rapide")
	void SelectHotbarSlot(int32 Index);

	/** Avance ou recule dans la barre, en boucle. */
	UFUNCTION(BlueprintCallable, Category = "Inventaire|Barre rapide")
	void CycleHotbarSelection(int32 Delta);

	UFUNCTION(BlueprintPure, Category = "Inventaire|Barre rapide")
	int32 GetSelectedHotbarIndex() const { return SelectedHotbarIndex; }

	UFUNCTION(BlueprintPure, Category = "Inventaire|Barre rapide")
	int32 GetHotbarSlotCount() const { return FMath::Min(HotbarSlotCount, Slots.Num()); }

	/** Contenu du slot actif. C'est ce que le joueur "a en main". */
	UFUNCTION(BlueprintPure, Category = "Inventaire|Barre rapide")
	FInventorySlot GetSelectedSlot() const;

	UPROPERTY(BlueprintAssignable, Category = "Inventaire|Barre rapide")
	FOnSelectedHotbarSlotChanged OnSelectedHotbarSlotChanged;

protected:
	/** Cases affichees dans la grille d'inventaire. N'inclut PAS la barre rapide. */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Inventaire", meta = (ClampMin = "0"))
	int32 BackpackSlotCount = 20;

	/** Slots supplementaires apportes par l'equipement (sac). Passe par SetBonusSlotCount(). */
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Inventaire")
	int32 BonusSlotCount = 0;

	/**
	 * Cases de la barre rapide, independantes de celles de la grille.
	 * Elles occupent les premiers index du tableau.
	 */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Inventaire|Barre rapide", meta = (ClampMin = "0"))
	int32 HotbarSlotCount = 10;

	/**
	 * Toujours dimensionne a GetTotalSlotCount().
	 *
	 * Replique a TOUT LE MONDE et non au seul proprietaire, pour deux raisons.
	 *
	 * Technique d'abord : les conditions de GetLifetimeReplicatedProps sont
	 * mises en cache par CLASSE, pas par instance. Un COND_OwnerOnly viserait
	 * donc aussi le composant d'un coffre -- or un coffre n'appartient a aucune
	 * connexion, et sa grille n'arriverait chez personne. Le meme composant sert
	 * le sac et le contenant, c'est une decision d'architecture qu'on ne veut pas
	 * defaire pour un drapeau de replication.
	 *
	 * De fond ensuite : les coffres sont a acces simultane, donc leur contenu
	 * DOIT arriver a tous ceux qui l'ouvrent. Et voir le sac d'un coequipier n'a
	 * rien d'un probleme dans une coop entre amis sans anti-triche -- c'est meme
	 * ce qui rendra un futur echange d'objets gratuit.
	 *
	 * Cout reel : 4 joueurs x 30 slots de trois champs, envoyes uniquement quand
	 * ils changent, et delta-compresses. Negligeable.
	 */
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, ReplicatedUsing = OnRep_Slots, Category = "Inventaire")
	TArray<FInventorySlot> Slots;

	/**
	 * Le contenu vient d'arriver du serveur : on rediffuse le delegue habituel.
	 *
	 * C'est LA ligne qui recable toute l'interface d'un coup. Grille, barre
	 * rapide, panneau de detail et coffres sont deja abonnes a
	 * OnInventoryChanged et ne font aucun Tick -- ils n'ont donc pas une seule
	 * ligne a changer pour devenir reseau.
	 */
	UFUNCTION()
	void OnRep_Slots();

private:
	// ------------------------------------------------------------------
	// Reseau
	//
	// Le routage vit DANS les fonctions publiques et non chez leurs appelants :
	// une case d'inventaire appelle MoveItem() sans savoir si elle tourne chez
	// l'hote ou chez un client, et c'est tres bien ainsi. Ajouter le test
	// d'autorite dans chaque widget serait dix occasions de l'oublier.
	//
	// Pas de prediction locale : le client envoie et attend OnRep_Slots. Sur un
	// relais Steam l'aller-retour est de quelques dizaines de millisecondes,
	// invisible pour un glisser-deposer -- alors qu'une prediction fausse
	// donnerait une pile qui saute, symptome bien plus desagreable.
	// ------------------------------------------------------------------

	/** Ce composant peut-il modifier son contenu ici et maintenant. */
	bool HasInventoryAuthority() const;

	/**
	 * Par quel composant faire transiter une RPC serveur.
	 *
	 * Renvoie celui des deux qui appartient au pawn du joueur LOCAL : un coffre
	 * n'appartient a aucune connexion, et une RPC emise depuis lui serait
	 * silencieusement jetee par le moteur. C'est le piege numero un des
	 * transferts vers un conteneur.
	 */
	static UInventoryComponent* FindNetProxy(UInventoryComponent* A, UInventoryComponent* B);

	UFUNCTION(Server, Reliable)
	void Server_TransferSlot(UInventoryComponent* From, int32 FromIndex, UInventoryComponent* To, int32 ToIndex, int32 Quantity);

	UFUNCTION(Server, Reliable)
	void Server_QuickTransferSlot(UInventoryComponent* From, int32 FromIndex, UInventoryComponent* To);

	/**
	 * From est passe explicitement et n'est PAS "this" cote serveur : la RPC
	 * transite par l'inventaire du joueur local, qui peut tres bien etre la
	 * DESTINATION -- c'est le cas du bouton "Tout prendre" d'un coffre. Deduire
	 * la source du recepteur viderait le sac du joueur dans lui-meme.
	 */
	UFUNCTION(Server, Reliable)
	void Server_TransferAllTo(UInventoryComponent* From, UInventoryComponent* To, int32 FirstIndex);

	UFUNCTION(Server, Reliable)
	void Server_ConsumeUse(int32 SlotIndex);

	/** Les regles de deplacement, une fois l'autorite acquise. */
	static bool ApplyTransferSlot(UInventoryComponent* From, int32 FromIndex, UInventoryComponent* To, int32 ToIndex, int32 Quantity);

	/** Redimensionne le tableau si la taille cible a change. */
	void EnsureSlotCount();

	/** Regroupe les slots occupes au debut du tableau. */
	void CompactSlots();

	/**
	 * Regles de deplacement entre deux cases, sans savoir a quel inventaire
	 * chacune appartient. Ne diffuse rien : c'est l'appelant qui sait combien
	 * de composants ont bouge.
	 */
	static bool ApplySlotMove(FInventorySlot& From, FInventorySlot& To, int32 Quantity);

	/**
	 * Verse un slot dans un autre inventaire via AddItem(). Ne diffuse que du
	 * cote destination (AddItem s'en charge) : la source reste a la charge de
	 * l'appelant, qui peut en enchainer vingt.
	 * @return exemplaires reellement partis.
	 */
	int32 PushSlotInto(int32 SlotIndex, UInventoryComponent& To);

	/**
	 * Diffusion differee. Un "Tout ranger" deplace jusqu'a vingt piles : sans
	 * regroupement, l'UI se reconstruirait vingt fois pour un seul clic.
	 * Toutes les modifications passent par BroadcastChanged() plutot que par
	 * OnInventoryChanged.Broadcast() directement, sinon le lot fuit.
	 */
	void BroadcastChanged();
	void BeginBatch();
	void EndBatch();

	int32 SelectedHotbarIndex = 0;

	int32 BatchDepth = 0;
	bool bBatchDirty = false;
};
