// The Dock's pinned applications, shown as the dock shows them.
//
// This replaces a sixteen-row list of names. The list was honest but it was not
// the thing being edited: the Dock is a horizontal row of icons, and the order
// of that row was being decided in a vertical column of text. Editing a
// picture of the result is faster than editing a description of it, and it
// removes the translation step where you count rows to work out which icon ends
// up third.
//
// Icons resolve exactly the way the Dock resolves them, so what is drawn here
// is what will be drawn there -- including the initial-letter fallback for an
// application whose icon the theme cannot find.
//
// Reordering is a drag, with two things that make a drag safe inside a
// scrolling page:
//
//   * preventStealing on the grabbing MouseArea. Without it the page's own
//     Flickable claims the gesture and the icon never moves, which reads as
//     breakage rather than as a page that scrolls.
//   * The order is held here while the drag runs and written once on release.
//     Committing on every cell crossed would rewrite settings.json a dozen
//     times for one gesture.
//
// Every icon is also a tab stop: Left and Right move it, Delete unpins it. A
// drag is not reachable from the keyboard, so the keyboard gets its own path
// rather than a pair of arrow buttons bolted to each cell.

import QtQuick
import Quickshell
import Quickshell.Widgets
import qs.config
import qs.services

Column {
    id: root

    spacing: 0

    // The cell is the icon plus the room its unpin button needs. Fixed, because
    // the drag arithmetic below counts cells rather than measuring them.
    readonly property int iconSize: 44
    readonly property int cellSize: 52
    readonly property int cellSpacing: 10
    readonly property int stride: root.cellSize + root.cellSpacing

    // How many cells fit on one line. The drag turns a pointer offset into an
    // index, and on a wrapped strip moving down a line is a jump of this many.
    readonly property int perRow: Math.max(1,
        Math.floor((flow.width + root.cellSpacing) / root.stride))

    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;
    }

    // Desktop entries usually carry a freedesktop icon *name*, but some ship an
    // absolute path. iconPath() only understands names, so branch on it. The
    // `true` argument makes a missing icon return "" instead of a placeholder
    // that renders as a black square in some themes.
    function iconFor(id: string): string {
        const entry = root.entriesById[id];
        const name = entry && entry.icon ? entry.icon : id;
        if (!name)
            return "";
        if (name.startsWith("/"))
            return "file://" + name;
        return Quickshell.iconPath(name, true);
    }

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

    // ── Dragging ────────────────────────────────────────────────────────────

    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);
    }

    // ── Keyboard ────────────────────────────────────────────────────────────
    //
    // The Repeater's model is a plain array, so committing a move rebuilds
    // every delegate and the focused one is destroyed mid-keystroke. The
    // focused position is remembered here instead of in the delegate, and the
    // cell that lands on it takes focus back as it is created.

    property int keyboardIndex: -1

    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.keyboardIndex = to;
        root.commit(next);
    }

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

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

    Flow {
        id: flow

        width: parent.width
        visible: root.displayed.length > 0
        spacing: root.cellSpacing

        Repeater {
            model: root.displayed

            delegate: Item {
                id: cell

                required property var modelData
                required property int index

                readonly property bool dragging: root.draggingIndex === cell.index
                readonly property string appName: root.nameFor(cell.modelData)
                readonly property string iconSource: root.iconFor(cell.modelData)

                width: root.cellSize
                height: root.cellSize

                // The dragged cell rides above its neighbours so the icon being
                // moved is the one that looks moved.
                z: cell.dragging ? 2 : 0

                activeFocusOnTab: true
                Accessible.role: Accessible.Button
                Accessible.name: cell.appName
                Accessible.description: "Pinned to the Dock, position "
                    + (cell.index + 1) + " of " + root.displayed.length
                    + ". Left and Right move it, Delete unpins it."
                Accessible.focusable: true
                Accessible.focused: cell.activeFocus

                Keys.onLeftPressed: root.move(cell.index, cell.index - 1)
                Keys.onRightPressed: root.move(cell.index, cell.index + 1)
                Keys.onDeletePressed: root.remove(cell.index)

                onActiveFocusChanged: if (cell.activeFocus) root.keyboardIndex = cell.index
                Component.onCompleted: if (root.keyboardIndex === cell.index) cell.forceActiveFocus()

                Connections {
                    target: root
                    function onKeyboardIndexChanged(): void {
                        if (root.keyboardIndex === cell.index)
                            cell.forceActiveFocus();
                    }
                }

                // Where the icon would land if the drag ended now. It sits in
                // the gap to the left of the cell rather than under it, because
                // the question a drop indicator answers is "between which two".
                Rectangle {
                    visible: cell.dragging
                    anchors.right: parent.left
                    anchors.rightMargin: Math.round(root.cellSpacing / 2) - 2
                    anchors.verticalCenter: parent.verticalCenter
                    width: 4
                    height: root.iconSize
                    radius: 2
                    border.width: 0
                    color: Theme.accent
                }

                Rectangle {
                    id: tile

                    anchors.centerIn: parent
                    width: root.iconSize
                    height: root.iconSize
                    radius: Theme.cardRadius
                    color: cell.dragging
                        ? Theme.alpha(Theme.accent, 0.18)
                        : Theme.alpha(Theme.fg, grab.containsMouse || cell.activeFocus ? 0.09 : 0.05)
                    border.width: cell.dragging || cell.activeFocus ? 2 : 1
                    border.color: cell.dragging
                        ? Theme.accent
                        : (cell.activeFocus ? Theme.accentSecondary : Theme.alpha(Theme.fg, 0.1))

                    IconImage {
                        anchors.fill: parent
                        anchors.margins: 5
                        visible: cell.iconSource !== ""
                        source: cell.iconSource
                        asynchronous: true
                        mipmap: true
                    }

                    // Last resort for an app with no resolvable icon: an
                    // initial, which is still recognizable, unlike the theme's
                    // broken-icon placeholder.
                    Text {
                        anchors.centerIn: parent
                        visible: cell.iconSource === ""
                        text: cell.appName ? cell.appName.charAt(0).toUpperCase() : "?"
                        color: Theme.fg
                        font.family: Theme.fontFamily
                        font.pixelSize: Math.round(root.iconSize * 0.5)
                    }
                }

                // The whole tile is the grip. preventStealing is the reason a
                // drag works at all inside a scrolling page.
                MouseArea {
                    id: grab

                    anchors.fill: parent
                    hoverEnabled: true
                    preventStealing: true
                    cursorShape: cell.dragging ? Qt.ClosedHandCursor : Qt.OpenHandCursor

                    property real pressX: 0
                    property real pressY: 0

                    onPressed: mouse => {
                        grab.pressX = mouse.x;
                        grab.pressY = mouse.y;
                        cell.forceActiveFocus();
                        root.beginDrag(cell.index);
                    }
                    onPositionChanged: mouse => {
                        if (root.draggingIndex < 0)
                            return;
                        // How many whole cells the pointer has travelled from
                        // where it started, across and down. Rounded, so the
                        // swap happens as the icon passes the midpoint of its
                        // neighbour; a line down is a jump of one full row.
                        const columns = Math.round((mouse.x - grab.pressX) / root.stride);
                        const rows = Math.round((mouse.y - grab.pressY) / root.stride);
                        const slots = rows * root.perRow + columns;
                        if (slots !== 0)
                            root.dragTo(root.draggingIndex + slots);
                    }
                    onReleased: root.endDrag()
                    onCanceled: root.endDrag()
                }

                // Unpin. Hidden until the icon is hovered or focused, because
                // eleven permanent × badges read as an error state rather than
                // as eleven applications you chose.
                Rectangle {
                    id: unpin

                    anchors.right: parent.right
                    anchors.top: parent.top
                    width: 18
                    height: 18
                    radius: 9
                    z: 3
                    visible: grab.containsMouse || unpinMouse.containsMouse
                        || cell.activeFocus
                    color: Theme.danger
                    border.width: 0

                    Text {
                        anchors.centerIn: parent
                        text: "×"
                        color: Theme.bgDark
                        font.family: Theme.fontFamily
                        font.pixelSize: 13
                        font.weight: Font.Bold
                    }

                    MouseArea {
                        id: unpinMouse
                        anchors.fill: parent
                        hoverEnabled: true
                        preventStealing: true
                        cursorShape: Qt.PointingHandCursor
                        onClicked: root.remove(cell.index)
                    }
                }
            }
        }
    }

    Text {
        width: parent.width
        visible: root.pinned.length === 0
        text: "Nothing is pinned — the Dock will only show running applications. Search below to add one."
        color: Theme.fgDim
        font.family: Theme.fontFamily
        font.pixelSize: Theme.fontSizeSmall
        wrapMode: Text.WordWrap
    }
}
