using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Ashwild.Player;
namespace Ashwild.UI
{
///
/// Generic panel-stack engine shared by every scene that drives a stack of s
/// (the menu and the in-game UI). It owns the panel registry, the navigation history and the
/// show/hide flow, and dispatches the Cancel (Escape) input from the bus to a scene-specific
/// policy via .
///
/// This is abstract: put the concrete MenuUIManager or GameUIManager on the object,
/// never this. Menu and game live in separate scenes, so only one manager is active at a time —
/// hence a single shared of the base type, which lets panels reach "their"
/// manager through UIManager.Instance regardless of which concrete subclass is running.
///
public abstract class UIManager : MonoBehaviour
{
#region State
///
/// The UI manager for the currently loaded scene (menu or game).
///
public static UIManager Instance { get; private set; }
[Header("Panels")]
[SerializeField] protected List panels;
[Tooltip("Panel shown on start. Leave empty for scenes that begin with no panel (e.g. gameplay).")]
[SerializeField] protected UIPanel defaultPanel;
private Dictionary map;
private Stack history;
private UIPanel current;
///
/// The panel currently on top of the stack (null when nothing is open).
///
public UIPanel Current => current;
///
/// True while at least one panel is open.
///
protected bool HasOpenPanel => current != null;
///
/// True when there is a panel below the current one to go back to.
///
protected bool CanGoBack => history != null && history.Count > 1;
#endregion
#region Unity Lifecycle
///
/// Establishes the singleton and builds the type→panel lookup.
///
protected virtual void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
map = panels.Where(p => p != null).ToDictionary(p => p.GetType());
history = new Stack();
}
///
/// Subscribes to the Cancel (Escape) input from the bus.
///
protected virtual void OnEnable()
{
PlayerEvents.CancelPressed += OnEscape;
}
///
/// Unsubscribes — mirrors OnEnable.
///
protected virtual void OnDisable()
{
PlayerEvents.CancelPressed -= OnEscape;
}
///
/// Hides every panel, then opens the default one (if any).
///
protected virtual void Start()
{
foreach (UIPanel p in panels)
if (p != null) p.HideInstant();
if (defaultPanel != null)
OpenPanel(defaultPanel.GetType());
}
///
/// Clears the singleton reference.
///
protected virtual void OnDestroy()
{
if (Instance == this) Instance = null;
}
#endregion
#region Public API
///
/// Opens the registered panel of type T.
///
public void OpenPanel() where T : UIPanel => OpenPanel(typeof(T));
///
/// Opens the given panel instance (resolved by its concrete type).
///
public void OpenPanel(UIPanel panel)
{
if (panel == null) return;
OpenPanel(panel.GetType());
}
///
/// Goes back one level in the navigation history. Never closes the last panel — use
/// for that (the menu always keeps its default panel visible).
///
public void Back()
{
if (history.Count <= 1) return;
UIPanel closing = history.Pop();
closing.Hide();
current = history.Peek();
current.Show();
OnCurrentChanged();
}
///
/// Closes every open panel, returning to the "no panel" state (used by gameplay to leave pause).
///
public void CloseAll()
{
if (history.Count == 0) return;
while (history.Count > 0)
history.Pop().Hide();
current = null;
OnCurrentChanged();
}
#endregion
#region Internal
///
/// Core open: hides the current panel, pushes and shows the requested one.
///
protected void OpenPanel(Type t)
{
if (map == null) return;
if (!map.TryGetValue(t, out UIPanel next))
{
Debug.LogWarning($"[{GetType().Name}] no panel of type {t.Name} registered.", this);
return;
}
if (current == next) return;
if (current != null) current.Hide();
history.Push(next);
current = next;
next.Show();
OnCurrentChanged();
}
///
/// Scene-specific Escape behaviour (menu = navigate back, game = toggle pause).
///
protected abstract void OnEscape();
///
/// Hook fired whenever the top panel changes (opened / went back / closed all).
/// Subclasses use it to react to the stack becoming empty or non-empty.
///
protected virtual void OnCurrentChanged() { }
#endregion
}
}