import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import Quickshell.Services.Notifications
import qs.Common
import qs.Services
import qs.Widgets

PanelWindow {
    id: win

    WindowBlur {
        targetWindow: win
        blurX: content.x + content.cardInset + swipeTx.x + tx.x
        blurY: content.y + content.cardInset + swipeTx.y + tx.y
        blurWidth: !win._finalized ? Math.max(0, content.width - content.cardInset * 2) : 0
        blurHeight: !win._finalized ? Math.max(0, content.height - content.cardInset * 2) : 0
        blurRadius: Theme.cornerRadius
    }

    WlrLayershell.namespace: "dms:notification-popup"

    required property var notificationData
    required property string notificationId
    readonly property bool hasValidData: notificationData && notificationData.notification
    readonly property alias hovered: cardHoverHandler.hovered
    property int screenY: 0
    property bool exiting: false
    property bool _isDestroying: false
    property bool _finalized: false
    property real _lastReportedAlignedHeight: -1
    property real _storedTopMargin: 0
    property real _storedBottomMargin: 0
    readonly property string clearText: I18n.tr("Dismiss")
    property bool descriptionExpanded: false
    readonly property bool hasExpandableBody: (notificationData?.htmlBody || "").replace(/<[^>]*>/g, "").trim().length > 0
    onDescriptionExpandedChanged: {
        popupHeightChanged();
    }
    onImplicitHeightChanged: {
        const aligned = Theme.px(implicitHeight, dpr);
        if (Math.abs(aligned - _lastReportedAlignedHeight) < 0.5)
            return;
        _lastReportedAlignedHeight = aligned;
        popupHeightChanged();
    }

    readonly property bool compactMode: SettingsData.notificationCompactMode
    readonly property real cardPadding: compactMode ? Theme.notificationCardPaddingCompact : Theme.notificationCardPadding
    readonly property real popupIconSize: compactMode ? Theme.notificationIconSizeCompact : Theme.notificationIconSizeNormal
    readonly property real contentSpacing: compactMode ? Theme.spacingXS : Theme.spacingS
    readonly property real contentBottomClearance: 8
    readonly property real actionButtonHeight: compactMode ? 20 : 24
    readonly property real collapsedContentHeight: Math.max(popupIconSize, Theme.fontSizeSmall * 1.2 + Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2)) + contentBottomClearance
    readonly property real privacyCollapsedContentHeight: Math.max(popupIconSize, Theme.fontSizeSmall * 1.2 + Theme.fontSizeMedium * 1.2) + contentBottomClearance
    readonly property real basePopupHeight: cardPadding * 2 + collapsedContentHeight + actionButtonHeight + contentSpacing
    readonly property real basePopupHeightPrivacy: cardPadding * 2 + privacyCollapsedContentHeight + actionButtonHeight + contentSpacing

    signal entered
    signal exitStarted
    signal exitFinished
    signal popupHeightChanged

    function startExit() {
        if (exiting || _isDestroying) {
            return;
        }
        exiting = true;
        exitStarted();
        exitAnim.restart();
        exitWatchdog.restart();
        if (NotificationService.removeFromVisibleNotifications)
            NotificationService.removeFromVisibleNotifications(win.notificationData);
    }

    function forceExit() {
        if (_isDestroying) {
            return;
        }
        _isDestroying = true;
        exiting = true;
        visible = false;
        exitWatchdog.stop();
        finalizeExit("forced");
    }

    function finalizeExit(reason) {
        if (_finalized) {
            return;
        }

        _finalized = true;
        _isDestroying = true;
        exitWatchdog.stop();
        wrapperConn.enabled = false;
        wrapperConn.target = null;
        win.exitFinished();
    }

    visible: !_finalized
    WlrLayershell.layer: {
        const envLayer = Quickshell.env("DMS_NOTIFICATION_LAYER");
        if (envLayer) {
            switch (envLayer) {
            case "bottom":
                return WlrLayershell.Bottom;
            case "overlay":
                return WlrLayershell.Overlay;
            case "background":
                return WlrLayershell.Background;
            case "top":
                return WlrLayershell.Top;
            }
        }

        if (!notificationData)
            return WlrLayershell.Top;

        SettingsData.notificationOverlayEnabled;

        const shouldUseOverlay = (SettingsData.notificationOverlayEnabled) || (notificationData.urgency === NotificationUrgency.Critical);

        return shouldUseOverlay ? WlrLayershell.Overlay : WlrLayershell.Top;
    }
    WlrLayershell.exclusiveZone: -1
    WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
    color: "transparent"
    readonly property real contentImplicitWidth: screen ? Math.min(400, Math.max(320, screen.width * 0.23)) : 380
    readonly property real contentImplicitHeight: {
        if (SettingsData.notificationPopupPrivacyMode && !descriptionExpanded)
            return basePopupHeightPrivacy;
        if (!descriptionExpanded)
            return basePopupHeight;
        const bodyTextHeight = bodyText.contentHeight || 0;
        const collapsedBodyHeight = Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2);
        if (bodyTextHeight > collapsedBodyHeight + 2)
            return basePopupHeight + bodyTextHeight - collapsedBodyHeight;
        return basePopupHeight;
    }
    implicitWidth: contentImplicitWidth + (windowShadowPad * 2)
    implicitHeight: contentImplicitHeight + (windowShadowPad * 2)

    Behavior on implicitHeight {
        enabled: !exiting && !_isDestroying
        NumberAnimation {
            id: implicitHeightAnim
            duration: descriptionExpanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration
            easing.type: Easing.BezierSpline
            easing.bezierCurve: Theme.expressiveCurves.emphasized
        }
    }

    onHasValidDataChanged: {
        if (!hasValidData && !exiting && !_isDestroying) {
            forceExit();
        }
    }
    Component.onCompleted: {
        _lastReportedAlignedHeight = Theme.px(implicitHeight, dpr);
        _storedTopMargin = getTopMargin();
        _storedBottomMargin = getBottomMargin();
        if (SettingsData.notificationPopupPrivacyMode)
            descriptionExpanded = false;
        if (hasValidData) {
            Qt.callLater(() => enterX.restart());
        } else {
            forceExit();
        }
    }
    onNotificationDataChanged: {
        if (!_isDestroying) {
            if (SettingsData.notificationPopupPrivacyMode)
                descriptionExpanded = false;
            wrapperConn.target = win.notificationData || null;
            notificationConn.target = (win.notificationData && win.notificationData.notification && win.notificationData.notification.Retainable) || null;
        }
    }
    onEntered: {
        if (!_isDestroying) {
            enterDelay.start();
        }
    }
    Component.onDestruction: {
        _isDestroying = true;
        exitWatchdog.stop();
        if (notificationData && notificationData.timer) {
            notificationData.timer.stop();
        }
    }

    property bool isTopCenter: SettingsData.notificationPopupPosition === -1
    property bool isBottomCenter: SettingsData.notificationPopupPosition === SettingsData.Position.BottomCenter
    property bool isCenterPosition: isTopCenter || isBottomCenter
    readonly property real maxPopupShadowBlurPx: Math.max((Theme.elevationLevel3 && Theme.elevationLevel3.blurPx !== undefined) ? Theme.elevationLevel3.blurPx : 12, (Theme.elevationLevel4 && Theme.elevationLevel4.blurPx !== undefined) ? Theme.elevationLevel4.blurPx : 16)
    readonly property real maxPopupShadowOffsetXPx: Math.max(Math.abs(Theme.elevationOffsetX(Theme.elevationLevel3)), Math.abs(Theme.elevationOffsetX(Theme.elevationLevel4)))
    readonly property real maxPopupShadowOffsetYPx: Math.max(Math.abs(Theme.elevationOffsetY(Theme.elevationLevel3, 6)), Math.abs(Theme.elevationOffsetY(Theme.elevationLevel4, 8)))
    readonly property real windowShadowPad: Theme.elevationEnabled && SettingsData.notificationPopupShadowEnabled ? Theme.snap(Math.max(16, maxPopupShadowBlurPx + Math.max(maxPopupShadowOffsetXPx, maxPopupShadowOffsetYPx) + 8), dpr) : 0

    anchors.top: true
    anchors.left: true
    anchors.bottom: false
    anchors.right: false

    mask: contentInputMask

    Region {
        id: contentInputMask
        item: contentMaskRect
    }

    Item {
        id: contentMaskRect
        visible: false
        x: content.x
        y: content.y
        width: alignedWidth
        height: alignedHeight
    }

    margins {
        top: getWindowTopMargin()
        bottom: 0
        left: getWindowLeftMargin()
        right: 0
    }

    function getBarInfo() {
        if (!screen)
            return {
                topBar: 0,
                bottomBar: 0,
                leftBar: 0,
                rightBar: 0
            };
        return SettingsData.getAdjacentBarInfo(screen, SettingsData.notificationPopupPosition, {
            id: "notification-popup",
            screenPreferences: [screen.name],
            autoHide: false
        });
    }

    function getTopMargin() {
        const popupPos = SettingsData.notificationPopupPosition;
        const isTop = isTopCenter || popupPos === SettingsData.Position.Top || popupPos === SettingsData.Position.Left;
        if (!isTop)
            return 0;

        const barInfo = getBarInfo();
        const base = barInfo.topBar > 0 ? barInfo.topBar : Theme.popupDistance;
        return base + screenY;
    }

    function getBottomMargin() {
        const popupPos = SettingsData.notificationPopupPosition;
        const isBottom = isBottomCenter || popupPos === SettingsData.Position.Bottom || popupPos === SettingsData.Position.Right;
        if (!isBottom)
            return 0;

        const barInfo = getBarInfo();
        const base = barInfo.bottomBar > 0 ? barInfo.bottomBar : Theme.popupDistance;
        return base + screenY;
    }

    function getLeftMargin() {
        if (isCenterPosition)
            return screen ? (screen.width - alignedWidth) / 2 : 0;

        const popupPos = SettingsData.notificationPopupPosition;
        const isLeft = popupPos === SettingsData.Position.Left || popupPos === SettingsData.Position.Bottom;
        if (!isLeft)
            return 0;

        const barInfo = getBarInfo();
        return barInfo.leftBar > 0 ? barInfo.leftBar : Theme.popupDistance;
    }

    function getRightMargin() {
        if (isCenterPosition)
            return 0;

        const popupPos = SettingsData.notificationPopupPosition;
        const isRight = popupPos === SettingsData.Position.Top || popupPos === SettingsData.Position.Right;
        if (!isRight)
            return 0;

        const barInfo = getBarInfo();
        return barInfo.rightBar > 0 ? barInfo.rightBar : Theme.popupDistance;
    }

    function getContentX() {
        if (!screen)
            return 0;

        const popupPos = SettingsData.notificationPopupPosition;
        const barLeft = getLeftMargin();
        const barRight = getRightMargin();

        if (isCenterPosition)
            return Theme.snap((screen.width - alignedWidth) / 2, dpr);
        if (popupPos === SettingsData.Position.Left || popupPos === SettingsData.Position.Bottom)
            return Theme.snap(barLeft, dpr);
        return Theme.snap(screen.width - alignedWidth - barRight, dpr);
    }

    function getContentY() {
        if (!screen)
            return 0;

        const popupPos = SettingsData.notificationPopupPosition;
        const barTop = getTopMargin();
        const barBottom = getBottomMargin();
        const isTop = isTopCenter || popupPos === SettingsData.Position.Top || popupPos === SettingsData.Position.Left;
        if (isTop)
            return Theme.snap(barTop, dpr);
        return Theme.snap(screen.height - alignedHeight - barBottom, dpr);
    }

    function getWindowLeftMargin() {
        if (!screen)
            return 0;
        return Theme.snap(getContentX() - windowShadowPad, dpr);
    }

    function getWindowTopMargin() {
        if (!screen)
            return 0;
        return Theme.snap(getContentY() - windowShadowPad, dpr);
    }

    readonly property bool screenValid: win.screen && !_isDestroying
    readonly property real dpr: screenValid ? CompositorService.getScreenScale(win.screen) : 1
    readonly property real alignedWidth: Theme.px(Math.max(0, implicitWidth - (windowShadowPad * 2)), dpr)
    readonly property real alignedHeight: Theme.px(Math.max(0, implicitHeight - (windowShadowPad * 2)), dpr)

    Item {
        id: content

        x: Theme.snap(windowShadowPad, dpr)
        y: Theme.snap(windowShadowPad, dpr)
        width: alignedWidth
        height: alignedHeight
        visible: !win._finalized
        scale: cardHoverHandler.hovered ? 1.01 : 1.0
        transformOrigin: Item.Center

        Behavior on scale {
            NumberAnimation {
                duration: Theme.shortDuration
                easing.type: Theme.standardEasing
            }
        }

        property real swipeOffset: 0
        readonly property real dismissThreshold: isCenterPosition ? height * 0.4 : width * 0.35
        readonly property real swipeFadeStartRatio: 0.75
        readonly property real swipeTravelDistance: isCenterPosition ? height : width
        readonly property real swipeFadeStartOffset: swipeTravelDistance * swipeFadeStartRatio
        readonly property real swipeFadeDistance: Math.max(1, swipeTravelDistance - swipeFadeStartOffset)
        readonly property bool swipeActive: swipeDragHandler.active
        property bool swipeDismissing: false

        readonly property bool shadowsAllowed: Theme.elevationEnabled && SettingsData.notificationPopupShadowEnabled
        readonly property var elevLevel: cardHoverHandler.hovered ? Theme.elevationLevel4 : Theme.elevationLevel3
        readonly property real cardInset: Theme.snap(4, win.dpr)
        readonly property real shadowRenderPadding: shadowsAllowed ? Theme.snap(Math.max(16, shadowBlurPx + Math.max(Math.abs(shadowOffsetX), Math.abs(shadowOffsetY)) + 8), win.dpr) : 0
        property real shadowBlurPx: shadowsAllowed ? (elevLevel && elevLevel.blurPx !== undefined ? elevLevel.blurPx : 12) : 0
        property real shadowOffsetX: shadowsAllowed ? Theme.elevationOffsetX(elevLevel) : 0
        property real shadowOffsetY: shadowsAllowed ? Theme.elevationOffsetY(elevLevel, 6) : 0

        Behavior on shadowBlurPx {
            NumberAnimation {
                duration: Theme.shortDuration
                easing.type: Theme.standardEasing
            }
        }

        Behavior on shadowOffsetX {
            NumberAnimation {
                duration: Theme.shortDuration
                easing.type: Theme.standardEasing
            }
        }

        Behavior on shadowOffsetY {
            NumberAnimation {
                duration: Theme.shortDuration
                easing.type: Theme.standardEasing
            }
        }

        ElevationShadow {
            id: bgShadowLayer
            anchors.fill: parent
            anchors.margins: -content.shadowRenderPadding
            level: content.elevLevel
            fallbackOffset: 6
            shadowBlurPx: content.shadowBlurPx
            shadowOffsetX: content.shadowOffsetX
            shadowOffsetY: content.shadowOffsetY
            shadowColor: content.shadowsAllowed && content.elevLevel ? Theme.elevationShadowColor(content.elevLevel) : "transparent"
            shadowEnabled: !win._isDestroying && win.screenValid && content.shadowsAllowed
            layer.textureSize: Qt.size(Math.round(width * win.dpr), Math.round(height * win.dpr))
            layer.textureMirroring: ShaderEffectSource.MirrorVertically

            sourceRect.anchors.fill: undefined
            sourceRect.x: content.shadowRenderPadding + content.cardInset
            sourceRect.y: content.shadowRenderPadding + content.cardInset
            sourceRect.width: Math.max(0, content.width - (content.cardInset * 2))
            sourceRect.height: Math.max(0, content.height - (content.cardInset * 2))
            sourceRect.radius: Theme.cornerRadius
            sourceRect.color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
            sourceRect.border.color: notificationData && notificationData.urgency === NotificationUrgency.Critical ? Theme.withAlpha(Theme.primary, 0.3) : Theme.withAlpha(Theme.outline, 0.08)
            sourceRect.border.width: notificationData && notificationData.urgency === NotificationUrgency.Critical ? 2 : 0

            Rectangle {
                x: bgShadowLayer.sourceRect.x
                y: bgShadowLayer.sourceRect.y
                width: bgShadowLayer.sourceRect.width
                height: bgShadowLayer.sourceRect.height
                radius: bgShadowLayer.sourceRect.radius
                visible: notificationData && notificationData.urgency === NotificationUrgency.Critical
                opacity: 1
                clip: true

                gradient: Gradient {
                    orientation: Gradient.Horizontal

                    GradientStop {
                        position: 0
                        color: Theme.primary
                    }

                    GradientStop {
                        position: 0.02
                        color: Theme.primary
                    }

                    GradientStop {
                        position: 0.021
                        color: "transparent"
                    }
                }
            }
        }

        Rectangle {
            anchors.fill: parent
            anchors.margins: content.cardInset
            radius: Theme.cornerRadius
            color: "transparent"
            border.color: BlurService.borderColor
            border.width: BlurService.borderWidth
            z: 100
        }

        Item {
            id: backgroundContainer
            anchors.fill: parent
            anchors.margins: content.cardInset
            clip: true

            HoverHandler {
                id: cardHoverHandler
            }

            Connections {
                target: cardHoverHandler
                function onHoveredChanged() {
                    if (!notificationData || win.exiting || win._isDestroying)
                        return;
                    if (cardHoverHandler.hovered) {
                        if (notificationData.timer)
                            notificationData.timer.stop();
                    } else if (notificationData.popup && notificationData.timer) {
                        notificationData.timer.restart();
                    }
                }
            }

            LayoutMirroring.enabled: I18n.isRtl
            LayoutMirroring.childrenInherit: true

            Item {
                id: notificationContent

                readonly property real expandedTextHeight: bodyText.contentHeight || 0
                readonly property real collapsedBodyHeight: Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2)
                readonly property real effectiveCollapsedHeight: (SettingsData.notificationPopupPrivacyMode && !descriptionExpanded) ? win.privacyCollapsedContentHeight : win.collapsedContentHeight
                readonly property real extraHeight: (descriptionExpanded && expandedTextHeight > collapsedBodyHeight + 2) ? (expandedTextHeight - collapsedBodyHeight) : 0

                anchors.top: parent.top
                anchors.left: parent.left
                anchors.right: parent.right
                anchors.topMargin: cardPadding
                anchors.leftMargin: Theme.spacingL
                anchors.rightMargin: Theme.spacingL + Theme.notificationHoverRevealMargin
                height: effectiveCollapsedHeight + extraHeight
                clip: SettingsData.notificationPopupPrivacyMode && !descriptionExpanded

                DankCircularImage {
                    id: iconContainer

                    readonly property string rawImage: notificationData?.image || ""
                    readonly property string iconFromImage: {
                        if (rawImage.startsWith("image://icon/"))
                            return rawImage.substring(13);
                        return "";
                    }
                    readonly property bool imageHasSpecialPrefix: {
                        const icon = iconFromImage;
                        return icon.startsWith("material:") || icon.startsWith("svg:") || icon.startsWith("unicode:") || icon.startsWith("image:");
                    }
                    readonly property bool hasNotificationImage: rawImage !== "" && !rawImage.startsWith("image://icon/")
                    readonly property bool needsImagePersist: hasNotificationImage && rawImage.startsWith("image://qsimage/") && !notificationData.persistedImagePath

                    width: popupIconSize
                    height: popupIconSize
                    anchors.left: parent.left
                    anchors.top: parent.top
                    anchors.topMargin: {
                        if (SettingsData.notificationPopupPrivacyMode && !descriptionExpanded) {
                            const headerSummary = Theme.fontSizeSmall * 1.2 + Theme.fontSizeMedium * 1.2;
                            return Math.max(0, headerSummary / 2 - popupIconSize / 2);
                        }
                        if (descriptionExpanded)
                            return Math.max(0, Theme.fontSizeSmall * 1.2 + (Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2)) / 2 - popupIconSize / 2);
                        return Math.max(0, Theme.fontSizeSmall * 1.2 + (textContainer.height - Theme.fontSizeSmall * 1.2) / 2 - popupIconSize / 2);
                    }

                    imageSource: {
                        if (!notificationData)
                            return "";
                        if (hasNotificationImage)
                            return notificationData.cleanImage || "";
                        if (imageHasSpecialPrefix)
                            return "";
                        const appIcon = notificationData.appIcon;
                        if (!appIcon)
                            return iconFromImage ? Paths.resolveIconUrl(iconFromImage) : "";
                        if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://") || appIcon.includes("/"))
                            return appIcon;
                        if (appIcon.startsWith("material:") || appIcon.startsWith("svg:") || appIcon.startsWith("unicode:") || appIcon.startsWith("image:"))
                            return "";
                        return Paths.resolveIconPath(appIcon);
                    }

                    hasImage: hasNotificationImage
                    fallbackIcon: {
                        if (imageHasSpecialPrefix)
                            return iconFromImage;
                        return notificationData?.appIcon || iconFromImage || "";
                    }
                    fallbackText: {
                        const appName = notificationData?.appName || "?";
                        return appName.charAt(0).toUpperCase();
                    }

                    onImageStatusChanged: {
                        if (imageStatus === Image.Ready && needsImagePersist) {
                            const cachePath = NotificationService.getImageCachePath(notificationData);
                            saveImageToFile(cachePath);
                        }
                    }

                    onImageSaved: filePath => {
                        if (!notificationData)
                            return;
                        notificationData.persistedImagePath = filePath;
                        const wrapperId = notificationData.notification?.id?.toString() || "";
                        if (wrapperId)
                            NotificationService.updateHistoryImage(wrapperId, filePath);
                    }
                }

                Column {
                    id: textContainer

                    anchors.left: iconContainer.right
                    anchors.leftMargin: Theme.spacingM
                    anchors.right: parent.right
                    anchors.top: parent.top
                    spacing: Theme.notificationContentSpacing

                    Row {
                        id: headerRow
                        width: parent.width
                        spacing: Theme.spacingXS
                        visible: headerAppNameText.text.length > 0 || headerTimeText.text.length > 0

                        StyledText {
                            id: headerAppNameText
                            text: notificationData ? (notificationData.appName || "") : ""
                            color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
                            font.pixelSize: Theme.fontSizeSmall
                            font.weight: Font.Normal
                            elide: Text.ElideRight
                            maximumLineCount: 1
                            width: Math.min(implicitWidth, parent.width - headerSeparator.implicitWidth - headerTimeText.implicitWidth - parent.spacing * 2)
                        }

                        StyledText {
                            id: headerSeparator
                            text: (headerAppNameText.text.length > 0 && headerTimeText.text.length > 0) ? " • " : ""
                            color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
                            font.pixelSize: Theme.fontSizeSmall
                            font.weight: Font.Normal
                        }

                        StyledText {
                            id: headerTimeText
                            text: notificationData ? (notificationData.timeStr || "") : ""
                            color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
                            font.pixelSize: Theme.fontSizeSmall
                            font.weight: Font.Normal
                        }
                    }

                    StyledText {
                        text: notificationData ? (notificationData.summary || "") : ""
                        color: Theme.surfaceText
                        font.pixelSize: Theme.fontSizeMedium
                        font.weight: Font.Medium
                        width: parent.width
                        elide: Text.ElideRight
                        horizontalAlignment: Text.AlignLeft
                        maximumLineCount: 1
                        visible: text.length > 0
                    }

                    StyledText {
                        id: bodyText
                        property bool hasMoreText: truncated

                        text: notificationData ? (notificationData.htmlBody || "") : ""
                        color: Theme.surfaceVariantText
                        font.pixelSize: Theme.fontSizeSmall
                        width: parent.width
                        elide: descriptionExpanded ? Text.ElideNone : Text.ElideRight
                        horizontalAlignment: Text.AlignLeft
                        maximumLineCount: descriptionExpanded ? -1 : (compactMode ? 1 : 2)
                        wrapMode: Text.WrapAtWordBoundaryOrAnywhere
                        visible: text.length > 0
                        opacity: (SettingsData.notificationPopupPrivacyMode && !descriptionExpanded) ? 0 : 1
                        linkColor: Theme.primary
                        onLinkActivated: link => Qt.openUrlExternally(link)

                        MouseArea {
                            anchors.fill: parent
                            cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : (bodyText.hasMoreText || descriptionExpanded) ? Qt.PointingHandCursor : Qt.ArrowCursor

                            onClicked: mouse => {
                                if (!parent.hoveredLink && (bodyText.hasMoreText || descriptionExpanded))
                                    win.descriptionExpanded = !win.descriptionExpanded;
                            }

                            propagateComposedEvents: true
                            onPressed: mouse => {
                                if (parent.hoveredLink)
                                    mouse.accepted = false;
                            }
                            onReleased: mouse => {
                                if (parent.hoveredLink)
                                    mouse.accepted = false;
                            }
                        }
                    }

                    StyledText {
                        text: I18n.tr("Message Content", "notification privacy mode placeholder")
                        color: Theme.surfaceVariantText
                        font.pixelSize: Theme.fontSizeSmall
                        width: parent.width
                        visible: SettingsData.notificationPopupPrivacyMode && !descriptionExpanded && win.hasExpandableBody
                    }
                }
            }

            DankActionButton {
                id: closeButton

                anchors.right: parent.right
                anchors.top: parent.top
                anchors.topMargin: cardPadding
                anchors.rightMargin: Theme.spacingL
                iconName: "close"
                iconSize: compactMode ? 14 : 16
                buttonSize: compactMode ? 20 : 24
                z: 15

                onClicked: {
                    if (notificationData && !win.exiting)
                        notificationData.popup = false;
                }
            }

            DankActionButton {
                id: expandButton

                anchors.right: closeButton.left
                anchors.rightMargin: Theme.spacingXS
                anchors.top: parent.top
                anchors.topMargin: cardPadding
                iconName: descriptionExpanded ? "expand_less" : "expand_more"
                iconSize: compactMode ? 14 : 16
                buttonSize: compactMode ? 20 : 24
                z: 15
                visible: SettingsData.notificationPopupPrivacyMode && win.hasExpandableBody

                onClicked: {
                    if (win.hasExpandableBody)
                        win.descriptionExpanded = !win.descriptionExpanded;
                }
            }

            Row {
                visible: cardHoverHandler.hovered
                opacity: visible ? 1 : 0
                anchors.right: clearButton.visible ? clearButton.left : parent.right
                anchors.rightMargin: clearButton.visible ? contentSpacing : Theme.spacingL
                anchors.top: notificationContent.bottom
                anchors.topMargin: contentSpacing
                spacing: contentSpacing
                z: 20

                Behavior on opacity {
                    NumberAnimation {
                        duration: Theme.shortDuration
                        easing.type: Theme.standardEasing
                    }
                }

                Repeater {
                    model: notificationData ? (notificationData.actions || []) : []

                    Rectangle {
                        property bool isHovered: false

                        width: Math.max(actionText.implicitWidth + Theme.spacingM, Theme.notificationActionMinWidth)
                        height: actionButtonHeight
                        radius: Theme.notificationButtonCornerRadius
                        color: isHovered ? Theme.withAlpha(Theme.primary, Theme.stateLayerHover) : "transparent"

                        StyledText {
                            id: actionText

                            text: modelData.text || "Open"
                            color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText
                            font.pixelSize: Theme.fontSizeSmall
                            font.weight: Font.Medium
                            anchors.centerIn: parent
                            elide: Text.ElideRight
                        }

                        MouseArea {
                            anchors.fill: parent
                            hoverEnabled: true
                            cursorShape: Qt.PointingHandCursor
                            acceptedButtons: Qt.LeftButton
                            onEntered: parent.isHovered = true
                            onExited: parent.isHovered = false
                            onClicked: {
                                if (modelData && modelData.invoke)
                                    modelData.invoke();
                                if (notificationData && !win.exiting)
                                    notificationData.popup = false;
                            }
                        }
                    }
                }
            }

            Rectangle {
                id: clearButton

                property bool isHovered: false
                readonly property int actionCount: notificationData ? (notificationData.actions || []).length : 0

                visible: actionCount < 3 && cardHoverHandler.hovered
                opacity: visible ? 1 : 0
                Behavior on opacity {
                    NumberAnimation {
                        duration: Theme.shortDuration
                        easing.type: Theme.standardEasing
                    }
                }
                anchors.right: parent.right
                anchors.rightMargin: Theme.spacingL
                anchors.top: notificationContent.bottom
                anchors.topMargin: contentSpacing
                width: Math.max(clearTextLabel.implicitWidth + Theme.spacingM, Theme.notificationActionMinWidth)
                height: actionButtonHeight
                radius: Theme.notificationButtonCornerRadius
                color: isHovered ? Theme.withAlpha(Theme.primary, Theme.stateLayerHover) : "transparent"
                z: 20

                StyledText {
                    id: clearTextLabel

                    text: win.clearText
                    color: clearButton.isHovered ? Theme.primary : Theme.surfaceVariantText
                    font.pixelSize: Theme.fontSizeSmall
                    font.weight: Font.Medium
                    anchors.centerIn: parent
                }

                MouseArea {
                    anchors.fill: parent
                    hoverEnabled: true
                    cursorShape: Qt.PointingHandCursor
                    acceptedButtons: Qt.LeftButton
                    onEntered: clearButton.isHovered = true
                    onExited: clearButton.isHovered = false
                    onClicked: {
                        if (notificationData && !win.exiting)
                            NotificationService.dismissNotification(notificationData);
                    }
                }
            }

            MouseArea {
                id: cardHoverArea

                anchors.fill: parent
                hoverEnabled: true
                acceptedButtons: Qt.LeftButton | Qt.RightButton
                cursorShape: Qt.PointingHandCursor
                propagateComposedEvents: true
                z: -1
                onClicked: mouse => {
                    if (!notificationData || win.exiting)
                        return;
                    if (mouse.button === Qt.RightButton) {
                        popupContextMenu.popup();
                    } else if (mouse.button === Qt.LeftButton) {
                        const canExpand = bodyText.hasMoreText || win.descriptionExpanded || (SettingsData.notificationPopupPrivacyMode && win.hasExpandableBody);
                        if (canExpand) {
                            win.descriptionExpanded = !win.descriptionExpanded;
                        } else if (notificationData.actions && notificationData.actions.length > 0) {
                            notificationData.actions[0].invoke();
                            NotificationService.dismissNotification(notificationData);
                        } else {
                            notificationData.popup = false;
                        }
                    }
                }
            }
        }

        DragHandler {
            id: swipeDragHandler
            target: null
            xAxis.enabled: !isCenterPosition
            yAxis.enabled: isCenterPosition

            onActiveChanged: {
                if (active || win.exiting || content.swipeDismissing)
                    return;

                if (Math.abs(content.swipeOffset) > content.dismissThreshold) {
                    content.swipeDismissing = true;
                    swipeDismissAnim.start();
                } else {
                    content.swipeOffset = 0;
                }
            }

            onTranslationChanged: {
                if (win.exiting)
                    return;

                const raw = isCenterPosition ? translation.y : translation.x;
                if (isTopCenter) {
                    content.swipeOffset = Math.min(0, raw);
                } else if (isBottomCenter) {
                    content.swipeOffset = Math.max(0, raw);
                } else {
                    const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
                    content.swipeOffset = isLeft ? Math.min(0, raw) : Math.max(0, raw);
                }
            }
        }

        opacity: {
            const swipeAmount = Math.abs(content.swipeOffset);
            if (swipeAmount <= content.swipeFadeStartOffset)
                return 1;
            const fadeProgress = (swipeAmount - content.swipeFadeStartOffset) / content.swipeFadeDistance;
            return Math.max(0, 1 - fadeProgress);
        }

        Behavior on opacity {
            enabled: !content.swipeActive
            NumberAnimation {
                duration: Theme.shortDuration
            }
        }

        Behavior on swipeOffset {
            enabled: !content.swipeActive && !content.swipeDismissing
            NumberAnimation {
                duration: Theme.notificationExitDuration
                easing.type: Theme.standardEasing
            }
        }

        NumberAnimation {
            id: swipeDismissAnim
            target: content
            property: "swipeOffset"
            to: isTopCenter ? -content.height : isBottomCenter ? content.height : (SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom ? -content.width : content.width)
            duration: Theme.notificationExitDuration
            easing.type: Easing.OutCubic
            onStopped: {
                NotificationService.dismissNotification(notificationData);
                win.forceExit();
            }
        }

        transform: [
            Translate {
                id: swipeTx
                x: isCenterPosition ? 0 : content.swipeOffset
                y: isCenterPosition ? content.swipeOffset : 0
            },
            Translate {
                id: tx
                x: {
                    if (isCenterPosition)
                        return 0;
                    const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
                    return isLeft ? -Anims.slidePx : Anims.slidePx;
                }
                y: isTopCenter ? -Anims.slidePx : isBottomCenter ? Anims.slidePx : 0
            }
        ]
    }

    NumberAnimation {
        id: enterX

        target: tx
        property: isCenterPosition ? "y" : "x"
        from: {
            if (isTopCenter)
                return -Anims.slidePx;
            if (isBottomCenter)
                return Anims.slidePx;
            const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
            return isLeft ? -Anims.slidePx : Anims.slidePx;
        }
        to: 0
        duration: Theme.notificationEnterDuration
        easing.type: Easing.BezierSpline
        easing.bezierCurve: isCenterPosition ? Theme.expressiveCurves.standardDecel : Theme.expressiveCurves.emphasizedDecel
        onStopped: {
            if (!win.exiting && !win._isDestroying) {
                if (isCenterPosition) {
                    if (Math.abs(tx.y) < 0.5)
                        win.entered();
                } else {
                    if (Math.abs(tx.x) < 0.5)
                        win.entered();
                }
            }
        }
    }

    ParallelAnimation {
        id: exitAnim

        onStopped: finalizeExit("animStopped")

        PropertyAnimation {
            target: tx
            property: isCenterPosition ? "y" : "x"
            from: 0
            to: {
                if (isTopCenter)
                    return -Anims.slidePx;
                if (isBottomCenter)
                    return Anims.slidePx;
                const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
                return isLeft ? -Anims.slidePx : Anims.slidePx;
            }
            duration: Theme.notificationExitDuration
            easing.type: Easing.BezierSpline
            easing.bezierCurve: Theme.expressiveCurves.emphasizedAccel
        }

        NumberAnimation {
            target: content
            property: "opacity"
            from: 1
            to: 0
            duration: Theme.notificationExitDuration
            easing.type: Easing.BezierSpline
            easing.bezierCurve: Theme.expressiveCurves.standardAccel
        }

        NumberAnimation {
            target: content
            property: "scale"
            from: 1
            to: 0.98
            duration: Theme.notificationExitDuration
            easing.type: Easing.BezierSpline
            easing.bezierCurve: Theme.expressiveCurves.emphasizedAccel
        }
    }

    Connections {
        id: wrapperConn

        function onPopupChanged() {
            if (!win.notificationData || win._isDestroying)
                return;
            if (!win.notificationData.popup && !win.exiting)
                startExit();
        }

        target: win.notificationData || null
        ignoreUnknownSignals: true
        enabled: !win._isDestroying
    }

    Connections {
        id: notificationConn

        function onDropped() {
            if (!win._isDestroying && !win.exiting)
                forceExit();
        }

        target: (win.notificationData && win.notificationData.notification && win.notificationData.notification.Retainable) || null
        ignoreUnknownSignals: true
        enabled: !win._isDestroying
    }

    Timer {
        id: enterDelay

        interval: 160
        repeat: false
        onTriggered: {
            if (notificationData && notificationData.timer && !exiting && !_isDestroying)
                notificationData.timer.start();
        }
    }

    Timer {
        id: exitWatchdog

        interval: 600
        repeat: false
        onTriggered: finalizeExit("watchdog")
    }

    Behavior on screenY {
        id: screenYAnim

        enabled: !exiting && !_isDestroying

        NumberAnimation {
            duration: Theme.shortDuration
            easing.type: Easing.BezierSpline
            easing.bezierCurve: Theme.expressiveCurves.standardDecel
        }
    }

    Menu {
        id: popupContextMenu
        width: 220
        contentHeight: 130
        margins: -1
        popupType: Popup.Window
        closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside

        background: Rectangle {
            color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
            radius: Theme.cornerRadius
            border.width: 0
            border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
        }

        MenuItem {
            id: setNotificationRulesItem
            text: I18n.tr("Set notification rules")

            contentItem: StyledText {
                text: parent.text
                font.pixelSize: Theme.fontSizeSmall
                color: Theme.surfaceText
                leftPadding: Theme.spacingS
                verticalAlignment: Text.AlignVCenter
            }

            background: Rectangle {
                color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
                radius: Theme.cornerRadius / 2
            }

            onTriggered: {
                const appName = notificationData?.appName || "";
                const desktopEntry = notificationData?.desktopEntry || "";
                SettingsData.addNotificationRuleForNotification(appName, desktopEntry);
                PopoutService.openSettingsWithTab("notifications");
            }
        }

        MenuItem {
            id: muteUnmuteItem
            readonly property bool isMuted: SettingsData.isAppMuted(notificationData?.appName || "", notificationData?.desktopEntry || "")
            text: isMuted ? I18n.tr("Unmute popups for %1").arg(notificationData?.appName || I18n.tr("this app")) : I18n.tr("Mute popups for %1").arg(notificationData?.appName || I18n.tr("this app"))

            contentItem: StyledText {
                text: parent.text
                font.pixelSize: Theme.fontSizeSmall
                color: Theme.surfaceText
                leftPadding: Theme.spacingS
                verticalAlignment: Text.AlignVCenter
            }

            background: Rectangle {
                color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
                radius: Theme.cornerRadius / 2
            }

            onTriggered: {
                const appName = notificationData?.appName || "";
                const desktopEntry = notificationData?.desktopEntry || "";
                if (isMuted) {
                    SettingsData.removeMuteRuleForApp(appName, desktopEntry);
                } else {
                    SettingsData.addMuteRuleForApp(appName, desktopEntry);
                    if (notificationData && !exiting)
                        NotificationService.dismissNotification(notificationData);
                }
            }
        }

        MenuItem {
            text: I18n.tr("Dismiss")

            contentItem: StyledText {
                text: parent.text
                font.pixelSize: Theme.fontSizeSmall
                color: Theme.surfaceText
                leftPadding: Theme.spacingS
                verticalAlignment: Text.AlignVCenter
            }

            background: Rectangle {
                color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
                radius: Theme.cornerRadius / 2
            }

            onTriggered: {
                if (notificationData && !exiting)
                    NotificationService.dismissNotification(notificationData);
            }
        }
    }
}
