/*
 * This file is part of smolbote. It's copyrighted by the contributors recorded
 * in the version control history of the file, available from its original
 * location: https://neueland.iserlohn-fortress.net/gitea/aqua/smolbote
 *
 * SPDX-License-Identifier: GPL-3.0
 */

#include "browser.h"
#include "builtins.h"
#include "configuration.h"
#include "crashhandler.h"
#include "session/session.h"
#include "session/sessiondialog.h"
#include "settings.h"
#include "util.h"
#include "version.h"
#include <QFile>
#include <QPluginLoader>
#include <QStandardPaths>
#include <iostream>
#include <memory>
#include <plugininterface.h>
#include <pluginloader.h>
#include <spdlog/spdlog.h>

// a helper function to join the keys of a command_map into a string
inline std::string join_keys(const command_map &map, const std::string sep = ", ")
{
    std::vector<std::string> keys(map.size());
    std::transform(map.begin(), map.end(), keys.begin(), [](auto pair) { return pair.first; });
    std::sort(keys.begin(), keys.end());

    std::string k;
    std::for_each(keys.begin(), keys.end() - 1, [&k, &sep](const std::string &piece) { k += piece + sep; });
    k += keys.back();

    return k;
}

int main(int argc, char **argv)
{
    // change log pattern
    spdlog::set_pattern("[%^%l%$] [%P:%t] %v");
#ifdef QT_DEBUG
    spdlog::set_level(spdlog::level::debug); // Set global log level to debug
#endif

    const command_map commands{
        { "configuration", builtins::configuration }
    };

    const std::vector<std::string> args(argv + 1, argv + argc);
    args::ArgumentParser parser("smolbote: yet another no-frills browser", "Subcommands: " + join_keys(commands));
    parser.Prog(argv[0]);

    args::HelpFlag cmd_help(parser, "help", "Display this help message.", { 'h', "help" });
    args::Flag cmd_version(parser, "version", "Display version information.", { 'v', "version" });
    args::Flag cmd_build(parser, "build", "Display build commit.", { 'b', "build" });

    args::ValueFlag<std::string> cmd_config(parser, "config", "Set the configuration file.", { 'c', "config" });

    args::Flag cmd_noRemote(parser, "no-remote", "Do not accept or send remote commands.", { "no-remote" });

    args::Flag cmd_pickSession(parser, "pick-session", "Show all available sessions and select which one to open", { "pick-session" });
    args::ValueFlag<std::string> cmd_session(parser, "session", "Open the specified session.", { 's', "session" });

    args::PositionalList<std::string> cmd_args(parser, "URL(s)", "List of URLs to open");
    cmd_args.KickOut(true);

    try {
        auto next = parser.ParseArgs(args);

        if(cmd_version)
            return builtins::version();
        if(cmd_build)
            return builtins::build();

        // create and load configuration
        spdlog::info("Loading configuration {}", init_conf(args::get(cmd_config)));

        if(cmd_args) {
            const auto front = args::get(cmd_args).front();
            const auto cmd = commands.find(front);
            if(cmd != commands.end()) {
                return cmd->second(argv[0], next, std::end(args));
            }
        }

    } catch(args::Help &) {
        std::cout << parser;
        return 0;

    } catch(args::Error &e) {
        std::cerr << e.what() << std::endl;
        std::cerr << parser;
        return -1;
    }

    QVector<QPluginLoader *> plugins;
    CommandHash_t pluginCommands;

    // Load plugins
    [&]() {
        Configuration conf;
        spdlog::debug("plugins.path={}", conf.value<std::string>("plugins.path").value());
        for(const QString &path : Util::files(conf.value<QString>("plugins.path").value(), { "*.so", "*.dll" })) {
            auto *loader = new PluginLoader(path);
            const bool loaded = loader->load();
            spdlog::info("{} plugin {}", loaded ? "Loaded" : "Failed to load", qUtf8Printable(path));

            if(loaded) {
                plugins.append(loader);
                auto *plugin = qobject_cast<PluginInterface *>(loader->instance());
                pluginCommands.unite(plugin->commands());
            } else {
                spdlog::warn("{}", qUtf8Printable(loader->errorString()));
                delete loader;
            }
        }
    }();

    // argc, argv, allowSecondary
    Browser app(argc, argv);
    // set this, otherwise the webview becomes black when using a stylesheet
    app.setAttribute(Qt::AA_DontCreateNativeWidgetSiblings, true);

    {
        Configuration conf;
        if(conf.value<bool>("usebreakpad").value()) {
            CrashHandler::Context ctx(
                conf.value<std::string>("path.crashdump").value(),
                conf.value<std::string>("path.crashhandler").value());

            if(CrashHandler::install_handler(ctx)) {
                spdlog::info("Installed breakpad crash handler: {}", ctx.dumppath);
            } else {
                spdlog::warn("Failed to install breakpad crash handler: {}", ctx.dumppath);
            }
        }
    }

    const auto profile = []() {
        Configuration c;
        return c.value<QString>("profile.default").value();
    }();

    app.setup(plugins);

    QStringList urls;

    for(const auto &u : args::get(cmd_args)) {
        if(pluginCommands.contains(QString::fromStdString(u))) {
            return pluginCommands.value(QString::fromStdString(u))();
        } else {
            urls.append(QString::fromStdString(u));
        }
    }
    if(urls.isEmpty())
        urls.append(QString());

    // if app is primary, create new sessions from received messages
    if(app.isPrimary() && !cmd_noRemote) {
        QObject::connect(&app, &Browser::receivedMessage, &app, [](quint32 instanceId, QByteArray message) {
            Q_UNUSED(instanceId);
            auto doc = QJsonDocument::fromJson(message);
            Session::restoreSession(doc.object());
        });
    }

    {
        QJsonObject sessionData;

        if(cmd_pickSession) {
            auto *dlg = new SessionDialog();
            if(const auto pick = dlg->pickSession())
                sessionData = pick.value();
            else
                sessionData = Session::fromCommandLine(profile, urls);
        } else if(cmd_session) {
            QFile sessionJson(QString::fromStdString(args::get(cmd_session)));
            if(sessionJson.open(QIODevice::ReadOnly | QIODevice::Text)) {
                sessionData = QJsonDocument::fromJson(sessionJson.readAll()).object();
                sessionJson.close();
            }
        } else {
            sessionData = Session::fromCommandLine(profile, urls);
        }

        if(app.isPrimary() || cmd_noRemote) {
            Session::restoreSession(sessionData);
        } else {
            // app is secondary and not standalone
            return app.sendMessage(QJsonDocument(sessionData).toJson());
        }
    }

    return app.exec();
}