// Copyright (C) 2022 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.Layouts
import QtQuick.Controls
import HelperWidgets
import StudioControls as StudioControls
import StudioTheme as StudioTheme

Row {
    id: root

    property variant backendValue
    property color textColor: colorLogic.highlight ? colorLogic.textColor
                                                   : StudioTheme.Values.themeTextColor
    property string filter: "*.png *.gif *.jpg *.bmp *.jpeg *.svg *.pbm *.pgm *.ppm *.xbm *.xpm *.hdr *.ktx *.webp"

    // This property takes an array of strings which define default items that should be added
    // to the ComboBox model in addition to the items from the FileResourcesModel. This is used
    // by QtQuick3D to add built-in primitives to the model.
    property var defaultItems

    // These paths will be used for default items if they are defined. Otherwise, default item
    // itself is used as the path.
    property var defaultPaths

    // Current item
    property string absoluteFilePath: ""

    // If this property is set, this path will be opened instead of the default path
    property string resourcesPath

    property alias comboBox: comboBox
    property alias spacer: spacer
    property alias actionIndicatorVisible: comboBox.actionIndicatorVisible

    property bool hideDuplicates: true

    FileResourcesModel {
        id: fileModel
        modelNodeBackendProperty: modelNodeBackend
        filter: root.filter
    }

    ColorLogic {
        id: colorLogic
        backendValue: root.backendValue
    }

    component ThumbnailToolTip: ToolTip {
        id: toolTip

        property alias checkerVisible: checker.visible
        property alias thumbnailSource: thumbnail.source

        property alias titleText: title.text
        property alias descriptionText: description.text

        property int maximumWidth: 420

        delay: StudioTheme.Values.toolTipDelay

        background: Rectangle {
            color: StudioTheme.Values.themeToolTipBackground
            border.color: StudioTheme.Values.themeToolTipOutline
            border.width: StudioTheme.Values.border
        }

        contentItem: Row {
            id: row

            readonly property real __epsilon: 2

            height: Math.max(wrapper.visible ? wrapper.height : 0, column.height)
            spacing: 10

            Item {
                id: wrapper
                visible: thumbnail.status === Image.Ready
                width: 96
                height: 96

                Image {
                    id: checker
                    anchors.fill: parent
                    fillMode: Image.Tile
                    source: "images/checkers.png"
                }

                Image {
                    id: thumbnail
                    anchors.fill: parent
                    sourceSize.width: wrapper.width
                    sourceSize.height: wrapper.height
                    asynchronous: true
                    fillMode: Image.PreserveAspectFit
                }
            }

            Column {
                id: column

                property int thumbnailSize: wrapper.visible ? wrapper.width + row.spacing : 0

                spacing: 10
                anchors.verticalCenter: parent.verticalCenter
                width: Math.min(toolTip.maximumWidth - column.thumbnailSize,
                                Math.max(titleTextMetrics.width + row.__epsilon,
                                         descriptionTextMetrics.width + row.__epsilon))

                Text {
                    id: title
                    font: toolTip.font
                    color: StudioTheme.Values.themeToolTipText

                    TextMetrics {
                        id: titleTextMetrics
                        text: title.text
                        font: title.font
                    }
                }

                Text {
                    id: description
                    width: column.width
                    font: toolTip.font
                    color: StudioTheme.Values.themeToolTipText
                    wrapMode: Text.Wrap

                    TextMetrics {
                        id: descriptionTextMetrics
                        text: description.text
                        font: description.font
                    }
                }
            }
        }
    }

    StudioControls.FilterComboBox {
        id: comboBox

        property ListModel listModel: ListModel {}

        hasActiveDrag: activeDragSuffix !== "" && root.filter.includes(activeDragSuffix)
        implicitWidth: StudioTheme.Values.singleControlColumnWidth
                        + StudioTheme.Values.actionIndicatorWidth
        width: implicitWidth
        allowUserInput: true

        // Note: highlightedIndex property isn't used because it has no setter and it doesn't reset
        // when the combobox is closed by focusing on some other control.
        property int hoverIndex: -1

        onCurrentIndexChanged: {
            // This is needed to correctly update root.absoluteFilePath in cases where selection
            // changes between two nodes of same type.
            if (currentIndex !== -1 && !root.backendValue.isBound)
                root.absoluteFilePath = fileModel.resolve(root.backendValue.value)
        }

        DropArea {
            id: dropArea

            anchors.fill: parent

            property string assetPath: ""

            onEntered: function(drag) {
                dropArea.assetPath = drag.getDataAsString(drag.keys[0]).split(",")[0]
                drag.accepted = comboBox.hasActiveDrag
                comboBox.hasActiveHoverDrag = drag.accepted
            }

            onExited: comboBox.hasActiveHoverDrag = false

            onDropped: function(drop) {
                drop.accepted = comboBox.hasActiveHoverDrag
                comboBox.editText = dropArea.assetPath
                comboBox.accepted()
                comboBox.hasActiveHoverDrag = false
                root.backendValue.commitDrop(dropArea.assetPath)
            }
        }

        ThumbnailToolTip {
            id: rootToolTip

            visible: comboBox.hover && rootToolTip.text !== ""
            text: root.backendValue?.valueToString ?? ""

            checkerVisible: !root.isMesh(root.absoluteFilePath)
            thumbnailSource: {
                if (root.isBuiltInPrimitive(root.absoluteFilePath))
                    return "image://qmldesigner_thumbnails/"
                        + root.absoluteFilePath.substring(1, root.absoluteFilePath.length)
                        + ".builtin"

                if (fileModel.isLocal(root.absoluteFilePath))
                    return "image://qmldesigner_thumbnails/" + root.absoluteFilePath

                return root.absoluteFilePath
            }
            titleText: root.fileName(rootToolTip.text)
            descriptionText: root.isBuiltInPrimitive(rootToolTip.text)
                                ? qsTr("Built-in primitive")
                                : rootToolTip.text
        }

        delegate: ItemDelegate {
            required property string absoluteFilePath
            required property string relativeFilePath
            required property string name
            required property int group
            required property int index

            id: delegateRoot
            width: comboBox.popup.width - comboBox.popup.leftPadding - comboBox.popup.rightPadding
            height: StudioTheme.Values.height - 2 * StudioTheme.Values.border
            padding: 0
            hoverEnabled: true
            highlighted: comboBox.highlightedIndex === delegateRoot.DelegateModel.itemsIndex

            onHoveredChanged: {
                if (delegateRoot.hovered && !comboBox.popupMouseArea.active)
                    comboBox.setHighlightedIndexItems(delegateRoot.DelegateModel.itemsIndex)
            }

            onClicked: comboBox.selectItem(delegateRoot.DelegateModel.itemsIndex)

            contentItem: Text {
                leftPadding: 8
                text: name
                color: {
                    if (!delegateRoot.enabled)
                        return comboBox.style.text.disabled

                    if (comboBox.currentIndex === delegateRoot.DelegateModel.itemsIndex)
                        return comboBox.style.text.selectedText

                    return comboBox.style.text.idle
                }
                font: comboBox.font
                elide: Text.ElideRight
                verticalAlignment: Text.AlignVCenter
            }

            background: Rectangle {
                x: 0
                y: 0
                width: delegateRoot.width
                height: delegateRoot.height
                color: {
                    if (!delegateRoot.enabled)
                        return "transparent"

                    if (delegateRoot.hovered && comboBox.currentIndex === delegateRoot.DelegateModel.itemsIndex)
                        return comboBox.style.interactionHover

                    if (comboBox.currentIndex === delegateRoot.DelegateModel.itemsIndex)
                        return comboBox.style.interaction

                    if (delegateRoot.hovered)
                        return comboBox.style.background.hover

                    return "transparent"
                }
            }

            ThumbnailToolTip {
                id: delegateToolTip

                visible: delegateRoot.hovered
                text: delegateRoot.relativeFilePath

                checkerVisible: !root.isMesh(delegateRoot.absoluteFilePath)
                thumbnailSource: {
                    if (root.isBuiltInPrimitive(delegateRoot.name))
                        return "image://qmldesigner_thumbnails/"
                            + delegateRoot.name.substring(1, delegateRoot.name.length)
                            + ".builtin"

                    return "image://qmldesigner_thumbnails/" + delegateRoot.absoluteFilePath
                }
                titleText: delegateRoot.name
                descriptionText: root.isBuiltInPrimitive(delegateToolTip.text)
                                    ? qsTr("Built-in primitive")
                                    : delegateToolTip.text
            }
        }

        actionIndicator.icon.color: extFuncLogic.color
        actionIndicator.icon.text: extFuncLogic.glyph
        actionIndicator.onClicked: extFuncLogic.show()
        actionIndicator.forceVisible: extFuncLogic.menuVisible

        ExtendedFunctionLogic {
            id: extFuncLogic
            backendValue: root.backendValue
            onReseted: comboBox.editText = ""
        }

        property bool isComplete: false
        property bool dirty: false

        onEditTextChanged: comboBox.dirty = true

        function setCurrentText(text) {
            comboBox.currentIndex = comboBox.find(text)
            comboBox.setHighlightedIndexItems(comboBox.currentIndex)
            comboBox.autocompleteString = ""
            comboBox.editText = text
            comboBox.dirty = false
        }

        // Takes into account applied bindings
        function updateTextValue() {
            if (root.backendValue.isBound) {
                comboBox.textValue = root.backendValue.expression
            } else {
                // Can be absolute or relative file path
                var filePath = root.backendValue.valueToString
                comboBox.textValue = filePath.substr(filePath.lastIndexOf('/') + 1)
            }

            comboBox.setCurrentText(comboBox.textValue)
        }

        Connections {
            target: root.backendValue

            function onIsBoundChanged() { comboBox.updateTextValue() }
            function onExpressionChanged() { comboBox.updateTextValue() }
            function onValueChangedQml() { comboBox.updateTextValue() }
        }

        property string textValue: ""

        onModelChanged: {
            if (!comboBox.isComplete)
                return

            comboBox.setCurrentText(comboBox.textValue)
        }

        onAccepted: {
            if (!comboBox.isComplete)
                return

            let inputValue = comboBox.editText

            // Check if value set by user matches with a name in the model then pick the full path
            let index = comboBox.find(inputValue)
            if (index !== -1)
                inputValue = comboBox.items.get(index).model.relativeFilePath

            root.backendValue.value = inputValue

            if (!root.backendValue.isBound)
                root.absoluteFilePath = fileModel.resolve(root.backendValue.value)

            comboBox.dirty = false
        }

        onFocusChanged: {
            if (comboBox.dirty)
               comboBox.handleActivate(comboBox.currentIndex)
        }

        onCompressedActivated: function (index) {
            comboBox.handleActivate(index)
        }

        function handleActivate(index) {
            if (root.backendValue === undefined || !comboBox.isComplete)
                return

            let inputValue = comboBox.editText

            if (index >= 0)
                inputValue = comboBox.items.get(index).model.relativeFilePath

            if (root.backendValue.value !== inputValue)
                root.backendValue.value = inputValue

            if (!root.backendValue.isBound)
                root.absoluteFilePath = fileModel.resolve(root.backendValue.value)

            comboBox.dirty = false
        }

        Component.onCompleted: {
            // Hack to style the text input
            for (var i = 0; i < comboBox.children.length; i++) {
                if (comboBox.children[i].text !== undefined) {
                    comboBox.children[i].color = root.textColor
                    comboBox.children[i].anchors.rightMargin = 34
                }
            }
            comboBox.isComplete = true
            comboBox.setCurrentText(comboBox.textValue)
        }
    }

    function isBuiltInPrimitive(value) {
        return value.startsWith('#')
    }

    function isMesh(value) {
        return root.isBuiltInPrimitive(value)
                || root.hasFileExtension(root.fileName(value), "mesh")
    }

    function hasFileExtension(fileName, extension) {
        return fileName.split('.').pop() === extension
    }

    function fileName(filePath) {
        return filePath.substr(filePath.lastIndexOf('/') + 1)
    }

    function createModel() {
        // Build the combobox model
        comboBox.listModel.clear()
        // While adding items to the model this binding needs to be interrupted, otherwise the
        // update function of the SortFilterModel is triggered every time on append() which makes
        // QtDS very slow. This will happen when selecting different items in the scene.
        comboBox.model = {}

        let nameMap = new Map;

        if (root.defaultItems !== undefined) {
            for (var i = 0; i < root.defaultItems.length; ++i) {
                comboBox.listModel.append({
                    absoluteFilePath: root.defaultPaths ? root.defaultPaths[i]
                                                        : "",
                    relativeFilePath: root.defaultPaths ? root.defaultPaths[i]
                                                        : root.defaultItems[i],
                    name: root.defaultItems[i],
                    group: 0
                })
                nameMap.set(root.defaultItems[i], i)
            }
        }

        const myModel = fileModel.model
        for (var j = 0; j < myModel.length; ++j) {
            let item = myModel[j]
            if (root.hideDuplicates && nameMap.has(item.fileName)) {
                // Prefer hiding generated component files rather than other project files
                let listIndex = nameMap.get(item.fileName)
                let absPath = comboBox.listModel.get(listIndex).absoluteFilePath
                if (absPath.includes("/Generated/") || absPath.includes("/asset_imports/")) {
                    comboBox.listModel.set(listIndex, {
                        absoluteFilePath: item.absoluteFilePath,
                        relativeFilePath: item.relativeFilePath,
                        name: item.fileName,
                        group: 1
                    })
                }
            } else {
                comboBox.listModel.append({
                    absoluteFilePath: item.absoluteFilePath,
                    relativeFilePath: item.relativeFilePath,
                    name: item.fileName,
                    group: 1
                })
                nameMap.set(item.fileName, comboBox.listModel.count - 1)
            }
        }

        comboBox.model = Qt.binding(function() { return comboBox.listModel })
    }

    Connections {
        target: fileModel
        function onModelChanged() {
            root.createModel()
            comboBox.setCurrentText(comboBox.textValue)
        }
    }

    onDefaultItemsChanged: root.createModel()
    onDefaultPathsChanged: root.createModel()
    onHideDuplicatesChanged: root.createModel()

    Component.onCompleted: {
        root.createModel()
        comboBox.updateTextValue()

        if (!root.backendValue.isBound)
            root.absoluteFilePath = fileModel.resolve(root.backendValue.value)
    }

    function indexOf(model, criteria) {
        for (var i = 0; i < model.count; ++i) {
            if (criteria(model.get(i)))
                return i
        }
        return -1
    }

    Connections {
        target: comboBox
        function onStateChanged(state) {
            // update currentIndex when the popup opens to override the default behavior in super classes
            // that selects currentIndex based on values in the combo box.
            if (comboBox.popup.opened && !root.backendValue.isBound) {
                var index = root.indexOf(comboBox.items,
                                               function(item) {
                                                   return item.relativeFilePath === root.backendValue.value
                                               })

                if (index !== -1) {
                    comboBox.currentIndex = index
                    comboBox.hoverIndex = index
                    comboBox.editText = comboBox.items.get(index).model.name
                }
            }
        }
    }

    Spacer { id: spacer
        implicitWidth: StudioTheme.Values.twoControlColumnGap
    }

    IconIndicator {
        icon: StudioTheme.Constants.addFile
        iconColor: root.textColor
        onClicked: {
            fileModel.openFileDialog(resourcesPath)
            if (fileModel.fileName !== "") {
                root.backendValue.value = fileModel.fileName
                root.absoluteFilePath = fileModel.resolve(root.backendValue.value)
            }
        }
    }
}
