// Continuum Mission Control: a GNOME-like workspace filmstrip above a large
// selected desktop, with cross-workspace window search when the user types.

import Quickshell
import Quickshell.Hyprland
import Quickshell.Io
import QtQuick
import qs.config
import qs.services

Item {
    id: root

    property int refreshToken: 0
    property bool open: false
    property int selectedWorkspaceId: 0
    property string query: ""
    property int actionRefreshToken: 0
    property int selectedResultIndex: 0

    readonly property int sideMargin: 52
    readonly property int filmCardWidth: 210
    readonly property int filmCardHeight: 140

    readonly property var workspaceList: {
        const out = [];
        const all = Hyprland.workspaces?.values ?? [];
        for (const workspace of all) {
            if (workspace && workspace.id > 0)
                out.push(workspace);
        }
        out.sort((a, b) => a.id - b.id);
        return out;
    }

    readonly property var scratchWorkspace: {
        const all = Hyprland.workspaces?.values ?? [];
        return all.find(workspace => workspace?.name === "special:scratch") ?? null;
    }

    readonly property int nextWorkspaceId: {
        let max = 0;
        for (const workspace of root.workspaceList)
            max = Math.max(max, workspace.id);
        return max + 1;
    }

    readonly property var selectedWorkspace: {
        for (const workspace of root.workspaceList) {
            if (workspace.id === root.selectedWorkspaceId)
                return workspace;
        }
        return Hyprland.focusedWorkspace?.id > 0 ? Hyprland.focusedWorkspace : (root.workspaceList[0] ?? null);
    }

    readonly property real screenAspect: {
        const monitor = Hyprland.focusedMonitor;
        return monitor && monitor.height > 0 ? monitor.width / monitor.height : 3 / 2;
    }

    readonly property var windowEntries: {
        const out = [];
        for (const workspace of root.workspaceList) {
            const windows = workspace.toplevels?.values ?? [];
            for (const toplevel of windows)
                out.push({ workspace, toplevel });
        }
        return out;
    }

    readonly property var filteredWindows: {
        const needle = root.query.trim().toLowerCase();
        if (!needle)
            return [];
        return root.windowEntries.filter(entry => {
            const title = entry.toplevel?.title ?? "";
            const appId = entry.toplevel?.wayland?.appId ?? "";
            return `${title} ${appId}`.toLowerCase().includes(needle);
        });
    }

    readonly property int resultColumns: Math.min(3, Math.max(1, root.filteredWindows.length))
    readonly property var selectedResult: root.filteredWindows.length > 0
        ? root.filteredWindows[Math.min(root.selectedResultIndex, root.filteredWindows.length - 1)]
        : null

    onQueryChanged: root.selectedResultIndex = 0
    onFilteredWindowsChanged: {
        if (root.selectedResultIndex >= root.filteredWindows.length)
            root.selectedResultIndex = Math.max(0, root.filteredWindows.length - 1);
    }

    function prepare(): void {
        root.query = ShellState.overviewQuery;
        const requested = ShellState.overviewWorkspaceId;
        const focused = Hyprland.focusedWorkspace?.id ?? 0;
        root.selectedWorkspaceId = root.hasWorkspace(requested) ? requested : focused;
        Qt.callLater(() => searchInput.forceActiveFocus());
    }

    function hasWorkspace(id: int): bool {
        if (id <= 0)
            return false;
        return root.workspaceList.some(workspace => workspace.id === id);
    }

    function selectWorkspace(workspace: var): void {
        if (!workspace)
            return;
        root.selectedWorkspaceId = workspace.id;
        workspace.activate();
    }

    function cycleWorkspace(delta: int): void {
        if (root.workspaceList.length === 0)
            return;
        let index = root.workspaceList.findIndex(workspace => workspace.id === root.selectedWorkspaceId);
        if (index < 0)
            index = 0;
        const next = Math.max(0, Math.min(root.workspaceList.length - 1, index + delta));
        root.selectWorkspace(root.workspaceList[next]);
    }

    function focusToplevel(toplevel: var): void {
        if (!toplevel)
            return;

        const workspace = toplevel.workspace;
        if (workspace)
            workspace.activate();

        if (toplevel.address) {
            const address = toplevel.address.startsWith("0x") ? toplevel.address : "0x" + toplevel.address;
            Hyprland.dispatch(`hl.dsp.focus({ window = "address:${address}" })`);
        } else if (toplevel.wayland) {
            toplevel.wayland.activate();
        }
        ShellState.close();
    }

    function addressOf(toplevel: var): string {
        const raw = String(toplevel?.address ?? "");
        if (!raw)
            return "";
        return raw.startsWith("0x") ? raw : "0x" + raw;
    }

    function closeToplevel(toplevel: var): void {
        const address = root.addressOf(toplevel);
        if (!address)
            return;
        Hyprland.dispatch(`hl.dsp.window.close({ window = "address:${address}" })`);
        root.refreshAfterAction();
    }

    function moveToplevel(toplevel: var, workspaceName: string): void {
        const address = root.addressOf(toplevel);
        if (!address || !/^(?:[1-9]\d*|special:scratch)$/.test(workspaceName))
            return;
        Hyprland.dispatch(`hl.dsp.window.move({ workspace = "${workspaceName}", follow = false, window = "address:${address}" })`);
        root.refreshAfterAction();
    }

    function restoreToplevel(toplevel: var): void {
        const address = root.addressOf(toplevel);
        const target = root.selectedWorkspaceId > 0 ? root.selectedWorkspaceId : (Hyprland.focusedWorkspace?.id ?? 1);
        if (!address || target <= 0)
            return;
        Hyprland.dispatch(`hl.dsp.window.move({ workspace = ${target}, follow = true, window = "address:${address}" })`);
        ShellState.close();
    }

    function refreshAfterAction(): void {
        actionRefresh.restart();
    }

    Timer {
        id: actionRefresh
        interval: 140
        onTriggered: {
            Hyprland.refreshWorkspaces();
            Hyprland.refreshToplevels();
            root.actionRefreshToken++;
        }
    }

    // Address-only actions make the exact same boundary testable end-to-end
    // without synthesizing pointer input. They are also useful to launcher
    // commands later; validation still limits destinations to Panama's model.
    IpcHandler {
        target: "overview-actions"
        function move(address: string, workspaceName: string): void {
            root.moveToplevel({ address }, workspaceName);
        }
        function close(address: string): void {
            root.closeToplevel({ address });
        }
        function restore(address: string): void {
            root.restoreToplevel({ address });
        }
    }

    // ── Header and search ──────────────────────────────────────────────────
    Item {
        id: header
        anchors.top: parent.top
        anchors.topMargin: 42
        anchors.left: parent.left
        anchors.right: parent.right
        anchors.leftMargin: root.sideMargin
        anchors.rightMargin: root.sideMargin
        height: 42

        Text {
            anchors.left: parent.left
            anchors.verticalCenter: parent.verticalCenter
            text: "Activities"
            color: Theme.fg
            font.family: Theme.fontFamily
            font.pixelSize: Theme.fontSizeTitle
            font.weight: Font.DemiBold
        }

        Rectangle {
            id: searchBox
            anchors.centerIn: parent
            width: 390
            height: 38
            radius: Theme.pillRadius
            border.width: searchInput.activeFocus ? 1 : 0
            border.color: Theme.alpha(Theme.accent, 0.72)
            color: Theme.alpha(Theme.bgPopover, 0.84)

            Text {
                anchors.left: parent.left
                anchors.leftMargin: 14
                anchors.verticalCenter: parent.verticalCenter
                text: "\u{F0349}" // magnify
                color: searchInput.activeFocus ? Theme.accent : Theme.fgDim
                font.family: Theme.fontMono
                font.pixelSize: 16
            }

            TextInput {
                id: searchInput
                anchors.left: parent.left
                anchors.leftMargin: 40
                anchors.right: parent.right
                anchors.rightMargin: 14
                anchors.verticalCenter: parent.verticalCenter
                text: root.query
                color: Theme.fg
                selectionColor: Theme.accent
                selectedTextColor: Theme.bgDark
                clip: true
                font.family: Theme.fontFamily
                font.pixelSize: Theme.fontSize
                onTextEdited: root.query = text

                Keys.onReturnPressed: {
                    if (root.query.trim() === "") {
                        if (root.selectedWorkspace)
                            root.selectWorkspace(root.selectedWorkspace);
                        ShellState.close();
                    } else if (root.selectedResult) {
                        root.focusToplevel(root.selectedResult.toplevel);
                    }
                }

                Keys.onLeftPressed: event => {
                    if (root.query.trim() !== "")
                        return;
                    root.cycleWorkspace(-1);
                    event.accepted = true;
                }

                Keys.onRightPressed: event => {
                    if (root.query.trim() !== "")
                        return;
                    root.cycleWorkspace(1);
                    event.accepted = true;
                }

                Keys.onUpPressed: event => {
                    if (!root.selectedResult)
                        return;
                    root.selectedResultIndex = Math.max(0, root.selectedResultIndex - root.resultColumns);
                    event.accepted = true;
                }

                Keys.onDownPressed: event => {
                    if (!root.selectedResult)
                        return;
                    root.selectedResultIndex = Math.min(root.filteredWindows.length - 1, root.selectedResultIndex + root.resultColumns);
                    event.accepted = true;
                }

                Keys.onDeletePressed: event => {
                    if (!root.selectedResult)
                        return;
                    root.closeToplevel(root.selectedResult.toplevel);
                    event.accepted = true;
                }
            }

            Text {
                anchors.left: searchInput.left
                anchors.verticalCenter: parent.verticalCenter
                visible: searchInput.text === ""
                text: "Find a window"
                color: Theme.fgMuted
                font.family: Theme.fontFamily
                font.pixelSize: Theme.fontSize
            }
        }

        Text {
            anchors.right: parent.right
            anchors.verticalCenter: parent.verticalCenter
            text: root.workspaceList.length + (root.workspaceList.length === 1 ? " workspace" : " workspaces")
            color: Theme.fgDim
            font.family: Theme.fontFamily
            font.pixelSize: Theme.fontSize
        }
    }

    ScratchpadShelf {
        id: scratchpadShelf
        anchors.left: parent.left
        anchors.right: parent.right
        anchors.leftMargin: root.sideMargin
        anchors.rightMargin: root.sideMargin
        anchors.bottom: parent.bottom
        anchors.bottomMargin: 20
        workspace: root.scratchWorkspace
        refreshToken: root.refreshToken + root.actionRefreshToken
        onWindowDropped: toplevel => root.moveToplevel(toplevel, "special:scratch")
        onRestoreRequested: toplevel => root.restoreToplevel(toplevel)
        onCloseRequested: toplevel => root.closeToplevel(toplevel)
    }

    // ── Workspace filmstrip ────────────────────────────────────────────────
    Flickable {
        id: filmstrip
        anchors.top: header.bottom
        anchors.topMargin: 18
        anchors.left: parent.left
        anchors.right: parent.right
        anchors.leftMargin: root.sideMargin
        anchors.rightMargin: root.sideMargin
        height: root.filmCardHeight
        clip: true
        contentWidth: Math.max(width, filmRow.implicitWidth)
        contentHeight: height
        boundsBehavior: Flickable.StopAtBounds

        MouseArea {
            anchors.fill: parent
            acceptedButtons: Qt.NoButton
            onWheel: event => root.cycleWorkspace(event.angleDelta.y > 0 ? -1 : 1)
        }

        Row {
            id: filmRow
            x: filmstrip.contentWidth > filmstrip.width ? 0 : (filmstrip.width - implicitWidth) / 2
            spacing: Theme.itemSpacing

            Repeater {
                model: root.workspaceList

                WorkspaceCard {
                    required property var modelData
                    workspace: modelData
                    refreshToken: root.refreshToken + root.actionRefreshToken
                    emphasized: modelData.id === root.selectedWorkspaceId
                    focusBound: FocusSession.active && modelData.id === FocusSession.workspaceId
                    width: root.filmCardWidth
                    height: root.filmCardHeight
                    onActivated: root.selectWorkspace(modelData)
                    onWindowActivated: toplevel => root.focusToplevel(toplevel)
                    onWindowCloseRequested: toplevel => root.closeToplevel(toplevel)
                    onWindowDropped: (toplevel, workspaceName) => root.moveToplevel(toplevel, workspaceName)
                }
            }

            WorkspaceCard {
                workspace: null
                newWorkspaceId: root.nextWorkspaceId
                emphasized: false
                width: root.filmCardWidth
                height: root.filmCardHeight
                onActivated: {
                    Hyprland.dispatch(`hl.dsp.focus({ workspace = ${root.nextWorkspaceId} })`);
                    root.selectedWorkspaceId = root.nextWorkspaceId;
                }
                onWindowCloseRequested: toplevel => root.closeToplevel(toplevel)
                onWindowDropped: (toplevel, workspaceName) => root.moveToplevel(toplevel, workspaceName)
            }
        }
    }

    // ── Large selected desktop ─────────────────────────────────────────────
    Item {
        id: desktopStage
        anchors.top: filmstrip.bottom
        anchors.topMargin: 26
        anchors.left: parent.left
        anchors.right: parent.right
        anchors.bottom: hint.top
        anchors.bottomMargin: 18
        visible: root.query.trim() === ""

        WorkspaceCard {
            id: largeWorkspace
            anchors.top: parent.top
            anchors.topMargin: 18
            anchors.horizontalCenter: parent.horizontalCenter
            workspace: root.selectedWorkspace
            refreshToken: root.refreshToken + root.actionRefreshToken
            emphasized: true
            focusBound: FocusSession.active && root.selectedWorkspace?.id === FocusSession.workspaceId
            width: Math.min(parent.width - root.sideMargin * 2, 1760)
            height: Math.min(parent.height, Math.round(width / root.screenAspect) + labelHeight)
            onActivated: {
                if (workspace)
                    workspace.activate();
                ShellState.close();
            }
            onWindowActivated: toplevel => root.focusToplevel(toplevel)
            onWindowCloseRequested: toplevel => root.closeToplevel(toplevel)
            onWindowDropped: (toplevel, workspaceName) => root.moveToplevel(toplevel, workspaceName)
        }
    }

    // ── Cross-workspace search results ─────────────────────────────────────
    Flickable {
        id: searchResults
        anchors.top: filmstrip.bottom
        anchors.topMargin: 28
        anchors.left: parent.left
        anchors.right: parent.right
        anchors.bottom: hint.top
        anchors.leftMargin: root.sideMargin
        anchors.rightMargin: root.sideMargin
        anchors.bottomMargin: 18
        visible: root.query.trim() !== ""
        clip: true
        contentWidth: width
        contentHeight: resultFlow.implicitHeight
        boundsBehavior: Flickable.StopAtBounds

        Flow {
            id: resultFlow
            readonly property real cardWidth: (searchResults.width - Theme.sectionSpacing * 2) / 3
            x: Math.max(0, (searchResults.width - width) / 2)
            width: root.resultColumns * cardWidth + (root.resultColumns - 1) * Theme.sectionSpacing
            spacing: Theme.sectionSpacing

            Repeater {
                model: root.filteredWindows

                WindowThumbnail {
                    required property var modelData
                    required property int index
                    toplevel: modelData.toplevel
                    refreshToken: root.refreshToken + root.actionRefreshToken
                    selected: index === root.selectedResultIndex
                    width: resultFlow.cardWidth
                    height: Math.round(width / root.screenAspect) + 18
                    onActivated: root.focusToplevel(modelData.toplevel)
                    onCloseRequested: root.closeToplevel(modelData.toplevel)
                }
            }
        }

        Text {
            anchors.centerIn: parent
            visible: root.filteredWindows.length === 0
            text: "No windows match “" + root.query + "”"
            color: Theme.fgDim
            font.family: Theme.fontFamily
            font.pixelSize: Theme.fontSizeLarge
        }
    }

    Text {
        id: hint
        anchors.bottom: scratchpadShelf.top
        anchors.bottomMargin: 8
        anchors.horizontalCenter: parent.horizontalCenter
        text: root.query.trim() === "" ? "Drag windows between workspaces or into Scratchpad · Esc to return" : "Type to search every workspace · Enter opens the first match"
        color: Theme.fgMuted
        font.family: Theme.fontFamily
        font.pixelSize: Theme.fontSizeSmall
    }
}
