/*
 *  SPDX-FileCopyrightText: 2018 Jouni Pentikäinen <joupent@gmail.com>
 *
 *  SPDX-License-Identifier: GPL-2.0-or-later
 */
#include "KisWindowLayoutResource.h"
#include "KisWindowLayoutManager.h"

#include <QVector>
#include <QList>
#include <QFile>
#include <QDomDocument>
#include <QApplication>
#include <QEventLoop>
#include <QMessageBox>
#include <QWindow>
#include <QScreen>

#include <KisPart.h>
#include <KisDocument.h>
#include <kis_dom_utils.h>
#include <KisMainWindow.h>

static const int WINDOW_LAYOUT_VERSION = 1;

struct KisWindowLayoutResource::Private
{
    struct WindowGeometry{
        int screen = -1;
        Qt::WindowStates stateFlags = Qt::WindowNoState;
        QByteArray data;

        static WindowGeometry fromWindow(const QWidget *window, QList<QScreen*> screens)
        {
            WindowGeometry geometry;
            QWindow *windowHandle = window->windowHandle();

            geometry.data = window->saveGeometry();
            geometry.stateFlags = windowHandle->windowState();

            int index = screens.indexOf(windowHandle->screen());
            if (index >= 0) {
                geometry.screen = index;
            }

            return geometry;
        }

        void forceOntoCorrectScreen(QWidget *window, QList<QScreen*> screens)
        {
            QWindow *windowHandle = window->windowHandle();

            if (screens.indexOf(windowHandle->screen()) != screen) {
                QScreen *qScreen = screens[screen];
                windowHandle->setScreen(qScreen);
                windowHandle->setPosition(qScreen->availableGeometry().topLeft());
            }

            if (stateFlags) {
                window->setWindowState(stateFlags);
            }
        }

        void save(QDomDocument &doc, QDomElement &elem) const
        {
            if (screen >= 0) {
                elem.setAttribute("screen", screen);
            }

            if (stateFlags & Qt::WindowMaximized) {
                elem.setAttribute("maximized", "1");
            }

            QDomElement geometry = doc.createElement("geometry");
            geometry.appendChild(doc.createCDATASection(data.toBase64()));
            elem.appendChild(geometry);
        }

        static WindowGeometry load(const QDomElement &element)
        {
            WindowGeometry geometry;
            geometry.screen = element.attribute("screen", "-1").toInt();

            if (element.attribute("maximized", "0") != "0") {
                geometry.stateFlags |= Qt::WindowMaximized;
            }

            QDomElement dataElement = element.firstChildElement("geometry");
            geometry.data = QByteArray::fromBase64(dataElement.text().toLatin1());

            return geometry;
        }
    };

    struct Window {
        QUuid windowId;
        QByteArray windowState;
        WindowGeometry geometry;

        bool canvasDetached = false;
        WindowGeometry canvasWindowGeometry;
    };

    QVector<Window> windows;
    bool showImageInAllWindows {false};
    bool primaryWorkspaceFollowsFocus {false};
    QUuid primaryWindow;

    Private() = default;
    Private(const Private &rhs) = default;


    explicit Private(QVector<Window> windows)
        : windows(std::move(windows))
    {}

    void openNecessaryWindows(QList<QPointer<KisMainWindow>> &currentWindows) {
        auto *kisPart = KisPart::instance();

        Q_FOREACH(const Window &window, windows) {
            QPointer<KisMainWindow> mainWindow = kisPart->windowById(window.windowId);

            if (mainWindow.isNull()) {
                mainWindow = kisPart->createMainWindow(window.windowId);
                currentWindows.append(mainWindow);
                mainWindow->show();
            }
        }
        
        QVector<QPointer<KisMainWindow>> windowsToMigrate;

        Q_FOREACH(KisMainWindow *mainWindow, currentWindows) {
            bool keep = false;
            Q_FOREACH(const Window &window, windows) {
                if (window.windowId == mainWindow->id()) {
                    keep = true;
                    break;
                }
            }

            if (!keep) {
                windowsToMigrate.append(mainWindow);

                // Set the window hidden to prevent "show image in all windows" feature from opening new views on it
                // while we migrate views onto the remaining windows
                if (mainWindow->isVisible()) {
                    mainWindow->hide();
                }
            }
        }
        
        migrateViewsFromClosingWindows(windowsToMigrate);
    }

    void closeUnneededWindows(QList<QPointer<KisMainWindow>> &currentWindows) {
        QVector<QPointer<KisMainWindow>> windowsToClose;

        Q_FOREACH(KisMainWindow *mainWindow, currentWindows) {
            bool keep = false;
            Q_FOREACH(const Window &window, windows) {
                if (window.windowId == mainWindow->id()) {
                    keep = true;
                    break;
                }
            }

            if (!keep) {
                mainWindow->close();
            }
        }
    }

    void migrateViewsFromClosingWindows(QVector<QPointer<KisMainWindow>> &closingWindows) const
    {
        auto *kisPart = KisPart::instance();
        KisMainWindow *migrationTarget = nullptr;

        Q_FOREACH(KisMainWindow *mainWindow, kisPart->mainWindows()) {
            if (!closingWindows.contains(mainWindow)) {
                migrationTarget = mainWindow;
                break;
            }
        }

        if (!migrationTarget) {
            qWarning() << "Problem: window layout with no windows would leave user with zero main windows.";
            migrationTarget = closingWindows.takeLast();
            migrationTarget->show();
        }

        QVector<KisDocument*> visibleDocuments;
        Q_FOREACH(KisView *view, kisPart->views()) {
            KisMainWindow *window = view->mainWindow();
            if (!closingWindows.contains(window)) {
                visibleDocuments.append(view->document());
            }
        }

        Q_FOREACH(KisDocument *document, kisPart->documents()) {
            if (!visibleDocuments.contains(document)) {
                visibleDocuments.append(document);
                migrationTarget->newView(document);
            }
        }
    }


    QList<QScreen*> getScreensInConsistentOrder() {
        QList<QScreen*> screens = QGuiApplication::screens();

        std::sort(screens.begin(), screens.end(), [](const QScreen *a, const QScreen *b) {
            QRect aRect = a->geometry();
            QRect bRect = b->geometry();

            if (aRect.y() == bRect.y()) return aRect.x() < bRect.x();
            return (aRect.y() < bRect.y());
        });

        return screens;
    }
};

KisWindowLayoutResource::KisWindowLayoutResource(const QString &filename)
    : KoResource(filename)
    , d(new Private)
{}

KisWindowLayoutResource::~KisWindowLayoutResource()
{}

KisWindowLayoutResource::KisWindowLayoutResource(const KisWindowLayoutResource &rhs)
    : KoResource(rhs)
    , d(new Private(*rhs.d))
{
}

KoResourceSP KisWindowLayoutResource::clone() const
{
    return KoResourceSP(new KisWindowLayoutResource(*this));
}

KisWindowLayoutResourceSP KisWindowLayoutResource::fromCurrentWindows(
    const QString &filename, const QList<QPointer<KisMainWindow>> &mainWindows, bool showImageInAllWindows,
    bool primaryWorkspaceFollowsFocus, KisMainWindow *primaryWindow
)
{
    KisWindowLayoutResourceSP resource(new KisWindowLayoutResource(filename));
    resource->setWindows(mainWindows);
    resource->d->showImageInAllWindows = showImageInAllWindows;
    resource->d->primaryWorkspaceFollowsFocus = primaryWorkspaceFollowsFocus;
    resource->d->primaryWindow = primaryWindow->id();
    return resource;
}

void KisWindowLayoutResource::applyLayout()
{
    auto *kisPart = KisPart::instance();
    auto *layoutManager= KisWindowLayoutManager::instance();

    layoutManager->setLastUsedLayout(this);

    QList<QPointer<KisMainWindow>> currentWindows = kisPart->mainWindows();
    bool createWindows = d->windows.isEmpty();

    if (createWindows) {
        // No windows defined (e.g. fresh new session). Leave things as they are, but make sure there's at least one visible main window
        if (kisPart->mainwindowCount() == 0) {
            kisPart->createMainWindow();
        } else {
            kisPart->mainWindows().first()->show();
        }
    } else {
        d->openNecessaryWindows(currentWindows);
    }

    // Wait for the windows to finish opening / closing before applying saved geometry.
    // If we don't, the geometry may get reset after we apply it.
    QApplication::processEvents(QEventLoop::ExcludeUserInputEvents);

    Q_FOREACH(const auto &window, d->windows) {
        QPointer<KisMainWindow> mainWindow = kisPart->windowById(window.windowId);
        KIS_SAFE_ASSERT_RECOVER_BREAK(mainWindow);

        mainWindow->restoreGeometry(window.geometry.data);
        mainWindow->restoreWorkspaceState(window.windowState);

        mainWindow->setCanvasDetached(window.canvasDetached);
        if (window.canvasDetached) {
            QWidget *canvasWindow = mainWindow->canvasWindow();
            canvasWindow->restoreGeometry(window.canvasWindowGeometry.data);
        }
    }

    QApplication::processEvents(QEventLoop::ExcludeUserInputEvents);

    QList<QScreen*> screens = d->getScreensInConsistentOrder();
    Q_FOREACH(const auto &window, d->windows) {
        Private::WindowGeometry geometry = window.geometry;
        QPointer<KisMainWindow> mainWindow = kisPart->windowById(window.windowId);
        KIS_SAFE_ASSERT_RECOVER_BREAK(mainWindow);

        if (geometry.screen >= 0 && geometry.screen < screens.size()) {
            geometry.forceOntoCorrectScreen(mainWindow, screens);
        }
        if (window.canvasDetached) {
            Private::WindowGeometry canvasWindowGeometry = window.canvasWindowGeometry;
            if (canvasWindowGeometry.screen >= 0 && canvasWindowGeometry.screen < screens.size()) {
                canvasWindowGeometry.forceOntoCorrectScreen(mainWindow->canvasWindow(), screens);
            }
        }
    }

    layoutManager->setShowImageInAllWindowsEnabled(d->showImageInAllWindows);
    layoutManager->setPrimaryWorkspaceFollowsFocus(d->primaryWorkspaceFollowsFocus, d->primaryWindow);
    if (!createWindows) {
        d->closeUnneededWindows(currentWindows);
    }
}

bool KisWindowLayoutResource::saveToDevice(QIODevice *dev) const
{
    QDomDocument doc;
    QDomElement root = doc.createElement("WindowLayout");
    root.setAttribute("name", name());
    root.setAttribute("version", WINDOW_LAYOUT_VERSION);

    saveXml(doc, root);

    doc.appendChild(root);

    QTextStream textStream(dev);
    KisPortingUtils::setUtf8OnStream(textStream);
    doc.save(textStream, 4);
    return true;
}

bool KisWindowLayoutResource::loadFromDevice(QIODevice *dev, KisResourcesInterfaceSP resourcesInterface)
{
    Q_UNUSED(resourcesInterface);

    QDomDocument doc;
    if (!doc.setContent(dev)) {
        return false;
    }

    QDomElement element = doc.documentElement();
    setName(element.attribute("name"));

    d->windows.clear();

    loadXml(element);

    setValid(true);
    return true;
}

void KisWindowLayoutResource::saveXml(QDomDocument &doc, QDomElement &root) const
{
    root.setAttribute("showImageInAllWindows", (int)d->showImageInAllWindows);
    root.setAttribute("primaryWorkspaceFollowsFocus", (int)d->primaryWorkspaceFollowsFocus);
    root.setAttribute("primaryWindow", d->primaryWindow.toString());

    Q_FOREACH(const auto &window, d->windows) {
        QDomElement elem = doc.createElement("window");
        elem.setAttribute("id", window.windowId.toString());

        window.geometry.save(doc, elem);

        if (window.canvasDetached) {
            QDomElement canvasWindowElement = doc.createElement("canvasWindow");
            window.canvasWindowGeometry.save(doc, canvasWindowElement);
            elem.appendChild(canvasWindowElement);
        }

        QDomElement state = doc.createElement("windowState");
        state.appendChild(doc.createCDATASection(window.windowState.toBase64()));
        elem.appendChild(state);
        root.appendChild(elem);
    }
}

void KisWindowLayoutResource::loadXml(const QDomElement &element) const
{
    d->showImageInAllWindows = KisDomUtils::toInt(element.attribute("showImageInAllWindows", "0"));
    d->primaryWorkspaceFollowsFocus = KisDomUtils::toInt(element.attribute("primaryWorkspaceFollowsFocus", "0"));
    d->primaryWindow = QUuid::fromString(element.attribute("primaryWindow"));

#ifdef Q_OS_ANDROID
    if (element.firstChildElement("window") != element.lastChildElement("window")) {
        QMessageBox::warning(qApp->activeWindow(), i18nc("@title:window", "Krita"),
                             "Workspaces with multiple windows isn't supported on Android");
        return;
    }
#endif

    for (auto windowElement = element.firstChildElement("window");
         !windowElement.isNull();
         windowElement = windowElement.nextSiblingElement("window")) {

        Private::Window window;

        window.windowId = QUuid(windowElement.attribute("id", QUuid().toString()));
        if (window.windowId.isNull()) {
            window.windowId = QUuid::createUuid();
        }

        window.geometry = Private::WindowGeometry::load(windowElement);

        QDomElement canvasWindowElement = windowElement.firstChildElement("canvasWindow");
        if (!canvasWindowElement.isNull()) {
            window.canvasDetached = true;
            window.canvasWindowGeometry = Private::WindowGeometry::load(canvasWindowElement);
        }

        QDomElement state = windowElement.firstChildElement("windowState");
        window.windowState = QByteArray::fromBase64(state.text().toLatin1());

        d->windows.append(window);
    }
}

QString KisWindowLayoutResource::defaultFileExtension() const
{
    return QString(".kwl");
}

void KisWindowLayoutResource::setWindows(const QList<QPointer<KisMainWindow>> &mainWindows)
{
    d->windows.clear();

    QList<QScreen*> screens = d->getScreensInConsistentOrder();

    Q_FOREACH(auto window, mainWindows) {
        if (!window->isVisible()) continue;

        Private::Window state;
        state.windowId = window->id();
        state.windowState = window->saveState();
        state.geometry = Private::WindowGeometry::fromWindow(window, screens);

        state.canvasDetached = window->canvasDetached();
        if (state.canvasDetached) {
            state.canvasWindowGeometry = Private::WindowGeometry::fromWindow(window->canvasWindow(), screens);
        }

        d->windows.append(state);
    }
}
