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

#include <contour/command/CommandCatalog.hpp>
#include <contour/display/TerminalDisplay.hpp>
#include <contour/display/WindowHost.hpp>
#include <contour/input/HorizontalWheelGesture.hpp>
#include <contour/platform/NativeWindowFrame.hpp>
#include <contour/platform/WindowShadowController.hpp>
#include <contour/session/PaneProxy.hpp>
#include <contour/window/CommandPaletteModel.hpp>
#include <contour/window/ContextMenuModel.hpp>

#include <vtbackend/Color.hpp>

#include <QtCore/QAbstractListModel>
#include <QtGui/QColor>
#include <QtQml/QtQml>

#include <cstdint>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>

#include <vtworkspace/Primitives.hpp>

namespace vtworkspace
{
class Tab;
class Window;
} // namespace vtworkspace

namespace contour::session
{
class TerminalSessionManager;
class TerminalSession;
} // namespace contour::session

namespace contour::window
{

class SettingsController;

/// Per-OS-window Qt/QML adapter over one vtworkspace::Window.
///
/// Clean-architecture role: this is the ADAPTER between the pure, Qt-free domain core (vtworkspace::Window /
/// SessionModel) and the QML window (ApplicationWindow / Main.qml). One instance exists per OS window.
/// It owns everything window-scoped and Qt-shaped:
///   - the tab-strip QAbstractListModel (rows = this window's tabs) + the tab invokables the strip calls;
///   - the PaneProxy tree for the active tab (the recursive PaneNode.qml renders from activeTabRootPane);
///   - this window's focused display (_activeDisplay) and the window services derived from it
///     (titleBarVisible), bridged from the display's own signals;
///   - the WINDOW-GEOMETRY AUTHORITY: initial pre-show sizing, WM size hints, show modes and the
///     grid->window choke point (see the "Window-geometry authority" section below). Nothing else
///     mutates QWindow geometry.
///
/// It holds NO session lifetime and NO authority over the model tree. Structural mutations
/// (create/close/move tab, split/close/focus pane, terminate) are delegated to the
/// TerminalSessionManager SERVICE, tagged with this controller's WindowId, so the model stays the single
/// source of truth and session ownership stays in one place. The manager also routes each vtworkspace
/// ModelEvent to the owning window's controller (the on*() hooks below).
///
/// Created only by TerminalSessionManager::createWindowController() (which mints the backing
/// vtworkspace::Window) and owned via QQmlEngine CppOwnership; destroyed when its ApplicationWindow closes.
///
/// It is also the window half of the display/window seam: it implements @c display::WindowHost, the
/// interface the display layer declares for "the OS window I am shown in". Everything a display asks of
/// its window arrives through those overrides, so the display names no window type at all.
class WindowController:
    public QAbstractListModel,
    public command::TabTitleProvider,
    public display::WindowHost
{
    Q_OBJECT
    Q_PROPERTY(int count READ count NOTIFY countChanged)
    /// This window's command palette list. Per-window (not per-app) because the filter text and the
    /// selection are things the user is doing IN this window; the recently-used list behind it is
    /// app-wide and lives on the manager.
    Q_PROPERTY(contour::window::CommandPaletteModel* commandPalette READ commandPalette CONSTANT)
    Q_PROPERTY(QVariantList contextMenuModel READ contextMenuModel NOTIFY contextMenuModelChanged)
    Q_PROPERTY(QVariantList titleBarContextMenuModel READ titleBarContextMenuModel NOTIFY
                   titleBarContextMenuModelChanged)
    Q_PROPERTY(int activeTabIndex READ activeTabIndex NOTIFY activeTabIndexChanged)
    Q_PROPERTY(bool multimediaReady READ isMultimediaReady NOTIFY multimediaReadyChanged)
    Q_PROPERTY(
        contour::session::PaneProxy* activeTabRootPane READ activeTabRootPane NOTIFY activeTabRootPaneChanged)
    Q_PROPERTY(
        contour::session::TerminalSession* activeSession READ activeSession NOTIFY activeSessionChanged)
    Q_PROPERTY(bool titleBarVisible READ titleBarVisible NOTIFY titleBarVisibleChanged)
    // All three follow titleBarVisible, so they re-evaluate on the same signal.
    Q_PROPERTY(bool needsFramelessHint READ needsFramelessHint NOTIFY titleBarVisibleChanged)
    Q_PROPERTY(
        bool needsClientFrameAffordances READ needsClientFrameAffordances NOTIFY titleBarVisibleChanged)
    // NOT on titleBarVisibleChanged: this one is measured from the OS's own buttons, which only
    // exist once the window is realized -- long after the title-bar value is seeded.
    Q_PROPERTY(qreal nativeControlsInset READ nativeControlsInset NOTIFY nativeControlsInsetChanged)
    // Tab-strip (tab bar) placement + visibility, exposed to Main.qml. `tabBarPosition` is an int
    // (0 = Top, 1 = Bottom) matching the TabBarPosition enumerator order. `tabBarShouldShow`
    // is the resolved gate (mode + live tab count) the QML binds its `visible` to.
    Q_PROPERTY(int tabBarPosition READ tabBarPosition NOTIFY tabBarPositionChanged)
    Q_PROPERTY(int tabBarVisibility READ tabBarVisibility NOTIFY tabBarVisibilityChanged)
    Q_PROPERTY(bool tabBarShouldShow READ tabBarShouldShow NOTIFY tabBarShouldShowChanged)
    Q_PROPERTY(int chromeHeight READ chromeHeight WRITE setChromeHeight NOTIFY chromeHeightChanged)
    Q_PROPERTY(bool settingsActive READ settingsActive NOTIFY settingsActiveChanged)
    Q_PROPERTY(contour::window::SettingsController* settingsController READ settingsController CONSTANT)
    QML_ELEMENT
    QML_UNCREATABLE("Created by the session manager")

  public:
    /// Tab-strip roles (one row per tab). Mirrors the old TerminalSessionManager::Roles so the delegates
    /// bind the same role names. The values are small (Qt::UserRole + n), so a 16-bit base suffices and
    /// they still convert cleanly to the `int role` of the QAbstractItemModel API.
    enum class Roles : std::uint16_t
    {
        DisplayRole = Qt::DisplayRole,    //!< Qt's built-in display role; answered like SessionIdRole.
        TitleRole = Qt::UserRole + 1,     //!< Resolved (expanded) tab label; what the tab strip displays.
        ColorRole = Qt::UserRole + 2,     //!< Tab accent color as QColor (transparent if uncolored).
        IsActiveRole = Qt::UserRole + 3,  //!< Whether this tab is the active tab of this window.
        PaneCountRole = Qt::UserRole + 4, //!< Number of panes in this tab.
        SessionIdRole = Qt::UserRole + 5, //!< The session id of the tab's active leaf.
        RawTitleRole = Qt::UserRole + 6,  //!< Un-expanded runtime rename template (empty if never renamed).
        ZoomedRole = Qt::UserRole + 7, //!< Whether this tab's active pane is zoomed (see vtworkspace::Tab).
        //!< The active pane's OSC 9;4 progress state, as the vtbackend::ProgressState enumerator's
        //!< value (0 = inactive, i.e. nothing to paint). @see vtbackend::ProgressState.
        ProgressStateRole = Qt::UserRole + 8,
        ProgressPercentageRole = Qt::UserRole + 9, //!< The active pane's progress percentage, 0..100.
    };

    /// @param manager  The session-lifetime service + model host (must outlive this controller).
    /// @param windowId The backing vtworkspace::Window this controller adapts.
    WindowController(session::TerminalSessionManager& manager, vtworkspace::WindowId windowId);
    ~WindowController() override;

    WindowController(WindowController const&) = delete;
    WindowController& operator=(WindowController const&) = delete;
    WindowController(WindowController&&) = delete;
    WindowController& operator=(WindowController&&) = delete;

    [[nodiscard]] vtworkspace::WindowId windowId() const noexcept { return _windowId; }

    // {{{ QAbstractListModel (tab strip)
    [[nodiscard]] QVariant data(QModelIndex const& index, int role = Qt::DisplayRole) const override;
    [[nodiscard]] int rowCount(QModelIndex const& parent = QModelIndex()) const override;
    [[nodiscard]] QHash<int, QByteArray> roleNames() const override;
    // }}}

    /// Watches the bound window for QEvent::DevicePixelRatioChange (no QWindow signal exists for it).
    bool eventFilter(QObject* watched, QEvent* event) override;

    /// Tab count of this window (one row per tab).
    [[nodiscard]] int count() const noexcept;
    /// 0-based row of this window's active tab, or -1.
    [[nodiscard]] int activeTabIndex() const noexcept;

    // {{{ GUI tab-strip invokables (delegated to the manager, tagged with _windowId)
    /// Opens a new tab. With an empty @p profileName the app-default profile is used; otherwise the
    /// named profile is launched (the new-tab profile dropdown). The default argument keeps the bare
    /// `createNewTab()` call site (the "+" button) working.
    Q_INVOKABLE void createNewTab(QString const& profileName = {});
    Q_INVOKABLE void activateTab(int index);
    /// The working directory of the tab at @p index, for its hover tooltip.
    ///
    /// Queried on demand rather than carried as a model role: the directory changes with every `cd`, and
    /// a role would have to emit dataChanged for each one to stay honest. A tooltip is only read when it
    /// opens, so asking then is both simpler and always current.
    ///
    /// @param index The tab's row.
    /// @return The directory, or an empty string when none can be determined.
    Q_INVOKABLE [[nodiscard]] QString tabWorkingDirectory(int index) const;
    /// Routes a wheel event that occurred over the tab strip.
    ///
    /// Horizontal notches resolve through the same built-in fallback mouse mappings the terminal grid
    /// consults (@see TerminalSession::applyFallbackMouseBinding), so the tab-switch behaviour and its
    /// config gate live in exactly one table. The vertical component is ignored: the strip does not
    /// scroll.
    ///
    /// Takes the whole event rather than its horizontal angle alone: a trackpad reports pixel deltas and
    /// no angle at all, so a swipe judged only by angle would never move a tab, and the gesture phase and
    /// the platform's natural-scrolling flag are what decide which way it should move.
    ///
    /// @param pixelDeltaX Horizontal pixel-precise delta (trackpads), or 0.
    /// @param pixelDeltaY Vertical pixel-precise delta, used to tell a sideways gesture from a vertical one.
    /// @param angleDeltaX Horizontal angle delta, in the same units Qt reports (120 per notch).
    /// @param angleDeltaY Vertical angle delta, used to tell a sideways gesture from a vertical one.
    /// @param phase       The gesture phase, as a vtbackend::ScrollPhase (QML has no enum to pass).
    /// @param inverted    Whether the platform already flipped the delta for natural scrolling.
    Q_INVOKABLE void dispatchTabStripWheel(
        int pixelDeltaX, int pixelDeltaY, int angleDeltaX, int angleDeltaY, int phase, bool inverted);
    Q_INVOKABLE void moveTab(int fromIndex, int toIndex);
    /// The raw id of the window this controller adapts, so a QML drag payload can name its source
    /// window and a DropArea can target the destination window across the shared engine.
    Q_INVOKABLE [[nodiscard]] quint64 windowIdValue() const noexcept { return _windowId.value; }
    /// Moves the tab at @p fromIndex of the window identified by @p sourceWindowId into THIS window at
    /// @p toIndex — the drop of a tab dragged from another window's strip onto this one.
    /// @param sourceWindowId The dragged tab's source window id (from windowIdValue()).
    /// @param fromIndex      The tab's row in the source window.
    /// @param toIndex        The destination row in this window.
    Q_INVOKABLE void moveTabIntoThisWindow(quint64 sourceWindowId, int fromIndex, int toIndex);
    /// Tears the tab at @p index of THIS window out into a brand-new OS window (drop on empty desktop).
    /// @param index The tab's row in this window.
    Q_INVOKABLE void tearOffTab(int index);
    Q_INVOKABLE void setTabTitle(int index, QString const& title);
    Q_INVOKABLE void resetTabTitle(int index);
    /// Asks the active tab's QML delegate to open its inline title editor (the keyboard entry point
    /// for the SetTabTitle action). No-op when this window has no active tab. Emits
    /// tabTitleEditRequested() with the active tab's row so exactly that TabItem starts editing.
    Q_INVOKABLE void beginActiveTabTitleEdit();
    /// Asks the active tab's QML delegate to open its color picker (the keyboard entry point for the
    /// SetTabColor action when it names no color). No-op when this window has no active tab. Emits
    /// tabColorPickRequested() with the active tab's row so exactly that TabItem opens its flyout.
    Q_INVOKABLE void beginActiveTabColorPick();
    /// Asks the QML to open this window's "save layout as" name prompt (the keyboard/palette entry point
    /// for a nameless SaveLayout action). Emits saveLayoutRequested(); the dialog calls saveLayoutAs()
    /// back with the name the user types.
    Q_INVOKABLE void beginSaveLayoutPrompt();
    /// Saves this window's tabs as the layout named @p name (the "save layout as" prompt's accept), via
    /// the manager's saveLayout(). A blank name or a window with no active session is a no-op, so an
    /// empty prompt never writes a nameless layout.
    /// @param name The layout name the user typed.
    Q_INVOKABLE void saveLayoutAs(QString const& name);
    /// Colors the ACTIVE tab (the keyboard entry point for `SetTabColor` carrying a color). Recorded
    /// as the user's own choice, so it outranks any color the application assigned via DECAC.
    /// @param color The color to apply.
    void setActiveTabColor(vtbackend::RGBColor color);
    /// Returns the ACTIVE tab to its default color (the `ResetTabColor` action): drops the user's
    /// choice, letting an application-assigned DECAC color resurface. No-op without an active tab.
    void resetActiveTabColor();
    /// Colors the tab at @p index on the user's behalf. A user color outranks any color the application
    /// assigned via DECAC, so this always takes visible effect.
    /// @param index The tab's row in this window.
    /// @param color The color the user picked.
    Q_INVOKABLE void setTabColor(int index, QColor const& color);
    /// Returns the tab at @p index to its default color, meaning: the color the application assigned via
    /// DECAC if it assigned one, otherwise the host's default tab color. It never discards an
    /// application's color, only the user's own choice.
    /// @param index The tab's row in this window.
    Q_INVOKABLE void resetTabColor(int index);
    Q_INVOKABLE void closeTabAtIndex(int index);
    Q_INVOKABLE void closeOtherTabs(int index);
    Q_INVOKABLE void closeTabsToRight(int index);
    Q_INVOKABLE [[nodiscard]] QVariantList tabColorPalette() const;

    /// The fill for a colored tab, faded per the focus flags (WT-style); math in TabColorScheme.h.
    /// @param tabColor The user's chosen tab color.
    /// @param rowBackground The tab-row / title-bar background the tab sits on.
    /// @param active Whether this is the active tab of its window.
    /// @param hovered Whether the pointer is over the (non-active) tab.
    /// @param windowActive Whether the window itself is focused.
    /// @return The background color to fill the tab with.
    Q_INVOKABLE [[nodiscard]] QColor tabBackgroundColor(QColor const& tabColor,
                                                        QColor const& rowBackground,
                                                        bool active,
                                                        bool hovered,
                                                        bool windowActive) const;

    /// A contrasting label color (black or white) for text over a tab background.
    /// @param tabBackground The (already composited) tab background.
    /// @return Opaque black for a light background, opaque white for a dark one.
    Q_INVOKABLE [[nodiscard]] QColor tabTextColor(QColor const& tabBackground) const;

    /// Closes this OS window: tears down its tabs/sessions and drops the controller. Main.qml onClosing.
    Q_INVOKABLE void closeWindow();
    /// Whether this window may close now (its last pane exited). TerminalPane.onTerminated.
    Q_INVOKABLE [[nodiscard]] bool canCloseWindow() const noexcept;
    // }}}

    // {{{ Command palette
    /// This window's palette list model (never null; owned by this controller).
    [[nodiscard]] CommandPaletteModel* commandPalette() const noexcept { return _commandPalette.get(); }

    /// Rebuilds the palette against the CURRENT state — live tabs, the configured profiles/layouts and
    /// the key bindings as they stand after any reload — and asks the QML to show it.
    ///
    /// Called by the manager for the OpenCommandPalette action. The refresh happens here, on open,
    /// rather than being cached: the tab list is the whole reason the palette's rows can go stale.
    void openCommandPalette();

    /// Runs the command with id @p id against this window's active session, and records it as recently
    /// used. Unknown ids are ignored (a row can go stale between a refresh and a click).
    /// @param id The command id (CommandPaletteModel's `commandId` role).
    Q_INVOKABLE void runCommand(QString const& id);
    // }}}

    // {{{ Settings page
    /// Whether this window's content area currently shows the in-app settings page instead of the
    /// active tab's terminal pane tree. The content Loader in Main.qml switches on this, so the settings
    /// page and the terminal are mutually-exclusive views of the same region (Windows-Terminal style).
    [[nodiscard]] bool settingsActive() const noexcept { return _settingsActive; }

    /// This window's editable settings bridge (never null; owned by this controller). The QML settings
    /// page binds to it for the profile/color-scheme/default-profile models and the save/delete actions.
    [[nodiscard]] SettingsController* settingsController() const noexcept
    {
        return _settingsController.get();
    }

    /// Shows the settings page over this window (the OpenConfiguration action's default, and the
    /// new-tab dropdown's "Settings" entry). Idempotent: showing it while already shown does nothing.
    Q_INVOKABLE void openSettings();

    /// Returns from the settings page to the active tab's terminal content. A no-op if not showing it.
    Q_INVOKABLE void closeSettings();

    /// Toggles between the settings page and the terminal content.
    Q_INVOKABLE void toggleSettings();
    // }}}

    // {{{ Terminal context menu
    /// Rebuilds the context menu against the CURRENT state of the active pane and asks the QML to pop it.
    ///
    /// Called by the manager for the OpenContextMenu action, which has already made the right-clicked
    /// pane the active one — so "the active pane" here IS the pane the user clicked.
    void openContextMenu();

    /// The context menu, as QML consumes it: a list of rows, each a QVariantMap of
    /// { kind, title, enabled, checkable, checked, actionId, children }.
    ///
    /// `actionId` indexes into this controller's own action list rather than naming a command, so a click
    /// runs the exact action the row was built with. Nothing has to be looked up by name at click time,
    /// and nothing depends on some other model having been populated first.
    [[nodiscard]] QVariantList contextMenuModel() const { return _paneContextMenu.model; }

    /// The title bar's context menu rows. Its OWN model, not the pane menu's: publishing into that one
    /// would rebuild and re-target a menu the user may still have open.
    [[nodiscard]] QVariantList titleBarContextMenuModel() const { return _titleBarContextMenu.model; }

    /// Builds and publishes the title bar's context menu, then asks QML to pop it.
    Q_INVOKABLE void openTitleBarContextMenu();

    /// Runs the title-bar menu action with id @p actionId. Out-of-range ids are ignored.
    /// @param actionId The row's `actionId`.
    Q_INVOKABLE void triggerTitleBarContextMenuAction(int actionId);

    /// Runs the context-menu action with id @p actionId against the pane the menu was opened over.
    /// Out-of-range ids are ignored.
    /// @param actionId The row's `actionId`.
    Q_INVOKABLE void triggerContextMenuAction(int actionId);
    // }}}

    /// The titles of this window's tabs, in tab order (TabTitleProvider, for TabCommandSource).
    [[nodiscard]] std::vector<std::string> tabTitles() const override;

    // {{{ PaneProxy tree + window-service reads
    [[nodiscard]] session::PaneProxy* activeTabRootPane() const noexcept { return _activeTabRootProxy; }
    [[nodiscard]] session::TerminalSession* activeSession() const noexcept;
    [[nodiscard]] bool titleBarVisible() const noexcept;
    [[nodiscard]] bool isMultimediaReady() const noexcept;
    // }}}

    // {{{ Window decoration (title bar)
    // Title-bar visibility is WINDOW state and lives here (the window authority), not per display:
    // per-pane storage silently reverted a runtime ToggleTitleBar on the next pane-focus change or
    // tab switch, and re-seeding from the profile on every session rebind reset it too.

    /// Seeds this window's initial title-bar visibility from the profile's show_title_bar setting.
    /// First-write-wins: only the first seed after construction takes effect, so session rebinds
    /// (tab switches, split collapses) never reset a runtime toggle.
    /// @param visible The profile's show_title_bar value.
    void seedTitleBarVisible(bool visible) override;

    /// Sets the window's title-bar visibility: stores the window-scoped state, applies the native
    /// window-frame decoration (Qt::FramelessWindowHint on the adopted OS window — shown => native
    /// frame, hidden => frameless so the custom client-side TitleBar is the only decoration; the C++
    /// counterpart of Main.qml's `flags` binding), and notifies the QML window bindings.
    /// @param visible The new visibility.
    void setTitleBarVisible(bool visible);

    /// Flips the window's title-bar visibility (the ToggleTitleBar action, routed here by the display).
    void toggleTitleBar() override;

    // The three gates Main.qml used to derive by negating titleBarVisible. They come off the frame
    // POLICY instead (platform::framePolicyFor), because the answer is no longer the same on every
    // OS: Windows keeps a real frame and zeroes it, and macOS keeps a decorated NSWindow whose own
    // traffic lights ours must then yield to. Bools, as the QML-facing carve-out allows, but each
    // reads as the question its use site asks.

    /// Whether the window should carry Qt::FramelessWindowHint.
    ///
    /// False on Windows and macOS even with our own title bar: the hint is exactly what costs a
    /// window its system shadow and rounded corners, and both platforms have a way to keep the frame
    /// while still giving the client area the whole window.
    [[nodiscard]] bool needsFramelessHint() const noexcept;

    /// Whether WE supply the window's frame affordances -- ResizeBorder.qml's hit zones and the tab
    /// strip's own min/max/close controls -- or the OS frame does.
    ///
    /// One question, not two: the frame that resizes the window is the frame that carries its
    /// buttons, which is why @ref platform::FrameAffordances is one field.
    [[nodiscard]] bool needsClientFrameAffordances() const noexcept;

    /// Leading space the tab strip must leave clear for the OS's own window controls.
    ///
    /// Zero unless the frame policy says the OS paints them OVER our chrome, which today is macOS:
    /// a full-size-content NSWindow keeps drawing the real traffic lights and its content view
    /// extends underneath them. The MAGNITUDE comes from the frame adapter rather than from our
    /// chrome tokens, because it is the operating system's number -- AppKit's buttons are a fixed
    /// size whatever font or `ui_style` Contour is using.
    [[nodiscard]] qreal nativeControlsInset() const noexcept;
    // }}}

    // {{{ Tab strip (tab bar) placement + visibility
    // Like title-bar visibility, these are WINDOW state seeded once from the profile (first-write-wins),
    // so later session rebinds (tab switch, split collapse) never clobber them.

    /// The tab strip position as an int for QML (0 = Top, 1 = Bottom; see TabBarPosition).
    [[nodiscard]] int tabBarPosition() const noexcept;

    /// The tab strip visibility mode as an int for QML (see TabBarVisibility).
    [[nodiscard]] int tabBarVisibility() const noexcept;

    /// Whether the tab strip should currently be shown, resolving the visibility mode against the live
    /// tab count: Always => true, Never => false, Multiple => count() > 1. Main.qml binds the tab
    /// strip's `visible` to this.
    /// @return True if the tab strip should be shown.
    [[nodiscard]] bool tabBarShouldShow() const noexcept;

    /// Seeds this window's tab strip position from the global tab_bar_position setting.
    /// First-write-wins (see seedTitleBarVisible).
    /// @param position The configured tab_bar_position value.
    void seedTabBarPosition(config::TabBarPosition position) override;

    /// Seeds this window's tab strip visibility mode from the global tab_bar_visibility setting.
    /// First-write-wins (see seedTitleBarVisible).
    /// @param visibility The configured tab_bar_visibility value.
    void seedTabBarVisibility(config::TabBarVisibility visibility) override;

    /// Re-applies the configured tab strip placement and visibility to this window.
    ///
    /// Unlike the seed methods above this is NOT first-write-wins. It runs only on an explicit
    /// configuration reload, where the user has just asked for the configured value to take effect --
    /// so it supersedes a runtime override, which is what reloading a configuration means. Without it
    /// the seed latch swallows the change and the setting appears to do nothing until restart.
    ///
    /// @param position   The configured tab_bar_position value.
    /// @param visibility The configured tab_bar_visibility value.
    void applyTabBarFromConfig(config::TabBarPosition position, config::TabBarVisibility visibility);

    /// Sets this window's tab strip visibility mode at runtime.
    ///
    /// A view toggle, in the same spirit as toggleTitleBar(): it applies to THIS window for as long as
    /// it lives, and the settings page remains where the choice is made permanent. Marks the mode as
    /// seeded so a later session rebind cannot revert it.
    void setTabBarVisibility(config::TabBarVisibility visibility) override;

    /// Sets this window's tab strip placement at runtime. @see setTabBarVisibility.
    void setTabBarPosition(config::TabBarPosition position) override;
    // }}}

    /// This window's currently focused display (for window services + status-line targeting).
    [[nodiscard]] display::TerminalDisplay* activeDisplay() const noexcept { return _activeDisplay; }

    // {{{ Window-geometry authority
    // The controller is the ONLY component that mutates QWindow geometry (WM size hints, window show
    // modes, content-driven resizes). Displays route their requests here.

    /// Adopts the QML window early (before any display focuses in), assigns the spawn target screen
    /// (the pre-show DPR predictor: spawning window's screen, else the cursor's screen on non-Wayland,
    /// else primary) and installs the DPR/screen-change hooks for the scale-settlement handler.
    /// Called from Main.qml's Component.onCompleted, before the first tab exists.
    /// @param osWindow The ApplicationWindow backing this controller.
    Q_INVOKABLE void bindWindow(QQuickWindow* osWindow);

    /// Toggles maximize/restore through the show-mode protocol (size-increment clearing on maximize,
    /// hint re-application on restore) — the QML entry point for the custom window controls and the
    /// title-bar double-click. QML must not call QWindow::showMaximized()/showNormal() directly: the
    /// controller is the only window-geometry mutator, and the direct calls skip the protocol (leaving
    /// stale WM size increments applied while maximized, and none after restore).
    Q_INVOKABLE void toggleMaximized();

    /// Minimizes this window — routed here so every show-mode change stays on the controller.
    Q_INVOKABLE void minimizeWindow();

    /// Sizes the still-unmapped window from the active pane's REAL cell metrics (FreeType math run
    /// headlessly during session attach) plus the declared chrome, applies the WM size hints, then maps
    /// the window directly in the profile's state (normal/maximized/fullscreen) — the first map is the
    /// final geometry, eliminating the map-then-correct spawn flicker. Falls back to showing at the
    /// default size (never leaves the user windowless) when no metrics are available.
    /// Called from Main.qml's Component.onCompleted, after the first tab was created.
    Q_INVOKABLE void showInitial();

    /// Window chrome height in logical pixels OUTSIDE the terminal content area, DECLARED by Main.qml
    /// (bound to the title bar's effective height: 0 when hidden). Geometry math must never measure
    /// window-minus-item deltas — those are transient during relayout and structurally wrong in splits.
    [[nodiscard]] int chromeHeight() const noexcept { return _chromeHeight; }
    /// Sets the declared chrome height (QML binding write) and refreshes the WM size hints.
    void setChromeHeight(int height);

    /// Recomputes and applies the WM size hints (minimum/base/increment) for this window from
    /// @p requester's cell geometry, profile margins, content scale and the declared chrome.
    /// Refresh triggers: font/DPI reconfiguration (via the display), chrome changes, and
    /// restore-from-fullscreen/maximize. NEVER called from a resize path.
    /// @param requester The display whose cell geometry defines the hints (the active pane).
    /// @param mode Whether to gate the resize grid on the live window state (@ref HintApplyMode).
    void updateSizeHintsFor(session::DisplaySurface& requester, HintApplyMode mode) override;

    /// Shows this window fullscreen (size increments cleared while non-normal).
    /// @param requester The display forwarding the request (resolves the OS window).
    void setWindowFullScreen(session::DisplaySurface& requester) override;
    /// Shows this window maximized (size increments cleared while non-normal).
    /// @param requester The display forwarding the request (resolves the OS window).
    void setWindowMaximized(session::DisplaySurface& requester) override;
    /// Restores this window to normal state and re-applies the WM size hints.
    /// @param requester The display forwarding the request (resolves the OS window).
    void setWindowNormal(session::DisplaySurface& requester) override;
    /// Toggles fullscreen, restoring the previous maximized state on exit.
    /// @param requester The display forwarding the request (resolves the OS window).
    void toggleFullScreen(session::DisplaySurface& requester) override;

    /// THE grid->window choke point: every programmatic window resize (DECSLPP / CSI 8 t, font zoom's
    /// grid restore, profile switch, DPR settlement) lands here; nothing else may resize the window.
    /// Computes the window size so that @p requester's pane receives @p totalPageSize — for a split
    /// window by solving the pane-tree ratios (vtworkspace::contentSizeForLeaf), identity for a single pane —
    /// plus the declared chrome, and issues ONE resize. The resulting WM resize event drives the grid
    /// through the normal window->grid path; the WM is free to refuse (the reflowed grid then stands).
    /// Refused (logged) when fullscreen/maximized or the requester has no window/session/pane.
    /// @param requester     The display whose pane the grid request targets.
    /// @param totalPageSize The requested total page (main page + status line).
    /// @return True if a window resize was issued.
    bool resizeWindowForPage(session::DisplaySurface& requester, vtbackend::PageSize totalPageSize) override;

    /// Pixel-flavored choke-point entry (CSI 4 t): requests the content area of @p requester's pane in
    /// device pixels; otherwise identical to resizeWindowForPage().
    /// @param requester       The display whose pane the request targets.
    /// @param contentDevicePx The requested pane content extent in device pixels.
    /// @return True if a window resize was issued.
    bool resizeWindowForContentPixels(session::DisplaySurface& requester,
                                      vtbackend::ImageSize contentDevicePx) override;
    // }}}

    /// Whether @p osWindow is this controller's OS window. A controller adopts the QQuickWindow of the
    /// first display that focuses into it; the manager uses this to route a focus/close to the right
    /// controller. Returns false until this controller has seen a display (its window is not yet known).
    [[nodiscard]] bool ownsOSWindow(QQuickWindow const* osWindow) const noexcept
    {
        return osWindow != nullptr && _osWindow == osWindow;
    }

    /// Makes @p display this window's focused display: re-points the window-service signal bridge and
    /// re-emits the window bindings. Called by the manager when a display in this window focuses in.
    void focusDisplay(display::TerminalDisplay* display);

    /// Clears _activeDisplay if it pointed at @p display (the display was destroyed). Called by the
    /// manager routing detachDisplay() by window.
    void onDisplayDetached(display::TerminalDisplay* display) noexcept;

    /// Get-or-create a PaneProxy for @p id (reused across rebuilds so QML items survive). Public so the
    /// PaneProxy can be constructed against the manager. (Used only by rebuildActiveTabPaneProxies.)
    [[nodiscard]] session::PaneProxy* getProxy(vtworkspace::PaneId id);

    // {{{ Called by the manager's ModelEvents router (dispatched here by WindowId)
    void onTabAboutToBeAdded(int index);
    void onTabAdded(int index);
    void onTabAboutToBeRemoved(int index);
    void onTabClosed();
    void onTabAboutToBeMoved(int fromIndex, int toIndex);
    void onTabMoved();
    void onActiveTabChanged();
    void notifyTabRowChanged(vtworkspace::TabId tab, QList<Roles> const& roles);
    void refreshAllTabTitles();
    void refreshActiveTabHighlight();
    void rebuildActiveTabPaneProxies();
    /// Re-points activeTabRootPane at the active tab's layout root (vtworkspace::Tab::layoutRoot), which zoom
    /// moves without reshaping the tree. O(1): the proxies stay valid, so no walk or rebuild is needed.
    void refreshActiveTabLayoutRoot();
    /// Notifies every proxy's active state changed (active-pane focus moved, no tree rebuild).
    void notifyActivePaneChanged();
    /// Notifies the split node proxy @p splitNode that its ratio changed.
    void notifyRatioChanged(vtworkspace::PaneId splitNode);
    /// Emits activeSessionChanged (the focused pane's session may have changed).
    void emitActiveSessionChanged() { emit activeSessionChanged(); }
    /// Rebuilds and republishes this window's status line (tab list + per-pane marker).
    void updateStatusLine();
    // }}}

  signals:
    void countChanged();
    void activeTabIndexChanged();
    /// Requests that the tab at @p index open its inline title editor. Per-window (this controller
    /// is the tab-strip model), so it only reaches this window's TabItems; the delegate whose row
    /// matches @p index starts editing.
    void tabTitleEditRequested(int index);
    /// Requests that the tab at @p index open its color picker. Per-window, exactly like
    /// tabTitleEditRequested: the delegate whose row matches @p index opens its TabColorFlyout.
    void tabColorPickRequested(int index);
    /// Requests that this window show its command palette. Per-window (like tabTitleEditRequested), so
    /// the popup opens over the window the user pressed the chord in — not over every open window.
    void commandPaletteRequested();
    /// Requests that this window open its "save layout as" name prompt. Per-window, exactly like
    /// commandPaletteRequested: the prompt opens over the window the SaveLayout action fired in.
    void saveLayoutRequested();
    /// The context menu's rows changed. Emitted before contextMenuRequested(), so the QML has already
    /// rebuilt the menu by the time it is asked to pop it.
    void contextMenuModelChanged();
    /// Requests that this window pop its terminal context menu, at the mouse cursor.
    void contextMenuRequested();
    void titleBarContextMenuModelChanged();
    void titleBarContextMenuRequested();
    void multimediaReadyChanged();
    void activeTabRootPaneChanged();
    void titleBarVisibleChanged();
    void nativeControlsInsetChanged();
    void tabBarPositionChanged();
    void tabBarVisibilityChanged();
    void tabBarShouldShowChanged();
    void activeSessionChanged();
    void chromeHeightChanged();
    /// The content area switched between the settings page and the terminal (settingsActive changed).
    void settingsActiveChanged();

  private:
    /// Sets whether the settings page is shown, emitting settingsActiveChanged only on a real change.
    /// Centralizes the notify so every entry point (openSettings/closeSettings/toggleSettings and the
    /// tab-activation reset) stays consistent.
    /// @param active The new settings-page visibility.
    void setSettingsActive(bool active);

    /// The backing vtworkspace::Window, or nullptr if it has been removed.
    [[nodiscard]] vtworkspace::Window* window() const noexcept;
    /// The active tab of this window's vtworkspace::Window, or nullptr.
    [[nodiscard]] vtworkspace::Tab* activeModelTab() const noexcept;
    /// The model tab backing list-model row @p index, or nullptr.
    [[nodiscard]] vtworkspace::Tab* tabAtRow(int index) const noexcept;
    /// The row index of @p tab in this window, or -1.
    [[nodiscard]] int rowOfTab(vtworkspace::TabId tab) const noexcept;
    /// Resolves the label shown on @p row's tab.
    [[nodiscard]] QString resolvedTabLabel(vtworkspace::Tab* tab,
                                           session::TerminalSession* session,
                                           int row) const;

    /// Resolves the OS window to operate on for a geometry request from @p requester, adopting the
    /// requester's QQuickWindow as _osWindow when none is recorded yet (same rule as focusDisplay).
    /// @return The window to operate on, or nullptr when none exists.
    [[nodiscard]] QQuickWindow* osWindowFor(session::DisplaySurface& requester) noexcept;

    /// The declared chrome as the geometry module's type (width is structurally 0 in this layout).
    [[nodiscard]] geometry::Chrome chrome() const noexcept { return { .width = 0, .height = _chromeHeight }; }

    /// Shared tail of the choke-point entries: guards the window state, solves the pane tree for
    /// @p requester's leaf, adds the declared chrome and issues the single window resize.
    /// @param requester          The display whose pane the request targets.
    /// @param leafContentLogical The pane's required content extent in logical pixels.
    /// @return True if a window resize was issued.
    bool applyContentDrivenResize(session::DisplaySurface& requester,
                                  geometry::LogicalSize leafContentLogical);

    /// Scale-settlement handler (DevicePixelRatioChange / screenChanged): when the window's resolved
    /// content scale actually changed, re-derives the font DPI (grid reflows in place) and issues ONE
    /// grid-preserving corrective window resize (user decision: monitor moves and late Wayland
    /// fractional-scale arrival keep the grid, not the pixel size). Idempotent per scale value.
    void onWindowScaleMaybeChanged();

    /// OS window activation handler (QWindow::activeChanged): grants focus ownership to this window
    /// when it becomes the active OS window and revokes it when it stops being active, so alt-tabbing
    /// away from Contour notifies the shell (DECSET 1004) even when no display holds Qt item focus.
    void onOSWindowActiveChanged();

    /// Which decoration this window is currently showing, as the frame policy names it.
    [[nodiscard]] platform::WindowDecoration decoration() const noexcept;

    /// Re-publishes the window's drop shadow for the state it is now in.
    ///
    /// A frameless window gets no server-side decoration, and on KWin the shadow is drawn as part of
    /// that decoration — so the window has to hand the compositor its own. Called from every place
    /// that changes an input to the decision: the first map, the presentation transitions, the
    /// title-bar toggle, and a window-manager-driven maximize.
    ///
    /// Creates the attachment on first use rather than in bindWindow(), because resolving it needs
    /// the platform window, and forcing that into existence before showInitial() has sized the
    /// still-unmapped window is exactly the premature realization WindowGeometry.hpp warns about.
    void refreshWindowShadow();

    /// QWindow::visibilityChanged handler.
    ///
    /// Every way the window changes presentation ends in this signal — our own setWindowMaximized
    /// and friends, the toggles, and the ones driven from outside the application entirely (a
    /// window-manager keybinding, a double click on the native frame) — so this one connection
    /// covers all of them, and the shadow reads the presentation back off the window rather than
    /// having it recorded at each site.
    void onWindowVisibilityChanged();

    /// The frame policy for this window's platform and current decoration.
    [[nodiscard]] platform::FramePolicy framePolicy() const noexcept;

    session::TerminalSessionManager& _manager;
    vtworkspace::WindowId _windowId;

    // Wheel state for the tab strip. Separate from the grid's (which lives on TerminalSession) so a
    // gesture over the strip is judged on its own, and so the accumulator does not jump when the
    // pointer crosses between strip and grid mid-scroll.
    input::HorizontalWheelGesture _tabStripWheelGesture;
    int _tabStripWheelAccumulator = 0;

    // {{{ Command palette
    // The sources this window's palette draws from, held by value and in PRECEDENCE order (see
    // collectCommands(): first source wins a duplicate id). The live-state sources come first so their
    // richer rows beat the generic ones — "Switch To Tab 2: vim" from the tab source outranks the bare
    // "Switch To Tab 2" a key binding would contribute. The action catalog comes last: it is the floor
    // that guarantees an unbound action is still reachable.
    command::TabCommandSource _tabCommands;
    command::ProfileCommandSource _profileCommands;
    command::LayoutCommandSource _layoutCommands;
    command::BoundCommandSource _boundCommands;
    command::ActionCommandSource _actionCommands;
    std::unique_ptr<CommandPaletteModel> _commandPalette;
    // }}}

    // {{{ Settings page
    /// Whether the content area currently shows the settings page instead of the terminal pane tree.
    bool _settingsActive = false;
    /// The editable settings bridge for this window (created in the constructor; never null).
    std::unique_ptr<SettingsController> _settingsController;
    // }}}

    // {{{ Terminal context menu
    /// The menu as QML sees it, rebuilt on every right-click.
    PublishedContextMenu _paneContextMenu;
    PublishedContextMenu _titleBarContextMenu;

    /// The actions those rows run, in the order the rows carry as `actionId`. Held here rather than in
    /// the QML so a row runs the exact action it was built with — no lookup by name at click time.

    /// The session the menu was BUILT over — the pane the user right-clicked.
    ///
    /// Pinned, not looked up again when a row is picked. The active pane can change while the menu stands
    /// open (the clicked pane's shell exits, the model activates a sibling in its place), and "Close Pane"
    /// or "Paste" would then land on a terminal the user never pointed at. A QPointer, so a pane that dies
    /// under the open menu makes the row a no-op rather than a use-after-free.
    QPointer<session::TerminalSession> _contextMenuSession;
    // }}}

    display::TerminalDisplay* _activeDisplay = nullptr;
    // The OS window (QQuickWindow) this controller adapts, adopted from the first display that focuses in.
    // Lets the manager route a focus/close to the controller that owns a given display's window.
    QQuickWindow* _osWindow = nullptr;

    // Guards closeWindow() against re-entry: closeWindow() closes _osWindow, whose QML `onClosing`
    // calls closeWindow() again — the flag makes that second call a no-op so teardown runs exactly once,
    // whichever door (user close button vs. programmatic close of an emptied window) started it.
    bool _closing = false;

    // PaneProxy tree for this window's active tab. Owned here, reused by PaneId across rebuilds so a
    // surviving pane's QML item / display is not torn down when a sibling splits or closes.
    std::unordered_map<uint64_t, session::PaneProxy*> _paneProxies;
    session::PaneProxy* _activeTabRootProxy = nullptr;

    // Carries beginMoveRows()'s result from onTabAboutToBeMoved() to onTabMoved().
    bool _tabMoveInProgress = false;

    // The window's drop shadow. Created lazily on the first refresh (see refreshWindowShadow) and
    // destroyed before the window closes, while its handle is still live enough to withdraw through.
    //
    // The presentation it is published for is DERIVED from the window on each refresh rather than
    // held here: a mirrored member needs a writer at every transition, and one refactor away from
    // having none it silently stops withdrawing the shadow at all. WindowPresentation::Tiled is
    // unreachable today — Qt reports no such visibility, so a KWin quick-tiled window keeps its
    // shadow, as every other client-decorated application does.
    std::unique_ptr<platform::WindowShadowController> _windowShadow;

    // The OS-frame adapter. Never null: platforms with nothing to offer get a NullWindowFrame, so
    // every call site can apply the policy unconditionally rather than guarding on the platform.
    std::unique_ptr<platform::NativeWindowFrame> _nativeFrame;

    // Window-scoped title-bar visibility (see the decoration block above). Seeded once from the
    // profile's show_title_bar (first display attach), flipped by ToggleTitleBar.
    bool _titleBarVisible = true;
    // First-write-wins latch for seedTitleBarVisible(): session rebinds must not reset a runtime toggle.
    bool _titleBarSeeded = false;

    // Window-scoped tab strip placement + visibility (see the tab-strip block above). Seeded once from
    // the profile's tab_bar_position / tab_bar_visibility (first display attach). Defaults mirror the
    // ConfigEntry defaults so a window with no seed yet behaves like the historical Top/Always.
    config::TabBarPosition _tabBarPosition = config::TabBarPosition::Top;
    bool _tabBarPositionSeeded = false;
    config::TabBarVisibility _tabBarVisibility = config::TabBarVisibility::Always;
    bool _tabBarVisibilitySeeded = false;

    // Declared title-bar chrome height in logical px (written by Main.qml's binding).
    int _chromeHeight = 0;
    // Whether the window was maximized before entering fullscreen (restored on toggle-out).
    bool _maximizedState = false;
    // The content scale the geometry was last derived for (settlement idempotence latch).
    double _lastAppliedScale = 0.0;
    // True while our own content-driven resize is in flight (straddle guard, see onWindowScaleMaybeChanged).
    bool _inContentDrivenResize = false;
};

} // namespace contour::window
