pragma Singleton

// Alt-Tab, with something on screen while you do it.
//
// Named WindowSwitcherState rather than WindowSwitcher: the overlay component
// in modules/switcher already owns that name, and a singleton sharing it is
// silently shadowed wherever both are imported -- the same failure that made an
// earlier Locale singleton resolve to QML's built-in type instead.
//
// Super+Tab already cycled windows; nothing was drawn, so you were choosing
// blind and could only confirm by arriving. This holds the selection while a
// switch is in progress and lets the overlay render it.
//
// MOST-RECENTLY-USED ORDER
//
// The list is ordered by when each window last had focus, not by when it was
// opened, because that is what makes the gesture useful: one Tab returns to the
// window you just came from, which is the overwhelmingly common case. Creation
// order would send you to whichever window happens to be first in Hyprland's
// list, which is arbitrary from the user's point of view.
//
// Hyprland does not report an MRU order, so it is tracked here: every time a
// toplevel becomes active it moves to the front. Addresses are used as the key
// because they are stable for a window's lifetime, where titles and app ids are
// not.
//
// HOW A SWITCH ENDS
//
// The compositor fires a bind on Super RELEASE, which commits. That is the only
// way to know the gesture is over -- there is no "modifier released" signal
// otherwise. It means close() runs on every Super release in the session, so it
// must be cheap and a no-op when nothing is open.

import Quickshell
import Quickshell.Hyprland
import QtQuick

Singleton {
    id: root

    property bool open: false
    property int index: 0

    // Window addresses, most recently focused first.
    property var recent: []

    // The switch candidates, resolved fresh each time the gesture starts.
    property var windows: []

    readonly property var selected: (root.index >= 0 && root.index < root.windows.length)
        ? root.windows[root.index] : null

    // Ordered by the MRU list, with anything unseen appended in Hyprland's own
    // order so a brand new window is still reachable.
    function orderedWindows(): var {
        const all = (Hyprland.toplevels?.values ?? []).filter(t => t && t.wayland && t.wayland.appId);
        const byAddress = {};
        for (const toplevel of all)
            byAddress[String(toplevel.address)] = toplevel;

        const ordered = [];
        for (const address of root.recent) {
            const match = byAddress[address];
            if (match) {
                ordered.push(match);
                delete byAddress[address];
            }
        }
        for (const toplevel of all)
            if (byAddress[String(toplevel.address)])
                ordered.push(toplevel);
        return ordered;
    }

    // Starts the gesture if it is not already running, then steps. The first
    // Tab lands on the PREVIOUS window rather than the current one, which is
    // what every other implementation of this gesture does.
    function step(forward: bool): void {
        if (!root.open) {
            root.windows = root.orderedWindows();
            if (root.windows.length < 2)
                return;
            root.open = true;
            root.index = forward ? 1 : root.windows.length - 1;
            return;
        }

        if (root.windows.length === 0)
            return;
        const count = root.windows.length;
        root.index = forward
            ? (root.index + 1) % count
            : (root.index - 1 + count) % count;
    }

    // Runs on every Super release in the session, so it does as little as
    // possible when no switch is in progress.
    function commit(): void {
        if (!root.open)
            return;
        const target = root.selected;
        root.open = false;
        root.windows = [];
        root.index = 0;
        if (target && target.wayland)
            target.wayland.activate();
    }

    function cancel(): void {
        root.open = false;
        root.windows = [];
        root.index = 0;
    }

    // Focus changes maintain the MRU order. This runs whether or not a switch
    // is in progress, because ordinary clicking between windows is most of how
    // the order is established.
    Connections {
        target: Hyprland
        function onActiveToplevelChanged(): void {
            const active = Hyprland.activeToplevel;
            if (!active || !active.address)
                return;
            const address = String(active.address);
            const next = [address];
            for (const existing of root.recent)
                if (existing !== address)
                    next.push(existing);
            // Bounded: a session can accumulate a lot of closed addresses, and
            // this list is only ever used to order what is currently open.
            root.recent = next.slice(0, 64);
        }
    }
}
