// The Dock's pinned applications: reorder, remove, and add.
//
// The list was a sixteen-entry literal in Settings.qml, so changing what sits
// in the Dock meant editing a QML file and reloading the shell. It is a plain
// ordered list of desktop entry ids, stored in the shared settings file, which
// means it is covered by Restore defaults like everything else.
//
// Move up / move down rather than drag-and-drop. Dragging inside a Flickable
// that is itself inside a scrolling page is a genuinely hard interaction to get
// right, and it fails in a way the user reads as the app being broken; two
// buttons are unambiguous and keyboard-reachable.

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

Column {
    id: root

    spacing: 0

    readonly property var pinned: {
        const stored = DesktopPreferences.get("dockPinned");
        return Array.isArray(stored) ? stored : [];
    }

    // DesktopEntries populates asynchronously, so this must be read as a
    // binding rather than looked up inside one -- byId() called during
    // evaluation registers no dependency and answers from an empty list.
    readonly property var entriesById: {
        const index = {};
        for (const entry of DesktopEntries.applications.values)
            index[entry.id] = entry;
        return index;
    }

    function nameFor(id: string): string {
        const entry = root.entriesById[id];
        return entry ? entry.name : id;
    }

    function commit(next: var): void {
        DesktopPreferences.set("dockPinned", next);
    }

    // ── Dragging ────────────────────────────────────────────────────────────
    //
    // By a grip rather than the whole row. The objection this file used to
    // record -- that dragging inside a Flickable inside a scrolling page is
    // hard to get right and fails in a way that reads as breakage -- is real,
    // and the answer is preventStealing on the grip: the Flickable cannot take
    // a gesture that started there, so a vertical drag reorders instead of
    // scrolling the page out from under it. The arrow buttons stay, because
    // they are the keyboard-reachable path and a grip is not.
    //
    // The order is held here while the drag runs and written once on release.
    // Committing on every slot crossed would rewrite settings.json a dozen
    // times for one gesture.
    property int draggingIndex: -1
    property var workingOrder: []

    readonly property var displayed: root.draggingIndex >= 0 ? root.workingOrder : root.pinned

    function beginDrag(index: int): void {
        root.workingOrder = root.pinned.slice();
        root.draggingIndex = index;
    }

    function dragTo(target: int): void {
        if (root.draggingIndex < 0 || target === root.draggingIndex)
            return;
        if (target < 0 || target >= root.workingOrder.length)
            return;
        const next = root.workingOrder.slice();
        const moved = next.splice(root.draggingIndex, 1)[0];
        next.splice(target, 0, moved);
        root.workingOrder = next;
        root.draggingIndex = target;
    }

    function endDrag(): void {
        if (root.draggingIndex < 0)
            return;
        const next = root.workingOrder.slice();
        root.draggingIndex = -1;
        root.workingOrder = [];
        root.commit(next);
    }

    function move(from: int, to: int): void {
        if (to < 0 || to >= root.pinned.length)
            return;
        const next = root.pinned.slice();
        const moved = next.splice(from, 1)[0];
        next.splice(to, 0, moved);
        root.commit(next);
    }

    function remove(index: int): void {
        const next = root.pinned.slice();
        next.splice(index, 1);
        root.commit(next);
    }

    function add(id: string): void {
        if (root.pinned.indexOf(id) >= 0)
            return;
        root.commit(root.pinned.concat([id]));
    }

    Repeater {
        model: root.displayed

        SettingRow {
            id: pin

            required property var modelData
            required property int index

            label: root.nameFor(pin.modelData)
            // The desktop id is shown only when the name alone would not say
            // which entry this is. It is developer text, and repeating it under
            // fifteen recognisable application names is noise that makes the
            // list harder to scan, not easier.
            detail: root.pinned.filter(other =>
                root.nameFor(other) === root.nameFor(pin.modelData)).length > 1
                ? pin.modelData
                : ""
            divider: pin.index < root.pinned.length - 1
            controlWidth: 132

            // Lifted while dragging so the row being moved is the one that
            // looks moved.
            z: root.draggingIndex === pin.index ? 2 : 0
            opacity: root.draggingIndex === pin.index ? 0.85 : 1

            Row {
                anchors.right: parent.right
                anchors.verticalCenter: parent.verticalCenter
                spacing: 4

                // The grip. preventStealing is the whole reason this works
                // inside a scrolling page: without it the Flickable claims the
                // vertical gesture and the row never moves.
                Item {
                    width: 26
                    height: 26
                    anchors.verticalCenter: parent.verticalCenter

                    Text {
                        anchors.centerIn: parent
                        text: "\u2261"
                        color: root.draggingIndex === pin.index ? Theme.accent : Theme.fgMuted
                        font.family: Theme.fontFamily
                        font.pixelSize: 15
                    }

                    MouseArea {
                        id: grip
                        anchors.fill: parent
                        preventStealing: true
                        cursorShape: Qt.SizeVerCursor

                        property real pressY: 0

                        onPressed: mouse => {
                            grip.pressY = mouse.y;
                            root.beginDrag(pin.index);
                        }
                        onPositionChanged: mouse => {
                            if (root.draggingIndex < 0 || pin.height <= 0)
                                return;
                            // How many whole rows the pointer has travelled from
                            // where it started. Rounded, so the swap happens as
                            // the grip passes the midpoint of the next row.
                            const travelled = (mouse.y - grip.pressY);
                            const slots = Math.round(travelled / pin.height);
                            if (slots !== 0)
                                root.dragTo(root.draggingIndex + slots);
                        }
                        onReleased: root.endDrag()
                        onCanceled: root.endDrag()
                    }
                }

                SettingsButton {
                    text: "↑"
                    enabled: pin.index > 0
                    onClicked: root.move(pin.index, pin.index - 1)
                }
                SettingsButton {
                    text: "↓"
                    enabled: pin.index < root.pinned.length - 1
                    onClicked: root.move(pin.index, pin.index + 1)
                }
                SettingsButton {
                    text: "Unpin"
                    onClicked: root.remove(pin.index)
                }
            }
        }
    }

    SettingRow {
        visible: root.pinned.length === 0
        label: "Nothing is pinned"
        detail: "The Dock will only show running applications"
        divider: false
    }
}
