// SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: LGPL-3.0-or-later

#include "dbus/applicationservice.h"
#include "APPobjectmanager1adaptor.h"
#include "applicationchecker.h"
#include "applicationmanagerstorage.h"
#include "config.h"
#include "constant.h"
#include "eventreporter.h"
#include "dbus/instanceadaptor.h"
#include "desktopentry.h"
#include "desktopfileparser.h"
#include "global.h"
#include "iniParser.h"
#include "launchoptions.h"
#include "prelaunchsplashhelper.h"
#include "propertiesForwarder.h"
#include <DConfig>
#include <QDBusMessage>
#include <QList>
#include <QLoggingCategory>
#include <QProcess>
#include <QRegularExpression>
#include <QStandardPaths>
#include <QStringList>
#include <QUrl>
#include <QUuid>
#include <algorithm>
#include <new>
#include <qcontainerfwd.h>
#include <qdbuserror.h>
#include <qfileinfo.h>
#include <qlogging.h>
#include <qnamespace.h>
#include <qtmetamacros.h>
#include <utility>
#include <wordexp.h>

using namespace Qt::StringLiterals;

namespace {
template <typename Adaptor>
void setAdaptorAutoRelaySignals(Adaptor *adaptor, bool enabled) noexcept
{
    struct Accessor final : Adaptor
    {
        using Adaptor::setAutoRelaySignals;
    };

    if (adaptor != nullptr) {
        static_cast<Accessor *>(adaptor)->setAutoRelaySignals(enabled);
    }
}

void sendObjectManagerSignal(const QString &path, const char *member, const QDBusObjectPath &objectPath, const QVariant &payload)
{
    auto msg = QDBusMessage::createSignal(path, fromStaticRaw(ObjectManagerInterface), member);
    msg << objectPath;
    msg << payload;
    ApplicationManager1DBus::instance().globalServerBus().send(msg);
}

void appendEnvs(const QVariant &var, QStringList &envs)
{
    const int typeId = var.userType();
    if (typeId == QMetaType::QStringList) {
        envs.append(*static_cast<const QStringList *>(var.constData()));
    } else if (typeId == QMetaType::QString) {
        const auto &rawStr = *static_cast<const QString *>(var.constData());

        auto tokens = qTokenize(rawStr, u';');
        for (auto token : tokens) {
            if (!token.isEmpty()) {
                envs.append(token.toString());
            }
        }
    }
}

void unescapeEnvs(QVariantMap &options) noexcept
{
    const auto &envKey = fromStaticRaw(EnvKey);
    auto it = options.constFind(envKey);
    if (it == options.cend()) {
        return;
    }

    QStringList result;
    auto envs = it->toStringList();
    for (const auto &var : std::as_const(envs)) {
        if (var.startsWith(u"DSG_APP_ID="_s)) {
            result << var;
            continue;
        }
        wordexp_t p;
        if (wordexp(var.toStdString().c_str(), &p, 0) == 0) {
            for (size_t i = 0; i < p.we_wordc; i++) {
                result << QString::fromLocal8Bit(p.we_wordv[i]);
            }
            wordfree(&p);
        } else {
            return;
        }
    }

    options.insert(envKey, result);
}

}  // namespace

void ApplicationService::appendExtraEnvironments(QVariantMap &runtimeOptions) const noexcept
{
    DCORE_USE_NAMESPACE
    QStringList envs;
    QStringList unsetEnvs;

    const auto &envKey = fromStaticRaw(EnvKey);
    const auto &unsetEnvKey = fromStaticRaw(UnsetEnvKey);

    const QString &env = environ();
    if (!env.isEmpty()) {
        envs.append(env);
    }

    if (auto it = runtimeOptions.constFind(envKey); it != runtimeOptions.cend()) {
        appendEnvs(*it, envs);
    }

    if (auto it = runtimeOptions.constFind(unsetEnvKey); it != runtimeOptions.cend()) {
        appendEnvs(*it, unsetEnvs);
    }

    std::unique_ptr<DConfig> config(DConfig::create(fromStaticRaw(ApplicationServiceID),
                                                    fromStaticRaw(ApplicationManagerConfig),
                                                    u'/' % id()));  // $appid as subpath
    if (config->isValid()) {
        auto extraEnvs = config->value(fromStaticRaw(AppExtraEnvironments)).toStringList();
        if (!extraEnvs.isEmpty()) {
            envs.append(std::move(extraEnvs));
        }

        auto envsBlacklist = config->value(fromStaticRaw(AppEnvironmentsBlacklist)).toStringList();
        if (!envsBlacklist.isEmpty()) {
            unsetEnvs.append(std::move(envsBlacklist));
        }
    }

    // it's useful for App to get itself AppId.
    envs.append(u"DSG_APP_ID="_s % id());

    runtimeOptions.insert(envKey, envs);
    runtimeOptions.insert(unsetEnvKey, unsetEnvs);
}

void ApplicationService::processCompatibility(const QString &action, QVariantMap &options, QString &execStr)
{
    auto compatibilityManager = parent()->getCompatibilityManager();

    auto getExec = [action, compatibilityManager](const QString &desktopID) -> QString {
        std::optional<QString> execValue;
        if (!action.isEmpty()) {
            const auto &actionHeader = fromStaticRaw(DesktopFileActionKey) % action;
            execValue = compatibilityManager->getExec(desktopID, actionHeader);
        } else {
            execValue = compatibilityManager->getExec(desktopID, fromStaticRaw(DesktopFileEntryKey));
        }

        if (!execValue.has_value() || execValue->isEmpty()) {
            return {};
        }
        return execValue.value();
    };

    auto addEnv = [this, &options, compatibilityManager]() {
        auto envValue = compatibilityManager->getEnv(m_desktopSource.desktopId(), fromStaticRaw(DesktopFileEntryKey));
        if (!envValue.isEmpty()) {
            const auto &envKey = fromStaticRaw(EnvKey);
            if (auto it = options.find(envKey); it != options.end()) {
                auto value = it->toStringList();
                value.append(envValue);
                options.insert(envKey, value);
            } else {
                options.insert(envKey, envValue);
            }
        }
    };

    auto exec = getExec(m_desktopSource.desktopId());
    if (!exec.isEmpty()) {
        execStr = std::move(exec);
        addEnv();
        qInfo() << "get compatibility : " << m_desktopSource.desktopId() << " Exec : " << execStr;
    }
}

ApplicationService::ApplicationService(DesktopFile source,
                                       ApplicationManager1Service *parent,
                                       std::weak_ptr<ApplicationManager1Storage> storage)
    : QObject(parent)
    , m_storage(std::move(storage))
    , m_desktopSource(std::move(source))
{
    auto storagePtr = m_storage.lock();
    if (!storagePtr) {
        m_lastLaunch = -1;
        return;
    }

    auto appId = id();
    auto value = storagePtr->readApplicationValue(appId, fromStaticRaw(ApplicationPropertiesGroup), fromStaticRaw(InstalledTime));

    if (storagePtr->firstLaunch()) {
        auto ret = value.isNull() ? storagePtr->createApplicationValue(
                                        appId, fromStaticRaw(ApplicationPropertiesGroup), fromStaticRaw(InstalledTime), 0) :
                                    storagePtr->updateApplicationValue(
                                        appId, fromStaticRaw(ApplicationPropertiesGroup), fromStaticRaw(InstalledTime), 0);
        if (!ret) {
            m_installedTime = -1;
            qWarning() << "failed to set InstalledTime for" << appId << "at first launch";
        }
    } else {
        if (value.isNull()) {
            auto newInstalledTime = QDateTime::currentMSecsSinceEpoch();
            if (!storagePtr->createApplicationValue(
                    appId, fromStaticRaw(ApplicationPropertiesGroup), fromStaticRaw(InstalledTime), newInstalledTime)) {
                m_installedTime = -1;
                qWarning() << "failed to set InstalledTime for new apps:" << appId;
            } else {
                m_installedTime = newInstalledTime;
            }
        } else {
            m_installedTime = value.toLongLong();
        }
    }

    value = storagePtr->readApplicationValue(appId, fromStaticRaw(ApplicationPropertiesGroup), fromStaticRaw(LastLaunchedTime));
    if (value.isNull()) {
        if (!storagePtr->createApplicationValue(
                appId, fromStaticRaw(ApplicationPropertiesGroup), fromStaticRaw(LastLaunchedTime), 0)) {
            qWarning() << "failed to set LastLaunchedTime for" << appId;
            m_lastLaunch = -1;
        }
    } else {
        m_lastLaunch = value.toLongLong();
    }

    value = storagePtr->readApplicationValue(appId, fromStaticRaw(ApplicationPropertiesGroup), fromStaticRaw(LaunchedTimes));
    if (value.isNull()) {
        if (!storagePtr->createApplicationValue(
                appId, fromStaticRaw(ApplicationPropertiesGroup), fromStaticRaw(::LaunchedTimes), 0)) {
            qWarning() << "failed to set LaunchedTimes for" << appId;
            m_launchedTimes = -1;
        }
    } else {
        m_launchedTimes = value.toLongLong();
    }

    value = storagePtr->readApplicationValue(appId, fromStaticRaw(ApplicationPropertiesGroup), fromStaticRaw(Environ));
    if (!value.isNull()) {
        m_environ = value.toString();
    }
}

ApplicationService::~ApplicationService()
{
    detachAllInstance();
}

bool ApplicationService::ensurePropertiesForwarder() noexcept
{
    if (m_propertiesForwarderInitialized) {
        return true;
    }

    if (auto *ptr = new (std::nothrow) PropertiesForwarder{m_applicationPath.path(), fromStaticRaw(ApplicationInterface), this};
        ptr == nullptr) {
        qCritical() << "new PropertiesForwarder of Application failed.";
        return false;
    }

    m_propertiesForwarderInitialized = true;
    return true;
}

QSharedPointer<ApplicationService> ApplicationService::createApplicationService(
    DesktopFile source, ApplicationManager1Service *parent, std::weak_ptr<ApplicationManager1Storage> storage) noexcept
{
    return createApplicationService(std::move(source), nullptr, parent, std::move(storage));
}

QSharedPointer<ApplicationService> ApplicationService::createApplicationService(DesktopFile source,
                                                                                std::unique_ptr<DesktopEntry> entry,
                                                                                ApplicationManager1Service *parent,
                                                                                std::weak_ptr<ApplicationManager1Storage> storage) noexcept
{
    QSharedPointer<ApplicationService> app{new (std::nothrow) ApplicationService{std::move(source), parent, std::move(storage)}};
    if (!app) {
        qCritical() << "new application service failed.";
        return nullptr;
    }

    QString objectPath;
    QTextStream sourceStream;

    objectPath = getObjectPathFromAppId(app->desktopFileSource().desktopId());
    if (!entry) {
        DesktopFileGuard guard{app->desktopFileSource()};

        if (!guard.try_open()) {
            qDebug() << "open source desktop failed.";
            return nullptr;
        }

        entry = std::make_unique<DesktopEntry>();
        auto error = entry->parse(app->desktopFileSource().sourceFileRef());

        if (error != ParserError::NoError) {
            qDebug() << "parse failed:" << error << app->desktopFileSource().sourcePath();
            return nullptr;
        }
    }

    if (!shouldBeShown(entry)) {
        qDebug() << "application shouldn't be shown:" << app->desktopFileSource().sourcePath();
        return nullptr;
    }

    app->m_entry.reset(entry.release());
    app->m_applicationPath = QDBusObjectPath{std::move(objectPath)};

    // TODO: icon lookup
    if (auto *ptr = new (std::nothrow) APPObjectManagerAdaptor{app.data()}; ptr == nullptr) {
        qCritical() << "new Object Manager of Application failed.";
        return nullptr;
    } else {
        setAdaptorAutoRelaySignals(ptr, false);
    }

    return app;
}

bool ApplicationService::shouldBeShown(const std::unique_ptr<DesktopEntry> &entry) noexcept
{
    if (ApplicationFilter::hiddenCheck(*entry)) {
        qDebug() << "hidden check failed.";
        return false;
    }

    if (ApplicationFilter::tryExecCheck(*entry)) {
        qDebug() << "tryExec check failed";
        return false;
    }

    if (ApplicationFilter::showInCheck(*entry)) {
        qDebug() << "showIn check failed.";
        return false;
    }

    return true;
}

QDBusObjectPath ApplicationService::Launch(const QString &action, const QStringList &fields, const QVariantMap &options)
{
    // Suppress splash for system autostart launches or singleton apps with existing instances.
    const bool isAutostartLaunch = options.value(fromStaticRaw(BuiltInAutostartOption), false).toBool();

    QString launchType;
    if (options.contains(u"_launch_type"_s)) {
        launchType = options.value(u"_launch_type"_s).toString();
    } else if (isAutostartLaunch) {
        launchType = u"dde-application-manager-autostart"_s;
    }
    if (launchType.isEmpty()) {
        launchType = u"unknown"_s;
    }
    auto instanceRandomUUID = QUuid::createUuid().toString(QUuid::Id128);
    qCDebug(DDEAM) << "launch app:" << id() << "launchType:" << launchType << "uniqueID:" << instanceRandomUUID;

    if (isAutostartLaunch) {
        if (!parent()->isNewSession()) {
            safe_sendErrorReply(QDBusError::Failed, "autostart launch has been ignored if not new session.");
        }
    }

    QString execStr;
    const auto &supportedActions = actions();
    auto optionsMap = options;
    appendExtraEnvironments(optionsMap);

    auto *desktopEntry = [this, isAutostartLaunch]() -> DesktopEntry * {
        if (!isAutostartLaunch) {
            return this->m_entry.get();
        }

        if (this->m_autostartSource.m_filePath.isEmpty()) {
            return nullptr;
        }

        if (!this->m_autostartSource.m_entry.data().isEmpty()) {
            return &this->m_autostartSource.m_entry;
        }

        return this->m_entry.get();
    }();

    if (desktopEntry == nullptr) {
        const QString msg = "This application is not set to autostart.";
        qWarning() << msg << "app:" << id()
                   << "isAutostartLaunch:" << isAutostartLaunch
                   << "desktopSource:" << m_desktopSource.sourcePath()
                   << "autostartSource:" << m_autostartSource.m_filePath;
        safe_sendErrorReply(QDBusError::Failed, msg);
        return {};
    }

    // ignore action if it's not supported or autostart launch
    while (!isAutostartLaunch && !action.isEmpty() && !supportedActions.isEmpty()) {  // break trick
        if (auto index = supportedActions.indexOf(action); index == -1) {
            qWarning() << "can't find " << action << " in supported actions List. application will use default action to launch.";
            break;
        }

        const auto &actionHeader = QString{"%1%2"}.arg(fromStaticRaw(DesktopFileActionKey), action);
        const auto &actionExec = desktopEntry->value(actionHeader, u"Exec"_s);
        if (!actionExec) {
            break;
        }

        execStr = toString(actionExec.value());
        if (execStr.isEmpty()) {
            qWarning() << "exec value to string failed, try default action.";  // we need this log.
        }

        break;
    }

    if (execStr.isEmpty()) {
        auto Actions = desktopEntry->value(fromStaticRaw(DesktopFileEntryKey), fromStaticRaw(DesktopEntryExec));
        if (!Actions) {
            const QString msg{"application can't be executed."};
            qWarning() << msg;
            safe_sendErrorReply(QDBusError::Failed, msg);
            return {};
        }

        execStr = Actions->get().toString();
        if (execStr.isEmpty()) {
            const QString msg{"maybe entry actions's format is invalid, abort launch."};
            qWarning() << msg;
            safe_sendErrorReply(QDBusError::Failed, msg);
            return {};
        }
    }

    auto linglongAppId = X_linglongAppId();
    if (!linglongAppId.isEmpty()) {
        setEventAppId(linglongAppId);
    }

    const bool isSingleton =
        findEntryValue(fromStaticRaw(DesktopFileEntryKey), fromStaticRaw(DesktopEntryXDeepinSingleton), EntryValueType::Boolean)
            .toBool();
    const bool singletonWithInstance = isSingleton && !m_Instances.isEmpty();

    // Those are internal properties, user shouldn't pass them to Application Manager
    optionsMap.remove(fromStaticRaw(BuiltInAutostartOption));
    optionsMap.remove(u"_hooks"_s);
    optionsMap.remove(u"_builtIn_searchExec"_s);
    optionsMap.remove(u"_launch_type"_s);

    QStringList extraArgs;
    if (auto it = optionsMap.constFind(fromStaticRaw(ExtraArgsOption)); it != optionsMap.cend()) {
        extraArgs = it->toStringList();
        optionsMap.remove(fromStaticRaw(ExtraArgsOption));
    }
    if (const auto &hooks = parent()->applicationHooks(); !hooks.isEmpty()) {
        optionsMap.insert(u"_hooks"_s, hooks);
    }
    optionsMap.insert(u"_builtIn_searchExec"_s, parent()->systemdPathEnv());

    processCompatibility(action, optionsMap, execStr);
    unescapeEnvs(optionsMap);

    QString workingDir;
    if (auto entryPath = desktopEntry->value(fromStaticRaw(DesktopFileEntryKey), u"Path"_s); entryPath) {
        workingDir = entryPath->get().value<QString>();
    }

    if (auto optionPath = optionsMap.value(u"path"_s).toString(); !optionPath.isEmpty()) {
        workingDir = std::move(optionPath);
    }

    if (!workingDir.isEmpty() && QDir::isRelativePath(workingDir)) {
        qWarning() << "relative working dir is not supported: " << workingDir;
        workingDir.clear();
    }

    auto cmds = generateCommand(optionsMap);
    auto task = processExec(execStr, fields, workingDir);
    if (!task) {
        safe_sendErrorReply(QDBusError::InternalError, "Invalid Command.");
        return {};
    }

    if (task.LaunchBin.isEmpty()) {
        qCritical() << "error command is detected, abort.";
        safe_sendErrorReply(QDBusError::Failed);
        return {};
    }

    if (terminal()) {
        // don't change this sequence
        cmds.push_back("deepin-terminal");
        cmds.push_back("--keep-open");
        cmds.push_back("-e");  // run all original execution commands in deepin-terminal
    }

    EventReporter::instance().reportAppLaunch(eventAppId(), QDateTime::currentMSecsSinceEpoch(), x_linglong(), launchType, instanceRandomUUID);

    // Notify the compositor to show a splash screen (after validation passes).
    if (isAutostartLaunch) {
        qCInfo(amPrelaunchSplash) << "Skip prelaunch splash (autostart)" << id();
    } else if (singletonWithInstance) {
        qCInfo(amPrelaunchSplash) << "Skip prelaunch splash (singleton with existing instance)" << id();
    } else if (auto *am = parent()) {
        if (auto *helper = am->splashHelper()) {
            const auto iconVar = findEntryValue(fromStaticRaw(DesktopFileEntryKey), "Icon", EntryValueType::IconString);
            const QString iconName = iconVar.isNull() ? QString{} : iconVar.toString();
            qCInfo(amPrelaunchSplash) << "Show prelaunch splash request" << id() << "instance" << instanceRandomUUID << "icon"
                                      << iconName;
            helper->show(id(), instanceRandomUUID, iconName);
            m_splashInstanceIds.insert(instanceRandomUUID);
        } else {
            qCInfo(amPrelaunchSplash) << "Skip prelaunch splash (no helper instance)" << id();
        }
    } else {
        qCWarning(amPrelaunchSplash) << "Skip prelaunch splash (no parent ApplicationManager1Service)" << id();
    }

    m_pendingLaunchTypes.insert(instanceRandomUUID, launchType);

    auto &jobManager = parent()->jobManager();
    return jobManager.addJob(
        m_applicationPath.path(),
        [this, task, instanceRandomUUID = std::move(instanceRandomUUID), cmds = std::move(cmds), launchType, extraArgs = std::move(extraArgs)](
            const QVariant &value) mutable -> QVariant {
            QStringList newCommands;
            const int estimatedSize = 6 + cmds.size() + task.command.size() + extraArgs.size() + (value.isValid() ? 1 : 0);
            newCommands.reserve(estimatedSize);
            newCommands
                << QStringLiteral("--unitName=app-DDE-%1@%2.service").arg(escapeApplicationId(this->id()), instanceRandomUUID);
            newCommands << QStringLiteral("--SyslogIdentifier=%1").arg(this->id());
            newCommands << QStringLiteral("--SourcePath=%1").arg(m_desktopSource.sourcePath());
            newCommands << std::move(cmds);

            QStringList formattedRes;
            if (!value.isNull()) {
                QList<QUrl> urls;
                if (value.canConvert<QList<QUrl>>()) {
                    urls = std::move(value).value<QList<QUrl>>();
                } else {
                    urls.append(std::move(value).value<QUrl>());
                }

                formattedRes.reserve(urls.size());
                for (const auto &url : std::as_const(urls)) {
                    if (!task.local) {
                        formattedRes << url.toString();
                    } else if (url.isLocalFile()) {
                        formattedRes << url.toLocalFile();
                    } else {
                        // TODO: Remote file handling logic
                        qWarning() << "Remote file not supported yet, skipping:" << url;
                    }
                }
            }

            int originalIndex{0};
            while (!task.command.isEmpty()) {
                auto currentArg = task.command.takeFirst();

                if (originalIndex != task.argNum) {
                    newCommands << std::move(currentArg);
                } else {
                    if (task.fieldLocation != -1) {
                        if (formattedRes.size() > 1) {
                            qWarning() << "multiple resources are found, only the first one will be used.";
                        }

                        currentArg.replace(task.fieldLocation, 2, formattedRes.isEmpty() ? QString{} : formattedRes.takeFirst());
                        newCommands << std::move(currentArg);

                        if (!formattedRes.isEmpty()) {
                            newCommands << std::move(formattedRes);
                        }
                    } else {
                        newCommands << std::move(formattedRes);
                    }
                }

                ++originalIndex;
            }

            newCommands << std::move(extraArgs);

            QProcess process;
            const auto &bin = getApplicationLauncherBinary();
            qDebug().noquote() << "Launcher path:" << bin << ", Run with commands:" << newCommands;

            process.start(bin, newCommands);
            process.waitForFinished();
            auto exitCode = process.exitCode();
            if (exitCode != 0) {
                qWarning() << "Launch Application Failed";
                EventReporter::instance().reportAppLaunchFailed(eventAppId(),
                                                     QStringLiteral("app-launch-helper exited with code %1").arg(exitCode),
                                                     x_linglong(),
                                                     launchType,
                                                     instanceRandomUUID);
                m_pendingLaunchTypes.remove(instanceRandomUUID);
                return QDBusError::Failed;
            }

            return QString{m_applicationPath.path() % u'/' % instanceRandomUUID};
        },
        std::move(task.Resources));
}

bool ApplicationService::SendToDesktop() const noexcept
{
    if (isOnDesktop()) {
        return true;
    }

    const auto &dir = getDesktopDir();
    if (dir.isEmpty()) {
        qDebug() << "no desktop directory found.";
        return false;
    }

    auto desktopFile = QDir{dir}.filePath(m_desktopSource.desktopId() % desktopSuffix);
    auto success = m_desktopSource.sourceFileRef().link(desktopFile);
    if (!success) {
        qDebug() << "create link failed:" << m_desktopSource.sourceFileRef().errorString() << "path:" << desktopFile;
        safe_sendErrorReply(QDBusError::ErrorType::Failed, m_desktopSource.sourceFileRef().errorString());
    }

    return success;
}

bool ApplicationService::RemoveFromDesktop() const noexcept
{
    if (!isOnDesktop()) {
        return true;
    }

    const auto &dir = getDesktopDir();
    if (dir.isEmpty()) {
        qDebug() << "no desktop directory found.";
        return false;
    }

    QFile desktopFile{QDir{dir}.filePath(m_desktopSource.desktopId() % desktopSuffix)};
    auto success = desktopFile.remove();

    if (!success) {
        qDebug() << "remove desktop file failed:" << desktopFile.errorString();
        safe_sendErrorReply(QDBusError::ErrorType::Failed, desktopFile.errorString());
    }

    return success;
}

bool ApplicationService::isOnDesktop() const noexcept
{
    const auto &dir = getDesktopDir();
    if (dir.isEmpty()) {
        qDebug() << "no desktop directory found.";
        return false;
    }

    const QFileInfo info{dir % QDir::separator() % m_desktopSource.desktopId() % desktopSuffix};
    auto target = info.symLinkTarget();
    if (target.isEmpty()) {
        return false;
    }

    return target == m_desktopSource.sourcePath();
}

bool ApplicationService::noDisplay() const noexcept
{
    auto val = findEntryValue(fromStaticRaw(DesktopFileEntryKey), u"NoDisplay"_s, EntryValueType::Boolean);

    if (val.isNull()) {
        return false;
    }

    return val.toBool();
}

QStringList ApplicationService::actions() const noexcept
{
    auto val = findEntryValue(fromStaticRaw(DesktopFileEntryKey), fromStaticRaw(DesktopEntryActions), EntryValueType::String);

    if (val.isNull()) {
        return {};
    }

    return toString(val).split(u';', Qt::SkipEmptyParts);
}

QStringList ApplicationService::categories() const noexcept
{
    auto val = findEntryValue(fromStaticRaw(DesktopFileEntryKey), u"Categories"_s, EntryValueType::String);

    if (val.isNull()) {
        return {};
    }

    return val.toString().split(u';', Qt::SkipEmptyParts);
}

PropMap ApplicationService::actionName() const noexcept
{
    PropMap ret;
    const auto &actionList = actions();

    for (const auto &action : actionList) {
        const QString rawActionKey = fromStaticRaw(DesktopFileActionKey) % action;
        auto value = m_entry->value(rawActionKey, fromStaticRaw(DesktopEntryName));
        if (!value.has_value()) {
            continue;
        }
        ret.insert(action, value->get().value<QStringMap>());
    }

    return ret;
}

QStringMap ApplicationService::name() const noexcept
{
    auto value = m_entry->value(fromStaticRaw(DesktopFileEntryKey), fromStaticRaw(DesktopEntryName));
    if (!value) {
        return {};
    }

    return value->get().value<QStringMap>();
}

QStringMap ApplicationService::genericName() const noexcept
{
    auto value = m_entry->value(fromStaticRaw(DesktopFileEntryKey), u"GenericName"_s);
    if (!value) {
        return {};
    }

    return value->get().value<QStringMap>();
}

QStringMap ApplicationService::icons() const noexcept
{
    QStringMap ret;
    const auto &actionList = actions();
    for (const auto &action : std::as_const(actionList)) {
        auto actionKey = QString{action}.prepend(fromStaticRaw(DesktopFileActionKey));
        auto value = m_entry->value(actionKey, u"Icon"_s);
        if (!value) {
            continue;
        }

        ret.insert(actionKey, value->get().value<QString>());
    }

    auto mainIcon = m_entry->value(fromStaticRaw(DesktopFileEntryKey), u"Icon"_s);
    if (mainIcon) {
        ret.insert(fromStaticRaw(DesktopFileEntryKey), mainIcon->get().value<QString>());
    }

    return ret;
}

ObjectMap ApplicationService::GetManagedObjects() const
{
    return dumpDBusObject(m_Instances);
}

const QString &ApplicationService::id() const noexcept
{
    return m_desktopSource.desktopId();
}

bool ApplicationService::x_Flatpak() const noexcept
{
    auto val = findEntryValue(fromStaticRaw(DesktopFileEntryKey), u"X-flatpak"_s, EntryValueType::String);
    return !val.isNull();
}

bool ApplicationService::x_linglong() const noexcept
{
    auto val = findEntryValue(fromStaticRaw(DesktopFileEntryKey), u"X-linglong"_s, EntryValueType::String);
    return !val.isNull();
}

QString ApplicationService::X_linglongAppId() const noexcept
{
    auto val = findEntryValue(fromStaticRaw(DesktopFileEntryKey), u"X-linglong"_s, EntryValueType::String);
    return val.isNull() ? QString{} : val.toString();
}

QString ApplicationService::X_Deepin_Vendor() const noexcept
{
    return findEntryValue(fromStaticRaw(DesktopFileEntryKey), u"X-Deepin-Vendor"_s, EntryValueType::String).toString();
}

QString ApplicationService::X_Deepin_CreateBy() const noexcept
{
    return findEntryValue(fromStaticRaw(DesktopFileEntryKey), fromStaticRaw(DesktopEntryXDeepinCreateBy), EntryValueType::String)
        .toString();
}

QStringMap ApplicationService::execs() const noexcept
{
    QStringMap ret;

    auto mainExec = m_entry->value(fromStaticRaw(DesktopFileEntryKey), "Exec");
    if (mainExec.has_value()) {
        ret.insert(fromStaticRaw(DesktopFileEntryKey), mainExec->get().value<QString>());
    }

    const auto &actionList = actions();
    for (const auto &action : std::as_const(actionList)) {
        auto actionKey = QString{action}.prepend(fromStaticRaw(DesktopFileActionKey));
        auto value = m_entry->value(actionKey, "Exec");
        if (!value.has_value()) {
            continue;
        }
        ret.insert(actionKey, value->get().value<QString>());
    }

    return ret;
}

QString ApplicationService::X_CreatedBy() const noexcept
{
    return findEntryValue(fromStaticRaw(DesktopFileEntryKey), "X-Created-By", EntryValueType::String).toString();
}

QString ApplicationService::desktopSourcePath() const noexcept
{
    return m_desktopSource.sourcePath();
}

bool ApplicationService::terminal() const noexcept
{
    auto val = findEntryValue(fromStaticRaw(DesktopFileEntryKey), "Terminal", EntryValueType::String);
    if (!val.isNull()) {
        return val.toBool();
    }
    return false;
}

QString ApplicationService::startupWMClass() const noexcept
{
    auto value = findEntryValue(fromStaticRaw(DesktopFileEntryKey), "StartupWMClass", EntryValueType::String);
    return value.isNull() ? QString{} : value.toString();
}

qint64 ApplicationService::installedTime() const noexcept
{
    return m_installedTime;
}

qint64 ApplicationService::lastLaunchedTime() const noexcept
{
    return m_lastLaunch;
}

qint64 ApplicationService::launchedTimes() const noexcept
{
    return m_launchedTimes;
}

QString ApplicationService::environ() const noexcept
{
    return m_environ;
}

void ApplicationService::setEnviron(const QString &value) noexcept
{
    if (environ().trimmed() == value.trimmed()) {
        return;
    }

    auto storagePtr = m_storage.lock();
    if (!storagePtr) {
        qCritical() << "broken storage.";
        safe_sendErrorReply(QDBusError::InternalError);
        return;
    }

    auto appId = id();
    if (!storagePtr->readApplicationValue(appId, fromStaticRaw(ApplicationPropertiesGroup), fromStaticRaw(Environ)).isNull()) {
        if (!storagePtr->updateApplicationValue(
                appId, fromStaticRaw(ApplicationPropertiesGroup), fromStaticRaw(Environ), value)) {
            safe_sendErrorReply(QDBusError::Failed, "update environ failed.");
            return;
        }
    } else {
        if (!storagePtr->createApplicationValue(
                appId, fromStaticRaw(ApplicationPropertiesGroup), fromStaticRaw(Environ), value)) {
            safe_sendErrorReply(QDBusError::Failed, "set environ failed.");
        }
    }

    m_environ = value;
    emit environChanged();
}

bool ApplicationService::autostartCheck() const noexcept
{
    const auto &entry = [this] {
        if (!m_autostartSource.m_entry.data().isEmpty()) {
            return m_autostartSource.m_entry;
        }

        return *this->m_entry;
    }();

    auto hiddenVal = entry.value(fromStaticRaw(DesktopFileEntryKey), fromStaticRaw(DesktopEntryHidden));
    if (!hiddenVal) {
        qDebug() << "no hidden in autostart desktop";
        return true;
    }

    const auto hidden = hiddenVal->get().toString();
    return hidden.compare("false", Qt::CaseInsensitive) == 0;
}

bool ApplicationService::isAutoStart() const noexcept
{
    if (!autostartSourceFileExists()) {
        return false;
    }

    return autostartCheck();
}

bool ApplicationService::autostartSourceFileExists() const noexcept
{
    return !m_autostartSource.m_filePath.isEmpty() && QFile::exists(m_autostartSource.m_filePath);
}

bool ApplicationService::hasGeneratedAutostartSource() const noexcept
{
    auto group = m_autostartSource.m_entry.group(fromStaticRaw(DesktopFileEntryKey));
    if (!group.has_value()) {
        return false;
    }

    return group->get().contains(fromStaticRaw(DesktopEntryXDeepinGenerateSource));
}

bool ApplicationService::saveAutostartEntry(const QString &fileName, const DesktopEntry &entry) noexcept
{
    const QFileInfo autostartFileInfo{fileName};
    if (autostartFileInfo.isSymLink()) {
        qWarning() << "refuse to overwrite symlink autostart file:" << fileName;
        return false;
    }

    QFile autostartFile{fileName};
    if (!autostartFile.open(QFile::WriteOnly | QFile::Text | QFile::Truncate)) {
        qWarning() << "open file" << fileName << "failed:" << autostartFile.error();
        return false;
    }

    auto content = toString(entry.data()).toLocal8Bit();
    auto writeBytes = autostartFile.write(content);

    if (writeBytes != content.size() || !autostartFile.flush()) {
        qWarning() << "incomplete write:" << autostartFile.error();
        return false;
    }

    return true;
}

void ApplicationService::syncGeneratedAutostartEntry() noexcept
{
    if (!m_entry || !autostartSourceFileExists()) {
        return;
    }

    QFile autostartFile{m_autostartSource.m_filePath};
    if (!autostartFile.open(QFile::ReadOnly | QFile::Text)) {
        qWarning() << "open file" << m_autostartSource.m_filePath << "failed:" << autostartFile.error();
        return;
    }

    DesktopEntry currentEntry;
    if (currentEntry.parse(autostartFile) != ParserError::NoError) {
        qWarning() << "parse autostart file" << m_autostartSource.m_filePath << "failed";
        return;
    }

    auto group = currentEntry.group(fromStaticRaw(DesktopFileEntryKey));
    if (!group.has_value() || !group->get().contains(fromStaticRaw(DesktopEntryXDeepinGenerateSource))) {
        return;
    }

    DesktopEntry newEntry = *m_entry;
    newEntry.insert(fromStaticRaw(DesktopFileEntryKey), fromStaticRaw(DesktopEntryXDeepinGenerateSource), m_desktopSource.sourcePath());

    auto hidden = currentEntry.value(fromStaticRaw(DesktopFileEntryKey), fromStaticRaw(DesktopEntryHidden));
    if (hidden) {
        auto hiddenValue = hidden->get();
        newEntry.insert(fromStaticRaw(DesktopFileEntryKey), fromStaticRaw(DesktopEntryHidden), std::move(hiddenValue));
    }

    if (!saveAutostartEntry(m_autostartSource.m_filePath, newEntry)) {
        return;
    }

    setAutostartSource({m_autostartSource.m_filePath, newEntry});
}

void ApplicationService::setAutoStart(bool autostart) noexcept
{
    if (isAutoStart() == autostart) {
        return;
    }

    if (!m_entry) {
        qWarning() << "set autostart failed, desktop entry is null:" << id();
        safe_sendErrorReply(QDBusError::InternalError);
        return;
    }

    const QDir startDir(getAutoStartDirs().constFirst());
    if (!startDir.exists() && !startDir.mkpath(startDir.path())) {
        qWarning() << "mkpath " << startDir.path() << "failed";
        safe_sendErrorReply(QDBusError::InternalError);
        return;
    }

    auto fileName = startDir.filePath(m_desktopSource.desktopId() % desktopSuffix);
    DesktopEntry newEntry;
    QString originalSource;
    const bool shouldReuseAutostartEntry = autostartSourceFileExists() && !hasGeneratedAutostartSource()
        && !m_autostartSource.m_entry.data().isEmpty();
    if (shouldReuseAutostartEntry) {
        newEntry = m_autostartSource.m_entry;

        auto source = newEntry.value(fromStaticRaw(DesktopFileEntryKey), fromStaticRaw(DesktopEntryXDeepinGenerateSource));
        if (source) {
            originalSource = source->get().toString();
        } else {
            originalSource = m_autostartSource.m_filePath;
        }
    } else {
        newEntry = *m_entry;
        originalSource = m_desktopSource.sourcePath();
    }

    newEntry.insert(fromStaticRaw(DesktopFileEntryKey), fromStaticRaw(DesktopEntryXDeepinGenerateSource), originalSource);
    newEntry.insert(fromStaticRaw(DesktopFileEntryKey), fromStaticRaw(DesktopEntryHidden), !autostart);

    if (!saveAutostartEntry(fileName, newEntry)) {
        qWarning() << "set autostart failed:" << id() << "autostart:" << autostart << "file:" << fileName;
        safe_sendErrorReply(QDBusError::Failed);
        return;
    }

    setAutostartSource({fileName, newEntry});
    emit autostartChanged();
}

QStringList ApplicationService::mimeTypes() const noexcept
{
    const auto &desktopFilePath = m_desktopSource.sourcePath();
    const auto &cacheList = parent()->mimeManager().infos();
    const auto &appId = id();

    auto itCache = std::find_if(
        cacheList.cbegin(), cacheList.cend(), [&](const MimeInfo &info) { return desktopFilePath.startsWith(info.directory()); });

    if (itCache == cacheList.cend()) {
        qWarning() << "No mime directory matches:" << desktopFilePath;
        return {};
    }

    QSet<QString> addedSet;
    QSet<QString> removedSet;

    if (const auto &info = itCache->cacheInfo()) {
        const auto baseTypes = info->queryTypes(appId);
        addedSet.reserve(baseTypes.size());
        for (const auto &type : baseTypes) {
            addedSet.insert(type);
        }
    }

    for (const auto &info : cacheList) {
        const auto &apps = info.appsList();
        for (const auto &app : apps) {
            auto [added, removed] = app.queryTypes(appId);
            for (auto &&a : added) {
                addedSet.insert(std::move(a));
            }

            for (auto &&r : removed) {
                removedSet.insert(std::move(r));
            }
        }
    }

    for (const auto &type : removedSet) {
        addedSet.remove(type);
    }

    return addedSet.values();
}

void ApplicationService::setMimeTypes(const QStringList &value) noexcept
{
    auto oldMimes = mimeTypes();
    auto newMimes = value;
    std::sort(oldMimes.begin(), oldMimes.end());
    std::sort(newMimes.begin(), newMimes.end());

    QStringList newAdds;
    QStringList newRemoved;

    std::set_difference(oldMimes.begin(), oldMimes.end(), newMimes.begin(), newMimes.end(), std::back_inserter(newRemoved));
    std::set_difference(newMimes.begin(), newMimes.end(), oldMimes.begin(), oldMimes.end(), std::back_inserter(newAdds));

    const auto &userDir = getXDGConfigHome();
    auto &infos = parent()->mimeManager().infos();
    auto userInfo =
        std::find_if(infos.begin(), infos.end(), [&userDir](const MimeInfo &info) { return info.directory() == userDir; });
    if (userInfo == infos.cend()) {
        safe_sendErrorReply(QDBusError::Failed, "user-specific config file doesn't exists.");
        return;
    }

    auto &userAppsList = userInfo->appsList();
    auto list = std::find_if(
        userAppsList.begin(), userAppsList.end(), [](const MimeApps &config) { return !config.isDesktopSpecific(); });
    if (list == userAppsList.end()) {
        safe_sendErrorReply(QDBusError::Failed, "user-specific config file doesn't exists.");
        return;
    }
    const auto &appId = id();
    for (const auto &add : std::as_const(newAdds)) {
        list->addAssociation(add, appId);
    }
    for (const auto &remove : std::as_const(newRemoved)) {
        list->removeAssociation(remove, appId);
    }

    if (!list->writeToFile()) {
        qWarning() << "error occurred when write mime association to file";
    }

    emit MimeTypesChanged();
}

QList<QDBusObjectPath> ApplicationService::instances() const noexcept
{
    return m_Instances.keys();
}

bool ApplicationService::addOneInstance(const QString &instanceId,
                                        const QString &application,
                                        const QString &systemdUnitPath,
                                        const QString &launcher,
                                        const QString &launchType) noexcept
{
    auto *service = new (std::nothrow) InstanceService{instanceId, application, systemdUnitPath, launcher, launchType};
    if (service == nullptr) {
        qCritical() << "couldn't new InstanceService.";
        return false;
    }

    auto *adaptor = new (std::nothrow) InstanceAdaptor{service};
    const QString objectPath{m_applicationPath.path() % u'/' % instanceId};
    if (adaptor != nullptr) {
        setAdaptorAutoRelaySignals(adaptor, false);
    }

    if (adaptor == nullptr || !registerObjectToDBus(service, objectPath, fromStaticRaw(InstanceInterface))) {
        adaptor->deleteLater();
        service->deleteLater();
        return false;
    }

    m_Instances.insert(QDBusObjectPath{objectPath}, QSharedPointer<InstanceService>{service});
    service->moveToThread(this->thread());
    adaptor->moveToThread(this->thread());

    if (!parent()->isStartupPhase()) {
        const auto dbusObjectPath = QDBusObjectPath{objectPath};
        const auto interfaces = getChildInterfacesAndPropertiesFromObject(service);
        emit InterfacesAdded(dbusObjectPath, interfaces);
        sendObjectManagerSignal(m_applicationPath.path(),
                                "InterfacesAdded",
                                dbusObjectPath,
                                QVariant::fromValue(interfaces));
    }

    return true;
}

void ApplicationService::removeOneInstance(const QDBusObjectPath &instance) noexcept
{
    if (auto it = m_Instances.constFind(instance); it != m_Instances.cend()) {
        closeSplashForInstance(it.value()->instanceId());
        const auto interfaces = getChildInterfacesFromObject(it->data());
        emit InterfacesRemoved(instance, interfaces);
        sendObjectManagerSignal(m_applicationPath.path(), "InterfacesRemoved", instance, QVariant::fromValue(interfaces));
        unregisterObjectFromDBus(instance.path());
        m_Instances.remove(instance);
    }
}

void ApplicationService::removeAllInstance() noexcept
{
    for (auto it = m_Instances.constBegin(); it != m_Instances.constEnd(); ++it) {
        removeOneInstance(it.key());
    }
}

void ApplicationService::handleUnitStarted(const QString &instanceId,
                                           const QString &systemdUnitPath,
                                           const QString &launcher,
                                           const QString &launchType,
                                           bool isNewLaunch) noexcept
{
    using namespace Qt::StringLiterals;

    updateAfterLaunch(isNewLaunch);

    auto lt = launchType;
    if (lt.isEmpty()) {
        lt = m_pendingLaunchTypes.take(instanceId);
    }
    if (lt.isEmpty()) {
        qCWarning(DDEAM) << "missing pending launch type for app" << id() << "instance" << instanceId;
        lt = u"unknown"_s;
    }

    if (!addOneInstance(instanceId, m_applicationPath.path(), systemdUnitPath, launcher, lt)) {
        qCCritical(DDEAM) << "failed to add instance" << systemdUnitPath << "to app" << id();
    }

    auto watcher = new UnitResultWatcher(QDBusObjectPath{systemdUnitPath}, this);
    connect(watcher, &UnitResultWatcher::resultReady, this, [this](const QDBusObjectPath &unitPath, const QString &result) {
        qCDebug(DDEAM) << "onUnitResultReady: unitPath=" << unitPath.path() << "result=" << result;
        m_unitResults.insert(unitPath.path(), result);
    });
    auto &bus = ApplicationManager1DBus::instance().globalDestBus();
    auto connected = bus.connect(QString::fromUtf8(SystemdService),
                                 systemdUnitPath,
                                 QString::fromUtf16(SystemdPropInterfaceName),
                                 u"PropertiesChanged"_s,
                                 watcher,
                                 SLOT(handlePropertyChanged(QString,QVariantMap,QStringList)));
    qCDebug(DDEAM) << "UnitResultWatcher connect for" << systemdUnitPath << "result:" << connected;
}

void ApplicationService::handleUnitRemoved(const QString &systemdUnitPath, const QString &unitName) noexcept
{
    using namespace Qt::StringLiterals;

    const QDBusObjectPath unitPath{systemdUnitPath};

    auto instanceIt =
        std::find_if(m_Instances.cbegin(), m_Instances.cend(), [&unitPath](const QSharedPointer<InstanceService> &value) {
            return value->property("SystemdUnitPath") == unitPath;
        });

    if (instanceIt == m_Instances.cend()) {
        return;
    }

    auto result = m_unitResults.take(systemdUnitPath);
    qCDebug(DDEAM) << "removeInstance: unitPath=" << systemdUnitPath << "cached result=" << result;

    static const QSet<QString> launchFailedResults{
        u"start-limit-hit"_s, u"resources"_s, u"exec-condition"_s, u"protocol"_s, u"timeout"_s};

    if (launchFailedResults.contains(result)) {
        EventReporter::instance().reportAppLaunchFailed(eventAppId(),
                                             QStringLiteral("systemd result: %1").arg(result),
                                             x_linglong(),
                                             (*instanceIt)->launchType(),
                                             (*instanceIt)->instanceId());
    } else if (!result.isEmpty() && result != u"success"_s) {
        QStringList logArgs{"--user", QStringLiteral("--unit=%1").arg(unitName),
                            "-p", "warning", "-n", "6", "-o", "cat", "-o", "with-unit", "--no-pager"};
        QProcess logProc;
        logProc.start("journalctl", logArgs);
        QString logInfo;
        if (!logProc.waitForStarted(1000)) {
            qCWarning(DDEAM) << "journalctl failed to start for unit:" << unitName;
        } else if (!logProc.waitForFinished(3000)) {
            qCWarning(DDEAM) << "journalctl timeout for unit:" << unitName;
            logProc.kill();
        } else {
            logInfo = QString::fromUtf8(logProc.readAllStandardOutput());
        }

        EventReporter::instance().reportAppAbnormalExit(eventAppId(),
                                             (*instanceIt)->launchType(),
                                             unitName,
                                             logInfo,
                                             x_linglong(),
                                             (*instanceIt)->instanceId());
    }

    removeOneInstance(instanceIt.key());
}

void ApplicationService::detachAllInstance() noexcept
{
    for (auto it = m_Instances.constBegin(); it != m_Instances.constEnd(); ++it) {
        const auto &instance = it.value();
        orphanedInstances.append(instance);
        instance->setProperty("Orphaned", true);
    }

    m_Instances.clear();
    closeAllSplashes();
}

void ApplicationService::closeSplashForInstance(const QString &instanceId) noexcept
{
    if (!m_splashInstanceIds.remove(instanceId)) {
        return;
    }

    if (auto *am = parent()) {
        if (auto *helper = am->splashHelper()) {
            helper->closeSplash(instanceId);
        } else {
            qCInfo(amPrelaunchSplash) << "Skip closeSplash (no helper instance)" << id();
        }
    } else {
        qCWarning(amPrelaunchSplash) << "Skip closeSplash (no parent ApplicationManager1Service)" << id();
    }
}

void ApplicationService::closeAllSplashes() noexcept
{
    const auto ids = m_splashInstanceIds;
    for (const auto &instanceId : ids) {
        closeSplashForInstance(instanceId);
    }
}

QDBusObjectPath ApplicationService::findInstance(const QString &instanceId) const
{
    for (auto it = m_Instances.constKeyValueBegin(); it != m_Instances.constKeyValueEnd(); ++it) {
        const auto &[path, ptr] = *it;
        if (ptr->instanceId() == instanceId) {
            return path;
        }
    }
    return {};
}

void ApplicationService::resetEntry(DesktopEntry *newEntry) noexcept
{
    m_entry.reset(newEntry);
    emit autostartChanged();
    emit noDisplayChanged();
    emit isOnDesktopChanged();
    emit installedTimeChanged();
    emit x_FlatpakChanged();
    emit x_linglongChanged();
    emit instanceChanged();
    emit lastLaunchedTimeChanged();
    emit iconsChanged();
    emit nameChanged();
    emit genericNameChanged();
    emit actionNameChanged();
    emit actionsChanged();
    emit categoriesChanged();
    emit MimeTypesChanged();
    emit terminalChanged();
    emit environChanged();
    emit launchedTimesChanged();
    emit startupWMClassChanged();
    emit xDeepinCreatedByChanged();
    emit execsChanged();
    emit xCreatedByChanged();
    emit desktopSourcePathChanged();
}

enum class SpliterState : uint8_t { Normal, InSingleQuote, InDoubleQuotes };

std::optional<QStringList> ApplicationService::splitExecArguments(QStringView str) noexcept
{
    if (str.isEmpty()) {
        return std::nullopt;
    }

    QStringList args;
    QString currentToken;
    currentToken.reserve(str.size());

    auto curState{SpliterState::Normal};
    bool hasToken{false};

    for (const auto *it = str.begin(); it != str.end(); ++it) {
        const auto c = *it;

        switch (curState) {
        case SpliterState::Normal: {
            if (c == u'\\') {
                if (++it == str.end()) {
                    return std::nullopt;
                }

                currentToken.append(*it);
                hasToken = true;
            } else if (c == u'"') {
                curState = SpliterState::InDoubleQuotes;
                hasToken = true;
            } else if (c == u'\'') {
                qWarning() << "Exec parsing: Single quote detected. "
                              "Note: Single quotes are not part of the Desktop Entry Spec, "
                              "supporting them for GIO/Shell compatibility.";
                curState = SpliterState::InSingleQuote;
                hasToken = true;
            } else if (c.isSpace()) {
                if (hasToken) {
                    args.append(currentToken);
                    currentToken.clear();
                    hasToken = false;
                }
            } else {
                currentToken.append(c);
                hasToken = true;
            }
        } break;
        case SpliterState::InDoubleQuotes: {
            if (c == u'\\') {
                if (++it == str.end()) {
                    return std::nullopt;
                }

                const auto next = *it;
                if (next == u'"' || next == u'\\' || next == u'$' || next == u'`') {
                    currentToken.append(next);
                } else {
                    currentToken.append(u'\\');
                    currentToken.append(next);
                }
            } else if (c == u'"') {
                curState = SpliterState::Normal;
            } else {
                currentToken.append(c);
            }
        } break;
        case SpliterState::InSingleQuote: {
            if (c == u'\'') {
                curState = SpliterState::Normal;
            } else {
                currentToken.append(c);
            }
        } break;
        }
    }

    if (curState != SpliterState::Normal) {
        qWarning() << "Exec parsing error: Unterminated quote";
        return std::nullopt;
    }

    if (hasToken) {
        args.append(currentToken);
    }

    return args;
}

LaunchTask ApplicationService::processExec(const QString &str, const QStringList &fields, const QString &dir) noexcept
{
    auto args = splitExecArguments(str);
    if (!args) {
        qWarning() << "splitExecArguments failed.";
        return {};
    }

    if (args->isEmpty()) {
        qWarning() << "exec format is invalid.";
        return {};
    }

    qDebug() << "splitExecArguments:" << *args;

    LaunchTask task;
    task.LaunchBin = args->first();
    task.command.reserve(args->size() + 2);  // 2 for icon

    const QChar percentage{u'%'};
    bool exclusiveField{false};

    for (auto &rawArg : *args) {
        if (!rawArg.contains(percentage)) {
            task.command.append(std::move(rawArg));
            continue;
        }

        QString processedArg;
        processedArg.reserve(rawArg.size());
        bool dynamicField{false};

        for (qsizetype j = 0; j < rawArg.size(); ++j) {
            const auto ch = rawArg[j];
            if (ch != percentage || j + 1 >= rawArg.size()) {
                processedArg.append(ch);
                continue;
            }

            const auto code = rawArg[++j];
            switch (code.unicode()) {
            case u'f':
            case u'F':
            case u'u':
                [[fallthrough]];
            case u'U': {
                if (exclusiveField) {
                    qDebug() << QString{"exclusive field is detected again, %%1 will be ignored."}.arg(code);
                    break;
                }
                exclusiveField = true;

                if (fields.empty()) {
                    qDebug() << QString{"fields is empty, %%1 will be ignored."}.arg(code);
                    break;
                }

                dynamicField = true;
                task.argNum = task.command.size();
                task.fieldLocation = processedArg.size();
                task.local = (code.toLower() == u'f');

                // respect the original url, replace it to exec directly
                QList<QUrl> resources;
                resources.reserve(fields.size());
                for (const auto &field : fields) {
                    resources.emplace_back(QUrl::fromUserInput(field, dir, QUrl::AssumeLocalFile));
                }

                if (code.isUpper()) {
                    task.Resources.emplace_back(std::in_place_type<QList<QUrl>>, std::move(resources));
                } else {
                    task.Resources.reserve(fields.size());
                    for (auto &&resource : resources) {
                        task.Resources.emplace_back(std::in_place_type<QUrl>, std::move(resource));
                    }
                }

                processedArg.append(percentage).append(code);
            } break;
            case u'i': {
                auto val = m_entry->value(fromStaticRaw(DesktopFileEntryKey), "Icon");
                if (!val) {
                    qDebug() << R"(Application Icons can't be found. %i will be ignored.)";
                    break;
                }

                auto iconStr = toIconString(val.value());
                if (iconStr.isEmpty()) {
                    qDebug() << R"(Icons Convert to string failed. %i will be ignored.)";
                    break;
                }

                if (!processedArg.isEmpty()) {
                    task.command.append(std::move(processedArg));
                    processedArg.clear();
                }

                // spec says:
                // The Icon key of the desktop entry expanded as two arguments, first --icon and then the value of the Icon key.
                task.command << QStringLiteral("--icon") << std::move(iconStr);
            } break;
            case u'c': {
                auto val = m_entry->value(fromStaticRaw(DesktopFileEntryKey), u"Name"_s);
                if (!val) {
                    qDebug() << R"(Application Name can't be found. %c will be ignored.)";
                    break;
                }

                const auto &rawValue = val.value().get();
                auto nameStr = toLocaleString(rawValue, getUserLocale());
                if (nameStr.isEmpty()) {
                    nameStr = toString(rawValue);
                }

                processedArg.append(nameStr);
            } break;
            case u'k': {
                processedArg.append(m_desktopSource.sourcePath());
            } break;
            case u'%': {
                processedArg.append(percentage);
            } break;
            case u'd':
            case u'D':
            case u'n':
            case u'N':
            case u'v':
                [[fallthrough]];  // Deprecated field codes should be removed from the command line and ignored.
            case u'm': {
                qWarning() << "field code" << code << "has been deprecated.";
            } break;
            default: {
                // spec says:
                // Command lines that contain a field code that is not listed in this specification are invalid and MUST NOT be
                // processed
                qCritical() << "unknown field code:" << code << ", Invalid.";
                return {};
            }
            }
        }

        if (dynamicField || !processedArg.isEmpty()) {
            task.command.append(std::move(processedArg));
        }
    }

    if (task.Resources.isEmpty()) {
        task.Resources.emplace_back(QVariant{});  // mapReduce should run once at least
    }

    qDebug() << "Parsed Exec:" << task.LaunchBin << "Cmds:" << task.command;
    return task;
}

QVariant ApplicationService::findEntryValue(const QString &group,
                                            const QString &valueKey,
                                            EntryValueType type,
                                            const QLocale &locale) const noexcept
{
    auto tmp = m_entry->value(group, valueKey);
    if (!tmp) {
        return {};
    }

    const auto &val = std::move(tmp).value().get();
    switch (type) {
    case EntryValueType::Raw: {
        auto valStr = toString(val, true);
        return valStr.isEmpty() ? QVariant{} : QVariant::fromValue(std::move(valStr));
    }
    case EntryValueType::String: {
        auto valStr = toString(val);
        return valStr.isEmpty() ? QVariant{} : QVariant::fromValue(std::move(valStr));
    }
    case EntryValueType::LocaleString: {
        auto valStr = toLocaleString(val, locale);
        if (valStr.isEmpty()) {
            valStr = toString(val);
        }

        return valStr.isEmpty() ? QVariant{} : QVariant::fromValue(std::move(valStr));
    }
    case EntryValueType::Boolean: {
        bool ok{false};
        auto valBool = toBoolean(val, ok);
        return ok ? valBool : QVariant{};
    }
    case EntryValueType::IconString: {
        auto valStr = toIconString(val);
        return valStr.isEmpty() ? QVariant{} : QVariant::fromValue(std::move(valStr));
    }
    }

    Q_UNREACHABLE();
}

void ApplicationService::updateAfterLaunch(bool isLaunch) noexcept
{
    if (!isLaunch) {
        return;
    }

    auto timestamp = QDateTime::currentMSecsSinceEpoch();
    if (auto ptr = m_storage.lock(); ptr) {
        if (!ptr->updateApplicationValue(m_desktopSource.desktopId(),
                                         fromStaticRaw(ApplicationPropertiesGroup),
                                         fromStaticRaw(LastLaunchedTime),
                                         QVariant::fromValue(timestamp),
                                         true)) {
            qWarning() << "failed to update LastLaunchedTime:" << id();
            return;
        }

        if (!ptr->updateApplicationValue(m_desktopSource.desktopId(),
                                         fromStaticRaw(ApplicationPropertiesGroup),
                                         fromStaticRaw(LaunchedTimes),
                                         QVariant::fromValue(m_launchedTimes + 1))) {
            qWarning() << "failed to update LaunchedTimes:" << id();
            return;
        }

        m_lastLaunch = timestamp;
        m_launchedTimes += 1;
        emit lastLaunchedTimeChanged();
        emit launchedTimesChanged();
    }
}

void ApplicationService::setAutostartSource(AutostartSource &&source) noexcept
{
    m_autostartSource = std::move(source);
    emit autostartChanged();
}
