/*
 * Copyright (C) 2024-2026 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
import "commoncomponents"

QtObject {
    id: root

    // When true, getView(..., forceCreate) will not create views (for test harness).
    property bool isTestContext: false

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

    // The `main` view of the application window.
    property StackView rootView
    property ViewManager viewManager
    property var appContext: null

    readonly property Item currentView: rootView && rootView.currentItem || null
    readonly property var currentViewName: currentView && currentView.objectName || null
    readonly property bool isDualPane: currentView && currentView instanceof DualPaneView
    readonly property bool isInSinglePaneMode: !isDualPane || currentView.isSinglePane
    readonly property bool isRTL: Qt.application.layoutDirection === Qt.RightToLeft
    // A list of the current visible views. This could be a single view or two views in
    // dual pane mode. The list is ordered [minor, major] where major is the view on the
    // right side when not in RTL and should represent the main or content-type view.
    readonly property var visibleViews: {
        if (!currentView)
            return [];
        if (isDualPane) {
            if (isInSinglePaneMode)
                return [currentView.rightPaneItem];
            return [currentView.leftPaneItem, currentView.rightPaneItem];
        }
        return [currentView];
    }
    // Aggregate this info and expose it as a single string for convenience.
    // JSON indented by 2 spaces.
    readonly property string currentViewInfo: {
        var info = {
            currentViewName: currentViewName,
            isDualPane: isDualPane,
            isInSinglePaneMode: isInSinglePaneMode,
            visibleViews: visibleViews.map(function (view) {
                    return view && view.objectName || null;
                }),
            visibleViewWidths: visibleViews.map(function (view) {
                    return view && view.width || null;
                })
        };
        return JSON.stringify(info, null, 2);
    }

    function init(mainStackView) {
        rootView = Qt.createQmlObject(`import QtQuick; import QtQuick.Controls
                                      StackView { anchors.fill: parent }`, mainStackView);
    }

    function deinit() {
        viewManager.destroyAllViews();
        if (rootView)
            rootView.destroy();
    }

    // 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;
    }

    // Create, present, and return a dialog object.
    function presentDialog(parent, path, props = {}, singleInstance = false) {
        // Open the dialog once the object is created
        const manager = root.viewManager;
        const createdDialog = function (obj, viewName) {
                const doneCb = function () {
                    if (manager && manager.destroyView)
                        manager.destroyView(viewName);
                };
                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();
            };
        return singleInstance ? manager.createView(path, parent, createdDialog, props)
                              : manager.createUniqueView(path, parent, createdDialog, props);
    }

    // Present a view by name.
    function present(viewName, props) {
        if (isTestContext)
            return null;

        const path = resources[viewName];

        // Check if the current view should inhibit the presentation of this view.
        if (rootView.currentItem && rootView.currentItem.inhibits.includes(viewName)) {
            print("inhibiting view:", viewName);
            return;
        }

        // If the view already exists in the StackView, the function will attempt
        // to navigate to its StackView position by dismissing elevated views.
        if (rootView.find(function (item) {
                return item.objectName === viewName;
            }, StackView.DontLoad)) {
            const viewIndex = getStackIndex(viewName);
            for (var i = (rootView.depth - 1); i > viewIndex; i--) {
                dismissObj(rootView.get(i, StackView.DontLoad));
            }
            return;
        }
        if (!viewManager.createView(path, rootView, function (view) {
                if (view.hasOwnProperty("appContext"))
                    view.appContext = appContext;
                // push the view onto the stack if it's not already there
                if (rootView.currentItem !== view) {
                    rootView.push(view, StackView.Immediate);
                }
                if (!view.managed)
                    view.presented();
            }, props)) {
            print("An error occurred while creating view:", viewName);
        }
    }

    // Dismiss by object.
    function dismissObj(obj) {
        // Check if it makes sense to remove this view at all.
        if (obj.StackView.view !== rootView || !viewManager.viewCount()) {
            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.
        const viewIndex = obj.StackView.index;
        var tempStack = [];
        for (var i = (rootView.depth - 1); i > viewIndex; i--) {
            var item = rootView.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++) {
                rootView.push(tempStack[i], StackView.Immediate);
            }
            if (rootView.depth > 0)
                rootView.currentItem.presented();
        };

        // Now we can dismiss the view at the top of the stack.
        const depth = rootView.depth;
        if (obj === rootView.get(depth - 1, StackView.DontLoad)) {
            var view;
            if (rootView.depth === 1) {
                view = rootView.currentItem;
                rootView.clear();
            } else
                view = rootView.pop(StackView.Immediate);
            if (!view) {
                print("An error occurred while attempting to 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("An error occurred while attempting to destroy view:", objectName);
                } else {
                    print("destroyed view:", objectName);
                }
            } else
                view.dismissed();
        }
        resolveStack();
    }

    // Dismiss a view by name or the top view if unspecified.
    function dismiss(viewName = undefined) {
        if (!rootView || rootView.depth === 0)
            return;
        if (viewName !== undefined) {
            const depth = rootView.depth;
            for (var i = (depth - 1); i >= 0; i--) {
                const view = rootView.get(i, StackView.DontLoad);
                if (view.objectName === viewName) {
                    dismissObj(view);
                    return;
                }
            }
            return;
        } else {
            dismissObj(rootView.currentItem);
        }
    }

    function getView(viewName, forceCreate = false) {
        // If the view is already loaded, return it.
        var view = viewManager.getView(viewName);
        if (view)
            return view;
        if (!forceCreate || isTestContext)
            return null;
        // Otherwise, create it.
        view = viewManager.createView(resources[viewName], null);
        if (!view) {
            console.log("Failed to load view: " + viewName);
            return null;
        }
        if (view.hasOwnProperty("appContext"))
            view.appContext = appContext;
        return view;
    }

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