// What a dock icon has open, shown after a dwell on hover.
//
// Its own surface, not something drawn inside the dock. The dock's input mask
// is a thin strip when hidden and the bar's own rectangle when revealed;
// widening it to cover a preview strip would hand the dock every click in the
// empty space above it, which is most of the screen.
//
// Capture is one-shot -- `live: false` plus an explicit captureFrame() -- for
// the same reason the overview's thumbnails are: streaming four windows for as
// long as a pointer rests on an icon repaints continuously for nothing. The
// deferral around that first capture is copied from
// modules/overview/WindowThumbnail.qml, where the reasoning is written out.

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

PopupWindow {
    id: root

    // The DockItem being hovered, and the app object behind it. Both are
    // written by the Dock from DockBody's dwell timer.
    property Item anchorItem: null
    property var app: null

    // Which edge the dock lives on, so the strip appears on the side of the
    // icon that faces the screen rather than off the edge.
    property string position: "bottom"

    readonly property bool vertical: root.position === "left" || root.position === "right"

    // Four is the cap. A strip of previews wider than the screen is not a
    // preview of anything, and the point of this surface is to answer "which
    // window do I want" at a glance -- past four, the answer is the overview.
    readonly property int previewCap: 4

    readonly property var allWindows: root.app && root.app.windows ? root.app.windows : []
    readonly property var windows: root.allWindows.length > root.previewCap
        ? root.allWindows.slice(0, root.previewCap)
        : root.allWindows

    readonly property int overflow: root.allWindows.length - root.windows.length

    // Read by the Dock, which feeds it back to DockBody's grace timer. Crossing
    // from the icon to a preview leaves the dock entirely -- these are separate
    // surfaces -- so without this the act of reaching for a preview closes it.
    readonly property bool hovered: pointer.hovered

    signal dismissed

    // Bumped each time the strip opens; each bump re-captures, so a window that
    // has changed since the last look is not shown as it was.
    property int refreshToken: 0

    anchor.item: root.anchorItem
    // Bottom dock: above the icon. Side dock: alongside it, away from the edge.
    anchor.edges: root.vertical
        ? (root.position === "left" ? Edges.Right : Edges.Left)
        : Edges.Top
    anchor.gravity: root.vertical
        ? (root.position === "left" ? Edges.Right : Edges.Left)
        : Edges.Top
    anchor.margins.bottom: root.vertical ? 0 : 10
    anchor.margins.left: root.position === "left" ? 10 : 0
    anchor.margins.right: root.position === "right" ? 10 : 0

    implicitWidth: strip.implicitWidth + 12
    implicitHeight: strip.implicitHeight + 12

    color: "transparent"
    visible: root.anchorItem !== null && root.windows.length > 0

    // Deliberately NOT grabFocus. A grab would close the strip on the first
    // click anywhere and take the pointer with it, which is exactly the
    // gesture that is supposed to focus a window.
    grabFocus: false

    onVisibleChanged: {
        if (root.visible)
            root.refreshToken++;
    }

    function addressOf(toplevel: var): string {
        const raw = String(toplevel?.address ?? "");
        if (!raw)
            return "";
        return raw.startsWith("0x") ? raw : "0x" + raw;
    }

    function focusToplevel(toplevel: var): void {
        if (!toplevel)
            return;
        if (toplevel.workspace)
            toplevel.workspace.activate();

        const address = root.addressOf(toplevel);
        if (address)
            Hyprland.dispatch(`hl.dsp.focus({ window = "address:${address}" })`);
        else if (toplevel.wayland)
            toplevel.wayland.activate();

        root.dismissed();
    }

    HoverHandler {
        id: pointer
    }

    Rectangle {
        anchors.fill: parent
        radius: Theme.popoverRadius
        color: Theme.alpha(Theme.bgPopover, Theme.popoverAlpha)
        border.width: 1
        border.color: Theme.alpha(Theme.fg, 0.08)

        Row {
            id: strip
            anchors.centerIn: parent
            padding: 10
            spacing: 8

            Repeater {
                model: root.windows

                Rectangle {
                    id: card

                    required property var modelData

                    readonly property var source: card.modelData ? card.modelData.wayland : null

                    width: 176
                    height: 132
                    radius: Theme.cardRadius
                    border.width: 0 // QTBUG-137166
                    color: cardHover.hovered
                        ? Theme.alpha(Theme.fg, Theme.hoverAlpha)
                        : Theme.alpha(Theme.bg, 0.5)
                    clip: true

                    Item {
                        id: frame
                        anchors.left: parent.left
                        anchors.right: parent.right
                        anchors.top: parent.top
                        anchors.bottom: caption.top
                        anchors.margins: 6
                        anchors.bottomMargin: 2

                        // Shown until a frame arrives, and forever on a
                        // compositor without screencopy -- an icon rather than
                        // an empty box or an error.
                        IconImage {
                            anchors.centerIn: parent
                            visible: root.app && root.app.entry && root.app.entry.icon
                            source: root.app && root.app.entry && root.app.entry.icon
                                ? (String(root.app.entry.icon).startsWith("/")
                                    ? "file://" + root.app.entry.icon
                                    : Quickshell.iconPath(root.app.entry.icon, true))
                                : ""
                            implicitSize: 32
                            asynchronous: true
                            mipmap: true
                            opacity: shotLoader.hasFrame ? 0 : 1
                        }

                        Loader {
                            id: shotLoader
                            anchors.fill: parent

                            readonly property bool hasFrame: item ? item.hasContent : false

                            // Gated on refreshToken for the reason spelled out
                            // in WindowThumbnail: a ScreencopyView created
                            // before its surface has mapped has no recording
                            // context, and its first capture fails silently.
                            active: !!card.source && root.refreshToken > 0
                            sourceComponent: shotComponent
                        }

                        Component {
                            id: shotComponent

                            ScreencopyView {
                                id: shot
                                captureSource: card.source
                                live: false
                                paintCursor: false
                                constraintSize: Qt.size(frame.width, frame.height)

                                readonly property real aspect: sourceSize.height > 0
                                    ? sourceSize.width / sourceSize.height
                                    : 16 / 9

                                anchors.centerIn: parent
                                width: Math.min(parent.width, parent.height * aspect)
                                height: aspect > 0 ? width / aspect : parent.height
                                opacity: hasContent ? 1 : 0

                                property bool recordingReady: false

                                function tryCapture(): void {
                                    if (shot.hasContent)
                                        return;
                                    if (!shot.recordingReady) {
                                        frameReady.restart();
                                        return;
                                    }
                                    shot.captureFrame();
                                }

                                // One frame of the popup's own rendering is
                                // what makes the capture context exist.
                                FrameAnimation {
                                    id: frameReady
                                    running: false
                                    onTriggered: {
                                        running = false;
                                        if (shot.hasContent)
                                            return;
                                        shot.recordingReady = true;
                                        shot.tryCapture();
                                    }
                                }

                                Component.onCompleted: tryCapture()
                            }
                        }
                    }

                    Text {
                        id: caption
                        anchors.bottom: parent.bottom
                        anchors.left: parent.left
                        anchors.right: parent.right
                        anchors.margins: 7
                        text: String(card.modelData?.title ?? "").trim() || (root.app ? root.app.appId : "")
                        elide: Text.ElideRight
                        horizontalAlignment: Text.AlignHCenter
                        color: Theme.fgDim
                        font.family: Theme.fontFamily
                        font.pixelSize: Theme.fontSizeSmall
                    }

                    HoverHandler {
                        id: cardHover
                    }

                    TapHandler {
                        onTapped: root.focusToplevel(card.modelData)
                    }
                }
            }

            // Only when there are more windows than fit. Says so rather than
            // silently showing four of nine.
            Text {
                anchors.verticalCenter: parent.verticalCenter
                visible: root.overflow > 0
                width: visible ? implicitWidth : 0
                text: "+" + root.overflow
                color: Theme.fgMuted
                font.family: Theme.fontFamily
                font.pixelSize: Theme.fontSizeSmall
            }
        }
    }
}
