// Copyright Mirage authors & contributors <https://github.com/mirukana/mirage>
// SPDX-License-Identifier: LGPL-3.0-or-later

import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Layouts 1.12
import Clipboard 0.1
import "Base"
import "ShortcutBundles"

HDrawer {
    id: debugConsole

    property Item previouslyFocused: null

    property QtObject target: null
    property alias t: debugConsole.target

    property var history: window.history.console
    property int historyEntry: -1
    property int maxHistoryLength: 4096
    property var textBeforeHistoryNavigation: null  // null or string

    property int selectedOutputDelegateIndex: -1
    property string selectedOutputText: ""

    property string pythonDebugKeybind:
        window.settings.Keys.python_debugger[0]

    property string help: qsTr(
        `Interact with the QML code using JavaScript ES6 syntax.

        Useful variables:
            t     Target item to debug for which this console was opened
            this  The console itself
            py    Python interpreter (${pythonDebugKeybind} to start debugger)

            window, mainUI, theme, settings, utils, mainPane, pageLoader

        Special commands:
            .j OBJECT, .json OBJECT  Print OBJECT as human-readable JSON

            .t, .top     Attach console to the parent window's top
            .b, .bottom  Attach console to the parent window's bottom
            .l, .left    Attach console to the parent window's left
            .r, .right   Attach console to the parent window's right
            .h, .help    Show this help`.replace(/^ {8}/gm, "")
    )

    property bool doUselessThing: false
    property real baseGIFSpeed: 1.0

    readonly property alias outputList: outputList

    function toggle(targetItem=null, js="", addToHistory=false) {
        if (debugConsole.visible) {
            debugConsole.visible = false
            return
        }

        debugConsole.visible = true
        debugConsole.target  =
            ! targetItem && ! debugConsole.target ? mainUI :
            targetItem ? targetItem :
            debugConsole.target

        if (js) debugConsole.runJS(js, addToHistory)
    }

    function runJS(input, addToHistory=true) {
        if (addToHistory && history.slice(-1)[0] !== input) {
            history.push(input)
            while (history.length > maxHistoryLength) history.shift()
            window.saveHistory()
        }

        let output = ""
        let error  = false

        try {
            if ([".h", ".help"].includes(input)) {
                output = debugConsole.help

            } else if ([".t", ".top"].includes(input)) {
                debugConsole.edge = Qt.TopEdge

            } else if ([".b", ".bottom"].includes(input)) {
                debugConsole.edge = Qt.BottomEdge

            } else if ([".l", ".left"].includes(input)) {
                debugConsole.edge = Qt.LeftEdge

            } else if ([".r", ".right"].includes(input)) {
                debugConsole.edge = Qt.RightEdge

            } else if (input.startsWith(".j ") || input.startsWith(".json ")) {
                output = JSON.stringify(eval(input.substring(2)), null, 4)

            } else {
                let result = eval(input)
                output     = result instanceof Array ?
                             "[" + String(result) + "]" : String(result)
            }

        } catch (err) {
            error  = true
            output = err.toString()
        }

        outputList.model.insert(0, { input, output, error })
    }

    objectName: "debugConsole"
    edge: Qt.TopEdge
    x: horizontal ? 0 : referenceSizeParent.width / 2 - width / 2
    y: vertical ? 0 : referenceSizeParent.height / 2 - height / 2
    width: horizontal ? calculatedSize : Math.min(window.width, 720)
    height: vertical ? calculatedSize : Math.min(window.height, 720)
    defaultSize: 400
    z: 9999
    position: 0

    onTargetChanged: {
        outputList.model.insert(0, {
            input: "t = " + String(target),
            output: "",
            error: false,
        })
    }

    onVisibleChanged: {
        if (visible) {
            previouslyFocused = window.activeFocusItem
            forceActiveFocus()
        } else if (previouslyFocused) {
            previouslyFocused.forceActiveFocus()
        }
    }

    onHistoryEntryChanged: {
        if (historyEntry === -1) {
            inputArea.clear()
            inputArea.append(textBeforeHistoryNavigation)
            textBeforeHistoryNavigation = null
            return
        }

        if (textBeforeHistoryNavigation === null)
            textBeforeHistoryNavigation = inputArea.text

        inputArea.clear()
        inputArea.append(history.slice(-historyEntry - 1)[0])
    }

    HShortcut {
        sequences: settings.Keys.qml_console
        onActivated: debugConsole.toggle()
    }

    HColumnLayout {
        anchors.fill: parent
        // Fixes inputArea cursor invisible when at cursorPosition 0
        anchors.leftMargin: 1

        HListView {
            id: outputList
            spacing: theme.spacing
            topMargin: theme.spacing
            bottomMargin: topMargin
            leftMargin: theme.spacing
            rightMargin: leftMargin
            clip: true
            verticalLayoutDirection: ListView.BottomToTop

            Layout.fillWidth: true
            Layout.fillHeight: true

            model: ListModel {}

            delegate: HSelectableLabel {
                id: delegate
                width: outputList.width -
                       outputList.leftMargin - outputList.rightMargin

                readonly property color inputColor:
                    model.error ? theme.colors.errorText :
                    model.output ? theme.colors.accentText :
                    theme.colors.positiveText

                text:
                    `<div style="white-space: pre-wrap">` +
                    `<font color="${inputColor}">` +
                    utils.plain2Html(model.input) +
                    "</font>" +
                    (model.input && model.output ? "<br>" : "") +
                    (model.output ? utils.plain2Html(model.output) : "") +
                    "</div>"

                leftPadding: theme.spacing
                textFormat: HSelectableLabel.RichText
                wrapMode: HLabel.Wrap
                font.family: theme.fontFamily.mono
                color:
                    model.error ?
                    Qt.darker(inputColor, 1.4) :
                    theme.colors.halfDimText

                Layout.fillWidth: true

                onSelectedTextChanged: if (selectedPlainText) {
                    selectedOutputDelegateIndex = model.index
                    selectedOutputText          = selectedPlainText
                } else if (selectedOutputDelegateIndex === model.index) {
                    selectedOutputDelegateIndex = -1
                    selectedOutputText          = ""
                }

                Connections {
                    target: debugConsole
                    onSelectedOutputDelegateIndexChanged: {
                        if (selectedOutputDelegateIndex !== model.index)
                            delegate.deselect()
                    }
                }

                TapHandler {
                    acceptedButtons: Qt.RightButton
                    gesturePolicy: TapHandler.ReleaseWithinBounds
                    acceptedPointerTypes:
                        PointerDevice.GenericPointer | PointerDevice.Pen

                    onTapped: menu.popup()
                }

                TapHandler {
                    acceptedPointerTypes:
                        PointerDevice.Finger | PointerDevice.Pen

                    onLongPressed: menu.popup()
                }

                HMenu {
                    id: menu
                    implicitWidth: Math.min(window.width, 150)
                    z: 10000

                    HMenuItem {
                        icon.name: "copy-text"
                        text: qsTr("Copy")
                        onTriggered: {
                            if (delegate.selectedPlainText) {
                                delegate.copy()
                                return
                            }
                            delegate.selectAll()
                            delegate.copy()
                            delegate.deselect()
                        }
                    }
                }

                Rectangle {
                    width: 1
                    height: parent.height
                    color:
                        model.error ?
                        theme.colors.negativeBackground :
                        model.output ?
                        theme.colors.accentElement :
                        theme.colors.positiveBackground
                }
            }

            FlickShortcuts {
                active: debugConsole.visible
                flickable: outputList
            }

            Rectangle {
                z: -10
                anchors.fill: parent
                color: theme.colors.weakBackground
            }
        }

        HTextArea {
            id: inputArea

            function accept() {
                if (! text) return
                runJS(text)
                clear()
                historyEntry = -1
            }

            focus: true
            backgroundColor: Qt.hsla(0, 0, 0, 0.85)
            bordered: false
            placeholderText: qsTr("QML/JavaScript debug console - Type .help")
            font.family: theme.fontFamily.mono

            Keys.onUpPressed: ev => {
                ev.accepted =
                    cursorRectangle.top < topPadding + font.pixelSize &&
                    historyEntry + 1 < history.length

                if (ev.accepted) {
                    historyEntry   += 1
                    cursorPosition  = length
                }
            }

            Keys.onDownPressed: ev => {
                ev.accepted =
                    cursorRectangle.bottom >=
                    height - bottomPadding - font.pixelSize &&
                    historyEntry - 1 >= -1

                if (ev.accepted) historyEntry -= 1
            }

            Keys.onTabPressed: inputArea.insertAtCursor("    ")

            Keys.onReturnPressed: ev => {
                ev.modifiers & Qt.ShiftModifier ||
                ev.modifiers & Qt.ControlModifier ||
                ev.modifiers & Qt.AltModifier ?
                inputArea.insertAtCursor("\n") :
                accept()
            }

            Keys.onEnterPressed: ev => Keys.returnPressed(ev)

            Keys.onEscapePressed: debugConsole.close()

            Keys.onPressed: ev => {
                if (
                    ev.matches(StandardKey.Copy) &&
                    ! inputArea.selectedPlainText &&
                    selectedOutputText
                ) {
                    ev.accepted = true
                    Clipboard.text = selectedOutputText
                }
            }

            Layout.fillWidth: true

        }
    }

    NumberAnimation {
        running: doUselessThing
        target: mainUI.mainPane.roomList
        property: "rotation"
        duration: 250
        from: 360
        to: 0
        loops: Animation.Infinite
        onStopped: target.rotation = 0
    }
}
