// One dock icon: the app, its running-window dots, hover scale and click
// behavior. Reproduces Dash-to-Dock's item: 48px icon, up to four dots
// underneath, grow-on-hover, click to focus / cycle / launch.

import Quickshell
import Quickshell.Hyprland
import Quickshell.Widgets
import QtQuick
import qs.config

Item {
    id: root

    // { entry: DesktopEntry|null, windows: [HyprlandToplevel], appId: string,
    //   pinned: bool }. Built by DockBody so this file stays presentational.
    required property var app

    readonly property DesktopEntry entry: app && app.entry ? app.entry : null
    readonly property var windows: app && app.windows ? app.windows : []
    readonly property string label: entry ? entry.name : (app && app.appId ? app.appId : "")
    readonly property bool running: windows.length > 0
    readonly property bool hovered: mouse.containsMouse

    // Set by DockBody. Which way the dock runs decides which axis a drag reads.
    property bool vertical: false

    // Reordering, driven from DockBody: whether this icon may be dragged at
    // all, how far it is currently displaced, and whether it is the one being
    // dragged rather than one being pushed aside.
    property bool draggable: false
    property real dragShift: 0
    property bool dragging: false

    // Emitted so DockBody can drive the single shared tooltip.
    signal entered
    signal exited
    signal contextMenuRequested

    // The drag, reported as travel along the dock's own axis from where the
    // press landed. DockBody owns what that means.
    //
    // The start also carries the pitch -- one cell plus the gap after it --
    // because the cell size is this file's business: an item is taller than it
    // is wide (the running dots sit under the icon), so a slot down a side dock
    // is further than a slot across a bottom one. Measuring it here rather than
    // recomputing it in DockBody keeps one copy of that arithmetic.
    signal dragStarted(real pitch)
    signal dragMoved(real travel)
    signal dragEnded
    signal dragCancelled

    // The icon may grow past the cell on hover; the cell itself stays a fixed
    // size so the row doesn't reflow.
    implicitWidth: Theme.dockIconSize
    implicitHeight: Theme.dockIconSize + dots.height + 4

    // Above the icons it is passing.
    z: root.dragging ? 2 : 0

    // A transform, not an x/y binding: the Grid owns those, and assigning them
    // in a delegate fights the positioner rather than moving the icon.
    transform: Translate {
        x: root.vertical ? 0 : root.dragShift
        y: root.vertical ? root.dragShift : 0
    }

    // 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.
    readonly property string iconSource: {
        const name = entry && entry.icon ? entry.icon : (app && app.appId ? app.appId : "");
        if (!name)
            return "";
        if (name.startsWith("/"))
            return "file://" + name;
        return Quickshell.iconPath(name, true);
    }

    Item {
        id: iconSlot
        width: Theme.dockIconSize
        height: Theme.dockIconSize
        anchors.horizontalCenter: parent.horizontalCenter
        y: 0

        // Grow upward out of the dock, the way GNOME's dash does, so the icon
        // never overlaps its own running dots.
        transformOrigin: Item.Bottom
        scale: root.hovered ? 1.15 : 1.0

        Behavior on scale {
            NumberAnimation {
                duration: Theme.durFast
                easing.type: Easing.OutBack
                easing.overshoot: 1.6
            }
        }

        IconImage {
            anchors.fill: parent
            visible: root.iconSource !== ""
            source: root.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.
        Rectangle {
            anchors.fill: parent
            visible: root.iconSource === ""
            radius: Theme.cardRadius
            border.width: 0
            color: Theme.alpha(Theme.fg, Theme.activeAlpha)

            Text {
                anchors.centerIn: parent
                text: root.label ? root.label.charAt(0).toUpperCase() : "?"
                color: Theme.fg
                font.family: Theme.fontFamily
                font.pixelSize: Math.round(Theme.dockIconSize * 0.5)
            }
        }
    }

    // Running indicators. Dash-to-Dock showed one dot per window up to four.
    Row {
        id: dots
        anchors.horizontalCenter: parent.horizontalCenter
        anchors.bottom: parent.bottom
        spacing: 3
        height: 4

        Repeater {
            model: Math.min(root.windows.length, 4)

            Rectangle {
                width: 4
                height: 4
                radius: 2
                border.width: 0 // QTBUG-137166: transparent rounded rects punch corners
                color: Theme.accent
            }
        }
    }

    MouseArea {
        id: mouse
        anchors.fill: parent
        hoverEnabled: true
        acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton

        // Where the press landed, and whether it has travelled far enough to
        // stop being a click. The threshold is the whole reason a drag can
        // share this MouseArea with the launch click: a hand that means to
        // click never moves eight pixels while the button is down.
        readonly property int dragThreshold: 8
        property real pressX: 0
        property real pressY: 0
        property bool dragActive: false
        property bool dragConsumed: false

        // A wheel notch is 120 units, but a touchpad sends far smaller ones and
        // a free-spinning wheel sends larger. Accumulating and spending whole
        // notches is what makes both feel the same -- reacting to every event
        // would make a touchpad flick blur through every window an app owns.
        property int wheelTravel: 0

        onEntered: root.entered()
        onExited: root.exited()

        onWheel: event => {
            if (!root.running) {
                mouse.wheelTravel = 0;
                return;
            }
            mouse.wheelTravel += event.angleDelta.y;
            while (mouse.wheelTravel >= 120) {
                mouse.wheelTravel -= 120;
                root.focusBy(-1);
            }
            while (mouse.wheelTravel <= -120) {
                mouse.wheelTravel += 120;
                root.focusBy(1);
            }
        }

        onPressed: mev => {
            mouse.pressX = mev.x;
            mouse.pressY = mev.y;
            mouse.dragActive = false;
            mouse.dragConsumed = false;
        }

        onPositionChanged: mev => {
            if (!root.draggable || !mouse.pressedButtons)
                return;
            const travel = root.vertical ? mev.y - mouse.pressY : mev.x - mouse.pressX;
            if (!mouse.dragActive) {
                if (Math.abs(travel) < mouse.dragThreshold)
                    return;
                mouse.dragActive = true;
                mouse.dragConsumed = true;
                root.dragStarted((root.vertical ? root.height : root.width) + Theme.dockGap);
            }
            root.dragMoved(travel);
        }

        onReleased: {
            if (!mouse.dragActive)
                return;
            mouse.dragActive = false;
            root.dragEnded();
        }

        onCanceled: {
            if (!mouse.dragActive)
                return;
            mouse.dragActive = false;
            mouse.dragConsumed = false;
            root.dragCancelled();
        }

        onClicked: mev => {
            // A gesture that reordered the dock is not also a launch.
            if (mouse.dragConsumed) {
                mouse.dragConsumed = false;
                return;
            }
            // Middle click always starts a new instance, as in GNOME.
            if (mev.button === Qt.MiddleButton) {
                root.launch();
                return;
            }
            if (mev.button === Qt.RightButton) {
                root.contextMenuRequested();
                return;
            }
            if (root.running)
                root.focusBy(1);
            else
                root.launch();
        }
    }

    function launch(): void {
        if (root.entry)
            root.entry.execute();
    }

    // Stepping through an app's windows: clicking a running app takes one step
    // forward, matching GNOME's dash, and the wheel takes one in either
    // direction. With nothing of this app focused, any step lands on its first
    // window rather than counting from a window the user is not looking at.
    function focusBy(delta: int): void {
        const wins = root.windows;
        if (wins.length === 0)
            return;

        let current = -1;
        for (let i = 0; i < wins.length; i++) {
            if (wins[i].activated) {
                current = i;
                break;
            }
        }

        const next = current < 0
            ? wins[0]
            : wins[((current + delta) % wins.length + wins.length) % wins.length];
        if (!next)
            return;

        // Quickshell reports the address without the 0x prefix Hyprland's
        // window selector expects; normalize so either form works.
        if (next.address) {
            const addr = next.address.startsWith("0x") ? next.address : "0x" + next.address;
            Hyprland.dispatch(`hl.dsp.focus({ window = "address:${addr}" })`);
        } else if (next.wayland) {
            // No address yet (the toplevel is still being reported) — the
            // wlr handle can still raise it.
            next.wayland.activate();
        }
    }
}
