/*
 *
 * Copyright (C) by Duncan Mac-Vicar P. <duncan@kde.org>
 *
 * 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 2 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.
 */
#include <QtGlobal>

#include "accountmanager.h"
#include "common/restartmanager.h"
#include "gui/application.h"
#include "gui/folderman.h"
#include "gui/logbrowser.h"
#include "gui/networkinformation.h"
#include "libsync/configfile.h"
#include "libsync/platform.h"
#include "libsync/theme.h"
#include "resources/loadresources.h"

#include "common/version.h"
#include "gui/translations.h"
#include "libsync/logger.h"
#include "socketapi/socketapi.h"

#include <kdsingleapplication.h>

#ifdef WITH_AUTO_UPDATER
#include "updater/updater.h"
#endif

#include <QApplication>
#include <QCommandLineParser>
#include <QLibraryInfo>
#include <QMessageBox>
#include <QProcess>
#include <QTimer>
#include <QTranslator>
#ifdef Q_OS_WIN
#include <qt_windows.h>
#endif

#include <iostream>

using namespace std::chrono_literals;

using namespace OCC;

Q_LOGGING_CATEGORY(lcMain, "gui.main", QtInfoMsg)

namespace {
inline auto msgParseOptionsC()
{
    return QStringLiteral("MSG_PARSEOPTIONS:");
}

// Helpers for displaying messages. Note that there is probably no console on Windows.
void displayHelpText(const QString &t)
{
    Logger::instance()->attacheToConsole();
    std::cout << qUtf8Printable(t) << std::endl;
#ifdef Q_OS_WIN
    // No console on Windows.
    QString spaces(80, QLatin1Char(' ')); // Add a line of non-wrapped space to make the messagebox wide enough.
    QString text =
        QStringLiteral("<qt><pre style='white-space:pre-wrap'>") + t.toHtmlEscaped() + QStringLiteral("</pre><pre>") + spaces + QStringLiteral("</pre></qt>");
    QMessageBox::information(nullptr, Theme::instance()->appNameGUI(), text);
#endif
}

struct CommandLineOptions
{
    bool show = false;
    bool quitInstance = false;

    QString logDir;
    QString logFile;
    bool logFlush = false;
    bool logDebug = false;

    bool debugMode = false;
};

CommandLineOptions parseOptions(const QStringList &arguments)
{
    QCommandLineParser parser;

    QString descriptionText;
    QTextStream descriptionTextStream(&descriptionText);

    descriptionTextStream << QApplication::translate("CommandLine", "%1 version %2\r\nFile synchronization desktop utility.")
                                 .arg(Theme::instance()->appName(), OCC::Version::displayString())
                          << Qt::endl;

    if (Resources::isVanillaTheme() && !Theme::instance()->helpUrl().isEmpty()) {
        descriptionTextStream
            << Qt::endl
            << Qt::endl
            << QApplication::translate("CommandLine", "For more information, see %1", "link to homepage").arg(Theme::instance()->helpUrl().toString());
    }

    parser.setApplicationDescription(descriptionText);

    auto helpOption = parser.addHelpOption();
    auto versionOption = parser.addVersionOption();

    // this little snippet saves a few lines below
    auto addOption = [&parser](const QCommandLineOption &option) {
        parser.addOption(option);
        return option;
    };

    auto showSettingsLegacyOption = QCommandLineOption{{QStringLiteral("showsettings")}, QStringLiteral("Hidden legacy option")};
    showSettingsLegacyOption.setFlags(QCommandLineOption::HiddenFromHelp);
    parser.addOption(showSettingsLegacyOption);

    auto showOption = addOption({{QStringLiteral("s"), QStringLiteral("show")},
        QApplication::translate("CommandLine",
            "Start with the main window visible, or if it is already running, bring it to the front. By default, the client launches in the background.")});
    auto quitInstanceOption = addOption({{QStringLiteral("q"), QStringLiteral("quit")}, QApplication::translate("CommandLine", "Quit the running instance.")});
    auto logFileOption = addOption(
        {QStringLiteral("logfile"), QApplication::translate("CommandLine", "Write log to file (use - to write to stdout)."), QStringLiteral("filename")});
    auto logDirOption = addOption(
        {QStringLiteral("logdir"), QApplication::translate("CommandLine", "Write each sync log output in a new file in folder."), QStringLiteral("name")});
    auto logFlushOption = addOption({QStringLiteral("logflush"), QApplication::translate("CommandLine", "Flush the log file after every write.")});
    auto logDebugOption = addOption({QStringLiteral("logdebug"), QApplication::translate("CommandLine", "Output debug-level messages in the log.")});
    auto debugOption = addOption({QStringLiteral("debug"), QApplication::translate("CommandLine", "Enable debug mode.")});
    addOption({QStringLiteral("cmd"), QApplication::translate("CommandLine", "Forward all arguments to the cmd client. This argument must be the first.")});

    parser.process(arguments);

    CommandLineOptions out;
    if (parser.isSet(showOption) || parser.isSet(showSettingsLegacyOption)) {
        out.show = true;
    }
    if (parser.isSet(quitInstanceOption)) {
        out.quitInstance = true;
    }
    if (parser.isSet(logFileOption)) {
        out.logFile = parser.value(logFileOption);
    }
    if (parser.isSet(logDirOption)) {
        if (parser.isSet(logFileOption)) {
            displayHelpText(QApplication::translate("CommandLine", "--logfile and --logdir are mutually exclusive"));
            std::exit(1);
        }
        out.logDir = parser.value(logDirOption);
    }
    if (parser.isSet(logFlushOption)) {
        out.logFlush = true;
    }
    if (parser.isSet(logDebugOption)) {
        out.logDebug = true;
    }
    if (parser.isSet(debugOption)) {
        out.logDebug = true;
        out.debugMode = true;
    }

    return out;
}

void showDowngradeDialog()
{
    QMessageBox box(QMessageBox::Warning, Theme::instance()->appNameGUI(),
        QCoreApplication::translate("version check",
            "Some settings were configured in newer versions of this client "
            "and use features that are not available in this version"));
    box.addButton(OCC::Application::tr("Quit"), QMessageBox::AcceptRole);
    box.exec();
    QTimer::singleShot(0, qApp, &QApplication::quit);
}

/**
 * Check if the last version used to write the config file differs from the current version.
 * If the current version is newer, update the config file with our current version. If the
 * current version is older, refuse to do anything: this is a downgrade, and it is too risky to
 * assume that things might work "just fine".
 */
bool checkClientVersion()
{
    ConfigFile configFile;

    // Did the client version change?
    // (The client version is adjusted further down)
    auto configVersion = QVersionNumber::fromString(configFile.clientVersionWithBuildNumberString());
    auto clientVersion = OCC::Version::versionWithBuildNumber();

    if (configVersion == clientVersion) {
        // no config backup needed
        return true;
    }

    // We allow downgrades as long as the major version stays the same.
    if (clientVersion.majorVersion() < configVersion.majorVersion()) {
        // We refuse to downgrade, too much can go wrong.
        showDowngradeDialog();
        return false;
    }

    // We're okay to continue. The settings will be updated in other parts, but here we bump the
    // version we store in the config file.
    configFile.backup();
    configFile.setClientVersionWithBuildNumberString(OCC::Version::versionWithBuildNumber().toString());
    return true;
}

void setupLogging(const CommandLineOptions &options)
{
    // might be called from second instance
    auto logger = Logger::instance();
    logger->install();
    // call setLogFlush first, other log settings might already imply flushing
    // so setting it false in the end will have undesired results.
    logger->setLogFlush(options.logFlush);

    if (!options.logDir.isEmpty()) {
        logger->setLogDir(options.logDir);
    }
    if (!options.logFile.isEmpty()) {
        Q_ASSERT(options.logDir.isEmpty());
        logger->setLogFile(options.logFile);
    }
    logger->setLogDebug(options.logDebug);

    // Possibly configure logging from config file
    LogBrowser::setupLoggingFromConfig();

    qCInfo(lcMain) << u"##################" << Theme::instance()->appName() << u"locale:" << QLocale::system().name() << u"version:"
                   << Theme::instance()->aboutVersions(Theme::VersionFormat::OneLiner);
    qCInfo(lcMain) << u"Arguments:" << qApp->arguments();
}

QString setupTranslations(QApplication *app)
{
    const auto trPath = Translations::translationsDirectoryPath();
    qCDebug(lcMain) << u"Translations directory path:" << trPath;

#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
    QStringList uiLanguages = QLocale::system().uiLanguages(QLocale::TagSeparator::Underscore);
#else
    QStringList uiLanguages;
    for (auto lang : QLocale::system().uiLanguages()) {
        uiLanguages << lang.replace(QLatin1Char('-'), QLatin1Char('_'));
    }
#endif
    qCDebug(lcMain) << u"UI languages:" << uiLanguages;

    // the user can also set a locale in the settings, so we need to load the config file
    const ConfigFile cfg;

    // we need to track the enforced language separately, since we need to distinguish between locale-provided
    // and user-enforced one below
    const QString enforcedLocale = cfg.uiLanguage();
    qCDebug(lcMain) << u"Enforced language:" << enforcedLocale;

    // note that user-enforced language are prioritized over the theme enforced one
    // to make testing easier.
    if (!enforcedLocale.isEmpty()) {
        uiLanguages.prepend(enforcedLocale);
    }

    QString displayLanguage;

    auto substLang = [](const QString &lang) {
        // Map the more appropriate script codes
        // to country codes as used by Qt and
        // transifex translation conventions.

        if (lang == QLatin1String("zh_Hans")) {
            // Simplified Chinese
            return QStringLiteral("zh_CN");
        } else if (lang == QLatin1String("zh_Hant")) {
            // Traditional Chinese
            return QStringLiteral("zh_TW");
        }

        return lang;
    };

    for (QString lang : std::as_const(uiLanguages)) {
        lang = substLang(lang);
        const QString trFile = Translations::translationsFilePrefix() + lang;
        if (QTranslator *translator = new QTranslator(app); translator->load(trFile, trPath) || lang.startsWith(QLatin1String("en"))) {
            // Permissive approach: Qt and keychain translations
            // may be missing, but Qt translations must be there in order
            // for us to accept the language. Otherwise, we try with the next.
            // "en" is an exception as it is the default language and may not
            // have a translation file provided.
            qCInfo(lcMain) << u"Using" << lang << u"translation" << translator->language();
            displayLanguage = lang;

            const QString qtTrPath = QLibraryInfo::path(QLibraryInfo::TranslationsPath);
            qCDebug(lcMain) << u"qtTrPath:" << qtTrPath;
            const QString qtTrFile = QLatin1String("qt_") + lang;
            qCDebug(lcMain) << u"qtTrFile:" << qtTrFile;
            const QString qtBaseTrFile = QLatin1String("qtbase_") + lang;
            qCDebug(lcMain) << u"qtBaseTrFile:" << qtBaseTrFile;

            QTranslator *qtTranslator = new QTranslator(app);
            QTranslator *qtkeychainTranslator = new QTranslator(app);

            if (!qtTranslator->load(qtTrFile, qtTrPath)) {
                if (!qtTranslator->load(qtTrFile, trPath)) {
                    if (!qtTranslator->load(qtBaseTrFile, qtTrPath)) {
                        if (!qtTranslator->load(qtBaseTrFile, trPath)) {
                            qCCritical(lcMain) << u"Could not load Qt translations";
                        }
                    }
                }
            }

            const QString qtkeychainTrFile = QLatin1String("qtkeychain_") + lang;
            if (!qtkeychainTranslator->load(qtkeychainTrFile, qtTrPath)) {
                if (!qtkeychainTranslator->load(qtkeychainTrFile, trPath)) {
                    qCCritical(lcMain) << u"Could not load qtkeychain translations";
                }
            }

            if (!translator->isEmpty() && !qApp->installTranslator(translator)) {
                qCCritical(lcMain) << u"Failed to install translator";
                translator->deleteLater();
            }
            if (!qtTranslator->isEmpty() && !qApp->installTranslator(qtTranslator)) {
                qCCritical(lcMain) << u"Failed to install Qt translator";
                qtTranslator->deleteLater();
            }
            if (!qtkeychainTranslator->isEmpty() && !qApp->installTranslator(qtkeychainTranslator)) {
                qCCritical(lcMain) << u"Failed to install qtkeychain translator";
                qtkeychainTranslator->deleteLater();
            }

            // makes sure widgets with locale-dependent formatting, e.g., QDateEdit, display the correct formatting
            // if the language is provided by the system locale anyway (i.e., coming from QLocale::system().uiLanguages()), we should
            // not mess with the system locale, though
            // if we did, we would enforce a locale for no apparent reason
            // see https://github.com/owncloud/client/issues/8608 for more information
            if (enforcedLocale == lang) {
                QLocale newLocale(lang);
                qCDebug(lcMain) << u"language" << lang << u"was enforced, changing default locale to" << newLocale;
                QLocale::setDefault(newLocale);
            }
            break;
        } else {
            translator->deleteLater();
        }
    }

    return displayLanguage;
}
} // Anonymous namespace

int main(int argc, char **argv)
{
    return RestartManager([](int argc, char **argv) {
        // when called with --cmd we run the cmd client in a sub process and forward everything
        if (argc > 1 && argv[1] == QByteArrayLiteral("--cmd")) {
#ifdef Q_OS_WIN
            // On Windows ui applications don't have console access by default
            // We can't use our normal workaround to attach to the parent console as it breaks the stdin handling.
            // Therefore, we create a new console and redirect our streams.
            AllocConsole();
            freopen("CONIN$", "r", stdin);
            freopen("CONOUT$", "w", stdout);
            freopen("CONOUT$", "w", stderr);
#endif
            QCoreApplication cmdApp(argc, argv);
            QProcess cmd;
            cmd.setProcessChannelMode(QProcess::ForwardedChannels);
            cmd.setInputChannelMode(QProcess::ForwardedInputChannel);

            const QString app = []() -> QString {
#ifdef Q_OS_WIN
                return QCoreApplication::applicationFilePath().chopped(4) + QStringLiteral("cmd.exe");
#else
                return QCoreApplication::applicationFilePath() + QStringLiteral("cmd");
#endif
            }();
            cmd.start(app, cmdApp.arguments().mid(2));
            if (!cmd.waitForFinished(-1)) {
                std::cout << "Failed to start" << qPrintable(cmd.program()) << std::endl;
            }
#ifdef Q_OS_WIN
            // readline to keep the console window open until closed by the user
            std::string dummy;
            std::cout << "Press enter to close";
            std::getline(std::cin, dummy);
#endif
            return cmd.exitCode();
        }

        // load the resources
        const OCC::ResourcesLoader resource;

        // Create a `Platform` instance so it can set-up/tear-down stuff for us, and do any
        // initialisation that needs to be done before creating a QApplication
        const auto platform = Platform::create(Platform::Type::Gui);

        // Create the (Q)Application instance:
        QApplication app(argc, argv);
        app.setOrganizationDomain(Theme::instance()->orgDomainName());
        app.setApplicationName(Theme::instance()->appName());
        app.setWindowIcon(Theme::instance()->applicationIcon());
        app.setApplicationVersion(Theme::instance()->versionSwitchOutput());

#ifdef Q_OS_LINUX
        // HACK:
        // With X11 arg0.name is used to map WM_CLASS to the desktop file.
        // With Wayland it uses app.desktopFileName() or app.organizationDomain()
        // As we preferably use AppImages the real deskop file name is unknown, we just ensure it mathces the StartupWMClass entry in our desktop file
        if (qgetenv("XDG_SESSION_TYPE") == "wayland") {
            // https://bugreports.qt.io/browse/QTBUG-77182 Qt 6.9 brings setAppId() but its private api
            app.setDesktopFileName(QFileInfo(app.applicationFilePath()).baseName());
        }
#endif

        // Load the translations before option parsing, so we can localize help text and error messages.
        const QString displayLanguage = setupTranslations(&app);

        // parse the arguments before we handle singleApplication
        // errors and help/version need to be handled in this instance
        const auto options = parseOptions(app.arguments());

        KDSingleApplication singleApplication;

        if (!singleApplication.isPrimaryInstance()) {
            // if the application is already running, notify it.
            qCInfo(lcMain) << u"Already running, exiting...";
            if (app.isSessionRestored()) {
                // This call is mirrored with the one in Application::slotParseMessage
                qCInfo(lcMain) << u"Session was restored, don't notify app!";
                return -1;
            }

            QStringList args = app.arguments();
            if (args.size() > 1) {
                QString msg = args.join(QLatin1String("|"));
                if (!singleApplication.sendMessage((msgParseOptionsC() + msg).toUtf8()))
                    return -1;
            }

            return 0;
        }

        // Check if the user upgraded or downgraded. We do this as early as possible, to detect
        // a possible downgrade.
        if (!checkClientVersion()) {
            return -1;
        }

        setupLogging(options);
        NetworkInformation::instance(); //

        platform->setApplication(&app);

        auto folderManager = FolderMan::createInstance();

        if (!AccountManager::instance()->restore()) {
            qCCritical(lcMain) << u"Could not read the account settings, quitting";
            QMessageBox::critical(nullptr, QCoreApplication::translate("account loading", "Error accessing the configuration file"),
                QCoreApplication::translate("account loading", "There was an error while accessing the configuration file at %1.")
                    .arg(ConfigFile::configFile()),
                QMessageBox::Close);
            return -1;
        }

        // Setup the folders. This includes a downgrade-detection, in which case the return value
        // is empty. Note that the value 0 (zero) is a valid return value (non-empty), in which case
        // the dialog is not shown.
        if (!FolderMan::instance()->loadFolders().has_value()) {
            // Empty return value: there was a downgrade detected on one of the databases
            showDowngradeDialog();
            return -1;
        }

        auto ocApp = Application::createInstance(displayLanguage, options.debugMode);

        QObject::connect(platform.get(), &Platform::requestAttention, ocApp.get(), &Application::showSettings);

        QObject::connect(&singleApplication, &KDSingleApplication::messageReceived, ocApp.get(), [&](const QByteArray &message) {
            const QString msg = QString::fromUtf8(message);
            qCInfo(lcMain) << Q_FUNC_INFO << msg;
            if (msg.startsWith(msgParseOptionsC())) {
                const QStringList optionsStrings = msg.mid(msgParseOptionsC().size()).split(QLatin1Char('|'));
                CommandLineOptions options = parseOptions(optionsStrings);
                if (options.show) {
                    ocApp->showSettings();
                }
                if (options.quitInstance) {
                    qApp->quit();
                }
            }
        });

        platform->startServices();

#ifdef WITH_AUTO_UPDATER
        if (Updater::instance()) {
            // validate whether the last update was successful.
            Updater::instance()->validateUpdate();
        }
#endif

        // Enable syncing. We cannot do this earlier, because the UI needs to be set up in order to
        // show sync errors. We also want to wait for the auto-updater to finish, in case it needs
        // to install an update.
        folderManager->setSyncEnabled(true);

        if (options.show) {
            ocApp->showSettings();
            // The user explicitly requested the settings dialog, so don't start the new-account wizard.
        }

        // Display the wizard if we don't have an account yet, and no other UI is showing.
        if (AccountManager::instance()->accounts().isEmpty()) {
            QTimer::singleShot(0, ocApp.get(), &Application::runNewAccountWizard);
        }

        // Now that everything is up and running, start accepting connections/requests from the shell integration.
        folderManager->socketApi()->startShellIntegration();

        return app.exec();
    }).exec(argc, argv);
}
