// Copyright Mirage authors & contributors <https://github.com/mirukana/mirage>
// and Moment contributors <https://gitlab.com/mx-moment/moment>
// SPDX-License-Identifier: LGPL-3.0-or-later

// This file creates the application, registers custom objects for QML
// and launches Window.qml (the root component).

#include <QDataStream>  // must be first include to avoid clipboard.h errors
#include <QApplication>
#include <QQmlEngine>
#include <QQmlContext>
#include <QQmlComponent>
#include <QFileInfo>
#include <QQuickStyle>
#include <QFontDatabase>
#include <QDateTime>
#include <QStandardPaths>
#include <QDir>
#include <QFile>
#include <QLockFile>
#include <QMessageBox>
#include <QCommandLineParser>
#include <iostream>
#include <signal.h>

#ifdef Q_OS_UNIX
#include <unistd.h>
#endif

#include "utils.h"
#include "clipboard.h"
#include "clipboard_image_provider.h"


QLockFile *lockFile = nullptr;


void loggingHandler(
    QtMsgType type,
    const QMessageLogContext &context,
    const QString &msg
) {
    // Override default QML logger to provide colorful logging with times

    Q_UNUSED(context)

    // Hide dumb warnings about thing we can't fix without breaking
    // compatibilty with Qt < 5.14/5.15
    if (msg.contains("QML Binding: Not restoring previous value because"))
        return;

    if (msg.contains("QML Connections: Implicitly defined onFoo properties"))
        return;

    // Hide layout-related spam introduced in Qt 5.14
    if (msg.contains("Qt Quick Layouts: Detected recursive rearrange."))
        return;

    const char* level =
        type == QtDebugMsg ?    "~" :
        type == QtInfoMsg ?     "i" :
        type == QtWarningMsg ?  "!" :
        type == QtCriticalMsg ? "X" :
        type == QtFatalMsg ?    "F" :
        "?";

    QString boldColor = "", color = "", clearFormatting = "";

#ifdef Q_OS_UNIX
    // Don't output escape codes if stderr is piped or redirected to a file
    if (isatty(fileno(stderr))) {
        const QString ansiColor =
            type == QtInfoMsg ?     "2" :  // green
            type == QtWarningMsg ?  "3" :  // yellow
            type == QtCriticalMsg ? "1" :  // red
            type == QtFatalMsg ?    "5" :  // purple
            "4";                           // blue

        boldColor       = "\e[1;3" + ansiColor + "m";
        color           = "\e[3" + ansiColor + "m";
        clearFormatting = "\e[0m";
    }
#endif

    fprintf(
        stderr,
        "%s%s%s %s%s |%s %s\n",
        boldColor.toUtf8().constData(),
        level,
        clearFormatting.toUtf8().constData(),
        color.toUtf8().constData(),
        QDateTime::currentDateTime().toString("hh:mm:ss").toUtf8().constData(),
        clearFormatting.toUtf8().constData(),
        msg.toUtf8().constData()
    );
}


void onExitSignal(int signum) {
    QApplication::exit(128 + signum);
}


void migrateFile(QDir source, QDir destination, QString fname) {
    if (! QFile::copy(source.filePath(fname), destination.filePath(fname)))
        qWarning() << "Could not migrate" << fname;
}


void migrateShallowDirectory(
        QDir sourceParent, QDir destinationParent, QString dname
        ) {
    if (sourceParent.exists(dname)) {
        if (! destinationParent.mkpath(dname)) {
            qWarning() << "Could not create directory"
                << destinationParent.filePath(dname);
            exit(EXIT_FAILURE);
        }
        QDir source(sourceParent.filePath(dname));
        QDir destination(destinationParent.filePath(dname));
        QFileInfoList files = source.entryInfoList();
        for (QFileInfo file : files) {
            if(file.fileName() == "." || file.fileName() == "..")
                continue;
            migrateFile(source, destination, file.fileName());
        }
    }
}


void offerMigrateFromMirage(
        QDir configDirMoment, QDir configDirMirage,
        QDir dataDirMoment, QDir dataDirMirage
        ) {
    QMessageBox dialog;
    dialog.setText("Would you like Moment to re-use your logins, "
            "encryption keys, configuration and themes from Mirage?");
    dialog.setInformativeText(
            "Will copy "+configDirMirage.path()+" → "+configDirMoment.path()
            +"\nWill copy "+dataDirMirage.path()+" → "+dataDirMoment.path());
    dialog.addButton(QMessageBox::Yes);
    dialog.addButton(QMessageBox::No);
    dialog.addButton(QMessageBox::Cancel);
    int result = dialog.exec();
    if (result == QMessageBox::Yes) {
        qWarning("Migrating config and data from Mirage");
        if (! configDirMoment.mkpath(".")) {
            qFatal("Could not create config directory");
            exit(EXIT_FAILURE);
        }
        if (! dataDirMoment.mkpath(".")) {
            qFatal("Could not create data directory");
            exit(EXIT_FAILURE);
        }
        migrateFile(configDirMirage, configDirMoment, "settings.py");
        migrateFile(configDirMirage, configDirMoment, "settings.gui.json");
        migrateFile(configDirMirage, configDirMoment, "accounts.json");
        migrateFile(dataDirMirage, dataDirMoment, "history.json");
        migrateFile(dataDirMirage, dataDirMoment, "state.json");
        migrateShallowDirectory(dataDirMirage, dataDirMoment, "themes");
        migrateShallowDirectory(dataDirMirage, dataDirMoment, "encryption");
    } else if (result == QMessageBox::No) {
        // Nothing to do. Proceed with starting the app
        qWarning("Not migrating");
        return;
    } else {
        // Neither "Yes" nor "No" was chosen.
        // We can't know what the user wants. Just quit.
        qWarning("Quitting. You can decide about migration next time.");
        exit(EXIT_SUCCESS);
    }
}


bool shouldMigrateFromMirage() {
    QString genericConfig = QStandardPaths::writableLocation(
            QStandardPaths::GenericConfigLocation);
    QString genericData = QStandardPaths::writableLocation(
            QStandardPaths::GenericDataLocation);

    // Check whether Moment config already exists
    {
        QString customConfigDirMoment(
            qEnvironmentVariable("MOMENT_CONFIG_DIR")
        );
        if (! customConfigDirMoment.isEmpty()) {
            // MOMENT_CONFIG_DIR is set.
            // Moment would definitely use this as the config directory.
            if (QDir(customConfigDirMoment).exists())
                // But it already exists.
                // So this is not the first time Moment was started.
                return false;
        } else {
            // No MOMENT_CONFIG_DIR, so check the default config directory.
            if (QDir(genericConfig + "/moment").exists())
                // The default config folder exists.
                // So this is not the first time Moment was started.
                return false;
        }
    }

    // Check whether Moment data already exists
    {
        QString customDataDirMoment(qEnvironmentVariable("MOMENT_DATA_DIR"));
        if (! customDataDirMoment.isEmpty()) {
            // MOMENT_DATA_DIR is set.
            // Moment would definitely use this as the data directory.
            if (QDir(customDataDirMoment).exists())
                // But it already exists.
                // So this is not the first time Moment was started.
                return false;
        } else {
            // No MOMENT_DATA_DIR, so check the default data directory.
            if (QDir(genericData + "/moment").exists())
                // The default data folder exists.
                // So this is not the first time Moment was started.
                return false;
        }
    }

    // Check whether Mirage config exists
    {
        QString customConfigDirMirage(
            qEnvironmentVariable("MIRAGE_CONFIG_DIR")
        );
        if (! customConfigDirMirage.isEmpty()) {
            // MIRAGE_CONFIG_DIR is set.
            // Mirage would definitely use this as the config directory.
            if (! QDir(customConfigDirMirage).exists())
                // But this directory does not exist.
                // So there is nowhere to migrate from.
                return false;
        } else {
            // No MIRAGE_CONFIG_DIR, so check the default config directory.
            // Check /matrix-mirage (Debian) first, since it is more specific
            if (! QDir(genericConfig + "/matrix-mirage").exists())
                // Default Debian config folder does not exist,
                // so check whether the normal /mirage exists
                if (! QDir(genericConfig + "/mirage").exists())
                    // No, neither /matrix-mirage nor /mirage exist.
                    // So there is nowhere to migrate from.
                    return false;
        }
    }

    // We found out that there is no Moment config dir nor Moment data dir,
    // but there is a Mirage config dir which can be migrated from.
    // We could also check for Mirage data dir but it doesn't really matter.
    // User should definitely be prompted for migration.
    return true;
}


void tryMigrateFromMirage() {
    QString genericConfig = QStandardPaths::writableLocation(
            QStandardPaths::GenericConfigLocation);
    QString genericData = QStandardPaths::writableLocation(
            QStandardPaths::GenericDataLocation);

    QDir configDirMoment, configDirMirage, dataDirMoment, dataDirMirage;

    QString customConfigDirMoment(qEnvironmentVariable("MOMENT_CONFIG_DIR"));
    configDirMoment = QDir(customConfigDirMoment.isEmpty()
            ? genericConfig + "/moment" : customConfigDirMoment);

    QString customDataDirMoment(qEnvironmentVariable("MOMENT_DATA_DIR"));
    dataDirMoment = QDir(customDataDirMoment.isEmpty()
            ? genericData + "/moment" : customDataDirMoment);

    QString customConfigDirMirage(qEnvironmentVariable("MIRAGE_CONFIG_DIR"));
    if (! customConfigDirMirage.isEmpty()) {
        // MIRAGE_CONFIG_DIR is set.
        // Mirage would definitely use this as the config directory.
        // So this is where we should migrate from.
        configDirMirage = QDir(customConfigDirMirage);
    } else {
        // No MIRAGE_CONFIG_DIR.
        // Check if Mirage default config directory exists
        // Check /matrix-mirage (Debian) first
        QDir dirDeb(genericConfig + "/matrix-mirage");
        if (dirDeb.exists()) {
            // Default Debian config dir exists.
            // So this is where we should migrate from.
            configDirMirage = dirDeb;
        } else {
            // No /matrix-mirage found, so check /mirage
            QDir dir(genericConfig + "/mirage");
            if (dir.exists())
                // Default config dir exists.
                // So this is where we should migrate from.
                configDirMirage = dir;
            else
                // No Mirage config dir found.
                // Do not migrate.
                return;
        }
    }

    QString customDataDirMirage(qEnvironmentVariable("MIRAGE_DATA_DIR"));
    if (! customDataDirMirage.isEmpty()) {
        // MIRAGE_DATA_DIR is set.
        // Mirage would definitely use this as the data directory.
        // So this is where we should migrate from.
        dataDirMirage = QDir(customDataDirMirage);
    } else {
        // No MIRAGE_DATA_DIR.
        // Check if Mirage default data directory exists
        // Check /matrix-mirage (Debian) first
        QDir dirDeb(genericData + "/matrix-mirage");
        if (dirDeb.exists()) {
            // Default Debian data dir exists.
            // So this is where we should migrate from.
            dataDirMirage = dirDeb;
        } else {
            // No /matrix-mirage found, so check /mirage
            QDir dir(genericData + "/mirage");
            if (dir.exists())
                // Default data dir exists.
                // So this is where we should migrate from.
                dataDirMirage = dir;
            else
                // No Mirage data dir found.
                // Do not migrate.
                return;
        }
    }

    offerMigrateFromMirage(
        configDirMoment, configDirMirage, dataDirMoment, dataDirMirage
    );
}


bool setLockFile(QString configPath) {

    QDir settingsFolder(configPath);

    if (! settingsFolder.mkpath(".")) {
        qFatal("Could not create config directory");
        exit(EXIT_FAILURE);
    }

    lockFile = new QLockFile(settingsFolder.absoluteFilePath(".lock"));
    lockFile->tryLock(0);

    switch (lockFile->error()) {
        case QLockFile::NoError:
            return true;
        case QLockFile::LockFailedError: {
            qWarning("Opening already running instance");
            QFile showFile(settingsFolder.absoluteFilePath(".show"));
            showFile.open(QIODevice::WriteOnly);
            showFile.close();
            return false;
        }
        default:
            qFatal("Cannot create lock file: no permission or unknown error");
            exit(EXIT_FAILURE);
    }
}


int main(int argc, char *argv[]) {
    qInstallMessageHandler(loggingHandler);

    // Define some basic info about the app before creating the QApplication
    QApplication::setOrganizationName("moment");
    QApplication::setApplicationName("moment");
    QApplication::setApplicationDisplayName("Moment");
    QApplication::setApplicationVersion("0.7.5");
    QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);

    // app needs to be constructed before attempting to migrate
    // because migrate displays a popup dialog
    QApplication app(argc, argv);

    QCommandLineParser args;

    QCommandLineOption startInTrayOption(QStringList() << "t" << "start-in-tray",
            "Start in the system tray, without a visible window.");
    args.addOption(startInTrayOption);

    QCommandLineOption loadQmlOption(QStringList() << "l" << "load-qml",
            "Override the file to be loaded as src/gui/UI.qml", "PATH");
    args.addOption(loadQmlOption);

    QCommandLineOption helpOption(QStringList() << "h" << "help",
            "Displays help on commandline options.");
    args.addOption(helpOption);

    args.addVersionOption();

    args.process(app);

    if (args.isSet(helpOption)) {
        std::cout << args.helpText().toStdString() << std::endl
            << "Environment variables:" << std::endl
            << "  MOMENT_CONFIG_DIR  Override the configuration folder"
            << std::endl
            << "  MOMENT_DATA_DIR    Override the application data folder"
            << std::endl
            << "  MOMENT_CACHE_DIR   Override the cache and downloads folder"
            << std::endl
            << "  http_proxy         Override the General.proxy setting, see "
            << "settings.py" << std::endl;
        exit(EXIT_SUCCESS);
    }

    if (shouldMigrateFromMirage()) tryMigrateFromMirage();

    QString customConfigDir(qEnvironmentVariable("MOMENT_CONFIG_DIR"));
    QString settingsFolder(
        customConfigDir.isEmpty() ?
        QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation)
        + "/" + QApplication::applicationName() :
        customConfigDir
    );

    // Attempt to create a lockfile in the settings folder
    if (! setLockFile(settingsFolder)) return EXIT_SUCCESS;

    // Register handlers for quit signals, e.g. SIGINT/Ctrl-C in unix terminals
    signal(SIGINT, onExitSignal);
    #ifdef Q_OS_UNIX
    signal(SIGHUP, onExitSignal);
    #endif


    // Force the default universal QML style, notably prevents
    // KDE from hijacking base controls and messing up everything
    QQuickStyle::setStyle("Fusion");
    QQuickStyle::setFallbackStyle("Default");

    // Register default theme fonts. Take the files  from the
    // Qt resource system if possible (resources stored in the app executable),
    // else the local file system.
    // The dev qmake flag disables the resource system for faster builds.
    QFileInfo qrcPath(":src/gui/Window.qml");
    QString src = qrcPath.exists() ? ":/src" : "src";

    QList<QString> fontFamilies;
    fontFamilies << "roboto" << "hack";

    QList<QString> fontVariants;
    fontVariants << "regular" << "italic" << "bold" << "bold-italic";

    foreach (QString family, fontFamilies) {
        foreach (QString var, fontVariants) {
            QFontDatabase::addApplicationFont(
                src + "/fonts/" + family + "/" + var + ".ttf"
            );
        }
    }

    // Create the QML engine and get the root context.
    // We will add it some properties that will be available globally in QML.
    QQmlEngine engine;
    QQmlContext *objectContext = new QQmlContext(engine.rootContext());

    // To able to use Qt.quit() from QML side
    QObject::connect(
        &engine, &QQmlEngine::quit, &app, &QApplication::quit, Qt::QueuedConnection
    );

    // Set the debugMode properties depending of if we're running in debug mode
    // or not (`qmake CONFIG+=dev ...`, default in autoreload.py)
#ifdef QT_DEBUG
    objectContext->setContextProperty("debugMode", true);
#else
    objectContext->setContextProperty("debugMode", false);
#endif

    // Register our custom non-visual QObject singletons,
    // that will be importable anywhere in QML. Example:
    //     import Clipboard 0.1
    //     ...
    //     Component.onCompleted: print(Clipboard.text)
    qmlRegisterSingletonType<Clipboard>(
        "Clipboard", 0, 1, "Clipboard",
        [](QQmlEngine *engine, QJSEngine *scriptEngine) -> QObject * {
            Q_UNUSED(scriptEngine)

            Clipboard *clipboard = new Clipboard();

            // Register out custom image providers.
            // QML will be able to request an image from them by setting an
            // `Image`'s `source` to `image://<providerId>/<id>`
            engine->addImageProvider(
                "clipboard", new ClipboardImageProvider(clipboard)
            );

            return clipboard;
        }
    );

    qmlRegisterSingletonType<Utils>(
        "CppUtils", 0, 1, "CppUtils",
        [](QQmlEngine *engine, QJSEngine *scriptEngine) -> QObject * {
            Q_UNUSED(engine)
            Q_UNUSED(scriptEngine)
            return new Utils();
        }
    );

    // Create the QML root component by loading its file from the Qt Resource
    // System or local file system if not possible.
    QQmlComponent component(
        &engine,
        qrcPath.exists() ? "qrc:/src/gui/Window.qml" : "src/gui/Window.qml"
    );

    if (component.isError()) {
        for (QQmlError e : component.errors()) {
            qCritical(
                "%s:%d:%d: %s",
                e.url().toString().toStdString().c_str(),
                e.line(),
                e.column(),
                e.description().toStdString().c_str()
            );
        }
        qFatal("One or more errors have occurred, exiting");
        app.exit(EXIT_FAILURE);
    }

    QObject *comp = component.create(objectContext);
    comp->setProperty("settingsFolder", "file://"+settingsFolder);
    comp->setProperty("startInTray",    args.isSet(startInTrayOption));
    comp->setProperty("loadQml",        args.value(loadQmlOption));

    // Finally, execute the app. Return its exit code after clearing the lock.
    int exit_code = app.exec();
    delete lockFile;
    return exit_code;
}
