/*
    SPDX-FileCopyrightText: 2006-2008 Robert Knight <robertknight@gmail.com>

    SPDX-License-Identifier: GPL-2.0-or-later
*/

// To time the creation and total launch time (i. e. until window is
// visible/responsive):
// #define PROFILE_STARTUP

// Own
#include "Application.h"
#include "KonsoleSettings.h"
#include "MainWindow.h"
#include "ViewManager.h"
#include "config-konsole.h"
#include "widgets/ViewContainer.h"

// OS specific
#include <QApplication>
#include <QCommandLineParser>
#include <QDir>
#include <QProxyStyle>
#include <QStandardPaths>
#include <qplatformdefs.h>

// KDE
#include <KAboutData>
#include <KCrash>
#include <KIconTheme>
#include <KLocalizedString>
#include <KWindowSystem>

#if HAVE_DBUS
#include <KDBusService>
#include <QDBusConnection>
#include <QDBusMessage>
#endif

#define HAVE_STYLE_MANAGER __has_include(<KStyleManager>)
#if HAVE_STYLE_MANAGER
#include <KStyleManager>
#endif

using Konsole::Application;

#ifdef PROFILE_STARTUP
#include <QDebug>
#include <QElapsedTimer>
#include <QTimer>
#endif

// fill the KAboutData structure with information about contributors to Konsole.
void fillAboutData(KAboutData &aboutData);

// check and report whether this konsole instance should use a new konsole
// process, or reuse an existing konsole process.
bool shouldUseNewProcess(int argc, char *argv[]);

// restore sessions saved by KDE.
void restoreSession(Application &app);

#if HAVE_DBUS
// Workaround for a bug in KDBusService: https://bugs.kde.org/show_bug.cgi?id=355545
// It calls exit(), but the program can't exit before the QApplication is deleted:
// https://bugreports.qt.io/browse/QTBUG-48709
static bool needToDeleteQApplication = false;
void deleteQApplication()
{
    if (needToDeleteQApplication) {
        delete qApp;
    }
}
#endif

// This override resolves following problem: since some qt version if
// XDG_CURRENT_DESKTOP ≠ kde, then pressing and immediately releasing Alt
// key makes focus get stuck in QMenu.
// Upstream report: https://bugreports.qt.io/browse/QTBUG-77355
class MenuStyle : public QProxyStyle
{
public:
    MenuStyle(const QString &name)
        : QProxyStyle(name)
    {
    }

    int styleHint(const StyleHint stylehint, const QStyleOption *opt, const QWidget *widget, QStyleHintReturn *returnData) const override
    {
        return (stylehint == QStyle::SH_MenuBar_AltKeyNavigation) ? 0 : QProxyStyle::styleHint(stylehint, opt, widget, returnData);
    }
};

// Used to control migrating config entries.
// Increment when there are new keys to migrate.
static int CurrentConfigVersion = 1;

static void migrateRenamedConfigKeys()
{
    KSharedConfigPtr konsoleConfig = KSharedConfig::openConfig(QStringLiteral("konsolerc"));
    KConfigGroup verGroup = konsoleConfig->group(QStringLiteral("General"));
    const int savedVersion = verGroup.readEntry<int>("ConfigVersion", 0);
    if (savedVersion < CurrentConfigVersion) {
        struct KeyInfo {
            const char *groupName;
            const char *oldKeyName;
            const char *newKeyName;
        };

        static const KeyInfo keys[] = {{"KonsoleWindow", "SaveGeometryOnExit", "RememberWindowSize"}};

        // Migrate renamed config keys
        for (const auto &[group, oldName, newName] : keys) {
            KConfigGroup cg = konsoleConfig->group(QLatin1String(group));
            if (cg.exists() && cg.hasKey(oldName)) {
                const bool value = cg.readEntry(oldName, false);
                cg.deleteEntry(oldName);
                cg.writeEntry(newName, value);
            }
        }

        // With 5.93 KColorSchemeManager from KConfigWidgets, handles the loading
        // and saving of the widget color scheme, and uses "ColorScheme" as the
        // entry name, so clean-up here
        KConfigGroup cg(konsoleConfig, QStringLiteral("UiSettings"));
        const QString schemeName = cg.readEntry("WindowColorScheme");
        cg.deleteEntry("WindowColorScheme");
        cg.writeEntry("ColorScheme", schemeName);

        verGroup.writeEntry("ConfigVersion", CurrentConfigVersion);
        konsoleConfig->sync();
    }
}

// ***
// Entry point into the Konsole terminal application.
// ***
int main(int argc, char *argv[])
{
#ifdef PROFILE_STARTUP
    QElapsedTimer timer;
    timer.start();
#endif

    /**
     * trigger initialisation of proper icon theme
     */
#if KICONTHEMES_VERSION >= QT_VERSION_CHECK(6, 3, 0)
    KIconTheme::initTheme();
#endif

#if HAVE_DBUS
    // Check if any of the arguments makes it impossible to reuse an existing process.
    // We need to do this manually and before creating a QApplication, because
    // QApplication takes/removes the Qt specific arguments that are incompatible.
    const bool needNewProcess = shouldUseNewProcess(argc, argv);
    if (!needNewProcess) { // We need to avoid crashing
        needToDeleteQApplication = true;
    }
#endif

    auto app = new QApplication(argc, argv);

#if HAVE_STYLE_MANAGER
    /**
     * trigger initialisation of proper application style
     */
    KStyleManager::initStyle();
#else
    /**
     * For Windows and macOS: use Breeze if available
     * Of all tested styles that works the best for us
     */
#if defined(Q_OS_MACOS) || defined(Q_OS_WIN)
    QApplication::setStyle(QStringLiteral("breeze"));
#endif
#endif

    // fix the alt key, ensure we keep the current selected style as base
    app->setStyle(new MenuStyle(app->style()->name()));

    migrateRenamedConfigKeys();

    app->setWindowIcon(QIcon::fromTheme(QStringLiteral("utilities-terminal")));

    KLocalizedString::setApplicationDomain("konsole");

    KAboutData about(QStringLiteral("konsole"),
                     i18nc("@title", "Konsole"),
                     QStringLiteral(KONSOLE_VERSION),
                     i18nc("@title", "Terminal emulator"),
                     KAboutLicense::GPL_V2,
                     i18nc("@info:credit", "(c) 1997-2022, The Konsole Developers"),
                     QString(),
                     QStringLiteral("https://konsole.kde.org/"));
    fillAboutData(about);

    KAboutData::setApplicationData(about);

    KCrash::initialize();

    QSharedPointer<QCommandLineParser> parser(new QCommandLineParser);
    parser->setApplicationDescription(about.shortDescription());
    about.setupCommandLine(parser.data());

    QStringList args = app->arguments();
    QStringList customCommand = Application::getCustomCommand(args);

    Application::populateCommandLineParser(parser.data());

    parser->process(args);
    about.processCommandLine(parser.data());

#if HAVE_DBUS
    // on wayland: init token if we are launched by Konsole and have none
    if (KWindowSystem::isPlatformWayland() && qEnvironmentVariable("XDG_ACTIVATION_TOKEN").isEmpty() && QDBusConnection::sessionBus().interface()) {
        // can we ask Konsole for a token?
        const auto konsoleService = qEnvironmentVariable("KONSOLE_DBUS_SERVICE");
        const auto konsoleSession = qEnvironmentVariable("KONSOLE_DBUS_SESSION");
        const auto konsoleActivationCookie = qEnvironmentVariable("KONSOLE_DBUS_ACTIVATION_COOKIE");
        if (!konsoleService.isEmpty() && !konsoleSession.isEmpty() && !konsoleActivationCookie.isEmpty()) {
            // we ask the current shell session
            QDBusMessage m =
                QDBusMessage::createMethodCall(konsoleService, konsoleSession, QStringLiteral("org.kde.konsole.Session"), QStringLiteral("activationToken"));

            // use the cookie from the environment
            m.setArguments({konsoleActivationCookie});

            // get the token, if possible and export it to environment for later use
            const auto tokenAnswer = QDBusConnection::sessionBus().call(m);
            if (tokenAnswer.type() == QDBusMessage::ReplyMessage && !tokenAnswer.arguments().isEmpty()) {
                const auto token = tokenAnswer.arguments().first().toString();
                if (!token.isEmpty()) {
                    qputenv("XDG_ACTIVATION_TOKEN", token.toUtf8());
                }
            }
        }
    }

    /// ! DON'T TOUCH THIS ! ///
    const KDBusService::StartupOption startupOption =
        Konsole::KonsoleSettings::useSingleInstance() && !needNewProcess ? KDBusService::Unique : KDBusService::Multiple;
    /// ! DON'T TOUCH THIS ! ///
    // If you need to change something here, add your logic _at the bottom_ of
    // shouldUseNewProcess(), after reading the explanations there for why you
    // probably shouldn't.

    atexit(deleteQApplication);
    // Ensure that we only launch a new instance if we need to
    // If there is already an instance running, we will quit here
    KDBusService dbusService(startupOption | KDBusService::NoExitOnFailure);

    needToDeleteQApplication = false;
#endif

    // If we reach this location, there was no existing copy of Konsole
    // running, so create a new instance.
    Application konsoleApp(parser, customCommand);

#if HAVE_DBUS
    // The activateRequested() signal is emitted when a second instance
    // of Konsole is started.
    QObject::connect(&dbusService, &KDBusService::activateRequested, &konsoleApp, &Application::slotActivateRequested);
#endif

    if (app->isSessionRestored()) {
        restoreSession(konsoleApp);
    } else {
        // Do not finish starting Konsole due to:
        // 1. An argument was given to just printed info
        // 2. An invalid situation occurred
        const bool continueStarting = (konsoleApp.newInstance() != 0);
        if (!continueStarting) {
            delete app;
            return 0;
        }
    }

#ifdef PROFILE_STARTUP
    qDebug() << "Construction completed in" << timer.elapsed() << "ms";
    QTimer::singleShot(0, [&timer]() {
        qDebug() << "Startup complete in" << timer.elapsed() << "ms";
    });
#endif

    // Since we've allocated the QApplication on the heap for the KDBusService workaround,
    // we need to delete it manually before returning from main().
    int ret = app->exec();
    delete app;
    return ret;
}

bool shouldUseNewProcess(int argc, char *argv[])
{
    // The "unique process" model of konsole is incompatible with some or all
    // Qt/KDE options. When those incompatible options are given, konsole must
    // use new process
    //
    // TODO: make sure the existing list is OK and add more incompatible options.

    // We need to manually parse the arguments because QApplication removes the
    // Qt specific arguments (like --reverse)
    QStringList arguments;
    arguments.reserve(argc);
    for (int i = 0; i < argc; i++) {
        arguments.append(QString::fromLocal8Bit(argv[i]));
    }

    if (arguments.contains(QLatin1String("--force-reuse"))) {
        return false;
    }

    // take Qt options into consideration
    QStringList qtProblematicOptions;
    qtProblematicOptions << QStringLiteral("--session") << QStringLiteral("--name") << QStringLiteral("--reverse") << QStringLiteral("--stylesheet")
                         << QStringLiteral("--graphicssystem");
#if WITH_X11
    qtProblematicOptions << QStringLiteral("--display") << QStringLiteral("--visual");
#endif
    for (const QString &option : std::as_const(qtProblematicOptions)) {
        if (arguments.contains(option)) {
            return true;
        }
    }

    // take KDE options into consideration
    QStringList kdeProblematicOptions;
    kdeProblematicOptions << QStringLiteral("--config") << QStringLiteral("--style");
#if WITH_X11
    kdeProblematicOptions << QStringLiteral("--waitforwm");
#endif

    for (const QString &option : std::as_const(kdeProblematicOptions)) {
        if (arguments.contains(option)) {
            return true;
        }
    }

    // if users have explicitly requested starting a new process
    // Support --nofork to retain argument compatibility with older
    // versions.
    if (arguments.contains(QStringLiteral("--separate")) || arguments.contains(QStringLiteral("--nofork"))) {
        return true;
    }

    // the only way to create new tab is to reuse existing Konsole process.
    if (arguments.contains(QStringLiteral("--new-tab"))) {
        return false;
    }

    // when starting Konsole from a terminal, a new process must be used
    // so that the current environment is propagated into the shells of the new
    // Konsole and any debug output or warnings from Konsole are written to
    // the current terminal
    bool hasControllingTTY = false;
    const int fd = QT_OPEN("/dev/tty", O_RDONLY);
    if (fd != -1) {
        hasControllingTTY = true;
        close(fd);
    }

    return hasControllingTTY;
}

void fillAboutData(KAboutData &aboutData)
{
    aboutData.setOrganizationDomain("kde.org");

    aboutData.addAuthor(i18nc("@info:credit", "Kurt Hindenburg"),
                        i18nc("@info:credit",
                              "General maintainer, bug fixes and general"
                              " improvements"),
                        QStringLiteral("kurt.hindenburg@gmail.com"));
    aboutData.addAuthor(i18nc("@info:credit", "Robert Knight"),
                        i18nc("@info:credit", "Previous maintainer, ported to KDE4"),
                        QStringLiteral("robertknight@gmail.com"));
    aboutData.addAuthor(i18nc("@info:credit", "Lars Doelle"), i18nc("@info:credit", "Original author"), QStringLiteral("lars.doelle@on-line.de"));
    aboutData.addCredit(i18nc("@info:credit", "Ahmad Samir"),
                        i18nc("@info:credit", "Major refactoring, bug fixes and major improvements"),
                        QStringLiteral("a.samirh78@gmail.com"));
    aboutData.addCredit(i18nc("@info:credit", "Carlos Alves"),
                        i18nc("@info:credit", "Major refactoring, bug fixes and major improvements"),
                        QStringLiteral("cbc.alves@gmail.com"));
    aboutData.addCredit(i18nc("@info:credit", "Tomaz Canabrava"),
                        i18nc("@info:credit", "Major refactoring, bug fixes and major improvements"),
                        QStringLiteral("tcanabrava@kde.org"));
    aboutData.addCredit(i18nc("@info:credit", "Gustavo Carneiro"),
                        i18nc("@info:credit", "Major refactoring, bug fixes and major improvements"),
                        QStringLiteral("gcarneiroa@hotmail.com"));
    aboutData.addCredit(i18nc("@info:credit", "Edwin Pujols"),
                        i18nc("@info:credit", "Bug fixes and general improvements"),
                        QStringLiteral("edwin.pujols5@outlook.com"));
    aboutData.addCredit(i18nc("@info:credit", "Martin T. H. Sandsmark"),
                        i18nc("@info:credit", "Bug fixes and general improvements"),
                        QStringLiteral("martin.sandsmark@kde.org"));
    aboutData.addCredit(i18nc("@info:credit", "Nate Graham"), i18nc("@info:credit", "Bug fixes and general improvements"), QStringLiteral("nate@kde.org"));
    aboutData.addCredit(i18nc("@info:credit", "Mariusz Glebocki"),
                        i18nc("@info:credit", "Bug fixes and major improvements"),
                        QStringLiteral("mglb@arccos-1.net"));
    aboutData.addCredit(i18nc("@info:credit", "Thomas Surrel"),
                        i18nc("@info:credit", "Bug fixes and general improvements"),
                        QStringLiteral("thomas.surrel@protonmail.com"));
    aboutData.addCredit(i18nc("@info:credit", "Jekyll Wu"), i18nc("@info:credit", "Bug fixes and general improvements"), QStringLiteral("adaptee@gmail.com"));
    aboutData.addCredit(i18nc("@info:credit", "Waldo Bastian"), i18nc("@info:credit", "Bug fixes and general improvements"), QStringLiteral("bastian@kde.org"));
    aboutData.addCredit(i18nc("@info:credit", "Stephan Binner"), i18nc("@info:credit", "Bug fixes and general improvements"), QStringLiteral("binner@kde.org"));
    aboutData.addCredit(i18nc("@info:credit", "Thomas Dreibholz"), i18nc("@info:credit", "General improvements"), QStringLiteral("dreibh@iem.uni-due.de"));
    aboutData.addCredit(i18nc("@info:credit", "Chris Machemer"), i18nc("@info:credit", "Bug fixes"), QStringLiteral("machey@ceinetworks.com"));
    aboutData.addCredit(i18nc("@info:credit", "Francesco Cecconi"), i18nc("@info:credit", "Bug fixes"), QStringLiteral("francesco.cecconi@gmail.com"));
    aboutData.addCredit(i18nc("@info:credit", "Stephan Kulow"), i18nc("@info:credit", "Solaris support and history"), QStringLiteral("coolo@kde.org"));
    aboutData.addCredit(i18nc("@info:credit", "Alexander Neundorf"),
                        i18nc("@info:credit", "Bug fixes and improved startup performance"),
                        QStringLiteral("neundorf@kde.org"));
    aboutData.addCredit(i18nc("@info:credit", "Peter Silva"), i18nc("@info:credit", "Marking improvements"), QStringLiteral("Peter.A.Silva@gmail.com"));
    aboutData.addCredit(i18nc("@info:credit", "Lotzi Boloni"),
                        i18nc("@info:credit",
                              "Embedded Konsole\n"
                              "Toolbar and session names"),
                        QStringLiteral("boloni@cs.purdue.edu"));
    aboutData.addCredit(i18nc("@info:credit", "David Faure"),
                        i18nc("@info:credit",
                              "Embedded Konsole\n"
                              "General improvements"),
                        QStringLiteral("faure@kde.org"));
    aboutData.addCredit(i18nc("@info:credit", "Antonio Larrosa"), i18nc("@info:credit", "Visual effects"), QStringLiteral("larrosa@kde.org"));
    aboutData.addCredit(i18nc("@info:credit", "Matthias Ettrich"),
                        i18nc("@info:credit",
                              "Code from the kvt project\n"
                              "General improvements"),
                        QStringLiteral("ettrich@kde.org"));
    aboutData.addCredit(i18nc("@info:credit", "Warwick Allison"),
                        i18nc("@info:credit", "Schema and text selection improvements"),
                        QStringLiteral("warwick@troll.no"));
    aboutData.addCredit(i18nc("@info:credit", "Dan Pilone"), i18nc("@info:credit", "SGI port"), QStringLiteral("pilone@slac.com"));
    aboutData.addCredit(i18nc("@info:credit", "Kevin Street"), i18nc("@info:credit", "FreeBSD port"), QStringLiteral("street@iname.com"));
    aboutData.addCredit(i18nc("@info:credit", "Sven Fischer"), i18nc("@info:credit", "Bug fixes"), QStringLiteral("herpes@kawo2.renditionwth-aachen.de"));
    aboutData.addCredit(i18nc("@info:credit", "Dale M. Flaven"), i18nc("@info:credit", "Bug fixes"), QStringLiteral("dflaven@netport.com"));
    aboutData.addCredit(i18nc("@info:credit", "Martin Jones"), i18nc("@info:credit", "Bug fixes"), QStringLiteral("mjones@powerup.com.au"));
    aboutData.addCredit(i18nc("@info:credit", "Lars Knoll"), i18nc("@info:credit", "Bug fixes"), QStringLiteral("knoll@mpi-hd.mpg.de"));
    aboutData.addCredit(i18nc("@info:credit", "Thanks to many others.\n"));
}

void restoreSession(Application &app)
{
    int n = 1;

    while (KMainWindow::canBeRestored(n)) {
        auto mainWindow = app.newMainWindow();
        mainWindow->restore(n++);
        mainWindow->viewManager()->toggleActionsBasedOnState();
        mainWindow->show();

        // TODO: HACK without the code below the sessions would be `uninitialized`
        // and the tabs wouldn't display the correct information.
        auto tabbedContainer = qobject_cast<Konsole::TabbedViewContainer *>(mainWindow->centralWidget());
        for (int i = 0; i < tabbedContainer->count(); i++) {
            tabbedContainer->setCurrentIndex(i);
        }
    }
}
