// Every shortcut, on one keypress.
//
// This is a different job from the Shortcuts settings page, which answers "how
// do I change this" and is worth opening a window for. This answers "what can
// I press", which is a question you have while your hands are already on the
// keyboard and which needs answering in under a second. So it is an overlay,
// it is one key, and it closes on the same key.
//
// It reads the live keymap rather than a written-down copy: Keybinds.grouped()
// comes from `hyprctl binds`, so a shortcut somebody rebound in Settings shows
// its new chord here without anything being kept in sync. Categories come from
// hypr/keybinds.lua's manifest -- see Keybinds.categoryManifest.
//
// Three columns, balanced by how many shortcuts each category holds. Not a
// Flow: with about 120 binds across six wildly uneven categories -- Windows has
// forty, Session has three -- a Flow wraps into as many columns as it likes and
// the result is unreadable. The columns are computed instead.

import Quickshell
import Quickshell.Wayland
import QtQuick
import qs.config
import qs.services
import qs.widgets

PanelWindow {
    id: root

    readonly property bool open: ShellState.cheatsheetOpen

    readonly property int columnCount: 3

    // Categories dealt into columns, longest first, each going to whichever
    // column is currently shortest. Sorting first is what stops the fortieth
    // window bind landing in a column that already has thirty workspace ones.
    readonly property var columns: {
        const buckets = [];
        for (let index = 0; index < root.columnCount; index++)
            buckets.push({ groups: [], weight: 0 });

        const groups = Array.from(Keybinds.grouped())
            .sort((a, b) => b.binds.length - a.binds.length);

        for (const group of groups) {
            let target = buckets[0];
            for (const bucket of buckets) {
                if (bucket.weight < target.weight)
                    target = bucket;
            }
            target.groups.push(group);
            // Two lines of overhead per heading, so a column of many small
            // categories is not treated as shorter than it looks.
            target.weight += group.binds.length + 2;
        }
        return buckets.map(bucket => bucket.groups);
    }

    anchors { top: true; bottom: true; left: true; right: true }
    color: "transparent"
    exclusiveZone: 0
    exclusionMode: ExclusionMode.Ignore

    // The `^qs-popover` prefix rule in hypr/rules.lua blurs what is behind
    // this. The dimming is painted here rather than added to the overlay rule,
    // the way PolkitPrompt does it, because this is a card on a scrim rather
    // than a full-screen takeover.
    WlrLayershell.namespace: "qs-popover-cheatsheet"
    WlrLayershell.layer: WlrLayer.Overlay
    WlrLayershell.keyboardFocus: root.open
        ? WlrKeyboardFocus.Exclusive
        : WlrKeyboardFocus.None

    // Stays mapped for the length of the close animation, or it vanishes
    // instantly and only the opening is ever seen.
    property bool mapped: false
    visible: root.mapped

    onOpenChanged: {
        if (root.open) {
            unmapTimer.stop();
            root.mapped = true;
            // The keymap can change while the session runs: a rebind in
            // Settings, or a compositor reload.
            Keybinds.refresh();
        } else {
            unmapTimer.restart();
        }
    }

    Timer {
        id: unmapTimer
        interval: Theme.durNormal
        onTriggered: root.mapped = false
    }

    Rectangle {
        anchors.fill: parent
        color: Theme.alpha(Theme.bgDark, Theme.overlayAlpha)
        opacity: root.open ? 1 : 0
        Behavior on opacity { NumberAnimation { duration: Theme.durNormal; easing.type: Easing.OutCubic } }

        // Anywhere outside the card closes it, which is what every other
        // transient surface on this desktop does.
        MouseArea {
            anchors.fill: parent
            onClicked: ShellState.close()
        }
    }

    Rectangle {
        id: card

        anchors.centerIn: parent
        width: Math.min(root.width - 120, 1240)
        // Sized from its content, capped at the screen. Deliberately NOT
        // computed from a child that fills it: that is a circular binding, and
        // it produced a card taller than the display with its contents running
        // off the bottom edge.
        height: Math.min(root.height - 120, header.height + body.contentHeight + 72)

        radius: Theme.popoverRadius
        color: Theme.alpha(Theme.bgPopover, Theme.popoverAlpha)
        border.width: 1
        border.color: Theme.alpha(Theme.fg, 0.1)

        opacity: root.open ? 1 : 0
        scale: root.open ? 1 : 0.98
        Behavior on opacity { NumberAnimation { duration: Theme.durNormal; easing.type: Easing.OutCubic } }
        Behavior on scale { NumberAnimation { duration: Theme.durNormal; easing.type: Easing.OutCubic } }

        PrismEdge {
            anchors.top: parent.top
            anchors.left: parent.left
            anchors.right: parent.right
            inset: Theme.popoverRadius
        }

        // Clicks on the card itself must not fall through to the scrim.
        MouseArea { anchors.fill: parent }

        Item {
            id: header

            anchors.top: parent.top
            anchors.left: parent.left
            anchors.right: parent.right
            anchors.margins: 28
            height: title.implicitHeight

            Text {
                id: title
                text: "Keyboard shortcuts"
                color: Theme.fg
                font.family: Theme.fontFamily
                font.pixelSize: Theme.fontSizeLarge
                font.weight: Font.DemiBold
            }

            Text {
                anchors.right: parent.right
                anchors.verticalCenter: title.verticalCenter
                text: Keybinds.loaded
                    ? Keybinds.binds.length + " shortcuts  ·  Esc to close"
                    : "Reading the keymap…"
                color: Theme.fgMuted
                font.family: Theme.fontFamily
                font.pixelSize: Theme.fontSizeSmall
            }
        }

        // Scrolls only when it has to. On a display tall enough for the whole
        // keymap this never moves, which is the common case and the one worth
        // optimising for -- a cheatsheet you have to scroll is a document.
        Flickable {
            id: body

            anchors.top: header.bottom
            anchors.topMargin: 18
            anchors.left: parent.left
            anchors.right: parent.right
            anchors.bottom: parent.bottom
            anchors.leftMargin: 28
            anchors.rightMargin: 28
            anchors.bottomMargin: 28

            contentWidth: width
            contentHeight: columnRow.implicitHeight
            clip: true
            boundsBehavior: Flickable.StopAtBounds

            Row {
                id: columnRow
                width: parent.width
                spacing: 24

                Repeater {
                    model: root.columns

                    Column {
                        required property var modelData
                        width: (columnRow.width - columnRow.spacing * (root.columnCount - 1)) / root.columnCount
                        spacing: 20

                        Repeater {
                            model: parent.modelData

                            CheatsheetGroup {
                                required property var modelData
                                width: parent.width
                                name: modelData.name
                                binds: modelData.binds
                            }
                        }
                    }
                }
            }
        }
    }

    // Escape closes, like every other dialog here. The same chord that opened
    // it also closes it, which ShellState.toggle handles.
    Item {
        anchors.fill: parent
        focus: true
        Keys.onEscapePressed: ShellState.close()
    }
}
