#include "PlatformImplementation.h"

#include "DolphinFileManager.h"
#include "MessageDialogOpener.h"
#include "NautilusFileManager.h"

#include <QFile>
#include <QGuiApplication>
#include <QHostInfo>
#include <QObject>
#include <QProgressBar>
#include <QRegularExpression>
#include <QScreen>
#include <QSet>
#include <QVariantMap>
#include <QX11Info>
#include <sys/statvfs.h>

#ifndef QT_NO_DBUS
#include <QDBusConnection>
#include <QDBusConnectionInterface>
#include <QDBusInterface>
#include <QDBusVariant>
#endif

#include <cstdlib>
#include <cstring>

using namespace std;
using namespace mega;

static const QString NotAllowedDefaultFactoryBiosName = QString::fromUtf8("To be filled by O.E.M.");

PlatformImplementation::PlatformImplementation()
{
    autostart_dir = QDir::homePath() + QString::fromLatin1("/.config/autostart/");
    desktop_file = autostart_dir + QString::fromLatin1("megasync.desktop");
    custom_icon = QString::fromUtf8("/usr/share/icons/hicolor/256x256/apps/mega.png");
}

PlatformImplementation::~PlatformImplementation()
{
    stopThemeMonitor();
}

void PlatformImplementation::initialize(int /*argc*/, char** /*argv*/)
{
    mShellNotifier = std::make_shared<SignalShellNotifier>();

    startThemeMonitor();
}

void PlatformImplementation::notifyItemChange(const QString& path, int)
{
    if (!path.isEmpty())
    {
        if (notify_server && !Preferences::instance()->overlayIconsDisabled())
        {
            notify_server->notifyItemChange(path);
        }
        mShellNotifier->notify(path);
    }
}

void PlatformImplementation::notifySyncFileChange(std::string *localPath, int newState)
{
    if(localPath && localPath->size())
    {
        notifyItemChange(QString::fromStdString(*localPath), newState);
    }
}

// enable or disable MEGASync launching at startup
// return true if operation succeeded
bool PlatformImplementation::startOnStartup(bool value)
{
    // copy desktop file into autostart directory
    if (value)
    {
        if (QFile(desktop_file).exists())
        {
            return true;
        }
        else
        {
            // make sure directory exist
            if (!QDir(autostart_dir).exists())
            {
                if (!QDir().mkdir(autostart_dir))
                {
                    //LOG_debug << "Failed to create autostart dir: " << autostart_dir;
                    return false;
                }
            }
            QString app_desktop = QString::fromLatin1("/usr/share/applications/megasync.desktop");
            if (QFile(app_desktop).exists())
            {
                return QFile::copy(app_desktop, desktop_file);
            }
            else
            {
                //LOG_debug << "Desktop file does not exist: " << app_desktop;
                return false;
            }
        }
    }
    else
    {
        // remove desktop file if it exists
        if (QFile(desktop_file).exists())
        {
            return QFile::remove(desktop_file);
        }
    }
    return true;
}

bool PlatformImplementation::isStartOnStartupActive()
{
    return QFile(desktop_file).exists();
}

bool PlatformImplementation::isTilingWindowManager()
{
    static const QSet<QString> tiling_wms = {
        QLatin1String("i3"),
        QLatin1String("Hyprland"),
        QLatin1String("sway")
    };

    return getValue("MEGASYNC_ASSUME_TILING_WM", false)
           || tiling_wms.contains(getWindowManagerName());
}

namespace
{
// Value of a -platform / --platform command-line switch if present (handles
// both "-platform xcb" and "-platform=xcb"), else empty. Read from
// /proc/self/cmdline because this can run before QApplication exists, so
// QCoreApplication::arguments() is unavailable — and the switch overrides
// QT_QPA_PLATFORM, so it must be honoured.
QString platformFromCommandLine()
{
    QFile cmdline(QString::fromUtf8("/proc/self/cmdline"));
    if (!cmdline.open(QIODevice::ReadOnly))
    {
        return {};
    }

    const QList<QByteArray> args = cmdline.readAll().split('\0');
    for (int i = 0; i < args.size(); ++i)
    {
        const QString arg = QString::fromUtf8(args.at(i));
        if (arg == QString::fromUtf8("-platform") || arg == QString::fromUtf8("--platform"))
        {
            return (i + 1 < args.size()) ? QString::fromUtf8(args.at(i + 1)) : QString();
        }
        if (arg.startsWith(QString::fromUtf8("-platform=")) ||
            arg.startsWith(QString::fromUtf8("--platform=")))
        {
            return arg.section(QLatin1Char('='), 1);
        }
    }
    return {};
}

// Pre-QApplication Wayland detection from the environment, mirroring Qt's
// platform-selection precedence: -platform switch, then QT_QPA_PLATFORM, then
// the session type.
bool isWaylandFromEnvironment()
{
    QString platform = platformFromCommandLine();
    if (platform.isEmpty())
    {
        platform = qEnvironmentVariable("QT_QPA_PLATFORM");
    }

    if (!platform.isEmpty())
    {
        // May be a fallback list ("wayland;xcb") or carry plugin options
        // ("wayland:..."); the first entry is the one Qt tries first.
        return platform.section(QLatin1Char(';'), 0, 0).startsWith(QString::fromUtf8("wayland"));
    }
    return qEnvironmentVariable("XDG_SESSION_TYPE") == QString::fromUtf8("wayland");
}
} // namespace

bool PlatformImplementation::isWayland()
{
    // Once the GUI application exists, the platform plugin name is
    // authoritative (base behaviour).
    if (!QGuiApplication::platformName().isEmpty())
    {
        return AbstractPlatform::isWayland();
    }

    // Called before the QApplication exists (e.g. the scale-factor setup):
    // detect from the environment instead.
    return isWaylandFromEnvironment();
}

QPoint PlatformImplementation::initialDialogPosition(const QSize& dialogSize) const
{
    auto primaryScreen = QGuiApplication::primaryScreen();
    if (!primaryScreen)
    {
        return QPoint();
    }

    const auto primaryGeometry = primaryScreen->geometry();

    constexpr double CENTERING_FACTOR(0.5);
    int xPos(primaryGeometry.x() + static_cast<int>(primaryGeometry.width() * CENTERING_FACTOR -
                                                    dialogSize.width() * CENTERING_FACTOR));
    int yPos(primaryGeometry.y() + static_cast<int>(primaryGeometry.height() * CENTERING_FACTOR -
                                                    dialogSize.height() * CENTERING_FACTOR));
    return QPoint(xPos, yPos);
}

QPoint PlatformImplementation::initialDialogPosition(const QSize& dialogSize,
                                                     const QRect& parentGeometry) const
{
    if (!parentGeometry.isValid())
    {
        return initialDialogPosition(dialogSize);
    }

    constexpr double CENTERING_FACTOR(0.5);
    int xPos(parentGeometry.x() + static_cast<int>(parentGeometry.width() * CENTERING_FACTOR -
                                                   dialogSize.width() * CENTERING_FACTOR));
    int yPos(parentGeometry.y() + static_cast<int>(parentGeometry.height() * CENTERING_FACTOR -
                                                   dialogSize.height() * CENTERING_FACTOR));
    return QPoint(xPos, yPos);
}

void PlatformImplementation::moveDialog(QWidget* dialog, const QPoint& pos, QWindow* visualParent)
{
    if (isWayland() && visualParent)
    {
        // On Wayland, clients cannot set absolute window positions — move() is
        // silently ignored. Setting a transient parent lets the compositor
        // centre the dialog over the parent window instead.
        dialog->winId(); // force native window creation before querying the handle
        if (QWindow* handle = dialog->windowHandle())
        {
            handle->setTransientParent(visualParent);
            return;
        }
    }
    dialog->move(pos);
}

bool PlatformImplementation::showInFolder(QString pathIn)
{
    QString fileBrowser = getDefaultFileBrowserApp();

    static const QMap<QString, QStringList> showInFolderCallMap
    {
        {QLatin1String("dolphin"), DolphinFileManager::getShowInFolderParams()},
        {QLatin1String("nautilus"), NautilusFileManager::getShowInFolderParams()}
    };

    QStringList params;
    auto itFoundAppParams = showInFolderCallMap.constFind(fileBrowser);
    if (itFoundAppParams != showInFolderCallMap.constEnd())
    {
        params << *itFoundAppParams;
        return QProcess::startDetached(fileBrowser, params << QUrl::fromLocalFile(pathIn).toString());
    }
    else
    {
        QString folderToOpen;
        QFileInfo file(pathIn);
        if(file.isFile())
        {
            //xdg-open open folders, so we choose the file parent folder
            folderToOpen = file.absolutePath();
        }
        else
        {
            folderToOpen = pathIn;
        }

        return QProcess::startDetached(QLatin1String("xdg-open"), params << QUrl::fromLocalFile(folderToOpen).toString());
    }

}

void PlatformImplementation::startShellDispatcher(MegaApplication *receiver)
{
    if (!ext_server)
    {
        ext_server = new ExtServer(receiver);
    }

    if (!notify_server)
    {
        notify_server = new NotifyServer();
    }
}

void PlatformImplementation::stopShellDispatcher()
{
    if (ext_server)
    {
        delete ext_server;
        ext_server = NULL;
    }

    if (notify_server)
    {
        delete notify_server;
        notify_server = NULL;
    }
}

void PlatformImplementation::syncFolderAdded(QString syncPath, QString /*syncName*/, QString /*syncID*/)
{
    if (QFile(custom_icon).exists())
    {
        QFile *folder = new QFile(syncPath);
        if (folder->exists())
        {
            NautilusFileManager::changeFolderIcon(syncPath, custom_icon);
            DolphinFileManager::changeFolderIcon(syncPath, custom_icon);
        }
        delete folder;

    }

    if (notify_server)
    {
        notify_server->notifySyncAdd(syncPath);
    }
}

void PlatformImplementation::syncFolderRemoved(QString syncPath, QString /*syncName*/, QString /*syncID*/)
{
    QFile *folder = new QFile(syncPath);
    if (folder->exists())
    {
        NautilusFileManager::changeFolderIcon(syncPath);
        DolphinFileManager::changeFolderIcon(syncPath);
    }
    delete folder;

    if (notify_server)
    {
        notify_server->notifySyncDel(syncPath);
    }
}

void PlatformImplementation::notifyRestartSyncFolders()
{

}

void PlatformImplementation::notifyAllSyncFoldersAdded()
{

}

void PlatformImplementation::notifyAllSyncFoldersRemoved()
{

}

QString PlatformImplementation::preparePathForSync(const QString& path)
{
    return QDir::toNativeSeparators(QDir::cleanPath(path));
}

void PlatformImplementation::processSymLinks() {}

bool PlatformImplementation::loadThemeResource(const QString& theme)
{
    static QString currentTheme = QString();

    if (!currentTheme.isEmpty())
    {
        QResource::unregisterResource(currentTheme);
    }

    QString execPath = MegaApplication::applicationDirPath();
    QString resourcePath = execPath + QString::fromUtf8("/../share/megasync/resources");

    if (QDir(resourcePath).exists())
    {
        // Look for installed files, if not, use rcc files from same path as binary.
        execPath = resourcePath;
    }

    QStringList rccFiles =
        QStringList() << execPath + QString::fromUtf8("/Resources_common.rcc")
                      << execPath + QString::fromUtf8("/Resources_qml.rcc")
                      << execPath + QString::fromUtf8("/qml.rcc")
                      << execPath + QString::fromUtf8("/Resources_%1.rcc").arg(theme.toLower());

    bool allLoaded = loadRccResources(rccFiles);

    if (allLoaded)
    {
        currentTheme = execPath + QString::fromUtf8("/Resources_%1.rcc").arg(theme.toLower());
    }

    return allLoaded;
}

QString PlatformImplementation::getDefaultFileBrowserApp()
{
    return getDefaultOpenAppByMimeType(QString::fromUtf8("inode/directory"));
}

QString PlatformImplementation::getDefaultOpenApp(QString extension)
{
    char *mimeType = MegaApi::getMimeType(extension.toUtf8().constData());
    if (!mimeType)
    {
        return QString();
    }
    QString qsMimeType(QString::fromUtf8(mimeType));
    delete mimeType;
    return getDefaultOpenAppByMimeType(qsMimeType);
}

QString PlatformImplementation::getDefaultOpenAppByMimeType(QString mimeType)
{
    QString exe = QLatin1String("xdg-mime");
    QStringList args;
    args << QLatin1String("query");
    args << QLatin1String("default");
    args << mimeType;

    QProcess process;
    process.start(exe, args, QIODevice::ReadWrite | QIODevice::Text);
    if(!process.waitForFinished(5000))
    {
        return QString();
    }

    QString desktopFileName = QString::fromUtf8(process.readAllStandardOutput());
    desktopFileName = desktopFileName.trimmed();
    desktopFileName.replace(QString::fromUtf8(";"), QString::fromUtf8(""));
    if (!desktopFileName.size())
    {
        return QString();
    }

    QFileInfo desktopFile(QString::fromUtf8("/usr/share/applications/") + desktopFileName);
    if (!desktopFile.exists())
    {
        return QString();
    }

    QFile f(desktopFile.absoluteFilePath());
    if (!f.open(QFile::ReadOnly | QFile::Text))
    {
        return QString();
    }

    QTextStream in(&f);
    QStringList contents = in.readAll().split(QString::fromUtf8("\n"));
    contents = contents.filter(QRegularExpression(QString::fromUtf8("^Exec=")));
    if (!contents.size())
    {
        return QString();
    }

    QString line = contents.first();
    QRegularExpression captureRegexCommand(QString::fromUtf8("^Exec=([^' ']*)"));
    auto reMatch = captureRegexCommand.match(line);

    if (reMatch.hasMatch())
    {
        return reMatch.captured(1); // return first group from regular expression.
    }

    return QString();
}

bool PlatformImplementation::getValue(const char * const name, const bool default_value)
{
    QString value = qEnvironmentVariable(name);

    if (value.isEmpty())
    {
        return default_value;
    }

    return value != QString::fromUtf8("0");
}

std::string PlatformImplementation::getValue(const char * const name, const std::string &default_value)
{
    QString value = qEnvironmentVariable(name);

    if (value.isEmpty())
    {
        return default_value;
    }

    return value.toUtf8().constData();
}

QString PlatformImplementation::getWindowManagerName()
{
    static QString wmName;
    static bool cached = false;

    if (!cached)
    {
        if (QX11Info::isPlatformX11())
        {
            const int maxLen = 1024;
            const auto connection = QX11Info::connection();
            const auto appRootWindow = static_cast<xcb_window_t>(QX11Info::appRootWindow());

            if (connection != nullptr)
            {
                auto wmCheckAtom = getAtom(connection, "_NET_SUPPORTING_WM_CHECK");
                // Get window manager
                auto reply = xcb_get_property_reply(connection,
                                                    xcb_get_property(connection,
                                                                     false,
                                                                     appRootWindow,
                                                                     wmCheckAtom,
                                                                     XCB_ATOM_WINDOW,
                                                                     0,
                                                                     maxLen),
                                                    nullptr);

                if (reply && reply->format == 32 && reply->type == XCB_ATOM_WINDOW)
                {
                    // Get window manager name
                    const xcb_window_t windowManager =
                        *(static_cast<xcb_window_t*>(xcb_get_property_value(reply)));

                    if (windowManager != XCB_WINDOW_NONE)
                    {
                        const auto utf8StringAtom = getAtom(connection, "UTF8_STRING");
                        const auto wmNameAtom = getAtom(connection, "_NET_WM_NAME");

                        auto wmReply = xcb_get_property_reply(connection,
                                                              xcb_get_property(connection,
                                                                               false,
                                                                               windowManager,
                                                                               wmNameAtom,
                                                                               utf8StringAtom,
                                                                               0,
                                                                               maxLen),
                                                              nullptr);
                        if (wmReply && wmReply->format == 8 && wmReply->type == utf8StringAtom)
                        {
                            wmName = QString::fromUtf8(
                                static_cast<const char*>(xcb_get_property_value(wmReply)),
                                xcb_get_property_value_length(wmReply));
                        }
                        free(wmReply);
                    }
                }
                free(reply);
                cached = true;
            }
        }
    }

    if (!cached)
    {
        // The previous method failed. We are most probably on Wayland.
        // Try to get info from environment.
        wmName = qEnvironmentVariable("XDG_CURRENT_DESKTOP");
        cached = !wmName.isEmpty();
    }
    return wmName;
}

bool PlatformImplementation::registerUpdateJob()
{
    return true;
}

bool PlatformImplementation::isUserActive()
{
    return true;
}

QString PlatformImplementation::getDeviceName()
{
    // First, try to read maker and model
    QString vendor;
    QFile vendorFile(QLatin1String("/sys/devices/virtual/dmi/id/board_vendor"));
    if (vendorFile.open(QIODevice::ReadOnly | QIODevice::Text))
    {
        vendor = QString::fromUtf8(vendorFile.readLine()).trimmed();
    }
    vendorFile.close();

    QString model;
    QFile modelFile(QLatin1String("/sys/devices/virtual/dmi/id/product_name"));
    if (modelFile.open(QIODevice::ReadOnly | QIODevice::Text))
    {
        model = QString::fromUtf8(modelFile.readLine()).trimmed();
    }
    modelFile.close();

    QString deviceName = vendor + QLatin1String(" ") + model;
    // If failure, empty strings or defaultFactoryBiosName, give hostname.
    if ((vendor.isEmpty() && model.isEmpty()) || deviceName.contains(NotAllowedDefaultFactoryBiosName))
    {
        deviceName = QHostInfo::localHostName();
    }

    return deviceName;
}

void PlatformImplementation::fileSelector(const SelectorInfo& info)
{
    if (info.defaultDir.isEmpty())
    {
        auto updateInfo = info;
        updateInfo.defaultDir = QLatin1String("/");
        AbstractPlatform::fileSelector(updateInfo);
    }
    else
    {
        AbstractPlatform::fileSelector(info);
    }
}

void PlatformImplementation::folderSelector(const SelectorInfo& info)
{
    if (info.defaultDir.isEmpty())
    {
        auto updateInfo = info;
        updateInfo.defaultDir = QLatin1String("/");
        AbstractPlatform::folderSelector(updateInfo);
    }
    else
    {
        AbstractPlatform::folderSelector(info);
    }
}

void PlatformImplementation::fileAndFolderSelector(const SelectorInfo& info)
{
    if (info.defaultDir.isEmpty())
    {
        auto updateInfo = info;
        updateInfo.defaultDir = QLatin1String("/");
        AbstractPlatform::fileAndFolderSelector(updateInfo);
    }
    else
    {
        AbstractPlatform::fileAndFolderSelector(info);
    }
}

void PlatformImplementation::streamWithApp(const QString &app, const QString &url)
{
    QProcess::startDetached(QDir::toNativeSeparators(app), {url});
}

DriveSpaceData PlatformImplementation::getDriveData(const QString& path)
{
    DriveSpaceData data;

    struct statvfs statData;
    const int result = statvfs(path.toUtf8().constData(), &statData);
    data.mIsReady = (result == 0);
    data.mTotalSpace = static_cast<long long>(statData.f_blocks * statData.f_bsize);
    data.mAvailableSpace = static_cast<long long>(statData.f_bfree * statData.f_bsize);
    return data;
}

#if defined(ENABLE_SDK_ISOLATED_GFX)
QString PlatformImplementation::getGfxProviderPath()
{
    return QCoreApplication::applicationDirPath() + QLatin1String("/mega-desktop-app-gfxworker");
}
#endif

xcb_atom_t PlatformImplementation::getAtom(xcb_connection_t * const connection, const char *name)
{
    xcb_intern_atom_cookie_t cookie =
      xcb_intern_atom(connection, 0, static_cast<uint16_t>(strlen(name)), name);
    xcb_intern_atom_reply_t *reply =
      xcb_intern_atom_reply(connection, cookie, nullptr);

    if (!reply)
        return XCB_ATOM_NONE;

    xcb_atom_t result = reply->atom;
    free(reply);

    return result;
}

bool PlatformImplementation::validateSystemTrayIntegration()
{
    if (QSystemTrayIcon::isSystemTrayAvailable() || verifyAndEnableAppIndicatorExtension())
    {
        return true;
    }

    QByteArray appIndicatorInstallFlag = qgetenv("MEGA_ALLOW_DNF_APPINDICATOR_INSTALL");

    bool isAppIndicatorInstallEnabled = !appIndicatorInstallFlag.isEmpty() &&
                                        (appIndicatorInstallFlag == "1" ||
                                         appIndicatorInstallFlag.toLower() == "true" ||
                                         appIndicatorInstallFlag.toLower() == "yes");

    if (isAppIndicatorInstallEnabled || isFedoraWithGnome())
    {
        constexpr qint64 SECONDS_IN_A_WEEK = 604800;

        qint64 currentTimestamp = QDateTime::currentSecsSinceEpoch();
        qint64 lastPromptTimestamp = Preferences::instance()->getSystemTrayLastPromptTimestamp();

        bool aWeekHasPassed = (currentTimestamp - lastPromptTimestamp >= SECONDS_IN_A_WEEK);
        bool oneTimeSystrayCheck = !Preferences::instance()->isOneTimeActionDone(Preferences::ONE_TIME_ACTION_NO_SYSTRAY_AVAILABLE);
        bool promptSuppressed = Preferences::instance()->isSystemTrayPromptSuppressed();

        // Check if it's time to prompt the user
        if ((oneTimeSystrayCheck || aWeekHasPassed) && !promptSuppressed)
        {
            promptFedoraGnomeUser();
        }
        return true; // Always return true for Fedora GNOME environment as the system tray integration is already handled
    }

    // Return false for non-Fedora GNOME environments
    return false;
}

bool PlatformImplementation::isFedoraWithGnome()
{
    // Check for GNOME Desktop Environment
    QByteArray desktopEnvironment = qgetenv("XDG_CURRENT_DESKTOP");
    if (!desktopEnvironment.toLower().contains(QStringLiteral("gnome").toUtf8()))
    {
        return false;
    }

    // Check for Fedora OS
    QString osReleasePath = QStringLiteral("/etc/os-release");
    QFile osReleaseFile(osReleasePath);
    if (osReleaseFile.open(QIODevice::ReadOnly | QIODevice::Text))
    {
        QString content = QString::fromUtf8(osReleaseFile.readAll());
        if (content.contains(QStringLiteral("ID=fedora"), Qt::CaseInsensitive))
        {
            return true;
        }
    }

    return false;
}

void PlatformImplementation::promptFedoraGnomeUser()
{
    MessageDialogInfo msgInfo;
    msgInfo.titleText = QCoreApplication::translate("LinuxPlatformNotificationAreaIcon",
                                                    "Install notification area icon");
    msgInfo.descriptionText =
        QCoreApplication::translate("LinuxPlatformNotificationAreaIcon",
                                    "For a better experience on Fedora with GNOME, we recommend "
                                    "you enable the notification area icon.\n"
                                    "Would you like to install the necessary components now?");
    msgInfo.buttons = QMessageBox::Yes | QMessageBox::No;
    msgInfo.defaultButton = QMessageBox::Yes;
    msgInfo.checkboxText =
        QCoreApplication::translate("LinuxPlatformNotificationAreaIcon", "Do not show again");
    msgInfo.finishFunc = [this](QPointer<MessageDialogResult> msg)
    {
        if (!msg)
            return;

        bool isInstallationAttempted = (msg->result() == QMessageBox::Yes);
        bool isInstallationSuccessful = false;

        if (isInstallationAttempted)
        {
            // Execute the bash script to install appindicator.
            isInstallationSuccessful = installAppIndicatorForFedoraGnome();
        }

        if (msg->isChecked())
        {
            Preferences::instance()->setSystemTrayPromptSuppressed(true);
        }
        else
        {
            Preferences::instance()->setSystemTrayLastPromptTimestamp(
                QDateTime::currentSecsSinceEpoch());
        }

        // Set the one-time action done if the user clicked "No" or if the installation was successful.
        if (!isInstallationAttempted || isInstallationSuccessful)
        {
            Preferences::instance()->setOneTimeActionDone(Preferences::ONE_TIME_ACTION_NO_SYSTRAY_AVAILABLE, true);
        }
    };

    MessageDialogOpener::question(msgInfo);
}

bool PlatformImplementation::installAppIndicatorForFedoraGnome()
{
    constexpr char GNOME_EXTENSIONS_CMD[] = "gnome-extensions";
    constexpr char APP_INDICATOR_EXTENSION_ID[] = "appindicatorsupport@rgcjonas.gmail.com";
    const int PROCESS_TIMEOUT_MS = 120000; // 2 minutes

    QProcess checkProcess;
    checkProcess.start(QString::fromUtf8(GNOME_EXTENSIONS_CMD), { QStringLiteral("version") });
    bool gnomeExtensionsAvailable = checkProcess.waitForFinished(PROCESS_TIMEOUT_MS) && checkProcess.exitCode() == 0;

    QStringList installArgs = { QStringLiteral("dnf"), QStringLiteral("install"), QStringLiteral("-y") };
    if (!gnomeExtensionsAvailable)
    {
        installArgs << QStringLiteral("gnome-shell-extensions");
    }
    installArgs << QStringLiteral("gnome-shell-extension-appindicator");

    QProgressDialog progressDialog(QCoreApplication::translate("LinuxPlatformNotificationAreaIcon", "Installing notification area icon..."), QCoreApplication::translate("LinuxPlatformNotificationAreaIcon","Cancel"), 0, 100);
    progressDialog.setWindowTitle(QCoreApplication::translate("LinuxPlatformNotificationAreaIcon","Installing"));
    progressDialog.setWindowModality(Qt::WindowModal);
    progressDialog.show();

    QProcess installProcess;
    QEventLoop loop;
    QByteArray byteArray;
    bool cancellationInitiated = false;

    auto updateProgress =  [this, &cancellationInitiated, &installProcess, &progressDialog, &byteArray, &loop]()
    {
        QString output = QString::fromUtf8(byteArray.data(), byteArray.size());
        int parsedValue = this->parseDnfOutput(output);
        progressDialog.setValue(parsedValue);

        if (progressDialog.wasCanceled() && !cancellationInitiated)
        {
            cancellationInitiated = true;
            installProcess.kill();
            installProcess.waitForFinished();
            progressDialog.close();

            MessageDialogInfo msgWarnInfo;
            msgWarnInfo.titleText = QCoreApplication::translate("LinuxPlatformNotificationAreaIcon",
                                                                "Installation Cancelled");
            msgWarnInfo.descriptionText = QCoreApplication::translate(
                "LinuxPlatformNotificationAreaIcon",
                "The notification area icon installation was cancelled.");
            MessageDialogOpener::warning(msgWarnInfo);

            loop.exit(1);
        }
    };

    QObject::connect(&installProcess, &QProcess::readyReadStandardOutput, [&]() {
        byteArray += installProcess.readAllStandardOutput();
        updateProgress();
    });

    QObject::connect(&installProcess, &QProcess::readyReadStandardError,  [&]() {
        byteArray += installProcess.readAllStandardError();
        updateProgress();
    });

    QObject::connect(&installProcess, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), [&](int exitCode) {
        if (exitCode == 0)
        {
            progressDialog.close();
            loop.exit(0);
        } else
        {
            MessageDialogInfo errorInfo;
            errorInfo.titleText =
                QCoreApplication::translate("LinuxPlatformNotificationAreaIcon",
                                            "Failed to install the necessary components.");
            errorInfo.descriptionText = QCoreApplication::translate(
                "LinuxPlatformNotificationAreaIcon",
                "To install manually, please run the following commands:\n\n"
                "sudo dnf install gnome-shell-extensions\n"
                "sudo dnf install gnome-shell-extension-appindicator\n"
                "gnome-extensions enable appindicatorsupport@rgcjonas.gmail.com");
            MessageDialogOpener::critical(errorInfo);
            loop.exit(1);
        }
    });

    installProcess.start(QStringLiteral("pkexec"), installArgs);
    int loopResult = loop.exec();

    if (loopResult != 0)
    {
        return false;
    }

    // After successful installation, enable the extension
    QProcess enableProcess;
    enableProcess.start(QString::fromUtf8(GNOME_EXTENSIONS_CMD), { QStringLiteral("enable"), QString::fromUtf8(APP_INDICATOR_EXTENSION_ID) });
    enableProcess.waitForFinished(PROCESS_TIMEOUT_MS);

    MessageDialogInfo successInfo;
    successInfo.titleText =
        QCoreApplication::translate("LinuxPlatformNotificationAreaIcon", "Install complete");
    successInfo.descriptionText = QCoreApplication::translate(
        "LinuxPlatformNotificationAreaIcon",
        "The notification area icon was installed successfully.\n"
        "Please log out of your computer to complete the installation.");
    MessageDialogOpener::information(successInfo);

    return true;
}

int PlatformImplementation::parseDnfOutput(const QString& dnfOutput)
{
    QRegularExpression progressRegExp(QString::fromUtf8(R"(\((\d+)/(\d+)\):)"));
    int totalPackages = 0;
    int packagesProcessed = 0;

    QStringList lines = dnfOutput.split(QLatin1Char('\n'));
    for (const QString& line : lines)
    {
        QRegularExpressionMatch match = progressRegExp.match(line);
        if (match.hasMatch())
        {
            int currentPackageIndex = match.captured(1).toInt();
            int currentTotalPackages = match.captured(2).toInt();
            totalPackages = qMax(totalPackages, currentTotalPackages);
            packagesProcessed = qMax(packagesProcessed, currentPackageIndex);
        }
    }

    return totalPackages > 0 ? static_cast<int>(100.0 * packagesProcessed / totalPackages) : 0;
}

bool PlatformImplementation::verifyAndEnableAppIndicatorExtension()
{
    constexpr char GNOME_EXTENSIONS_CMD[] = "gnome-extensions";
    constexpr char APP_INDICATOR_EXTENSION_ID[] = "appindicatorsupport@rgcjonas.gmail.com";
    constexpr char GNOME_EXTENSION_PATH[] = "/usr/share/gnome-shell/extensions/";
    const int PROCESS_TIMEOUT_MS = 120000; // 2 minutes

    QProcess process;

    // Check if the extension directory exists in the filesystem
    const QString extensionDirPath = QString::fromUtf8(GNOME_EXTENSION_PATH) + QString::fromUtf8(APP_INDICATOR_EXTENSION_ID);
    if (!QDir(extensionDirPath).exists())
    {
        return false;
    }

    // Check if GNOME extensions command is available
    process.start(QString::fromUtf8(GNOME_EXTENSIONS_CMD), { QStringLiteral("version") });
    bool gnomeExtensionsAvailable = process.waitForFinished(PROCESS_TIMEOUT_MS) && process.exitCode() == 0;
    if (!gnomeExtensionsAvailable)
    {
        return false;
    }

    // List all the available extensions to ensure the extension is recognized by GNOME
    process.start(QString::fromUtf8(GNOME_EXTENSIONS_CMD), {QLatin1String("list")});
    if (!process.waitForFinished(PROCESS_TIMEOUT_MS))
    {
        return false;
    }

    // Check if the app indicator extension is recognized by GNOME
    QString output = QString::fromUtf8(process.readAllStandardOutput());
    if (!output.contains(QString::fromUtf8(APP_INDICATOR_EXTENSION_ID))) {
        return false;
    }

    // Enable the app indicator extension using GNOME extensions command
    process.start(QString::fromUtf8(GNOME_EXTENSIONS_CMD), { QStringLiteral("enable"), QString::fromUtf8(APP_INDICATOR_EXTENSION_ID) });
    if (!process.waitForFinished(PROCESS_TIMEOUT_MS) || process.exitCode() != 0)
    {
        return false;
    }

    return true;
}

void PlatformImplementation::calculateInfoDialogCoordinates(const QRect& rect, int* posx, int* posy)
{
    int xSign = 1;
    int ySign = 1;
    QPoint position;
    QRect screenGeometry;

    position = QCursor::pos();
    QScreen* currentScreen = QGuiApplication::screenAt(position);
    if (currentScreen)
    {
        screenGeometry = currentScreen->availableGeometry();

        QString otherInfo = QString::fromUtf8("pos = [%1,%2], name = %3").arg(position.x()).arg(position.y()).arg(currentScreen->name());
        logInfoDialogCoordinates("availableGeometry", screenGeometry, otherInfo);

        if (!screenGeometry.isValid())
        {
            screenGeometry = currentScreen->geometry();
            otherInfo = QString::fromUtf8("dialog rect = %1").arg(rectToString(rect));
            logInfoDialogCoordinates("screenGeometry", screenGeometry, otherInfo);

            if (screenGeometry.isValid())
            {
                screenGeometry.setTop(28);
            }
            else
            {
                screenGeometry = rect;
                screenGeometry.setBottom(screenGeometry.bottom() + 4);
                screenGeometry.setRight(screenGeometry.right() + 4);
            }

            logInfoDialogCoordinates("screenGeometry 2", screenGeometry, otherInfo);
        }
        else
        {
            if (screenGeometry.y() < 0)
            {
                ySign = -1;
            }

            if (screenGeometry.x() < 0)
            {
                xSign = -1;
            }
        }

        if (position.x() * xSign > (screenGeometry.right() / 2) * xSign)
        {
            *posx = screenGeometry.right() - rect.width() - 2;
        }
        else
        {
            *posx = screenGeometry.left() + 2;
        }

        if (position.y() * ySign > (screenGeometry.bottom() / 2) * ySign)
        {
            *posy = screenGeometry.bottom() - rect.height() - 2;
        }
        else
        {
            *posy = screenGeometry.top() + 2;
        }
    }

    QString otherInfo = QString::fromUtf8("dialog rect = %1, posx = %2, posy = %3").arg(rectToString(rect)).arg(*posx).arg(*posy);
    logInfoDialogCoordinates("Final", screenGeometry, otherInfo);
}

Preferences::SystemColorScheme PlatformImplementation::getCurrentThemeAppearance() const
{
    const auto theme = effectiveTheme();
    return {theme, theme};
}

void PlatformImplementation::setRenderingBackend() const
{
    QByteArray megaLibGL = qgetenv("MEGA_LIBGL_ALWAYS_SOFTWARE");
    if (!megaLibGL.isEmpty() && megaLibGL == "1")
    {
        MegaApi::log(MegaApi::LOG_LEVEL_INFO, "Setting LIBGL_ALWAYS_SOFTWARE to 1");
        qputenv("LIBGL_ALWAYS_SOFTWARE", "1");
    }

    // Improve QtQuickWidget compatibility
    QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts);
}

void PlatformImplementation::startThemeMonitor()
{
#ifndef QT_NO_DBUS
    // Setup portal monitoring
    setupSettingsPortalMonitor();
#endif
    if (!mIsSettingsPortalActive)
    {
        // Setup gsettings monitoring
        setupGSettingsThemeCli();
    }
}

void PlatformImplementation::stopThemeMonitor()
{
    mIsSettingsPortalActive = false;
    mThemeMonitor.kill();
    mThemeMonitor.waitForFinished(1000);
    mThemeMonitor.close();
    mCurrentPortalTheme = Preferences::ThemeAppeareance::UNINITIALIZED;
    mCurrentGSettingsTheme = Preferences::ThemeAppeareance::UNINITIALIZED;
    mLastEmittedTheme = Preferences::ThemeAppeareance::UNINITIALIZED;
    mLastEmittedPanelTheme = Preferences::ThemeAppeareance::UNINITIALIZED;
    mUseGtkTheme = false;
}

#ifndef QT_NO_DBUS

void PlatformImplementation::setupSettingsPortalMonitor()
{
    const QString DBUS_SERVICE = QLatin1String("org.freedesktop.portal.Desktop");
    const QString DBUS_PATH = QLatin1String("/org/freedesktop/portal/desktop");
    const QString DBUS_CONNECTION = QLatin1String("org.freedesktop.portal.Settings");
    const QString DBUS_NAME = QLatin1String("SettingChanged");
    const QString DBUS_SIG = QLatin1String("ssv");

    auto sessionbus = QDBusConnection::connectToBus(QDBusConnection::BusType::SessionBus,
                                                    QLatin1String("session"));

    mIsSettingsPortalActive = sessionbus.interface()->isServiceRegistered(DBUS_SERVICE);
    if (mIsSettingsPortalActive)
    {
        // Connect to setting changed signal.
        mIsSettingsPortalActive = sessionbus.connect(DBUS_SERVICE,
                                                     DBUS_PATH,
                                                     DBUS_CONNECTION,
                                                     DBUS_NAME,
                                                     DBUS_SIG,
                                                     this,
                                                     SLOT(onSettingsPortalChanged(QDBusMessage)));

        if (mIsSettingsPortalActive)
        {
            // Init value
            mCurrentPortalTheme = readSettingsPortal();
            mIsSettingsPortalActive =
                mCurrentPortalTheme != Preferences::ThemeAppeareance::UNINITIALIZED;
        }
    }
}

Preferences::ThemeAppeareance PlatformImplementation::themeFromVariant(const QVariant& var)
{
    // Documentation:
    // https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Settings.html
    static const unsigned int PORTAL_NO_PREFERENCE = 0;
    static const unsigned int PORTAL_DARK = 1;
    static const unsigned int PORTAL_LIGHT = 2;

    auto theme = Preferences::ThemeAppeareance::UNINITIALIZED;

    bool ok = false;
    auto extractedValue = var.toUInt(&ok);

    if (ok)
    {
        switch (extractedValue)
        {
            case PORTAL_DARK:
            {
                theme = Preferences::ThemeAppeareance::DARK;
                break;
            }
            case PORTAL_LIGHT:
                [[fallthrough]];
            case PORTAL_NO_PREFERENCE:
                [[fallthrough]];
            default:
            {
                // Default to light theme
                theme = Preferences::ThemeAppeareance::LIGHT;
            }
        }
    }

    return theme;
}

Preferences::ThemeAppeareance PlatformImplementation::readSettingsPortal()
{
    const QString DBUS_SERVICE = QLatin1String("org.freedesktop.portal.Desktop");
    const QString DBUS_PATH = QLatin1String("/org/freedesktop/portal/desktop");
    const QString DBUS_CONNECTION = QLatin1String("org.freedesktop.portal.Settings");
    const QString DBUS_NS = QLatin1String("org.freedesktop.appearance");
    const QString DBUS_KEY = QLatin1String("color-scheme");

    auto settingsPortal = QDBusInterface(DBUS_SERVICE, DBUS_PATH, DBUS_CONNECTION);

    // Prefer ReadOne(ns, key); fall back to Read(ns, [keys])
    QString DBUS_CMD = QLatin1String("ReadOne");
    QDBusMessage dbusMsg = settingsPortal.call(DBUS_CMD, DBUS_NS, DBUS_KEY);

    QVariant val;
    if (dbusMsg.type() == QDBusMessage::ReplyMessage && !dbusMsg.arguments().isEmpty())
    {
        val = dbusMsg.arguments().at(0);
    }
    else
    {
        DBUS_CMD = QLatin1String("Read");
        dbusMsg = settingsPortal.call(DBUS_CMD, DBUS_NS, QStringList{DBUS_KEY});

        if (dbusMsg.type() == QDBusMessage::ReplyMessage && !dbusMsg.arguments().isEmpty())
        {
            val = dbusMsg.arguments().at(0);
            if (val.canConvert<QVariantMap>())
            {
                val = val.toMap().value(DBUS_KEY);
            }
        }
    }

    if (val.userType() == qMetaTypeId<QDBusVariant>())
    {
        val = qvariant_cast<QDBusVariant>(val).variant();
    }

    return themeFromVariant(val);
}

void PlatformImplementation::onSettingsPortalChanged(const QDBusMessage& msg)
{
    const QString DBUS_NS = QLatin1String("org.freedesktop.appearance");
    const QString DBUS_KEY = QLatin1String("color-scheme");

    const auto args = msg.arguments();
    if (args.size() == 3)
    {
        const QString ns = args.at(0).toString();
        const QString key = args.at(1).toString();

        if (ns == DBUS_NS && key == DBUS_KEY)
        {
            QVariant val = args.at(2);

            if (val.userType() == qMetaTypeId<QDBusVariant>())
            {
                val = qvariant_cast<QDBusVariant>(val).variant();
            }
            mCurrentPortalTheme = themeFromVariant(val);
            maybeEmitTheme();
        }
    }
}

#endif

Preferences::ThemeAppeareance
    PlatformImplementation::themeFromColorSchemeString(const QString& schemeStr)
{
    // Documentation: https://gitlab.gnome.org/GNOME/Initiatives/-/wikis/Dark-Style-Preference
    static const QString GSETTINGS_PREFER_LIGHT = QLatin1String("prefer-light");
    static const QString GSETTINGS_PREFER_DARK = QLatin1String("prefer-dark");
    static const QString GSETTINGS_DEFAULT = QLatin1String("default");

    auto theme = Preferences::ThemeAppeareance::UNINITIALIZED;

    // Default to light theme
    if (schemeStr.contains(GSETTINGS_DEFAULT, Qt::CaseInsensitive) ||
        schemeStr.contains(GSETTINGS_PREFER_LIGHT, Qt::CaseInsensitive))
    {
        theme = Preferences::ThemeAppeareance::LIGHT;
    }
    else if (schemeStr.contains(GSETTINGS_PREFER_DARK, Qt::CaseInsensitive))
    {
        theme = Preferences::ThemeAppeareance::DARK;
    }
    return theme;
}

Preferences::ThemeAppeareance
    PlatformImplementation::themeFromGtkThemeString(const QString& themeStr)
{
    // Dark theme usually end with "-dark", like "Adwaita-dark"
    static const QString GTK_DARK_ID = QLatin1String("-dark");
    auto theme = Preferences::ThemeAppeareance::UNINITIALIZED;

    if (themeStr.contains(GTK_DARK_ID, Qt::CaseInsensitive))
    {
        theme = Preferences::ThemeAppeareance::DARK;
    }
    else
    {
        // Default to light theme
        theme = Preferences::ThemeAppeareance::LIGHT;
    }
    return theme;
}

Preferences::ThemeAppeareance PlatformImplementation::effectiveTheme() const
{
    auto theme = Preferences::ThemeAppeareance::UNINITIALIZED;
    // Portal wins when it is initialized; otherwise use GSettings if initialized.
    if (mCurrentPortalTheme != theme)
    {
        theme = mCurrentPortalTheme;
    }
    // Use fallback
    else if (mCurrentGSettingsTheme != theme)
    {
        theme = mCurrentGSettingsTheme;
    }
    return theme;
}

void PlatformImplementation::maybeEmitTheme()
{
    const auto appTheme = effectiveTheme();
    const auto panelTheme = getPanelTheme();

    if (appTheme != mLastEmittedTheme || panelTheme != mLastEmittedPanelTheme)
    {
        mLastEmittedTheme = appTheme;
        mLastEmittedPanelTheme = panelTheme;
        emit themeChanged({appTheme, appTheme});
    }
}

void PlatformImplementation::setupGSettingsThemeCli()
{
    const QString GSETTINGS_BIN = QLatin1String("gsettings");
    const QString GSETTINGS_DI_PATH = QLatin1String("org.gnome.desktop.interface");
    const QString GSETTINGS_GET_CMD = QLatin1String("get");
    const QString GSETTINGS_MONITOR_CMD = QLatin1String("monitor");
    // Gnome 42+ key
    const QString GSETTINGS_COLOR_SCHEME_KEY = QLatin1String("color-scheme");
    // GTK theme for fallback
    const QString GSETTINGS_GTK_THEME_KEY = QLatin1String("gtk-theme");

    const int PROCESS_TIMEOUT_MS = 1500;

    QProcess process;
    process.start(GSETTINGS_BIN,
                  {GSETTINGS_GET_CMD, GSETTINGS_DI_PATH, GSETTINGS_COLOR_SCHEME_KEY});
    bool gSettingsAvailable =
        process.waitForFinished(PROCESS_TIMEOUT_MS) && process.exitCode() == 0;
    if (gSettingsAvailable)
    {
        const QString output = QString::fromUtf8(process.readAllStandardOutput());
        mCurrentGSettingsTheme = themeFromColorSchemeString(output);
    }

    // gtk-theme fallback
    if (mCurrentGSettingsTheme == Preferences::ThemeAppeareance::UNINITIALIZED)
    {
        process.start(GSETTINGS_BIN,
                      {GSETTINGS_GET_CMD, GSETTINGS_DI_PATH, GSETTINGS_GTK_THEME_KEY});
        gSettingsAvailable = process.waitForFinished(PROCESS_TIMEOUT_MS) && process.exitCode() == 0;
        if (gSettingsAvailable)
        {
            const QString output = QString::fromUtf8(process.readAllStandardOutput());
            mCurrentGSettingsTheme = themeFromGtkThemeString(output);
            mUseGtkTheme = true;
        }
    }

    if (mCurrentGSettingsTheme != Preferences::ThemeAppeareance::UNINITIALIZED)
    {
        mThemeMonitor.start(GSETTINGS_BIN,
                            {GSETTINGS_MONITOR_CMD,
                             GSETTINGS_DI_PATH,
                             mUseGtkTheme ? GSETTINGS_GTK_THEME_KEY : GSETTINGS_COLOR_SCHEME_KEY});
        QObject::connect(&mThemeMonitor,
                         &QProcess::readyReadStandardOutput,
                         this,
                         &PlatformImplementation::onGsettingsThemeReadyRead);
    }
}

void PlatformImplementation::onGsettingsThemeReadyRead()
{
    QRegularExpression themeRx;
    if (mUseGtkTheme)
    {
        themeRx = QRegularExpression(QLatin1String(R"(.*gtk-theme.*'([^']+)')"));
    }
    else
    {
        themeRx = QRegularExpression(QLatin1String(R"(.*color-scheme.*'([^']+)')"));
    }

    while (mThemeMonitor.canReadLine())
    {
        const QString line = QString::fromUtf8(mThemeMonitor.readLine()).trimmed();
        const auto match = themeRx.match(line);

        if (match.hasMatch())
        {
            const QString val = match.captured(1);
            mCurrentGSettingsTheme =
                mUseGtkTheme ? themeFromGtkThemeString(val) : themeFromColorSchemeString(val);
            maybeEmitTheme();
        }
    }
}

Preferences::ThemeAppeareance PlatformImplementation::getPanelTheme() const
{
    const QString desktop = qEnvironmentVariable("XDG_CURRENT_DESKTOP").toUpper();

    if (desktop.contains(QLatin1String("GNOME")) || desktop.contains(QLatin1String("CINNAMON")) ||
        desktop.contains(QLatin1String("X-CINNAMON")))
    {
        return Preferences::ThemeAppeareance::DARK;
    }

    return effectiveTheme();
}
