// SPDX-FileCopyrightText: 2015 Marco Martin <mart@kde.org>
// SPDX-FileCopyrightText: 2021-2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later

import QtQuick 2.4
import QtQuick.Layouts 1.1
import QtQuick.Window 2.15
import QtQuick.Shapes 1.8

import org.kde.kirigami as Kirigami

import org.kde.taskmanager 0.1 as TaskManager
import org.kde.plasma.plasmoid 2.0
import org.kde.plasma.core as PlasmaCore
import org.kde.kquickcontrolsaddons 2.0

import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
import org.kde.plasma.private.mobileshell.windowplugin as WindowPlugin
import org.kde.plasma.private.mobileshell.state as MobileShellState
import org.kde.plasma.workspace.keyboardlayout as Keyboards
import org.kde.layershell 1.0 as LayerShell

ContainmentItem {
    id: root
    Plasmoid.backgroundHints: PlasmaCore.Types.NoBackground
    Plasmoid.status: PlasmaCore.Types.PassiveStatus // ensure that the panel never takes focus away from the running app

    // filled in by the shell (Panel.qml) with the plasma-workspace PanelView
    property var panel: null
    onPanelChanged: {
        setWindowProperties()
    }

    MobileShell.HapticsEffect {
        id: haptics
    }

    readonly property bool inLandscape: MobileShell.Constants.navigationPanelOnSide(Screen.width, Screen.height)

    readonly property bool gamingMode: ShellSettings.Settings.gamingModeEnabled
    readonly property int navigationAnimationDuration: MobileShell.Motion.duration(MobileShell.Motion.SpatialDefault)
    readonly property int resetAnimationDuration: Math.round(MobileShell.Motion.duration(MobileShell.Motion.SpatialDefault) * 1.5)

    readonly property real navigationPanelHeight: gamingMode ? 0 : MobileShell.Constants.navigationPanelThickness
    readonly property real convergenceWorkspaceFrameThickness: ShellSettings.Settings.convergenceModeEnabled && !gamingMode
        ? MobileShell.Constants.convergenceWorkspaceFrameThickness
        : 0
    onNavigationPanelHeightChanged: setWindowProperties()

    readonly property real intendedWindowThickness: navigationPanelHeight
    readonly property real intendedWindowLength: inLandscape ? Screen.height : Screen.width
    readonly property real intendedWindowOffset: (inLandscape && !gamingMode) ? MobileShell.Constants.topPanelHeight : 0; // offset for top panel
    readonly property int intendedWindowLocation: inLandscape ? PlasmaCore.Types.RightEdge : PlasmaCore.Types.BottomEdge

    onIntendedWindowLengthChanged: maximizeTimer.restart() // ensure it always takes up the full length of the screen
    onIntendedWindowLocationChanged: setPanelLocationTimer.restart()
    onIntendedWindowOffsetChanged: {
        if (root.panel) {
            root.panel.offset = intendedWindowOffset;
        }
    }

    // HACK: the entire shell seems to crash sometimes if this is applied immediately after a display change (ex. screen rotation)
    // see https://invent.kde.org/plasma/plasma-mobile/-/issues/321
    Timer {
        id: setPanelLocationTimer
        running: false
        interval: 100
        onTriggered: {
            root.panel.location = intendedWindowLocation;
        }
    }

    // use a timer so we don't have to maximize for every single pixel
    // - improves performance if the shell is run in a window, and can be resized
    Timer {
        id: maximizeTimer
        running: false
        interval: 100
        onTriggered: {
            // maximize first, then we can apply offsets (otherwise they are overridden)
            root.panel.maximize();
            root.panel.offset = intendedWindowOffset;
        }
    }


    function setWindowProperties() {
        if (root.panel) {
            root.panel.floating = false;
            root.panel.maximize(); // maximize first, then we can apply offsets (otherwise they are overridden)
            root.panel.offset = intendedWindowOffset;
            root.panel.thickness = navigationPanelHeight;
            root.panel.location = intendedWindowLocation;
            root.panel.visibilityMode = (ShellSettings.Settings.autoHidePanelsEnabled || ShellSettings.Settings.convergenceModeEnabled) ? 3 : 0;
            MobileShell.ShellUtil.setWindowLayer(root.panel, LayerShell.Window.LayerOverlay);
            root.updateTouchArea();
        }
    }

    // update the touch area when hidden to minimize the space the panel takes for touch input
    function updateTouchArea() {
        const hiddenTouchAreaThickness = Kirigami.Units.gridUnit;

        if (navigationPanel.state == "hidden") {
            if (inLandscape) {
                MobileShell.ShellUtil.setInputRegion(root.panel, Qt.rect(root.panel.width - hiddenTouchAreaThickness, 0, hiddenTouchAreaThickness, root.panel.height));
            } else {
                MobileShell.ShellUtil.setInputRegion(root.panel, Qt.rect(0, root.panel.height - hiddenTouchAreaThickness, root.panel.width, hiddenTouchAreaThickness));
            }
        } else {
            MobileShell.ShellUtil.setInputRegion(root.panel, Qt.rect(0, 0, 0, 0));
        }
    }

    Connections {
        target: root.panel

        // HACK: There seems to be some component that overrides our initial bindings for the panel,
        //   which is particularly problematic on first start (since the panel is misplaced)
        // - We set an event to override any attempts to override our bindings.
        function onLocationChanged() {
            if (root.panel.location !== root.intendedWindowLocation) {
                root.setWindowProperties();
            }
        }

        function onThicknessChanged() {
            if (root.panel.thickness !== root.intendedWindowThickness) {
                root.setWindowProperties();
            }
        }
    }

    Connections {
        target: ShellSettings.Settings

        function onAutoHidePanelsEnabledChanged() {
            root.setWindowProperties();
        }

        function onConvergenceModeEnabledChanged() {
            root.setWindowProperties();
        }

        function onGamingModeEnabledChanged() {
            root.setWindowProperties();
            navigationPanel.offset = ShellSettings.Settings.gamingModeEnabled ? MobileShell.Constants.navigationPanelThickness : 0;
        }
    }

    Component.onCompleted: setWindowProperties();

    // Invisible layer-shell surface that reserves screen space for the dock
    // in convergence mode.  Sits at LayerBottom (below windows, above the
    // desktop) with an exclusive zone equal to the dock height so that KWin
    // shrinks MaximizeArea accordingly.  Input-transparent so clicks fall
    // through to the homescreen dock underneath.
    // When auto-hide is enabled and a window is maximized the surface is
    // destroyed so the strut is removed and the window reclaims the full
    // screen.  Dynamic exclusive-zone changes alone are not enough because
    // the contentless surface never repaints, so the Wayland commit that
    // would apply the new zone never happens.
    Window {
        id: dockSpaceReserver
        visible: ShellSettings.Settings.convergenceModeEnabled
                 && !ShellSettings.Settings.gamingModeEnabled
                 && !(ShellSettings.Settings.autoHidePanelsEnabled
                      && windowMaximizedTracker.showingWindow)
        color: "transparent"
        flags: Qt.FramelessWindowHint | Qt.WindowTransparentForInput
        // height is set by layer-shell anchoring; provide a fallback.
        height: MobileShell.Constants.convergenceDockHeight + root.convergenceWorkspaceFrameThickness
        width: 1 // layer-shell stretches it via AnchorLeft|AnchorRight

        LayerShell.Window.scope: "dock-space"
        LayerShell.Window.layer: LayerShell.Window.LayerBottom
        LayerShell.Window.anchors: LayerShell.Window.AnchorBottom | LayerShell.Window.AnchorLeft | LayerShell.Window.AnchorRight
        LayerShell.Window.exclusionZone: MobileShell.Constants.convergenceDockHeight + root.convergenceWorkspaceFrameThickness
        LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone
    }

    Window {
        id: leftWorkspaceFrameReserver
        visible: root.convergenceWorkspaceFrameThickness > 0
        color: "transparent"
        flags: Qt.FramelessWindowHint | Qt.WindowTransparentForInput
        width: Math.max(1, root.convergenceWorkspaceFrameThickness)
        height: 1

        LayerShell.Window.scope: "workspace-frame-left"
        LayerShell.Window.layer: LayerShell.Window.LayerBottom
        LayerShell.Window.anchors: LayerShell.Window.AnchorLeft | LayerShell.Window.AnchorTop | LayerShell.Window.AnchorBottom
        LayerShell.Window.exclusionZone: Math.max(1, root.convergenceWorkspaceFrameThickness)
        LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone
    }

    Window {
        id: rightWorkspaceFrameReserver
        visible: root.convergenceWorkspaceFrameThickness > 0
        color: "transparent"
        flags: Qt.FramelessWindowHint | Qt.WindowTransparentForInput
        width: Math.max(1, root.convergenceWorkspaceFrameThickness)
        height: 1

        LayerShell.Window.scope: "workspace-frame-right"
        LayerShell.Window.layer: LayerShell.Window.LayerBottom
        LayerShell.Window.anchors: LayerShell.Window.AnchorRight | LayerShell.Window.AnchorTop | LayerShell.Window.AnchorBottom
        LayerShell.Window.exclusionZone: Math.max(1, root.convergenceWorkspaceFrameThickness)
        LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone
    }

    // only opaque if there are no maximized windows on this screen
    readonly property bool showingStartupFeedback: MobileShellState.ShellDBusObject.startupFeedbackModel.activeWindowIsStartupFeedback && startupFeedbackColorAnimation.visible && windowMaximizedTracker.windowCount === 1
    readonly property bool opaqueBar: {
        if (Keyboards.KWinVirtualKeyboard.visible) {
            return true;
        }
        return (windowMaximizedTracker.showingWindow || isCurrentWindowFullscreen) && !showingStartupFeedback
    }
    readonly property alias isCurrentWindowFullscreen: windowMaximizedTracker.isCurrentWindowFullscreen
    readonly property bool fullscreen: ShellSettings.Settings.gamingModeEnabled
                                     || isCurrentWindowFullscreen
                                     || (ShellSettings.Settings.autoHidePanelsEnabled && opaqueBar)

    WindowPlugin.WindowMaximizedTracker {
        id: windowMaximizedTracker
        screenGeometry: Plasmoid.containment.screenGeometry

        onShowingWindowChanged: {
            if (windowMaximizedTracker.showingWindow && MobileShellState.ShellDBusClient.isTaskSwitcherVisible && (ShellSettings.Settings.autoHidePanelsEnabled || fullscreen)) {
                navigationPanel.offset = root.navigationPanelHeight;
            }
        }
    }

    MobileShell.StartupFeedbackPanelFill {
        id: startupFeedbackColorAnimation
        anchors.top: parent.top
        anchors.left: parent.left
        anchors.right: parent.right

        fullHeight: root.height
        screen: Plasmoid.screen
        maximizedTracker: windowMaximizedTracker

        visible: !root.fullscreen
    }

    Item {
        id: navigationPanel
        visible: !ShellSettings.Settings.gamingModeEnabled
        anchors.fill: parent

        property real offset: 0

        Component {
            id: navigationPanelComponent

            NavigationPanelComponent {
                isVertical: root.inLandscape
                opaqueBar: root.opaqueBar
                forcedComplementary: !opaqueBar && !startupFeedbackColorAnimation.isShowing

                transform: [
                    Translate {
                        y: inLandscape ? 0 : navigationPanel.offset
                        x: inLandscape ? navigationPanel.offset : 0
                    }
                ]
            }
        }

        Component {
            id: gesturePanelComponent

            GesturePanelComponent {
                opaqueBar: root.opaqueBar

                Kirigami.Theme.inherit: false
                Kirigami.Theme.colorSet: (!opaqueBar && !startupFeedbackColorAnimation.isShowing) ? Kirigami.Theme.Complementary : Kirigami.Theme.Window

                transform: [
                    Translate {
                        y: inLandscape ? 0 : navigationPanel.offset
                        x: inLandscape ? navigationPanel.offset : 0
                    }
                ]
            }
        }

        // load appropriate system navigation component
        // In convergence mode, always use the button navigation panel (gesture-only mode is hostile to mouse/keyboard)
        Loader {
            anchors.fill: parent
            sourceComponent: (ShellSettings.Settings.navigationPanelEnabled || ShellSettings.Settings.convergenceModeEnabled) ? navigationPanelComponent : gesturePanelComponent
        }

        state: MobileShellState.ShellDBusClient.panelState
        onStateChanged: {
            if (navigationPanel.state != "hidden") {
                root.setWindowProperties();
            }
        }

        states: [
            State {
                name: "default"
                PropertyChanges {
                    target: navigationPanel; offset: 0
                }
            },
            State {
                name: "visible"
                PropertyChanges {
                    target: navigationPanel; offset: 0
                }
            },
            State {
                name: "hidden"
                PropertyChanges {
                    target: navigationPanel; offset: root.navigationPanelHeight
                }
            }
        ]

        transitions: Transition {
            SequentialAnimation {
                ParallelAnimation {
                    MobileShell.MotionNumberAnimation {
                        properties: "offset"
                        type: navigationPanel.state === "hidden" ? MobileShell.Motion.EmphasizedAccel : MobileShell.Motion.SpatialDefault
                        duration: root.navigationAnimationDuration
                    }
                }
                ScriptAction {
                    script: {
                        root.setWindowProperties();
                    }
                }
            }
        }
    }

    MobileShell.SwipeArea {
        id: swipeArea
        mode: inLandscape ? MobileShell.SwipeArea.HorizontalOnly : MobileShell.SwipeArea.VerticalOnly
        anchors.fill: navigationPanel
        enabled: navigationPanel.state == "hidden"

        function startSwipeWithPoint(point) {
            root.setWindowProperties();
            resetAn.stop();
            dragEffect.startPoint = inLandscape ? point.y - Screen.height / 2 : point.x - Screen.width / 2;
            dragEffect.sidePoint = 0
            dragEffect.offsetPoint = 0;
        }

        function updateOffset(offsetX, offsetY) {
            dragEffect.sidePoint = inLandscape ? offsetY : offsetX;
            dragEffect.offsetPoint = Math.min(0, inLandscape ? offsetX : offsetY);
            if (dragEffect.offsetPoint < -Kirigami.Units.gridUnit * 5 && navigationPanel.state == "hidden") {
                swipeArea.resetSwipe();
                resetAn.restart();
                haptics.buttonVibrate();
                MobileShellState.ShellDBusClient.panelState = "visible";
            }
        }

        onSwipeStarted: (point) => startSwipeWithPoint(point)
        onSwipeEnded:  resetAn.start()
        onSwipeMove: (totalDeltaX, totalDeltaY, deltaX, deltaY) => updateOffset(totalDeltaX, totalDeltaY);

        onPressedChanged: {
            if (!pressed && dragEffect.offsetPoint == 0) {
                haptics.buttonVibrate();
                MobileShellState.ShellDBusClient.panelState = "visible";
            }
        }

        MobileShell.MotionNumberAnimation {
            id: resetAn
            running: false
            target: dragEffect
            property: "offsetPoint"
            to: 0
            type: MobileShell.Motion.SpatialDefault
            duration: root.resetAnimationDuration
            onRunningChanged: {
                if (!running && navigationPanel.state == "hidden") {
                    root.setWindowProperties();
                }
            }
        }

        MobileShell.ScreenEdgeDragEffect {
            id: dragEffect

            offsetLimit: root.inLandscape ? swipeArea.width : swipeArea.height
            isHorizontal: root.inLandscape

            states: [
                State {
                    name: "vertical"
                    when: !root.inLandscape
                    AnchorChanges {
                        target: dragEffect
                        anchors.right: undefined
                        anchors.bottom: swipeArea.bottom
                        anchors.horizontalCenter: swipeArea.horizontalCenter
                        anchors.verticalCenter: undefined
                    }
                },
                State {
                    name: "horizontal"
                    when: root.inLandscape
                    AnchorChanges {
                        target: dragEffect
                        anchors.right: swipeArea.right
                        anchors.bottom: undefined
                        anchors.horizontalCenter: undefined
                        anchors.verticalCenter: swipeArea.verticalCenter
                    }
                }
            ]
        }
    }
}
