/*
 * Copyright (C) 2023 Savoir-faire Linux Inc.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts

import net.jami.Constants 1.1
import net.jami.Models 1.1

// This object should be implemented as a QML singleton, or be instantiated
// once in the main application window component. The top-level application window
// contains a loader[mainview, wizardview] and "rootView" MUST parent a horizontal
// SplitView with a StackView in each pane.
QtObject {
    id: root

    required property QtObject viewManager

    signal requestAppWindowWizardView

    // A map of view names to file paths for QML files that define each view.
    property variant resources: {
        "WelcomePage": "mainview/components/WelcomePage.qml",
        "SidePanel": "mainview/components/SidePanel.qml",
        "ConversationView": "mainview/ConversationView.qml",
        "NewSwarmPage": "mainview/components/NewSwarmPage.qml",
        "WizardView": "wizardview/WizardView.qml",
        "SettingsView": "settingsview/SettingsView.qml",
    }

    // Maybe this state needs to be toggled because the SidePanel content is replaced.
    // This makes it so the state can't be inferred from loaded views in single pane mode.
    property bool inSettings: viewManager.hasView("SettingsView")
    property bool inWizard: viewManager.hasView("WizardView")
    property bool inNewSwarm: viewManager.hasView("NewSwarmPage")
    property bool inhibitConversationView: inSettings || inWizard || inNewSwarm

    property bool busy: false

    // The `main` view of the application window.
    property Item rootView: null

    // HACKS.
    property real mainViewWidth: rootView ? rootView.width : 0
    property real previousWidth: mainViewWidth
    property real mainViewSidePanelRectWidth: sv1 ? sv1.width : 0
    property real lastSideBarSplitSize: mainViewSidePanelRectWidth
    onMainViewWidthChanged: resolvePanes()

    function resolvePanes(force=false) {
        if (forceSinglePane) return
        const isExpanding = previousWidth < mainViewWidth
        if (mainViewWidth < JamiTheme.chatViewHeaderMinimumWidth + mainViewSidePanelRectWidth
                && sv2.visible && (!isExpanding || force)) {
            lastSideBarSplitSize = mainViewSidePanelRectWidth
            singlePane = true
        } else if (mainViewWidth >= lastSideBarSplitSize + JamiTheme.chatViewHeaderMinimumWidth
                   && !sv2.visible && (isExpanding || force) && !layoutManager.isFullScreen) {
            singlePane = false
        }
        previousWidth = mainViewWidth
    }

    // Must be the child of `rootView`.
    property Item splitView: null

    // StackView objects, which are children of `splitView`.
    property StackView sv1: null
    property StackView sv2: null

    // The StackView object that is currently active, determined by the value
    // of singlePane.
    readonly property StackView activeStackView: singlePane ? sv1 : sv2

    readonly property string currentViewName: {
        if (activeStackView == null || activeStackView.depth === 0) return ''
        return activeStackView.currentItem.objectName
    }

    readonly property var currentView: {
        return activeStackView ? activeStackView.currentItem : null
    }

    // Handle single/dual pane mode.
    property bool forceSinglePane: false
    property bool singlePane
    onForceSinglePaneChanged: {
        if (forceSinglePane) singlePane = true
        else resolvePanes(true)
    }

    onSinglePaneChanged: {
        // Hiding sv2 before moving items from, and after moving
        // items to, reduces stack item visibility change events.
        if (singlePane) {
            sv2.visible = false
            if (forceSinglePane) Qt.callLater(move, sv2, sv1)
            else move(sv2, sv1)
        } else {
            move(sv1, sv2)
            sv2.visible = true
        }
    }

    // Emitted once at the end of setRootView.
    signal initialized()

    // Create, present, and return a dialog object.
    function presentDialog(parent, path, props={}) {
        // Open the dialog once the object is created
        return viewManager.createView(path, parent, function(obj) {
            const doneCb = function() { viewManager.destroyView(path) }
            if (obj.closed !== undefined) {
                obj.closed.connect(doneCb)
            } else {
                if (obj.accepted !== undefined) { obj.accepted.connect(doneCb) }
                if (obj.rejected !== undefined) { obj.rejected.connect(doneCb) }
            }
            obj.open()
        }, props)
    }

    // Dismiss all views.
    function dismissAll() {
        for (var path in viewManager.views) {
            viewManager.destroyView(path)
        }
    }

    // Get a view regardless of whether it is currently active.
    function getView(viewName) {
        if (!viewManager.hasView(viewName)) {
            return null
        }
        return viewManager.views[viewManager.viewPaths[viewName]]
    }

    // Sets object references, onInitialized is a good time to present views.
    function setRootView(obj) {
        rootView = obj
        splitView = rootView.splitView
        sv1 = rootView.sv1
        sv1.parent = Qt.binding(() => singlePane ? rootView : splitView)
        sv1.anchors.fill = Qt.binding(() => singlePane ? rootView : undefined)
        sv2 = rootView.sv2

        initialized()
        resolvePanes()
    }

    // Finds a view and gets its index within the StackView it's in.
    function getStackIndex(viewName) {
        for (const [key, value] of Object.entries(viewManager.views)) {
            if (value.objectName === viewName) {
                return value.StackView.index
            }
        }
        return -1
    }

    // Load a view without presenting it.
    function preload(viewName) {
        if (!viewManager.createView(resources[viewName], null)) {
            console.log("Failed to load view: " + viewName)
        }
    }

    // This function presents the view with the given viewName in the
    // specified StackView. Return the view if successful.
    function present(viewName, sv=activeStackView) {
        if (!rootView) return

        if (viewName === "ConversationView" && inhibitConversationView) {
            return
        }

        // If the view already exists in the StackView, the function will attempt
        // to navigate to its StackView position by dismissing elevated views.
        if (sv.find(function(item) {
            return item.objectName === viewName;
        })) {
            const viewIndex = getStackIndex(viewName)
            if (viewIndex >= 0) {
                for (var i = (sv.depth - 1); i > viewIndex; i--) {
                    dismissObj(sv.get(i, StackView.DontLoad))
                }
                return true
            }
            return false
        }

        // If we are in single-pane mode and the view was previously forced into
        // sv2, we can move it back to the top of sv1.
        if (singlePane && sv === sv1) {
            // See if the item is at the top of sv2
            if (sv2.currentItem && sv2.currentItem.objectName === viewName) {
                // Move it to the top of sv1
                const view = sv2.pop(StackView.Immediate)
                sv1.push(view, StackView.Immediate)
                view.presented()
                return view
            }
        }

        const obj = viewManager.createView(resources[viewName], appWindow)
        if (!obj) {
            print("could not create view:", viewName)
            return null
        }
        if (obj === currentView) {
            print("view is current:", viewName)
            return null
        }

        // If we are in single-pane mode and the view should start hidden
        // (requiresIndex), we can push it into sv2.
        if (singlePane && sv === sv1 && obj.requiresIndex) {
            sv = sv2
        } else {
            forceSinglePane = obj.singlePaneOnly
        }

        const view = sv.push(obj, StackView.Immediate)
        if (!view) {
            return null
        }
        if (view.objectName === '') {
            view.objectName = viewName
        }
        view.presented()
        return view
    }

    // Dismiss by object.
    function dismissObj(obj, sv=activeStackView) {
        if (obj.StackView.view !== sv) {
            print("view not in the stack:", obj)
            return
        }

        // If we are dismissing a view that is not at the top of the stack,
        // we need to store each of the views on top into a temporary stack
        // and then restore them after the view is dismissed.
        // So we get the index of the view we are dismissing.
        const viewIndex = obj.StackView.index
        var tempStack = []
        for (var i = (sv.depth - 1); i > viewIndex; i--) {
            var item = sv.pop(StackView.Immediate)
            tempStack.push(item)
        }
        // And we define a function to restore and resolve the views.
        var resolveStack = () => {
            for (var i = 0; i < tempStack.length; i++) {
                sv.push(tempStack[i], StackView.Immediate)
            }

            forceSinglePane = sv.currentItem.singlePaneOnly
            sv.currentItem.presented()
        }

        // Now we can dismiss the view at the top of the stack.
        const depth = sv.depth
        if (obj === sv.get(depth - 1, StackView.DontLoad)) {
            var view = sv.pop(StackView.Immediate)
            if (!view) {
                print("could not pop view:", obj.objectName)
                resolveStack()
                return
            }

            // If the view is managed, we can destroy it, otherwise, it can
            // be reused and destroyed by it's parent.
            if (view.managed) {
                var objectName = view ? view.objectName : obj.objectName
                if (!viewManager.destroyView(resources[objectName])) {
                    print("could not destroy view:", objectName)
                }
            } else {
                view.dismissed()
            }
        }
        resolveStack()
    }

    // Dismiss by view name.
    function dismiss(viewName) {
        if (!rootView) return

        const depth = activeStackView.depth
        for (var i = (depth - 1); i >= 0; i--) {
            const view = activeStackView.get(i, StackView.DontLoad)
            if (view.objectName === viewName) {
                dismissObj(view)
                return
            }
        }

        // Check if the view is hidden on the top of sv2 (if in single-pane mode),
        // and dismiss it in that case.
        if (singlePane && sv2.currentItem && sv2.currentItem.objectName === viewName) {
            dismissObj(sv2.currentItem, sv2)
        }
    }

    // Move items from one stack to another. We avoid the recursive technique to
    // avoid  visibility change events.
    function move(from, to, depth=1) {
        busy = true
        var tempStack = []
        while (from.depth > depth) {
            var item = from.pop(StackView.Immediate)
            tempStack.push(item)
        }
        while (tempStack.length) {
            to.push(tempStack.pop(), StackView.Immediate)
        }
        busy = false
    }

    // Effectively hide the current view by moving it to the other StackView.
    // This function only works when in single-pane mode.
    function hideCurrentView() {
        if (singlePane) move(sv1, sv2)
    }
}
