// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Templates as T
import HelperWidgets as HelperWidgets
import StudioControls as StudioControls
import StudioTheme as StudioTheme
import EffectComposerBackend

ColumnLayout {
    id: root

    spacing: 1

    readonly property var backendModel: EffectComposerBackend.effectComposerModel

    property var draggedSec: null
    property var secsY: []
    property int moveFromIdx: 0
    property int moveToIdx: 0
    property bool previewAnimationRunning: false
    property var expandStates: null

    // Invoked after save changes is done
    property var onSaveChangesCallback: () => {}

    // Invoked from C++ side when open composition is requested and there are unsaved changes
    function promptToSaveBeforeOpen() {
        root.onSaveChangesCallback = () => { EffectComposerBackend.rootView.doOpenComposition() }

        saveChangesDialog.open()
    }

    // Invoked from C++ side before resetting the model to store current expanded state of nodes
    function storeExpandStates() {
        expandStates = new Map()

        for (let i = 0; i < repeater.count; ++i) {
            var curItem = repeater.itemAt(i)
            expandStates.set(curItem.caption, curItem.expanded)
        }
    }

    // Invoked after model has been reset to restore expanded state for nodes
    function restoreExpandStates() {
        if (expandStates) {
            for (let i = 0; i < repeater.count; ++i) {
                var curItem = repeater.itemAt(i)
                if (expandStates.has(curItem.caption))
                    curItem.expanded = expandStates.get(curItem.caption)
            }
            expandStates = null
        }
    }

    Connections {
        target: root.backendModel
        function onIsEmptyChanged() {
            if (root.backendModel.isEmpty)
                saveAsDialog.close()
        }
    }

    SaveAsDialog {
        id: saveAsDialog
        anchors.centerIn: parent
    }

    SaveChangesDialog {
        id: saveChangesDialog
        anchors.centerIn: parent

        onSave: {
            if (root.backendModel.currentComposition === "") {
                // if current composition is unsaved, show save as dialog and clear afterwards
                saveAsDialog.clearOnClose = true
                saveAsDialog.open()
            } else {
                root.onSaveChangesCallback()
            }
        }

        onDiscard: {
            root.onSaveChangesCallback()
        }
    }

    ConfirmClearAllDialog {
        id: confirmClearAllDialog
        anchors.centerIn: parent
    }

    EffectComposerTopBar {
        Layout.fillWidth: true

        onAddClicked: {
            root.onSaveChangesCallback = () => { root.backendModel.clear(true) }

            if (root.backendModel.hasUnsavedChanges)
                saveChangesDialog.open()
            else
                root.backendModel.clear(true)
        }

        onSaveClicked: {
            let name = root.backendModel.currentComposition

            if (name === "")
                saveAsDialog.open()
            else
                root.backendModel.saveComposition(name)
        }

        onSaveAsClicked: saveAsDialog.open()

        onAssignToSelectedClicked: {
            root.backendModel.assignToSelected()
        }
    }

    SplitView {
        id: splitView

        Layout.fillWidth: true
        Layout.fillHeight: true

        orientation: root.width > root.height ? Qt.Horizontal : Qt.Vertical

        handle: Rectangle {
            implicitWidth: splitView.orientation === Qt.Horizontal ? 6 : splitView.width
            implicitHeight: splitView.orientation === Qt.Horizontal ? splitView.height : 6
            color: T.SplitHandle.pressed ? StudioTheme.Values.themeSliderHandleInteraction
                : (T.SplitHandle.hovered ? StudioTheme.Values.themeSliderHandleHover
                                         : "transparent")
        }

        EffectComposerPreview {
            mainRoot: root

            SplitView.minimumWidth: 250
            SplitView.minimumHeight: 200
            SplitView.preferredWidth: 300
            SplitView.preferredHeight: 300
            Layout.fillWidth: true
            Layout.fillHeight: true

            FrameAnimation {
                id: previewFrameTimer
                running: true
                paused: !previewAnimationRunning
            }
        }

        Column {
            spacing: 1

            SplitView.minimumWidth: 250
            SplitView.minimumHeight: 100

            Component.onCompleted: HelperWidgets.Controller.mainScrollView = scrollView

            Rectangle {
                width: parent.width
                height: StudioTheme.Values.toolbarHeight
                color: StudioTheme.Values.themeToolbarBackground

                EffectNodesComboBox {
                    id: nodesComboBox

                    mainRoot: root

                    anchors.verticalCenter: parent.verticalCenter
                    x: 5
                    width: parent.width - 50
                }

                HelperWidgets.AbstractButton {
                    anchors.right: parent.right
                    anchors.rightMargin: 5
                    anchors.verticalCenter: parent.verticalCenter

                    style: StudioTheme.Values.viewBarButtonStyle
                    buttonIcon: StudioTheme.Constants.clearList_medium
                    tooltip: qsTr("Remove all effect nodes.")
                    enabled: root.backendModel ? !root.backendModel.isEmpty : false

                    onClicked: {
                        if (root.backendModel.hasUnsavedChanges)
                            confirmClearAllDialog.open()
                        else
                            root.backendModel.clear()
                    }
                }

                HelperWidgets.AbstractButton {
                    anchors.right: parent.right
                    anchors.rightMargin: 5
                    anchors.verticalCenter: parent.verticalCenter

                    style: StudioTheme.Values.viewBarButtonStyle
                    buttonIcon: StudioTheme.Constants.code
                    tooltip: qsTr("Open Shader in Code Editor.")
                    visible: false // TODO: to be implemented

                    onClicked: {} // TODO
                }
            }

            Item {
                width: parent.width
                height: parent.height - y

                HelperWidgets.ScrollView {
                    id: scrollView

                    readonly property int dragScrollMargin: 50

                    anchors.fill: parent
                    clip: true
                    interactive: !HelperWidgets.Controller.contextMenuOpened

                    onContentHeightChanged: {
                        // Expand states are stored before full model reset.
                        // Content height change indicates the model has been updated after full
                        // reset, so we restore expand states if any are stored.
                        root.restoreExpandStates()

                        // If content height change was because a recent node addition, we want to
                        // scroll to the end of the content so the newly added item is visible.
                        if (nodesComboBox.nodeJustAdded && scrollView.contentItem.height > scrollView.height) {
                            let lastItemH = repeater.itemAt(repeater.count - 1).height
                            scrollView.contentY = scrollView.contentItem.height - lastItemH
                            nodesComboBox.nodeJustAdded = false
                        }
                    }

                    Column {
                        id: nodesCol
                        width: scrollView.width
                        spacing: 1

                        Repeater {
                            id: repeater

                            width: parent.width
                            model: root.backendModel

                            onCountChanged: {
                                HelperWidgets.Controller.setCount("EffectComposer", repeater.count)
                            }

                            delegate: EffectCompositionNode {
                                width: parent.width
                                modelIndex: index

                                property bool wasExpanded: false

                                Behavior on y {
                                    id: dragAnimation
                                    PropertyAnimation {
                                        duration: 300
                                        easing.type: Easing.InOutQuad
                                    }
                                }

                                onStartDrag: (section) => {
                                    root.draggedSec = section
                                    root.moveFromIdx = index
                                    // We only need to animate non-dragged sections
                                    dragAnimation.enabled = false
                                    wasExpanded = expanded
                                    expanded = false
                                    highlightBorder = true
                                    root.secsY = []
                                }

                                onStopDrag: {
                                    if (root.secsY.length !== 0) {
                                        if (root.moveFromIdx === root.moveToIdx)
                                            root.draggedSec.y = root.secsY[root.moveFromIdx]
                                        else
                                            root.backendModel.moveNode(root.moveFromIdx, root.moveToIdx)
                                    }

                                    highlightBorder = false
                                    root.draggedSec = null
                                    expanded = wasExpanded
                                    dragAnimation.enabled = true
                                }
                            }
                        } // Repeater
                    } // Column
                } // ScrollView

                Text {
                    text: root.backendModel ? root.backendModel.isEnabled
                                              ? qsTr("Add an effect node to start")
                                              : qsTr("Effect Composer is disabled on MCU projects")
                                            : ""
                    color: StudioTheme.Values.themeTextColor
                    font.pixelSize: StudioTheme.Values.baseFontSize

                    anchors.centerIn: parent

                    visible: root.backendModel ? root.backendModel.isEmpty : false
                }
            } // Item
        } // Column
    } // SplitView

    function handleDragMove() {
        dragTimer.stop()
        if (root.secsY.length === 0) {
            for (let i = 0; i < repeater.count; ++i)
                root.secsY[i] = repeater.itemAt(i).y
        }

        let oldContentY = scrollView.contentY
        if (root.draggedSec.y < scrollView.dragScrollMargin + scrollView.contentY
                && scrollView.contentY > 0) {
            scrollView.contentY -= scrollView.dragScrollMargin / 2
        } else if (root.draggedSec.y > scrollView.contentY + scrollView.height - scrollView.dragScrollMargin
                    && scrollView.contentY < scrollView.contentHeight - scrollView.height) {
            scrollView.contentY += scrollView.dragScrollMargin / 2
            if (scrollView.contentY > scrollView.contentHeight - scrollView.height)
                scrollView.contentY = scrollView.contentHeight - scrollView.height
        }
        if (scrollView.contentY < 0)
            scrollView.contentY = 0

        if (oldContentY !== scrollView.contentY) {
            // Changing dragged section position in drag handler doesn't seem to stick
            // when triggered by mouse move, so do it again async
            dragTimer.targetY = root.draggedSec.y - oldContentY + scrollView.contentY
            dragTimer.restart()
            dragConnection.enabled = false
            root.draggedSec.y = dragTimer.targetY
            dragConnection.enabled = true
        }

        root.moveToIdx = root.moveFromIdx
        for (let i = 0; i < repeater.count; ++i) {
            let currItem = repeater.itemAt(i)
            if (i > root.moveFromIdx) {
                if (root.draggedSec.y > currItem.y) {
                    currItem.y = root.secsY[i] - root.draggedSec.height - nodesCol.spacing
                    root.moveToIdx = i
                } else {
                    currItem.y = root.secsY[i]
                }
            } else if (i < root.moveFromIdx) {
                if (!repeater.model.isDependencyNode(i) && root.draggedSec.y < currItem.y) {
                    currItem.y = root.secsY[i] + root.draggedSec.height + nodesCol.spacing
                    root.moveToIdx = Math.min(root.moveToIdx, i)
                } else {
                    currItem.y = root.secsY[i]
                }
            }
        }
    }

    Connections {
        id: dragConnection
        target: root.draggedSec
        function onYChanged() { root.handleDragMove() }
    }

    Timer {
        id: dragTimer
        running: false
        interval: 16
        repeat: false

        property real targetY: -1

        onTriggered: {
            // Ensure we get position change triggers even if user holds mouse still to
            // make scrolling smooth
            root.draggedSec.y = targetY
            root.handleDragMove()
        }
    } // Timer
}
