// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt.labs.qmlmodels

import Spreadsheets

ApplicationWindow {
    width: 960
    height: 720
    visible: true
    title: qsTr("Spreadsheets")

    header: HeaderToolBar {
        id: toolbar
        panEnabled: false
        onHelpRequested: helpDialog.open()
        onPasteRequested: tableView.pasteFromClipboard()
        onCopyRequested: tableView.copyToClipboard()
        onCutRequested: tableView.cutToClipboard()
    }

    background: Rectangle {
        // to make contrast with the cells of the TableView,
        // HorizontalHeaderView and VerticalHeaderView
        color: Qt.styleHints.colorScheme === Qt.Light ? palette.dark : palette.light
    }

    GridLayout {
        id: gridlayout
        anchors.fill: parent
        anchors.margins: 4
        columns: 2
        rows: 2
        columnSpacing: 3
        rowSpacing: 3

        HorizontalHeaderView {
            id: horizontalHeaderView
            Layout.row: 0
            Layout.column: 1
            Layout.fillWidth: true
            implicitHeight: 36
            clip: true
            interactive: toolbar.panEnabled
            syncView: tableView

            selectionModel: HeaderSelectionModel {
                id: horizontalHeaderSelectionModel
                selectionModel: selectionModel
                orientation: Qt.Horizontal
            }

            movableColumns: true
            onColumnMoved: (index, old_column, new_column) => model.mapColumn(index, new_column)

            delegate: Rectangle {
                id: horizontalHeaderDelegate

                required property var index
                required property bool selected
                required property bool containsDrag
                readonly property real cellPadding: 8
                readonly property bool containsMenu: columnMenu.column === column

                implicitWidth: horizontalTitle.implicitWidth + (cellPadding * 2)
                implicitHeight: Math.max(horizontalHeaderView.height,
                                         horizontalTitle.implicitHeight + (cellPadding * 2))
                border {
                    width: containsDrag || containsMenu ? 1 : 0
                    color: palette.highlight
                }
                color: selected ? palette.highlight : palette.button

                gradient: Gradient {
                    GradientStop {
                        position: 0
                        color: Qt.styleHints.colorScheme === Qt.Light ? horizontalHeaderDelegate.color
                                                                      : Qt.lighter(horizontalHeaderDelegate.color, 1.3)
                    }
                    GradientStop {
                        position: 1
                        color: Qt.styleHints.colorScheme === Qt.Light ? Qt.darker(horizontalHeaderDelegate.color, 1.3)
                                                                      : horizontalHeaderDelegate.color
                    }
                }

                Label {
                    id: horizontalTitle
                    anchors.centerIn: parent
                    text: model.columnName
                }

                MouseArea {
                    anchors.fill: parent
                    anchors.leftMargin: horizontalHeaderDelegate.cellPadding / 2
                    anchors.rightMargin: horizontalHeaderDelegate.cellPadding / 2
                    acceptedButtons: Qt.LeftButton | Qt.RightButton

                    onPressed: function(event) {
                        if (event.modifiers === Qt.AltModifier) {
                            event.accepted = false
                            return
                        }
                    }

                    onClicked: function(event) {
                        switch (event.button) {
                        case Qt.LeftButton:
                            if (event.modifiers & Qt.ControlModifier)
                                selectionModel.toggleColumn(index)
                            else
                                selectionModel.selectColumn(index)
                            break
                        case Qt.RightButton:
                            columnMenu.column = index
                            const menu_pos = mapToItem(horizontalHeaderView, -anchors.margins, height + anchors.margins)
                            columnMenu.popup(menu_pos)
                            break
                        }
                    }
                }
            }
            Menu {
                id: columnMenu

                property int column: -1

                onOpened: {
                    horizontalHeaderSelectionModel.setCurrent(column)
                }

                onClosed: {
                    horizontalHeaderSelectionModel.setCurrent()
                    column = -1
                }

                MenuItem {
                    text: qsTr("Insert 1 column left")
                    icon {
                        source: "icons/insert_column_left.svg"
                        color: palette.highlightedText
                    }

                    onClicked: {
                        if (columnMenu.column < 0)
                            return
                        SpreadModel.insertColumn(columnMenu.column)
                    }
                }

                MenuItem {
                    text: qsTr("Insert 1 column right")
                    icon {
                        source: "icons/insert_column_right.svg"
                        color: palette.highlightedText
                    }

                    onClicked: {
                        if (columnMenu.column < 0)
                            return
                        SpreadModel.insertColumn(columnMenu.column + 1)
                    }
                }

                MenuItem {
                    text: selectionModel.hasSelection ? qsTr("Remove selected columns")
                                                      : qsTr("Remove column")
                    icon {
                        source: "icons/remove_column.svg"
                        color: palette.text
                    }

                    onClicked: {
                        if (selectionModel.hasSelection)
                            SpreadModel.removeColumns(selectionModel.selectedColumns())
                        else if (columnMenu.column >= 0)
                            SpreadModel.removeColumn(columnMenu.column)
                    }
                }

                MenuItem {
                    text: selectionModel.hasSelection ? qsTr("Hide selected columns")
                                                      : qsTr("Hide column")
                    icon {
                        source: "icons/hide.svg"
                        color: palette.text
                    }

                    onClicked: {
                        if (selectionModel.hasSelection) {
                            let columns = selectionModel.selectedColumns()
                            columns.sort(function(lhs, rhs){ return rhs.column - lhs.column })
                            for (let i in columns)
                                tableView.hideColumn(columns[i].column)
                            selectionModel.clearSelection()
                        } else {
                            tableView.hideColumn(columnMenu.column)
                        }
                    }
                }

                MenuItem {
                    text: qsTr("Show hidden column(s)")
                    icon {
                        source: "icons/show.svg"
                        color: palette.text
                    }

                    enabled: tableView.hiddenColumnCount

                    onClicked: {
                        tableView.showHiddenColumns()
                        selectionModel.clearSelection()
                    }
                }

                MenuItem {
                    text: qsTr("Reset column reordering")
                    icon {
                        source: "icons/reset_reordering.svg"
                        color: palette.text
                    }

                    onClicked: tableView.resetColumnReordering()
                }
            }
        }

        VerticalHeaderView {
            id: verticalHeaderView

            Layout.fillHeight: true
            implicitWidth: 50
            clip: true
            syncView: tableView
            interactive: toolbar.panEnabled
            movableRows: true

            selectionModel: HeaderSelectionModel {
                id: verticalHeaderSelectionModel
                selectionModel: selectionModel
                orientation: Qt.Vertical
            }

            onRowMoved: (index, old_row, new_row) => model.mapRow(index, new_row)

            delegate: Rectangle {
                id: verticalHeaderDelegate

                required property var index
                required property bool selected
                required property bool current
                required property bool containsDrag
                readonly property real cellPadding: 8

                implicitHeight: verticalTitle.implicitHeight + (cellPadding * 2)
                implicitWidth: Math.max(verticalHeaderView.width,
                                        verticalTitle.implicitWidth + (cellPadding * 2))

                border {
                    width: containsDrag || current ? 1 : 0
                    color: palette.highlight
                }

                color: selected ? palette.highlight : palette.button

                gradient: Gradient {
                    GradientStop {
                        position: 0
                        color: Qt.styleHints.colorScheme === Qt.Light ? verticalHeaderDelegate.color
                                                                      : Qt.lighter(verticalHeaderDelegate.color, 1.3)
                    }
                    GradientStop {
                        position: 1
                        color: Qt.styleHints.colorScheme === Qt.Light ? Qt.darker(verticalHeaderDelegate.color, 1.3)
                                                                      : verticalHeaderDelegate.color
                    }
                }

                Label {
                    id: verticalTitle
                    anchors.centerIn: parent
                    text: model.rowName
                }

                MouseArea {
                    anchors.fill: parent
                    anchors.topMargin: verticalHeaderDelegate.cellPadding / 2
                    anchors.bottomMargin: verticalHeaderDelegate.cellPadding / 2
                    acceptedButtons: Qt.LeftButton | Qt.RightButton

                    onPressed: function(event) {
                        if (event.modifiers === Qt.AltModifier) {
                            event.accepted = false
                            return
                        }
                    }

                    onClicked: function(event) {
                        switch (event.button) {
                        case Qt.LeftButton:
                            if (event.modifiers & Qt.ControlModifier)
                                selectionModel.toggleRow(index)
                            else
                                selectionModel.selectRow(index)
                            break
                        case Qt.RightButton:
                            rowMenu.row = index
                            const menu_pos = mapToItem(verticalHeaderView, width + anchors.margins, -anchors.margins)
                            rowMenu.popup(menu_pos)
                            break
                        }
                    }
                }
            }
            Menu {
                id: rowMenu

                property int row: -1

                onOpened: {
                    verticalHeaderSelectionModel.setCurrent(row)
                }

                onClosed: {
                    verticalHeaderSelectionModel.setCurrent()
                    row = -1
                }

                MenuItem {
                    text: qsTr("Insert 1 row above")
                    icon {
                        source: "icons/insert_row_above.svg"
                        color: palette.highlightedText
                    }

                    onClicked: {
                        if (rowMenu.row < 0)
                            return
                        SpreadModel.insertRow(rowMenu.row)
                    }
                }

                MenuItem {
                    text: qsTr("Insert 1 row bellow")
                    icon {
                        source: "icons/insert_row_below.svg"
                        color: palette.text
                    }

                    onClicked: {
                        if (rowMenu.row < 0)
                            return
                        SpreadModel.insertRow(rowMenu.row + 1)
                    }
                }

                MenuItem {
                    text: selectionModel.hasSelection ? qsTr("Remove selected rows")
                                                      : qsTr("Remove row")
                    icon {
                        source: "icons/remove_row.svg"
                        color: palette.text
                    }

                    onClicked: {
                        if (selectionModel.hasSelection)
                            SpreadModel.removeRows(selectionModel.selectedRows())
                        else if (rowMenu.row >= 0)
                            SpreadModel.removeRow(rowMenu.row)
                    }
                }

                MenuItem {
                    text: selectionModel.hasSelection ? qsTr("Hide selected rows")
                                                      : qsTr("Hide row")
                    icon {
                        source: "icons/hide.svg"
                        color: palette.text
                    }

                    onClicked: {
                        if (selectionModel.hasSelection) {
                            let rows = selectionModel.selectedRows()
                            rows.sort(function(lhs, rhs){ return rhs.row - lhs.row })
                            for (let i in rows)
                                tableView.hideRow(rows[i].row)
                            selectionModel.clearSelection()
                        } else {
                            tableView.hideRow(rowMenu.row)
                        }
                    }
                }

                MenuItem {
                    text: qsTr("Show hidden row(s)")
                    icon {
                        source: "icons/show.svg"
                        color: palette.text
                    }
                    enabled: tableView.hiddenRowCount

                    onClicked: {
                        tableView.showHiddenRows()
                        selectionModel.clearSelection()
                    }
                }

                MenuItem {
                    text: qsTr("Reset row reordering")
                    icon {
                        source: "icons/reset_reordering.svg"
                        color: palette.text
                    }

                    onClicked: tableView.resetRowReordering()
                }
            }
        }

        Item {
            Layout.fillWidth: true
            Layout.fillHeight: true

            TableView {
                id: tableView

                property int hiddenColumnCount: 0
                property int hiddenRowCount: 0

                anchors.fill: parent
                clip: true
                columnSpacing: 2
                rowSpacing: 2
                boundsBehavior: Flickable.StopAtBounds
                selectionBehavior: TableView.SelectCells
                selectionMode: TableView.ExtendedSelection
                selectionModel: selectionModel
                interactive: toolbar.panEnabled
                model: SpreadModel

                function showHiddenColumns()
                {
                    for (let column = 0; column < columns; ++column) {
                        if (explicitColumnWidth(column) === 0)
                            setColumnWidth(column, -1)
                    }
                    hiddenColumnCount = 0
                }

                function hideColumn(column)
                {
                    if (column < 0)
                        return
                    setColumnWidth(column, 0)
                    ++hiddenColumnCount
                }

                function showHiddenRows()
                {
                    for (let row = 0; row < rows; ++row) {
                        if (explicitRowHeight(row) === 0)
                            setRowHeight(row, -1)
                    }
                    hiddenRowCount = 0
                }

                function hideRow(row)
                {
                    if (row < 0)
                        return
                    setRowHeight(row, 0)
                    ++hiddenRowCount
                }

                function copyToClipboard()
                {
                    mimeDataProvider.reset()
                    if (selectionModel.hasSelection) {
                        const source_index = selectionModel.selectedIndexes[0]
                        mimeDataProvider.sourceCell = cellAtIndex(source_index)
                        mimeDataProvider.loadSelectedData()
                    } else {
                        const current_index = selectionModel.currentIndex
                        const current_cell = cellAtIndex(current_index)
                        mimeDataProvider.sourceCell = current_cell
                        mimeDataProvider.loadDataFromModel(current_cell, current_index, model)
                    }
                }

                function cutToClipboard()
                {
                    mimeDataProvider.reset()
                    if (selectionModel.hasSelection) {
                        const source_index = selectionModel.selectedIndexes[0]
                        mimeDataProvider.sourceCell = cellAtIndex(source_index)
                        mimeDataProvider.loadSelectedData()
                    } else {
                        const current_index = selectionModel.currentIndex
                        const current_cell = cellAtIndex(current_index)
                        mimeDataProvider.sourceCell = current_cell
                        mimeDataProvider.loadDataFromModel(current_cell, current_index, model)
                    }
                    mimeDataProvider.includeCutData = true
                }

                function pasteFromClipboard()
                {
                    visibleCellsConnection.blockConnection(true)
                    const current_index = selectionModel.currentIndex
                    const current_cell = cellAtIndex(current_index)

                    let target_cells = new Set()
                    if (mimeDataProvider.size() === 1) {
                        if (selectionModel.hasSelection) {
                            for (let i in selectionModel.selectedIndexes) {
                                const selected_index = selectionModel.selectedIndexes[i]
                                mimeDataProvider.saveDataToModel(0, selected_index, model)
                                target_cells.add(tableView.cellAtIndex(selected_index))
                            }
                        } else {
                            const old_cell = mimeDataProvider.cellAt(0)
                            let new_cell = Qt.point(old_cell.x, old_cell.y)
                            new_cell.x += current_cell.x - mimeDataProvider.sourceCell.x
                            new_cell.y += current_cell.y - mimeDataProvider.sourceCell.y
                            mimeDataProvider.saveDataToModel(0, index(new_cell.y, new_cell.x), model)
                            target_cells.add(new_cell)
                        }
                    } else if (mimeDataProvider.size() > 1) {
                        for (let i = 0; i < mimeDataProvider.size(); ++i) {
                            let cell_i = mimeDataProvider.cellAt(i)
                            cell_i.x += current_cell.x - mimeDataProvider.sourceCell.x
                            cell_i.y += current_cell.y - mimeDataProvider.sourceCell.y
                            mimeDataProvider.saveDataToModel(i, index(cell_i.y, cell_i.x), model)
                            target_cells.add(cell_i)
                        }
                    }
                    if (mimeDataProvider.includeCutData) {
                        for (let i = 0; i < mimeDataProvider.size(); ++i) {
                            const cell_i = mimeDataProvider.cellAt(i)
                            if (!target_cells.has(cell_i))
                                model.clearItemData(index(cell_i.y, cell_i.x))
                        }
                        mimeDataProvider.includeCutData = false
                    }
                    visibleCellsConnection.blockConnection(false)
                    visibleCellsConnection.updateViewArea()
                }

                function resetColumnReordering()
                {
                    clearColumnReordering()
                    model.resetColumnMapping()
                }

                function resetRowReordering()
                {
                    clearRowReordering()
                    model.resetRowMapping()
                }

                ScrollBar.horizontal: ScrollBar { }
                ScrollBar.vertical: ScrollBar { }

                rowHeightProvider: function(row) {
                    const height = explicitRowHeight(row)
                    if (height === 0)
                        return 0
                    else if (height > 0)
                        return Math.max(height, 30)
                    return implicitRowWidth(row)
                }

                columnWidthProvider: function(column) {
                    const width = explicitColumnWidth(column)
                    if (width === 0)
                        return 0
                    else if (width > 0)
                        return Math.max(width, 30)
                    return implicitColumnWidth(column)
                }

                delegate: TableCell {
                    required property var model

                    implicitWidth: 90
                    implicitHeight: 36
                    text: model.display ?? ""
                    // We don't create data for empty cells to reduce
                    // the memory usage in case of huge model.
                    // If a cell does not have data and it's not highlighted neither
                    // the model.highlight is undefined which is replaced with false value.
                    highlight: model.highlight ?? false
                    edit: model.edit ?? ""

                    onCommit: text => model.edit = text
                }

                Keys.onPressed: function (event) {
                    if (event.matches(StandardKey.Copy)) {
                        copyToClipboard()
                    } else if (event.matches(StandardKey.Cut)) {
                        cutToClipboard()
                    } else if (event.matches(StandardKey.Paste)) {
                        pasteFromClipboard()
                    } else if (event.matches(StandardKey.Delete)) {
                        visibleCellsConnection.blockConnection()
                        if (selectionModel.hasSelection)
                            model.clearItemData(selectionModel.selectedIndexes)
                        else
                            model.clearItemData(selectionModel.currentIndex)
                        visibleCellsConnection.blockConnection(false)
                        visibleCellsConnection.updateViewArea()
                    }
                }

                Connections {
                    id: visibleCellsConnection
                    target: SpreadModel

                    function onDataChanged(tl, br, roles)
                    {
                        updateViewArea()
                    }

                    // The model is updated, then the visible area needs to be updated as well.
                    // Maybe some cells need to get the display data again
                    // due to their data, if it's a formula.
                    function updateViewArea()
                    {
                        for (let row = tableView.topRow; row <= tableView.bottomRow; ++row) {
                            for (let column = tableView.leftColumn; column <= tableView.rightColumn; ++column) {
                                SpreadModel.update(row, column)
                            }
                        }
                    }

                    // Blocks/unblocks the connection. This function is useful when
                    // some actions may update a large amount of data in the model,
                    // or may update a cell which affects other cells,
                    // for example clipboard actions and drag/drop actions.
                    // Block the connection, update the model, unblock the connection,
                    // and the call the updateViewArea() function to update the view.
                    function blockConnection(block=true)
                    {
                        visibleCellsConnection.enabled = !block
                    }
                }

                MouseArea {
                    id: dragArea

                    property point dragCell: Qt.point(-1, -1)
                    property bool hadSelection: false

                    anchors.fill: parent
                    drag.axis: Drag.XandYAxis
                    drag.target: dropArea
                    acceptedButtons: Qt.LeftButton
                    cursorShape: drag.active ? Qt.ClosedHandCursor : Qt.ArrowCursor

                    onPressed: function(mouse) {
                        mouse.accepted = false
                        // only when Alt modifier is pressed
                        if (mouse.modifiers !== Qt.AltModifier)
                            return
                        // check cell under press position
                        const position = Qt.point(mouse.x, mouse.y)
                        const cell = tableView.cellAtPosition(position, true)
                        if (cell.x < 0 || cell.y < 0)
                            return
                        // check selected indexes
                        const index = tableView.index(cell.y, cell.x)
                        hadSelection = selectionModel.hasSelection
                        if (!hadSelection)
                            selectionModel.select(index, ItemSelectionModel.Select)
                        if (!selectionModel.isSelected(index))
                            return
                        // store selected data
                        mimeDataProvider.reset()
                        mimeDataProvider.loadSelectedData()
                        // accept dragging
                        if (mimeDataProvider.size() > 0) {
                            mouse.accepted = true
                            dragCell = cell
                        }

                        dropArea.startDragging()
                    }

                    onReleased: {
                        dropArea.stopDragging()
                        // reset selection, if dragging caused the selection
                        if (!hadSelection)
                            selectionModel.clearSelection()
                        hadSelection = false
                        dragCell = Qt.point(-1, -1)
                    }
                }
            }

            DropArea {
                id: dropArea

                property point dropCell: Qt.point(-1, -1)

                anchors.fill: tableView
                Drag.active: dragArea.drag.active

                function startDragging()
                {
                    // block updating visible area
                    visibleCellsConnection.blockConnection()
                }

                function stopDragging()
                {
                    Drag.drop()
                    // unblock update visible area
                    visibleCellsConnection.blockConnection(false)
                    visibleCellsConnection.updateViewArea()  // now update visible area
                }

                onDropped: {
                    const position = Qt.point(dragArea.mouseX, dragArea.mouseY)
                    dropCell = tableView.cellAtPosition(position, true)
                    if (dropCell.x < 0 || dropCell.y < 0)
                        return
                    if (dragArea.dragCell === dropCell)
                        return

                    tableView.model.clearItemData(selectionModel.selectedIndexes)
                    for (let i = 0; i < mimeDataProvider.size(); ++i) {
                        let cell = mimeDataProvider.cellAt(i)
                        cell.x += dropCell.x - dragArea.dragCell.x
                        cell.y += dropCell.y - dragArea.dragCell.y
                        const index = tableView.index(cell.y, cell.x)
                        mimeDataProvider.saveDataToModel(i, index, tableView.model)
                    }
                    mimeDataProvider.reset()
                    selectionModel.clearSelection()

                    const drop_index = tableView.index(dropCell.y, dropCell.x)
                    selectionModel.setCurrentIndex(drop_index, ItemSelectionModel.Current)

                    tableView.model.clearHighlight()
                }

                onPositionChanged: {
                    const position = Qt.point(dragArea.mouseX, dragArea.mouseY)
                    // cell is the cell that currently mouse is over it
                    const cell = tableView.cellAtPosition(position, true)
                    // dropCell is the cell that it was under the mouse's last position
                    // if the last and current cells are the same, then there is no need
                    // to update highlight, as nothing is changed since last time.
                    if (cell === dropCell)
                        return
                    // if something is changed, it means that if the current cell is changed,
                    // then clear highlighted cells and update the dropCell.
                    tableView.model.clearHighlight()
                    dropCell = cell
                    // if the current cell was invalid (mouse is out side of the TableView)
                    // then no need to update highlight
                    if (cell.x < 0 || cell.y < 0)
                        return
                    // if dragged cell is the same as the (possibly) dropCell
                    // then no need to highlight any cells
                    if (dragArea.dragCell === dropCell)
                        return
                    // if the dropCell is not the same as the dragging cell and also
                    // is not the same as the cell at the mouse's last position
                    // then highlights the target cells
                    for (let i in selectionModel.selectedIndexes) {
                        const old_index = selectionModel.selectedIndexes[i]
                        let cell = tableView.cellAtIndex(old_index)
                        cell.x += dropCell.x - dragArea.dragCell.x
                        cell.y += dropCell.y - dragArea.dragCell.y
                        const new_index = tableView.index(cell.y, cell.x)
                        tableView.model.setHighlight(new_index, true)
                    }
                }
            }
        }
    }

    SelectionRectangle {
        id: selectionRectangle
        target: tableView
        selectionMode: SelectionRectangle.Auto

        topLeftHandle: Rectangle {
            width: 20
            height: 20
            radius: 10
            color: Qt.styleHints.colorScheme === Qt.Light ? palette.highlight.lighter(1.4)
                                                          : palette.highlight.darker(1.4)
            visible: SelectionRectangle.control.active
        }

        bottomRightHandle: Rectangle {
            width: 20
            height: 20
            radius: 10
            color: Qt.styleHints.colorScheme === Qt.Light ? palette.highlight.lighter(1.4)
                                                          : palette.highlight.darker(1.4)
            visible: SelectionRectangle.control.active
        }
    }

    SpreadSelectionModel {
        id: selectionModel
        behavior: SpreadSelectionModel.SelectCells
    }

    SpreadMimeDataProvider {
        id: mimeDataProvider

        property bool includeCutData: false
        property point sourceCell: Qt.point(-1, -1)

        function loadSelectedData()
        {
            for (let i in selectionModel.selectedIndexes) {
                const index = selectionModel.selectedIndexes[i]
                const cell = tableView.cellAtIndex(index)
                loadDataFromModel(cell, index, tableView.model)
            }
        }

        function resetProvider()
        {
            sourceCell = Qt.point(-1, -1)
            includeCutData = false
            reset()
        }
    }

    HelpDialog {
        id: helpDialog
        anchors.centerIn: parent
    }
}
