// SPDX-License-Identifier: Apache-2.0
#pragma once

#include <vtbackend/Color.hpp>

#include <memory>
#include <optional>
#include <span>
#include <tuple>
#include <utility>
#include <vector>

#include <vtworkspace/ModelEvents.hpp>
#include <vtworkspace/Pane.hpp>
#include <vtworkspace/Primitives.hpp>
#include <vtworkspace/Tab.hpp>

namespace vtworkspace
{

/// A logical window: an ordered list of tabs with one active tab.
///
/// In single-process GUI mode there is one Window per OS window; in daemon mode a Window is a
/// server-side layout that one or more clients may attach to.
class Window
{
  public:
    explicit Window(WindowId id) noexcept: _id { id } {}

    Window(Window const&) = delete;
    Window& operator=(Window const&) = delete;
    Window(Window&&) = delete;
    Window& operator=(Window&&) = delete;
    ~Window() = default;

    [[nodiscard]] WindowId id() const noexcept { return _id; }
    [[nodiscard]] int tabCount() const noexcept { return static_cast<int>(_tabs.size()); }
    [[nodiscard]] bool empty() const noexcept { return _tabs.empty(); }

    /// The active tab, or nullptr if the window has no tabs.
    [[nodiscard]] Tab* activeTab() const noexcept;
    [[nodiscard]] int activeTabIndex() const noexcept { return _activeTabIndex; }

    /// The tab that was active before the current one (the target of a "switch to previous tab"
    /// toggle), or nullptr if there is no valid previous tab (never switched, or the previous tab was
    /// closed). Tracked here — the authoritative, Qt-free model — so switch-to-previous-tab is a pure
    /// model query and lives per window automatically.
    [[nodiscard]] Tab* previousActiveTab() const noexcept;
    [[nodiscard]] int previousActiveTabIndex() const noexcept { return _previousActiveTabIndex; }

    /// The tab at @p index, or nullptr if out of range.
    [[nodiscard]] Tab* tabAt(int index) const noexcept;

    /// The index of @p tab, or -1 if not found.
    [[nodiscard]] int indexOf(TabId tab) const noexcept;
    [[nodiscard]] int indexOf(Tab const* tab) const noexcept;

  private:
    friend class SessionModel;

    WindowId _id;
    std::vector<std::unique_ptr<Tab>> _tabs;
    int _activeTabIndex = -1;
    /// The slot that was active before _activeTabIndex, or -1 if none. Maintained by SessionModel: set on
    /// a genuine activation (createTab/activateTab), and translated/invalidated (never left dangling) by
    /// the reindexing operations (moveTab/closeTabAt).
    int _previousActiveTabIndex = -1;
};

/// The single source of truth for all windows, tabs and panes.
///
/// SessionModel is deliberately free of Qt and of any transport. Every mutation is performed through
/// one of its methods, which updates the tree and notifies the host through ModelEvents. The Qt GUI
/// supplies a ModelEvents implementation that turns each callback into a Qt signal; a future daemon
/// supplies one that fans each callback out to subscribed network clients. The model is the same in
/// both worlds, which is what lets a daemon own it and many clients observe it.
///
/// Session lifetime is *not* owned here: the model refers to a session only by SessionId. Creating
/// the underlying terminal/PTY for a new SessionId, and tearing it down for a closed one, is the
/// host's responsibility (it learns of both through the SessionAllocator and ModelEvents).
class SessionModel
{
  public:
    /// Allocates a fresh session for a new pane and returns its id. The host creates the backing
    /// terminal/PTY for that id. Invoked when a tab or split needs a new session.
    using SessionAllocator = std::function<SessionId()>;

    /// @param events    Observer notified of every change. Must outlive the model.
    /// @param allocator Supplies a new SessionId (and its backing terminal) on demand.
    SessionModel(ModelEvents& events, SessionAllocator allocator);

    SessionModel(SessionModel const&) = delete;
    SessionModel& operator=(SessionModel const&) = delete;
    SessionModel(SessionModel&&) = delete;
    SessionModel& operator=(SessionModel&&) = delete;
    ~SessionModel() = default;

    // {{{ Windows

    /// Creates a new, empty window and returns it.
    [[nodiscard]] Window* createWindow();

    /// Removes a window and everything in it. Each contained session is reported closed.
    void removeWindow(WindowId window);

    [[nodiscard]] Window* window(WindowId id) const noexcept;

    /// @return How many windows the model holds.
    [[nodiscard]] int windowCount() const noexcept { return static_cast<int>(_windows.size()); }

    /// @return The window at @p index (0-based), or nullptr if out of range. For
    ///         enumerating every window (e.g. the daemon serializing all of them).
    [[nodiscard]] Window* windowAt(int index) const noexcept
    {
        return index >= 0 && std::cmp_less(index, _windows.size())
                   ? _windows[static_cast<std::size_t>(index)].get()
                   : nullptr;
    }

    // }}}
    // {{{ Tabs

    /// Appends a new tab (with a single fresh-session pane) to @p window and makes it active.
    [[nodiscard]] Tab* createTab(WindowId window);

    /// Closes the tab @p tab in @p window, reporting each of its sessions closed. If it was the
    /// last tab, the window becomes empty (the host decides whether to close the OS window).
    void closeTab(WindowId window, TabId tab);

    /// Closes every tab in @p window except @p keep.
    void closeOtherTabs(WindowId window, TabId keep);

    /// Closes every tab positioned after @p anchor in @p window.
    void closeTabsToRight(WindowId window, TabId anchor);

    /// Makes @p tab the active tab of @p window.
    void activateTab(WindowId window, TabId tab);

    /// Moves @p tab to @p toIndex within @p window.
    void moveTab(WindowId window, TabId tab, int toIndex);

    /// Moves @p tab out of window @p from and into window @p to at @p toIndex, transplanting its whole
    /// pane subtree (and the sessions it carries — none are closed). Fixes the active/previous-tab
    /// bookkeeping on both windows. No-op if either window is unknown or @p tab is not in @p from.
    ///
    /// This is the model half of dragging a tab between windows (or tearing it into a fresh window):
    /// the Tab object moves intact, so its sessions survive and the host only re-binds them to the
    /// destination window's displays.
    /// @param from    The window currently holding @p tab.
    /// @param tab     The tab to move.
    /// @param to      The destination window (may equal @p from, in which case this reorders).
    /// @param toIndex The destination position, clamped into the destination's tab range.
    void moveTabToWindow(WindowId from, TabId tab, WindowId to, int toIndex);

    // }}}
    // {{{ Panes

    /// Splits @p tab's active pane along @p direction, allocating a new session for the new pane.
    /// Returns the new (now active) leaf.
    Pane* splitActivePane(TabId tab, SplitState direction, double ratio = 0.5);

    /// Closes pane @p leaf in @p tab. If it was the tab's last pane, the whole tab is closed
    /// instead; a window left with no tabs stays in the model (empty), for the host to close or
    /// keep — see closeTab. Reports the closed session(s).
    void closePane(WindowId window, TabId tab, PaneId leaf);

    /// Sets the active pane of @p tab.
    void setActivePane(TabId tab, PaneId leaf);

    /// Moves focus within @p tab in @p direction.
    void focusDirection(TabId tab, FocusDirection direction);

    /// Updates the split ratio of the internal node @p splitNode in @p tab.
    void setPaneRatio(TabId tab, PaneId splitNode, double ratio);

    /// Grows or shrinks @p tab's active pane along @p direction by @p fraction, by nudging the ratio
    /// of the nearest ancestor split on the matching axis. No-op if the active pane has no such
    /// ancestor (a single pane, or only cross-axis splits above it).
    /// @param tab       The tab whose active pane resizes.
    /// @param direction The side the active pane grows toward.
    /// @param fraction  The ratio delta magnitude in (0, 1); the sign is derived from @p direction.
    void resizeActivePane(TabId tab, FocusDirection direction, double fraction);

    /// Flips the orientation of @p tab's active pane's parent split (Horizontal<->Vertical). No-op if
    /// the active pane is the tab's only pane.
    void toggleActivePaneOrientation(TabId tab);

    /// Swaps @p tab's active pane with its neighbor in @p direction (the two terminals trade slots),
    /// keeping the moved session focused. No-op if there is no neighbor.
    void swapActivePane(TabId tab, FocusDirection direction);

    /// Moves @p tab's active pane across its neighbor in @p direction, re-parenting it in the tree
    /// (see Tab::moveActivePane). No-op if there is no neighbor.
    void moveActivePane(TabId tab, FocusDirection direction);

    /// Toggles zoom on @p tab's active pane: zoomed, that pane alone fills the tab area (see Tab's
    /// zoom block). No-op on a single-pane tab.
    void toggleActivePaneZoom(TabId tab);

    // }}}
    // {{{ Title & color (authoritative state lives here, below the GUI)

    void setTabTitle(TabId tab, std::string title);
    void resetTabTitle(TabId tab);

    /// Assigns @p tab's color on behalf of @p source. Sources are independent and ranked, so a user's
    /// choice and an application's DECAC color coexist; see TabColorSource.
    /// @param tab The tab to color.
    /// @param source The assigning source.
    /// @param color The color to assign.
    void setTabColor(TabId tab, TabColorSource source, vtbackend::RGBColor color);

    /// Clears @p source's color for @p tab, falling back to the next-highest source that has one.
    /// @param tab The tab to un-color.
    /// @param source The source whose color to clear.
    void resetTabColor(TabId tab, TabColorSource source);

    /// Installs the session-title resolver — a cross-layer bridge the host supplies so tabs can
    /// derive their title from the active pane's program title.
    ///
    /// This is a post-construction setter (a documented rebinding seam) because the resolver reaches
    /// into the host's terminal-session registry, which is built from SessionIds the model itself
    /// allocates. The resolver cannot exist before the model, and the model cannot resolve titles
    /// without it — the two are wired once both are live.
    void setSessionTitleResolver(Tab::SessionTitleResolver resolver) { _titleResolver = std::move(resolver); }

    /// Returns the currently installed session-title resolver.
    /// @see setSessionTitleResolver
    [[nodiscard]] Tab::SessionTitleResolver const& sessionTitleResolver() const noexcept
    {
        return _titleResolver;
    }

    // }}}
    // {{{ Lookups & palette

    [[nodiscard]] Tab* findTab(TabId tab) const noexcept;

    /// The id of the window owning @p tab, or a default WindowId{} (value 0) if @p tab is unknown. Lets
    /// the Qt host route a pane event (which carries only a TabId) to the owning window's controller.
    [[nodiscard]] WindowId windowOfTab(TabId tab) const noexcept;

    /// Locates the leaf pane hosting @p session, searching every window. For hosts that resolve a
    /// session to its pane without knowing which window owns it (the daemon's session-exit pruning,
    /// per-session output mapping, and client-authored split verbs).
    /// @param session The session to locate.
    /// @return The {window, tab, pane} triple; all nullptr when @p session is unknown.
    [[nodiscard]] std::tuple<Window*, Tab*, Pane*> findSessionLeaf(SessionId session) const;

    /// Locates the LEAF pane with id @p pane, searching every window (pane ids are minted
    /// model-globally). For hosts resolving a wire pane reference without knowing which window owns
    /// it (the tmux control session's %N targets — tmux names leaf panes only, so internal split
    /// nodes deliberately do not match).
    /// @param pane The leaf pane id to locate.
    /// @return The leaf pane, or nullptr when @p pane is unknown.
    [[nodiscard]] Pane* findLeafPane(PaneId pane) const;

    /// Locates any pane — leaf or split node — of @p tab.
    ///
    /// The pane mutators all begin by resolving a (tab, pane) pair, and so does a host answering a
    /// pane event, which carries exactly that pair and nothing else. Whether the node must be a leaf
    /// differs per caller, so that stays the caller's guard; only the lookup is shared.
    /// @param tab  The tab owning the pane.
    /// @param pane The pane id to locate.
    /// @return The pane, or nullptr when either id is unknown.
    [[nodiscard]] Pane* findPane(TabId tab, PaneId pane) const;

    /// Invokes @p f(Window&, Tab&) for every tab of every window, in window order. Single-sources
    /// the windows×tabs traversal for hosts that project the whole model (the daemon's resize
    /// reprojection and attach snapshots).
    /// @param f The per-tab visitor.
    template <typename F>
    void forEachTab(F&& f) const
    {
        // @p f is invoked once per tab, so it is forwarded once into a reference binding and
        // called through that (the same idiom Pane::walkTree documents).
        auto&& visit = std::forward<F>(f);
        for (auto const& win: _windows)
            for (auto const& tab: win->_tabs)
                visit(*win, *tab);
    }

    /// The predefined tab-color palette offered to the user (a grid of swatches, WT-style). Both the
    /// GUI and a future daemon expose the same set so all clients see identical choices. Backed by a
    /// single constexpr table with static storage duration, so this hands back a view over it — no
    /// per-instance copy or allocation.
    [[nodiscard]] std::span<vtbackend::RGBColor const> colorPalette() const noexcept;

    // }}}

  private:
    /// Locates the window owning @p tab. Returns {window, index-of-tab} or {nullptr, -1}.
    [[nodiscard]] std::pair<Window*, int> locateTab(TabId tab) const noexcept;

    void closeTabAt(Window& window, int index);

    /// Announces everything that follows from @p tab's zoom having changed during a mutation, given
    /// the zoomed leaf captured @p zoomedLeafBefore it ran. Every pane mutator ends with this call.
    ///
    /// This is the ONE place the zoom contract turns into events, so no mutator has to re-derive it.
    /// Comparing Tab::zoomedLeafId() rather than the raw flag is what collapses three cases into one
    /// comparison: it changes when zoom is entered, when it is cleared, and when it follows focus to
    /// another leaf — and it is stable, so this emits nothing, whenever an unzoomed tab is mutated.
    ///
    /// The retitle rides along here because Tab::title() reads the zoom: a tab is named after the
    /// pane on screen while zoomed and "Multiple panes" once it is not. Deriving that per mutator is
    /// how the three restructuring paths (orientation flip, swap, move) came to silently omit it.
    ///
    /// @pre @p tab must still exist — never call this where the mutation could have closed the tab
    ///      (closePane's last-pane branch), for which tabClosed already tells the host everything.
    /// @param tab               The tab that was mutated.
    /// @param zoomedLeafBefore  Tab::zoomedLeafId() sampled before the mutation.
    /// @return true if a zoom change — and with it a retitle — was announced. A caller that would emit
    ///         its own tabTitleChanged for an unrelated reason checks this so the tab is not retitled
    ///         twice, which would run the host's whole status-line fan-out for one keystroke.
    bool announceZoomChange(Tab& tab, std::optional<PaneId> zoomedLeafBefore);

    ModelEvents& _events;
    SessionAllocator _allocateSession;
    Tab::SessionTitleResolver _titleResolver;

    std::vector<std::unique_ptr<Window>> _windows;

    WindowId _nextWindowId { 1 };
    TabId _nextTabId { 1 };
    PaneId _nextPaneId { 1 };
};

} // namespace vtworkspace
