// The dock — a replacement for Dash-to-Dock configured as: bottom edge, 48px
// icons, intellihide against all windows, dot running-indicators, show-apps
// button at the leading edge.
//
// Intellihide means the dock reserves no space at all (exclusiveZone 0) and
// floats over windows, appearing when the pointer reaches the bottom edge or
// when nothing is in the way.

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

PanelWindow {
    id: root

    // Set by Variants when instantiated per-screen. Plain rather than
    // `required` so the dock can also be created standalone while testing;
    // Variants injects into the existing property either way.
    property var modelData: null
    screen: root.modelData

    // Which edge this dock lives on, and everything that follows from it. The
    // bottom case is unchanged in every particular: same anchors, same
    // geometry, same slide -- so a machine that never touches the setting sees
    // exactly the dock it had.
    readonly property string position: Settings.dockPosition
    readonly property bool vertical: root.position === "left" || root.position === "right"

    // Only on the screens asked for. An empty list means all of them, which is
    // what a single-monitor machine wants and what an unplugged display should
    // not be able to change.
    readonly property bool onThisScreen: {
        const wanted = Settings.dockScreens;
        if (!wanted || wanted.length === 0)
            return true;
        return wanted.indexOf(String(root.screen?.name ?? "")) >= 0;
    }

    visible: root.onThisScreen

    // A dock spans the edge it lives on, which means anchoring BOTH ends of
    // that edge: bottom+left+right across the screen, or top+bottom plus one
    // side down it. Anchoring only one end leaves the surface free to collapse
    // to its implicit size on that axis -- a side dock came out one pixel tall.
    anchors {
        top: root.vertical
        bottom: true
        left: root.position !== "right"
        right: root.position !== "left"
    }

    color: "transparent"

    // A dock that reserved space would not be intellihiding.
    exclusiveZone: 0
    exclusionMode: ExclusionMode.Ignore

    // Matched by the `qs-dock` layer rule in hypr/rules.lua — do not rename.
    WlrLayershell.namespace: "qs-dock"
    WlrLayershell.layer: WlrLayer.Top
    WlrLayershell.keyboardFocus: WlrKeyboardFocus.None

    // ── Geometry ────────────────────────────────────────────────────────────
    // Height = room for the tooltip above the bar + the bar + the gap under it.
    readonly property int revealStripHeight: 3
    readonly property int edgeMargin: Theme.barGap
    readonly property int tooltipSpace: 34

    // Only the axis the dock is thin on gets an implicit size; the other is
    // spanned by the anchors above. Setting both would fight them.
    implicitHeight: root.vertical ? 0 : tooltipSpace + body.implicitHeight + edgeMargin
    implicitWidth: root.vertical ? tooltipSpace + body.implicitWidth + edgeMargin : 0

    // ── Intellihide ─────────────────────────────────────────────────────────
    // This instance's own monitor, the same lookup Workspaces.qml uses to
    // scope a per-screen bar to its own screen. Falls back to null when this
    // Dock is created standalone (no `screen` set) rather than via Variants.
    readonly property HyprlandMonitor monitor: root.screen ? Hyprland.monitorFor(root.screen) : null

    // Hyprland does not expose live toplevel geometry, so exact overlap cannot
    // be computed. "Is anything on this workspace at all" is the robust proxy,
    // and it is what Dash-to-Dock's all-windows intellihide felt like in
    // practice: an empty workspace keeps the dock out.
    //
    // Deliberately this instance's own monitor's active workspace, not the
    // globally-focused one -- with one Dock per screen, keying off the global
    // focus would make focusing an empty workspace on monitor A hide the dock
    // on monitor B even though B's own workspace is still busy.
    readonly property bool workspaceOccupied: {
        const ws = root.monitor ? root.monitor.activeWorkspace : Hyprland.focusedWorkspace;
        return !!ws && ws.toplevels.values.length > 0;
    }

    // Anything anchored to the dock that the dock would drag off-screen with
    // it. The pointer leaves the dock the moment it enters an open menu -- the
    // menu is its own surface -- so without this the dock slides away under
    // the menu it opened, and the row the hand was reaching for goes with it.
    // The same is true mid-drag, and while a preview is up.
    readonly property bool interactionHeld: dockContextMenu.visible
        || body.dragging
        || dockPreviews.visible

    readonly property bool wantRevealed: !Settings.dockAutohide || !workspaceOccupied || pointer.hovered || root.interactionHeld

    property bool revealed: true

    onWantRevealedChanged: {
        if (wantRevealed) {
            hideTimer.stop();
            revealTimer.restart();
        } else {
            revealTimer.stop();
            hideTimer.restart();
        }
    }

    Timer {
        id: revealTimer
        interval: Settings.dockRevealDelayMs
        onTriggered: root.revealed = true
    }

    Timer {
        id: hideTimer
        interval: Settings.dockHideDelayMs
        onTriggered: root.revealed = false
    }

    // Other modules (the bar, the capture overlay) read this. It is one
    // shared flag but there is one Dock per monitor, so only the instance on
    // the currently-focused monitor is allowed to write it -- otherwise
    // whichever instance last changed reveal state would stomp the others,
    // and a reader would see an arbitrary monitor's value. This scopes the
    // flag to mean "is the dock revealed on the monitor the user is on",
    // which is what a capture overlay or the bar actually care about.
    // (A true per-monitor flag would need ShellState.dockRevealed itself to
    // become keyed by screen, which is out of scope here -- see the report.)
    readonly property bool isFocusedMonitorInstance: root.monitor === null || root.monitor === Hyprland.focusedMonitor

    onRevealedChanged: root._syncShellState()
    onIsFocusedMonitorInstanceChanged: root._syncShellState()

    function _syncShellState(): void {
        if (root.isFocusedMonitorInstance)
            ShellState.dockRevealed = root.revealed;
    }

    // wantRevealed's first evaluation emits no change signal when it lands on
    // false (the default), so the initial state has to be taken explicitly —
    // otherwise a shell started on a busy workspace would leave the dock up.
    Component.onCompleted: {
        revealed = wantRevealed;
        root._syncShellState();
    }

    // ── Input region ────────────────────────────────────────────────────────
    // Revealed: the bar plus everything below it, so crossing the gap under the
    // dock does not count as leaving. Hidden: a sliver along the screen edge,
    // which is the only thing that can still trigger the dock — every other
    // click passes straight through to the window underneath.
    mask: Region {
        item: maskItem
    }

    Item {
        id: surface
        anchors.fill: parent

        // Reports the pointer anywhere inside the input region. A HoverHandler
        // rather than a MouseArea because it keeps reporting while the icons'
        // own MouseAreas are hovered.
        HoverHandler {
            id: pointer
        }

        // Revealed: the dock plus everything between it and the edge, so
        // crossing the gap does not count as leaving. Hidden: a sliver along
        // the edge the dock lives on, which is the only thing that can bring
        // it back -- every other click passes through to the window beneath.
        Item {
            id: maskItem

            x: {
                if (!root.revealed)
                    return root.position === "right" ? surface.width - root.revealStripHeight : 0;
                // A horizontal dock is centred on its edge, so the region has
                // to start where the body starts -- starting at 0 puts it over
                // the left of the screen while the pointer that summoned the
                // dock is in the middle, and the hover drops on the very frame
                // the dock arrives. A vertical dock reaches from the body out
                // to its own edge, which is x 0 on the left and the body on
                // the right.
                if (!root.vertical)
                    return body.x;
                return root.position === "right" ? body.x : 0;
            }
            y: {
                if (!root.revealed)
                    return root.vertical ? 0 : surface.height - root.revealStripHeight;
                return body.y;
            }
            width: {
                if (!root.revealed)
                    return root.vertical ? root.revealStripHeight : surface.width;
                return root.vertical
                    ? (root.position === "right" ? surface.width - body.x : body.x + body.width)
                    : body.width;
            }
            height: {
                if (!root.revealed)
                    return root.vertical ? surface.height : root.revealStripHeight;
                return root.vertical ? body.height : surface.height - body.y;
            }
        }

        DockBody {
            id: body

            onContextMenuRequested: (anchorItem, app) => {
                // The whole app object, not its desktop entry: the menu lists
                // the app's own windows and offers to pin or unpin it, and
                // neither fact survives being narrowed to an entry.
                body.dismissPreview();
                dockContextMenu.anchorItem = anchorItem;
                dockContextMenu.app = app;
                dockContextMenu.visible = true;
            }

            vertical: root.vertical
            leftSide: root.position === "left"

            // Both axes are computed rather than anchored. Anchoring the centre
            // on one axis and binding a position on the other looks tidier and
            // is a conflict: an anchored centre owns that coordinate, so the
            // binding beside it is fighting for the same value.
            //
            // Centred on the long axis; on the short one it sits a tooltip's
            // width in from the edge when revealed, and off-screen when not.
            x: {
                if (!root.vertical)
                    return (surface.width - width) / 2;
                if (root.position === "left")
                    return root.revealed ? root.tooltipSpace : -width;
                return root.revealed ? surface.width - width - root.tooltipSpace : surface.width;
            }
            y: {
                if (root.vertical)
                    return (surface.height - height) / 2;
                return root.revealed ? root.tooltipSpace : surface.height;
            }
            opacity: root.revealed ? 1 : 0

            Behavior on x {
                NumberAnimation {
                    duration: root.revealed ? Theme.durDockReveal : Theme.durNormal
                    easing.type: root.revealed ? Easing.OutQuint : Easing.InCubic
                }
            }

            // Asymmetric on purpose. Revealing is a response to something the
            // user just did, so it has to feel immediate — any delay there
            // reads as lag. Hiding is not a response to anything, so it can
            // take its time and stay calm in peripheral vision.
            Behavior on y {
                NumberAnimation {
                    duration: root.revealed ? Theme.durDockReveal : Theme.durNormal
                    easing.type: root.revealed ? Easing.OutQuint : Easing.InCubic
                }
            }

            Behavior on opacity {
                NumberAnimation {
                    duration: root.revealed ? Theme.durDockReveal : Theme.durNormal
                    easing.type: Easing.OutCubic
                }
            }
        }
    }

    DockContextMenu {
        id: dockContextMenu
    }

    // Its own surface rather than something drawn inside the dock: the dock's
    // input mask is a thin strip when hidden and the bar's own rectangle when
    // shown, and widening it to cover a preview would hand the dock every
    // click in the empty space above it.
    DockPreviews {
        id: dockPreviews

        anchorItem: body.previewAnchor
        app: body.previewApp
        position: root.position

        // The pointer crossing from the icon to the previews leaves the dock
        // entirely -- these are separate surfaces -- so the previews report
        // their own hover back, and DockBody's grace timer uses it to tell
        // "reaching for a preview" from "moved away".
        onHoveredChanged: body.previewHovered = dockPreviews.hovered
        onDismissed: body.dismissPreview()
    }
}
