/********************************************************************
Copyright 2016  Eike Hein <hein.org>

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) version 3, or any
later version accepted by the membership of KDE e.V. (or its
successor approved by the membership of KDE e.V.), which shall
act as a proxy defined in Section 6 of version 3 of the license.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library.  If not, see <http://www.gnu.org/licenses/>.
*********************************************************************/

#include "tasktools.h"
#include "abstracttasksmodel.h"

#include <KActivities/ResourceInstance>
#include <KConfigGroup>
#include <KDesktopFile>
#include <kemailsettings.h>
#include <KFileItem>
#include <KMimeTypeTrader>
#include <KRun>
#include <KServiceTypeTrader>
#include <KSharedConfig>
#include <KStartupInfo>
#include <KWindowSystem>

#include <processcore/processes.h>
#include <processcore/process.h>

#include <config-X11.h>

#include <QDir>
#include <QGuiApplication>
#include <QRegularExpression>
#include <QScreen>
#include <QUrlQuery>
#if HAVE_X11
#include <QX11Info>
#endif

namespace TaskManager
{

AppData appDataFromUrl(const QUrl &url, const QIcon &fallbackIcon)
{
    AppData data;
    data.url = url;

    if (url.hasQuery()) {
        QUrlQuery uQuery(url);

        if (uQuery.hasQueryItem(QLatin1String("iconData"))) {
            QString iconData(uQuery.queryItemValue(QLatin1String("iconData")));
            QPixmap pixmap;
            QByteArray bytes = QByteArray::fromBase64(iconData.toLocal8Bit(), QByteArray::Base64UrlEncoding);
            pixmap.loadFromData(bytes);
            data.icon.addPixmap(pixmap);
        }

        if (uQuery.hasQueryItem(QLatin1String("skipTaskbar"))) {
            QString skipTaskbar(uQuery.queryItemValue(QLatin1String("skipTaskbar")));
            data.skipTaskbar = (skipTaskbar == QStringLiteral("true"));
        }
    }

    // applications: URLs are used to refer to applications by their KService::menuId
    // (i.e. .desktop file name) rather than the absolute path to a .desktop file.
    if (url.scheme() == QStringLiteral("applications")) {
        const KService::Ptr service = KService::serviceByMenuId(url.path());

        if (service && url.path() == service->menuId()) {
            data.name = service->name();
            data.genericName = service->genericName();
            data.id = service->storageId();

            if (data.icon.isNull()) {
                data.icon = QIcon::fromTheme(service->icon());
            }
        }
    }

    if (url.isLocalFile() && KDesktopFile::isDesktopFile(url.toLocalFile())) {
        KDesktopFile f(url.toLocalFile());

        const KService::Ptr service = KService::serviceByStorageId(f.fileName());

        // Resolve to non-absolute menuId-based URL if possible.
        if (service) {
            const QString &menuId = service->menuId();

            if (!menuId.isEmpty()) {
                data.url = QUrl(QStringLiteral("applications:") + menuId);
            }
        }

        if (service && QUrl::fromLocalFile(service->entryPath()) == url) {
            data.name = service->name();
            data.genericName = service->genericName();
            data.id = service->storageId();

            if (data.icon.isNull()) {
                data.icon = QIcon::fromTheme(service->icon());
            }
        } else if (f.tryExec()) {
            data.name = f.readName();
            data.genericName = f.readGenericName();
            data.id = QUrl::fromLocalFile(f.fileName()).fileName();

            if (data.icon.isNull()) {
                data.icon = QIcon::fromTheme(f.readIcon());
            }
        }

        if (data.id.endsWith(".desktop")) {
            data.id = data.id.left(data.id.length() - 8);
        }
    } else if (url.scheme() == QLatin1String("preferred")) {
        data.id = defaultApplication(url);

        const KService::Ptr service = KService::serviceByStorageId(data.id);

        if (service) {
            const QString &menuId = service->menuId();
            const QString &desktopFile = service->entryPath();

            data.name = service->name();
            data.genericName = service->genericName();
            data.id = service->storageId();

            if (data.icon.isNull()) {
                data.icon = QIcon::fromTheme(service->icon());
            }

            // Update with resolved URL.
            if (!menuId.isEmpty()) {
                data.url = QUrl(QStringLiteral("applications:") + menuId);
            } else {
                data.url = QUrl::fromLocalFile(desktopFile);
            }
        }
    }

    if (data.name.isEmpty()) {
        data.name = url.fileName();
    }

    if (data.icon.isNull()) {
        data.icon = fallbackIcon;
    }

    return data;
}

AppData appDataFromAppId(const QString &appId)
{
    AppData data;

    KService::Ptr service = KService::serviceByStorageId(appId);

    if (service) {
        data.id = service->storageId();
        data.name = service->name();
        data.genericName = service->genericName();

        const QString &menuId = service->menuId();

        // applications: URLs are used to refer to applications by their KService::menuId
        // (i.e. .desktop file name) rather than the absolute path to a .desktop file.
        if (!menuId.isEmpty()) {
            data.url = QUrl(QStringLiteral("applications:") + menuId);
        } else {
            data.url = QUrl::fromLocalFile(service->entryPath());
        }

        return data;
    }

    QString desktopFile = appId;

    if (!desktopFile.endsWith(QLatin1String(".desktop"))) {
        desktopFile.append(QLatin1String(".desktop"));
    }

    if (KDesktopFile::isDesktopFile(desktopFile) && QFile::exists(desktopFile)) {
        KDesktopFile f(desktopFile);

        data.id = QUrl::fromLocalFile(f.fileName()).fileName();

        if (data.id.endsWith(QLatin1String(".desktop"))) {
            data.id = data.id.left(data.id.length() - 8);
        }

        data.name = f.readName();
        data.genericName = f.readGenericName();
        data.url = QUrl::fromLocalFile(desktopFile);
    }

    return data;
}

QUrl windowUrlFromMetadata(const QString &appId, quint32 pid,
    KSharedConfig::Ptr rulesConfig, const QString &xWindowsWMClassName)
{
    if (!rulesConfig) {
        return QUrl();
    }

    QUrl url;
    KService::List services;
    bool triedPid = false;

    // The code below this function goes on a hunt for services based on the metadata
    // that has been passed in. Occasionally, it will find more than one matching
    // service. In some scenarios (e.g. multiple identically-named .desktop files)
    // there's a need to pick the most useful one. The function below promises to "sort"
    // a list of services by how closely their KService::menuId() relates to the key that
    // has been passed in. The current naive implementation simply looks for a menuId
    // that starts with the key, prepends it to the list and returns it. In practice,
    // that means a KService with a menuId matching the appId will win over one with a
    // menuId that encodes a subfolder hierarchy.
    // A concrete example: Valve's Steam client is sometimes installed two times, once
    // natively as a Linux application, once via Wine. Both have .desktop files named
    // (S|)steam.desktop. The Linux native version is located in the menu by means of
    // categorization ("Games") and just has a menuId() matching the .desktop file name,
    // but the Wine version is placed in a folder hierarchy by Wine and gets a menuId()
    // of wine-Programs-Steam-Steam.desktop. The weighing done by this function makes
    // sure the Linux native version gets mapped to the former, while other heuristics
    // map the Wine version reliably to the latter.
    // In lieu of this weighing we just used whatever KServiceTypeTrader returned first,
    // so what we do here can be no worse.
    auto sortServicesByMenuId = [](KService::List &services, const QString &key) {
        if (services.count() == 1) {
            return;
        }

        for (const auto service : services) {
            if (service->menuId().startsWith(key, Qt::CaseInsensitive)) {
                services.prepend(service);
                return;
            }
        }
    };

    if (!(appId.isEmpty() && xWindowsWMClassName.isEmpty())) {
        // Check to see if this wmClass matched a saved one ...
        KConfigGroup grp(rulesConfig, "Mapping");
        KConfigGroup set(rulesConfig, "Settings");

        // Evaluate MatchCommandLineFirst directives from config first.
        // Some apps have different launchers depending upon command line ...
        QStringList matchCommandLineFirst = set.readEntry("MatchCommandLineFirst", QStringList());

        if (!appId.isEmpty() && matchCommandLineFirst.contains(appId)) {
            triedPid = true;
            services = servicesFromPid(pid, rulesConfig);
        }

        // Try to match using xWindowsWMClassName also.
        if (!xWindowsWMClassName.isEmpty() && matchCommandLineFirst.contains("::"+xWindowsWMClassName)) {
            triedPid = true;
            services = servicesFromPid(pid, rulesConfig);
        }

        if (!appId.isEmpty()) {
            // Evaluate any mapping rules that map to a specific .desktop file.
            QString mapped(grp.readEntry(appId + "::" + xWindowsWMClassName, QString()));

            if (mapped.endsWith(QLatin1String(".desktop"))) {
                url = QUrl(mapped);
                return url;
            }

            if (mapped.isEmpty()) {
                mapped = grp.readEntry(appId, QString());

                if (mapped.endsWith(QLatin1String(".desktop"))) {
                    url = QUrl(mapped);
                    return url;
                }
            }

            // Some apps, such as Wine, cannot use xWindowsWMClassName to map to launcher name - as Wine itself is not a GUI app
            // So, Settings/ManualOnly lists window classes where the user will always have to manualy set the launcher ...
            QStringList manualOnly = set.readEntry("ManualOnly", QStringList());

            if (!appId.isEmpty() && manualOnly.contains(appId)) {
                return url;
            }

            // Try matching both appId and xWindowsWMClassName against StartupWMClass.
            // We do this before evaluating the mapping rules further, because StartupWMClass
            // is essentially a mapping rule, and we expect it to be set deliberately and
            // sensibly to instruct us what to do. Also, mapping rules
            //
            // StartupWMClass=STRING
            //
            //   If true, it is KNOWN that the application will map at least one
            //   window with the given string as its WM class or WM name hint.
            //
            // Source: https://specifications.freedesktop.org/startup-notification-spec/startup-notification-0.1.txt
            if (services.isEmpty()) {
                services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ StartupWMClass)").arg(appId));
                sortServicesByMenuId(services, appId);
            }

            if (services.isEmpty()) {
                services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ StartupWMClass)").arg(xWindowsWMClassName));
                sortServicesByMenuId(services, xWindowsWMClassName);
            }

            // Evaluate rewrite rules from config.
            if (services.isEmpty()) {
                KConfigGroup rewriteRulesGroup(rulesConfig, QStringLiteral("Rewrite Rules"));
                if (rewriteRulesGroup.hasGroup(appId)) {
                    KConfigGroup rewriteGroup(&rewriteRulesGroup, appId);

                    const QStringList &rules = rewriteGroup.groupList();
                    for (const QString &rule : rules) {
                        KConfigGroup ruleGroup(&rewriteGroup, rule);

                        const QString propertyConfig = ruleGroup.readEntry(QStringLiteral("Property"), QString());

                        QString matchProperty;
                        if (propertyConfig == QLatin1String("ClassClass")) {
                            matchProperty = appId;
                        } else if (propertyConfig == QLatin1String("ClassName")) {
                            matchProperty = xWindowsWMClassName;
                        }

                        if (matchProperty.isEmpty()) {
                            continue;
                        }

                        const QString serviceSearchIdentifier = ruleGroup.readEntry(QStringLiteral("Identifier"), QString());
                        if (serviceSearchIdentifier.isEmpty()) {
                            continue;
                        }

                        QRegularExpression regExp(ruleGroup.readEntry(QStringLiteral("Match")));
                        const auto match = regExp.match(matchProperty);

                        if (match.hasMatch()) {
                            const QString actualMatch = match.captured(QStringLiteral("match"));
                            if (actualMatch.isEmpty()) {
                                continue;
                            }

                            QString rewrittenString = ruleGroup.readEntry(QStringLiteral("Target")).arg(actualMatch);
                            // If no "Target" is provided, instead assume the matched property (appId/xWindowsWMClassName).
                            if (rewrittenString.isEmpty()) {
                                rewrittenString = matchProperty;
                            }

                            services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ %2)").arg(rewrittenString, serviceSearchIdentifier));
                            sortServicesByMenuId(services, serviceSearchIdentifier);

                            if (!services.isEmpty()) {
                                break;
                            }
                        }
                    }
                }
            }

            // The appId looks like a path.
            if (services.isEmpty() && appId.startsWith(QStringLiteral("/"))) {
                // Check if it's a path to a .desktop file.
                if (KDesktopFile::isDesktopFile(appId) && QFile::exists(appId)) {
                    return QUrl::fromLocalFile(appId);
                }

                // Check if the appId passes as a .desktop file path if we add the extension.
                const QString appIdPlusExtension(appId + QStringLiteral(".desktop"));

                if (KDesktopFile::isDesktopFile(appIdPlusExtension) && QFile::exists(appIdPlusExtension)) {
                    return QUrl::fromLocalFile(appIdPlusExtension);
                }
            }

            // Try matching mapped name against DesktopEntryName.
            if (!mapped.isEmpty() && services.isEmpty()) {
                services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ DesktopEntryName) and (not exist NoDisplay or not NoDisplay)").arg(mapped));
                sortServicesByMenuId(services, mapped);
            }

            // Try matching mapped name against 'Name'.
            if (!mapped.isEmpty() && services.isEmpty()) {
                services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ Name) and (not exist NoDisplay or not NoDisplay)").arg(mapped));
                sortServicesByMenuId(services, mapped);
            }

            // Try matching appId against DesktopEntryName.
            if (services.isEmpty()) {
                services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ DesktopEntryName) and (not exist NoDisplay or not NoDisplay)").arg(appId));
                sortServicesByMenuId(services, appId);
            }

            // Try matching appId against 'Name'.
            // This has a shaky chance of success as appId is untranslated, but 'Name' may be localized.
            if (services.isEmpty()) {
                services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ Name) and (not exist NoDisplay or not NoDisplay)").arg(appId));
                sortServicesByMenuId(services, appId);
            }

            // Check rules configuration for whether we want to hide this task.
            // Some window tasks update from bogus to useful metadata early during startup.
            // This config key allows listing the bogus metadata, and the matching window
            // tasks are hidden until they perform a metadate update that stops them from
            // matching.
            QStringList skipTaskbar = set.readEntry("SkipTaskbar", QStringList());

            if (skipTaskbar.contains(appId)) {
                QUrlQuery query(url);
                query.addQueryItem(QStringLiteral("skipTaskbar"), QStringLiteral("true"));
                url.setQuery(query);
            } else if (skipTaskbar.contains(mapped)) {
                QUrlQuery query(url);
                query.addQueryItem(QStringLiteral("skipTaskbar"), QStringLiteral("true"));
                url.setQuery(query);
            }
        }

        // Ok, absolute *last* chance, try matching via pid (but only if we have not already tried this!) ...
        if (services.isEmpty() && !triedPid) {
            services = servicesFromPid(pid, rulesConfig);
        }
    }

    // Try to improve on a possible from-binary fallback.
    // If no services were found or we got a fake-service back from getServicesViaPid()
    // we attempt to improve on this by adding a loosely matched reverse-domain-name
    // DesktopEntryName. Namely anything that is '*.appId.desktop' would qualify here.
    //
    // Illustrative example of a case where the above heuristics would fail to produce
    // a reasonable result:
    // - org.kde.dragonplayer.desktop
    // - binary is 'dragon'
    // - qapp appname and thus appId is 'dragonplayer'
    // - appId cannot directly match the desktop file because of RDN
    // - appId also cannot match the binary because of name mismatch
    // - in the following code *.appId can match org.kde.dragonplayer though
    if (services.isEmpty() || services.at(0)->desktopEntryName().isEmpty()) {
        auto matchingServices = KServiceTypeTrader::self()->query(QStringLiteral("Application"),
            QStringLiteral("exist Exec and ('%1' ~~ DesktopEntryName)").arg(appId));
        QMutableListIterator<KService::Ptr> it(matchingServices);
        while (it.hasNext()) {
            auto service = it.next();
            if (!service->desktopEntryName().endsWith("." + appId)) {
                it.remove();
            }
        }
        // Exactly one match is expected, otherwise we discard the results as to reduce
        // the likelihood of false-positive mappings. Since we essentially eliminate the
        // uniqueness that RDN is meant to bring to the table we could potentially end
        // up with more than one match here.
        if (matchingServices.length() == 1) {
            services = matchingServices;
        }
    }

    if (!services.isEmpty()) {
        const QString &menuId = services.at(0)->menuId();

        // applications: URLs are used to refer to applications by their KService::menuId
        // (i.e. .desktop file name) rather than the absolute path to a .desktop file.
        if (!menuId.isEmpty()) {
            url.setUrl(QStringLiteral("applications:") + menuId);
            return url;
        }

        QString path = services.at(0)->entryPath();

        if (path.isEmpty()) {
            path = services.at(0)->exec();
        }

        if (!path.isEmpty()) {
            QString query = url.query();
            url = QUrl::fromLocalFile(path);
            url.setQuery(query);
            return url;
        }
    }

    return url;
}

KService::List servicesFromPid(quint32 pid, KSharedConfig::Ptr rulesConfig)
{
    if (pid == 0) {
        return KService::List();
    }

    if (!rulesConfig) {
        return KService::List();
    }

    KSysGuard::Processes procs;
    procs.updateOrAddProcess(pid);

    KSysGuard::Process *proc = procs.getProcess(pid);
    const QString &cmdLine = proc ? proc->command().simplified() : QString(); // proc->command has a trailing space???

    if (cmdLine.isEmpty()) {
        return KService::List();
    }

    return servicesFromCmdLine(cmdLine, proc->name(), rulesConfig);
}

KService::List servicesFromCmdLine(const QString &_cmdLine, const QString &processName,
    KSharedConfig::Ptr rulesConfig)
{
    QString cmdLine = _cmdLine;
    KService::List services;

    if (!rulesConfig) {
        return services;
    }

    const int firstSpace = cmdLine.indexOf(' ');
    int slash = 0;

    services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ Exec)").arg(cmdLine));

    if (services.isEmpty()) {
        // Could not find with complete command line, so strip out the path part ...
        slash = cmdLine.lastIndexOf('/', firstSpace);

        if (slash > 0) {
            services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ Exec)").arg(cmdLine.mid(slash + 1)));
        }
    }

    if (services.isEmpty() && firstSpace > 0) {
        // Could not find with arguments, so try without ...
        cmdLine = cmdLine.left(firstSpace);

        services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ Exec)").arg(cmdLine));

        if (services.isEmpty()) {
            slash = cmdLine.lastIndexOf('/');

            if (slash > 0) {
                services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ Exec)").arg(cmdLine.mid(slash + 1)));
            }
        }
    }

    if (services.isEmpty()) {
        KConfigGroup set(rulesConfig, "Settings");
        const QStringList &runtimes = set.readEntry("TryIgnoreRuntimes", QStringList());

        bool ignore = runtimes.contains(cmdLine);

        if (!ignore && slash > 0) {
            ignore = runtimes.contains(cmdLine.mid(slash + 1));
        }

        if (ignore) {
            return servicesFromCmdLine(_cmdLine.mid(firstSpace + 1), processName, rulesConfig);
        }
    }

    if (services.isEmpty() && !processName.isEmpty() && !QStandardPaths::findExecutable(cmdLine).isEmpty()) {
        // cmdLine now exists without arguments if there were any.
        services << QExplicitlySharedDataPointer<KService>(new KService(processName, cmdLine, QString()));
    }

    return services;
}

QString defaultApplication(const QUrl &url)
{
    if (url.scheme() != QLatin1String("preferred")) {
        return QString();
    }

    const QString &application = url.host();

    if (application.isEmpty()) {
        return QString();
    }

    if (application.compare(QLatin1String("mailer"), Qt::CaseInsensitive) == 0) {
        KEMailSettings settings;

        // In KToolInvocation, the default is kmail; but let's be friendlier.
        QString command = settings.getSetting(KEMailSettings::ClientProgram);

        if (command.isEmpty()) {
            if (KService::Ptr kontact = KService::serviceByStorageId(QStringLiteral("kontact"))) {
                return kontact->storageId();
            } else if (KService::Ptr kmail = KService::serviceByStorageId(QStringLiteral("kmail"))) {
                return kmail->storageId();
            }
        }

        if (!command.isEmpty()) {
            if (settings.getSetting(KEMailSettings::ClientTerminal) == QLatin1String("true")) {
                KConfigGroup confGroup(KSharedConfig::openConfig(), "General");
                const QString preferredTerminal = confGroup.readPathEntry("TerminalApplication",
                                                  QStringLiteral("konsole"));
                command = preferredTerminal + QLatin1String(" -e ") + command;
            }

            return command;
        }
    } else if (application.compare(QLatin1String("browser"), Qt::CaseInsensitive) == 0) {
        KConfigGroup config(KSharedConfig::openConfig(), "General");
        QString browserApp = config.readPathEntry("BrowserApplication", QString());

        if (browserApp.isEmpty()) {
            const KService::Ptr htmlApp = KMimeTypeTrader::self()->preferredService(QStringLiteral("text/html"));

            if (htmlApp) {
                browserApp = htmlApp->storageId();
            }
        } else if (browserApp.startsWith('!')) {
            browserApp = browserApp.mid(1);
        }

        return browserApp;
    } else if (application.compare(QLatin1String("terminal"), Qt::CaseInsensitive) == 0) {
        KConfigGroup confGroup(KSharedConfig::openConfig(), "General");

        return confGroup.readPathEntry("TerminalApplication", QStringLiteral("konsole"));
    } else if (application.compare(QLatin1String("filemanager"), Qt::CaseInsensitive) == 0) {
        KService::Ptr service = KMimeTypeTrader::self()->preferredService(QStringLiteral("inode/directory"));

        if (service) {
            return service->storageId();
        }
    } else if (KService::Ptr service = KMimeTypeTrader::self()->preferredService(application)) {
        return service->storageId();
    } else {
        // Try the files in share/apps/kcm_componentchooser/*.desktop.
        QStringList directories = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("kcm_componentchooser"), QStandardPaths::LocateDirectory);
        QStringList services;

        foreach(const QString& directory, directories) {
            QDir dir(directory);
            foreach(const QString& f, dir.entryList(QStringList("*.desktop")))
                services += dir.absoluteFilePath(f);
        }

        foreach (const QString & service, services) {
            KConfig config(service, KConfig::SimpleConfig);
            KConfigGroup cg = config.group(QByteArray());
            const QString type = cg.readEntry("valueName", QString());

            if (type.compare(application, Qt::CaseInsensitive) == 0) {
                KConfig store(cg.readPathEntry("storeInFile", QStringLiteral("null")));
                KConfigGroup storeCg(&store, cg.readEntry("valueSection", QString()));
                const QString exec = storeCg.readPathEntry(cg.readEntry("valueName", "kcm_componenchooser_null"),
                                     cg.readEntry("defaultImplementation", QString()));

                if (!exec.isEmpty()) {
                    return exec;
                }

                break;
            }
        }
    }

    return QString("");
}

bool launcherUrlsMatch(const QUrl &a, const QUrl &b, UrlComparisonMode mode)
{
    QUrl sanitizedA = a;
    QUrl sanitizedB = b;

    if (mode == IgnoreQueryItems) {
        sanitizedA = a.adjusted(QUrl::RemoveQuery);
        sanitizedB = b.adjusted(QUrl::RemoveQuery);
    }

    auto tryResolveToApplicationsUrl = [](const QUrl &url) -> QUrl {
        QUrl resolvedUrl = url;

        if (url.isLocalFile() && KDesktopFile::isDesktopFile(url.toLocalFile())) {
            KDesktopFile f(url.toLocalFile());

            const KService::Ptr service = KService::serviceByStorageId(f.fileName());

            // Resolve to non-absolute menuId-based URL if possible.
            if (service) {
                const QString &menuId = service->menuId();

                if (!menuId.isEmpty()) {
                    resolvedUrl = QUrl(QStringLiteral("applications:") + menuId);
                    resolvedUrl.setQuery(url.query());
                }
            }
        }

        return resolvedUrl;
    };

    sanitizedA = tryResolveToApplicationsUrl(sanitizedA);
    sanitizedB = tryResolveToApplicationsUrl(sanitizedB);

    return (sanitizedA == sanitizedB);
}

bool appsMatch(const QModelIndex &a, const QModelIndex &b)
{
    const QString &aAppId = a.data(AbstractTasksModel::AppId).toString();
    const QString &bAppId = b.data(AbstractTasksModel::AppId).toString();

    if (!aAppId.isEmpty() && aAppId == bAppId) {
        return true;
    }

    const QUrl &aUrl = a.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl();
    const QUrl &bUrl = b.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl();

    if (aUrl.isValid() && aUrl == bUrl) {
        return true;
    }

    return false;
}

QRect screenGeometry(const QPoint &pos)
{
    if (pos.isNull()) {
        return QRect();
    }

    const QList<QScreen *> &screens = QGuiApplication::screens();
    QRect screenGeometry;
    int shortestDistance = INT_MAX;

    for (int i = 0; i < screens.count(); ++i) {
        const QRect &geometry = screens.at(i)->geometry();

        if (geometry.contains(pos)) {
            return geometry;
        }

        int distance = QPoint(geometry.topLeft() - pos).manhattanLength();
        distance = qMin(distance, QPoint(geometry.topRight() - pos).manhattanLength());
        distance = qMin(distance, QPoint(geometry.bottomRight() - pos).manhattanLength());
        distance = qMin(distance, QPoint(geometry.bottomLeft() - pos).manhattanLength());

        if (distance < shortestDistance) {
            shortestDistance = distance;
            screenGeometry = geometry;
        }
    }

    return screenGeometry;
}

void runApp(const AppData &appData, const QList<QUrl> &urls)
{
    if (appData.url.isValid()) {
        quint32 timeStamp = 0;

#if HAVE_X11
        if (KWindowSystem::isPlatformX11()) {
            timeStamp = QX11Info::appUserTime();
        }
#endif

        KService::Ptr service;

        // applications: URLs are used to refer to applications by their KService::menuId
        // (i.e. .desktop file name) rather than the absolute path to a .desktop file.
        if (appData.url.scheme() == QStringLiteral("applications")) {
            service = KService::serviceByMenuId(appData.url.path());
        } else if (appData.url.scheme() == QLatin1String("preferred")) {
            const KService::Ptr service = KService::serviceByStorageId(defaultApplication(appData.url));
        } else {
            service = KService::serviceByDesktopPath(appData.url.toLocalFile());
        }

        if (service && service->isApplication()) {
            KRun::runApplication(*service, urls, nullptr, KRun::RunFlags(), QString(),
                KStartupInfo::createNewStartupIdForTimestamp(timeStamp));

            KActivities::ResourceInstance::notifyAccessed(QUrl(QStringLiteral("applications:") + service->storageId()),
                QStringLiteral("org.kde.libtaskmanager"));
        } else {
            new KRun(appData.url, nullptr, false, KStartupInfo::createNewStartupIdForTimestamp(timeStamp));

            if (!appData.id.isEmpty()) {
                KActivities::ResourceInstance::notifyAccessed(QUrl(QStringLiteral("applications:") + appData.id),
                    QStringLiteral("org.kde.libtaskmanager"));
            }
        }
    }
}

}
