using UnityEngine;
using TMPro;
using Ashwild.Inventory;
namespace Ashwild.Storage
{
///
/// The chest module of the inventory window: the right-side grid that shows the opened chest's
/// contents, mirror of the hover-description module it replaces while a chest is open. It is a pure
/// view driven by — it builds its grid of cells from
/// the bound chest's replicated view and refreshes them live, but it never routes transfers itself:
/// every cell reports its drop to , which turns it into the
/// matching server-authoritative deposit/withdraw/move on the .
///
[DisallowMultipleComponent]
public class ChestPanelUI : MonoBehaviour
{
#region Serialized Fields
[Header("Window")]
[Tooltip("Root holder toggled on/off as the module is shown or hidden.")]
[SerializeField] private GameObject root;
[SerializeField] private TextMeshProUGUI titleText;
[Header("Grid")]
[Tooltip("Parent the chest cells are laid out under.")]
[SerializeField] private Transform slotContainer;
[Tooltip("Prefab carrying a SlotUI — the same cell used by the inventory grid and the hotbar.")]
[SerializeField] private GameObject slotPrefab;
#endregion
#region State
private SlotUI[] slots;
private Chest boundChest;
///
/// The chest currently shown (null while the module is hidden). Read by InventoryUI to route
/// transfers.
///
public Chest Chest => boundChest;
#endregion
#region Unity Lifecycle
///
/// Parks the module hidden so it only appears once a chest is opened.
///
private void Awake()
{
if (root != null) root.SetActive(false);
}
///
/// Unbinds the chest event if the module is torn down while open.
///
private void OnDestroy()
{
if (boundChest != null) boundChest.SlotViewChanged -= HandleChestSlotChanged;
}
#endregion
#region Show / Hide
///
/// Binds a chest and shows the module: (re)builds the grid to the chest's slot count, subscribes
/// to its replicated view so cells refresh live, sets the title and draws every cell.
///
public void Bind(Chest chest)
{
if (boundChest != null) boundChest.SlotViewChanged -= HandleChestSlotChanged;
boundChest = chest;
if (root != null) root.SetActive(true);
BuildGrid();
if (boundChest != null)
{
boundChest.SlotViewChanged += HandleChestSlotChanged;
if (titleText != null) titleText.text = boundChest.DisplayName;
}
RefreshAll();
}
///
/// Hides the module, unbinds the chest and releases it.
///
public void Hide()
{
if (boundChest != null) boundChest.SlotViewChanged -= HandleChestSlotChanged;
boundChest = null;
if (root != null) root.SetActive(false);
}
#endregion
#region Grid Building
///
/// Builds (or rebuilds when the size differs) the grid to match the bound chest's slot count, so
/// opening a small chest then a large one lays out the right number of cells.
///
private void BuildGrid()
{
int count = boundChest != null ? boundChest.SlotCount : 0;
if (slots != null && slots.Length == count) return;
if (slots != null)
foreach (SlotUI s in slots)
if (s != null) Destroy(s.gameObject);
slots = new SlotUI[count];
for (int i = 0; i < count; i++)
{
SlotUI slot = Instantiate(slotPrefab, slotContainer).GetComponent();
slot.Initialize(SlotContainer.Chest, i, InventoryUI.HandleSlotDrop, HandleChestSlotClicked);
slots[i] = slot;
}
}
#endregion
#region Interaction
///
/// Right-click on a chest cell quick-transfers its whole stack into the inventory (auto-placed).
///
private void HandleChestSlotClicked(SlotContainer container, int index, bool rightClick)
{
if (!rightClick || boundChest == null) return;
PredictEmptied(index);
boundChest.RequestQuickTransfer(SlotContainer.Chest, index);
}
///
/// Draws a cell as emptied straight away, while its move is still in flight to the server.
///
/// Emptying the chest travels on the SyncList (flushed at end of tick) but the matching grant is
/// a TargetRpc (sent immediately), so the item would otherwise appear in the inventory a moment
/// *before* leaving the chest — a visible double. This only repaints the cell: the authoritative
/// model is untouched, so if the server disagrees (a co-op partner grabbed the stack first) the
/// next replicated update simply paints the item back.
///
public void PredictEmptied(int index)
{
if (slots == null || index < 0 || index >= slots.Length) return;
slots[index].UpdateVisual((ItemData)null, 0, -1);
}
#endregion
#region Refresh
///
/// Refreshes a single cell, or the whole grid when the collection was (re)seeded (index -1).
///
private void HandleChestSlotChanged(int index)
{
if (slots == null) return;
if (index < 0) { RefreshAll(); return; }
if (index < slots.Length) RefreshSlot(index);
}
///
/// Redraws every chest cell.
///
private void RefreshAll()
{
if (slots == null) return;
for (int i = 0; i < slots.Length; i++) RefreshSlot(i);
}
///
/// Draws one cell from the chest's replicated view, or empty when the slot holds nothing.
///
private void RefreshSlot(int index)
{
if (boundChest != null && boundChest.TryGetSlot(index, out ItemData item, out int qty, out int uses))
slots[index].UpdateVisual(item, qty, uses);
else
slots[index].UpdateVisual((ItemData)null, 0, -1);
}
#endregion
}
}