/*
 * Copyright 2023 KylinSoft Co., Ltd.
 *
 * This program is free software: you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later
 * version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program. If not, see <https://www.gnu.org/licenses/>.
 */

#include "applauncher.h"
#include <QtConcurrent>
#include <QProcess>
#include <QStandardPaths>
#include <QFileSystemWatcher>
#include <QProcessEnvironment>
#include <QGuiApplication>
#include <windowmanager/windowmanager.h>
#include "core/eventwatcher.h"

extern "C" {
#include <unistd.h>
#include <fcntl.h>
#include <gio/gdesktopappinfo.h>
#include <gio/gappinfo.h>
}

#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
#define KWIN_SERVICE "org.ukui.KWin"
#else
#define KWIN_SERVICE "org.kde.KWin"
#endif

AppLauncher::AppLauncher(QObject *parent)
    : QObject(parent)
    , m_kwinInterface(new org::ukui::KWin(KWIN_SERVICE, "/KWin",
                                          QDBusConnection::sessionBus(), this))
    , m_isWayland(false)
{
    if (QGuiApplication::platformName().startsWith("wayland", Qt::CaseInsensitive)) {
        m_isWayland = true;
    }
}

bool AppLauncher::LaunchApp(const QString &desktopFile)
{
    return doLaunchApp(desktopFile);
}

bool AppLauncher::ActiveProcessByWid(const uint &wid)
{
    return m_appInfoManager.thawApp(wid);
}

bool AppLauncher::ActiveProcessByPid(const int &pid)
{
     qDebug() << current_func << pid;
     return m_appInfoManager.thawApp(pid);
}

bool AppLauncher::LaunchAppWithArguments(const QString &desktopFile, const QStringList &args)
{
    return doLaunchApp(desktopFile, args);
}

bool AppLauncher::Open(const QString &fileName)
{
    if (fileName.isEmpty()) {
        qWarning() << __FUNCTION__ << "fileName is NULL!";
        return false;
    }
    QUrl url(fileName);
    QFile file(fileName);
    if (!url.scheme().isEmpty()) {
        // 传入的参数为url类型
        return LaunchDefaultAppWithUrl(fileName);
    }
    if (!file.exists()) {
        qWarning() << __FUNCTION__ << fileName << "is invalid!";
        return false;
    }
    QVector<QStringList> launchApplists = getLaunchAppLists(fileName);
    if (launchApplists.isEmpty()) {
        qWarning() << __FUNCTION__ << fileName << "is not have apps to launch!";
        return false;
    }
    if (!launchApplists.first().isEmpty()) {
        // 选用默认应用打开文件
        for (int i=0; i<launchApplists.first().count(); ++i) {
            if (LaunchAppWithArguments(launchApplists.first().at(i), QStringList() << fileName)) {
                return true;
            }
        }
    }
    // 选用推荐应用列表打开文件，直到应用打开文件为止
    for (int i=0; i<launchApplists.last().count(); ++i) {
        if (LaunchAppWithArguments(launchApplists.last().at(i), QStringList() << fileName)) {
            return true;
        }
    }

    return false;
}

bool AppLauncher::LaunchDefaultAppWithUrl(const QString &url)
{
    int ret = g_app_info_launch_default_for_uri(url.toUtf8().constData(),
                                                nullptr,
                                                nullptr);
    qDebug() << __func__ << url << ret;
    if (ret) {
        return true;
    }

    QUrl url_(url);
    if (url_.isEmpty() || !url_.isValid()) {
        return false;
    }
    QProcess p;
    if (url_.isLocalFile()) {
        QString fileName = url_.toLocalFile();
        qDebug ()<<"---fileName---" << fileName;
        p.start("xdg-mime", QStringList() << "query" << "filetype" << fileName);
        p.waitForFinished(3000);
        if (p.error() != QProcess::UnknownError) {
            qWarning() << __FUNCTION__ << "query filetype for "
                       << fileName << "failed!" << p.errorString();
            return false;
        }
        QString fileType = p.readAll();
        if (fileType.endsWith("\n")) {
            fileType.remove("\n");
        }
        qDebug ()<<"---fileType---" << fileType;
        p.start("xdg-mime", QStringList() << "query" << "default" << fileType);
        p.waitForFinished(3000);
        if (p.error() != QProcess::UnknownError) {
            qWarning() << __FUNCTION__ << "query default for "
                       << fileType << "failed!" << p.errorString();
            return false;
        }

        QString desktopFile = p.readAll();
        if (desktopFile.endsWith("\n")) {
            desktopFile.remove("\n");
        }
        return doLaunchApp(desktopFile, QStringList() << fileName);
    } else {
        QString scheme = url_.scheme();
        QString mimeType = "x-scheme-handler/" + scheme;
        qDebug ()<<"---mimeType---" << mimeType;
        p.start("xdg-mime", QStringList() << "query" << "default" << mimeType);
        p.waitForFinished(3000);
        if (p.error() != QProcess::UnknownError) {
            qWarning() << __FUNCTION__ << "query default for "
                       << mimeType << "failed!" << p.errorString();
            return false;
        }
        QString desktopFile = p.readAll();
        if (desktopFile.endsWith("\n")) {
            desktopFile.remove("\n");
        }
        return doLaunchApp(desktopFile, QStringList() << url);
    }
    return true;
}

QString AppLauncher::AppDesktopFileNameByPid(qint64 pid)
{
    return m_appInfoManager.desktopFile((int)pid);
}

QString AppLauncher::AppDesktopFileNameByWid(qint64 wid)
{
    return m_appInfoManager.desktopFile((quint32)wid);
}

QVector<QStringList> AppLauncher::RecommendAppLists(const QString &fileName)
{
    if (fileName.isEmpty()) {
        qWarning() << __FUNCTION__ << fileName << "is NULL!";
        return QVector<QStringList>();
    }
    QUrl url(fileName);
    QFile file(fileName);
    if (url.scheme().isEmpty()) {
        if (file.exists() && url.isRelative()) {
            // fileName为本地文件类型
            return getLaunchAppLists(fileName);
        }
    } else {
        QString localFileName = url.toLocalFile();
        if (QFile(localFileName).exists()) {
            // fileName为url类型
            return getLaunchAppLists(localFileName);
        }
    }
    qWarning() << fileName << "is not exists!";
    return QVector<QStringList>();
}

QVector<QStringList> AppLauncher::getLaunchAppLists(const QString &fileName)
{
    QVector <QStringList> appLists;
    QStringList defaultLists;
    QStringList recommendLists;
    // 获取文件的mime-type
    QMimeDatabase fileDate;
    QMimeType fileMimeType = fileDate.mimeTypeForFile(fileName);
    // 获取默认打开应用
    GAppInfo *appInfo = g_app_info_get_default_for_type(fileMimeType.name().toUtf8().constData(), false);

    if (appInfo == nullptr) {
        qWarning() << "appInfo is NULL, localFilePath not have default launch app!";
        appLists.push_back(QStringList());
    } else {
        const char *defaultAppName = g_app_info_get_id(appInfo);
        defaultLists.append(QString::fromUtf8(defaultAppName));
        appLists.push_back(defaultLists);
    }
    // 获取推荐打开应用列表
    GList *app_infos = g_app_info_get_recommended_for_type(fileMimeType.name().toUtf8().constData());
    GList *l = app_infos;
    if (app_infos == nullptr) {
        qWarning() << "app_infos is NULL, localFilePath not have recommend launch apps!";
        appLists.push_back(QStringList());
    } else {
        while (l != nullptr) {
            auto app_info = static_cast <GAppInfo*>(l->data);
            if (app_info == nullptr) {
                qWarning() << "app_info is nullptr!";
                recommendLists.append("");
                l = l->next;
                continue;
            } else {
                const char *appDesktopName = g_app_info_get_id(app_info);
                if (defaultLists.contains(QString::fromUtf8(appDesktopName))) {
                    l = l->next;
                    continue;
                }
                recommendLists.append(QString::fromUtf8(appDesktopName));
                l = l->next;
            }
            g_object_unref(app_info);
        }
        appLists.push_back(recommendLists);
    }
    g_list_free(l);
    g_list_free(app_infos);
    return appLists;
}

QPair<QString, QStringList> AppLauncher::desktopFileExec(const QString &desktopFileName)
{
    if (desktopFileName.isEmpty()) {
        qWarning() << __FUNCTION__ << "desktopFileName is NULL！！！";
        return QPair<QString, QString>();
    } else {
        QString fullDesktopName = desktopFullFilename(desktopFileName);
        KDesktopFile desktop(fullDesktopName);
        QString exec = desktop.entryMap("Desktop Entry").value("Exec");
        if (exec.isEmpty()) {
            qWarning() << __FUNCTION__ << "Exec is empty!";
            return {QString(), QStringList()};
        }
        
        exec.remove(QRegExp("%."));
        exec.remove(QRegExp("^\""));
        exec.remove(QRegExp(" *$"));
        exec.remove(QRegExp("\""));

        QStringList execArgs = exec.split(" ");
        if (execArgs.count() > 0) {
            exec = execArgs.first();
        }
        return { exec, execArgs };
    }
}

bool AppLauncher::hasWindow(const QString &desktopFile)
{
    QString fullDesktopName = desktopFullFilename(desktopFile);
    QScopedPointer<QSettings> desktop(new QSettings(fullDesktopName, QSettings::IniFormat));
    desktop->setIniCodec(QTextCodec::codecForName("UTF-8"));
    QString keywords = desktop->value("Desktop Entry/Keywords").toString();
    keywords.remove(QRegExp("%."));
    keywords.remove(QRegExp("^\""));
    keywords.remove(QRegExp(" *$"));
    if (keywords.contains("screenshot") || keywords.contains("capture") || keywords.contains("shutter")) {
        return false;
    }

    return true;
}

QString AppLauncher::desktopFullFilename(const QString &desktopFileName)
{
    QFile desktopFile(desktopFileName);
    if (desktopFile.exists()) {
        return desktopFileName;
    }
    QStringList desktopfilePaths = QStandardPaths::standardLocations(QStandardPaths::ApplicationsLocation);
    for (auto &path : qAsConst(desktopfilePaths)) {
        if (desktopFileName.contains(path)) {
            return desktopFileName;
        } else if (QFile::exists(path + "/" + desktopFileName)) {
            return path + "/" + desktopFileName;
        }
    }
    return QString();
}

bool AppLauncher::doLaunchApp(const QString &desktopFile, const QStringList &args)
{
    qDebug() << current_func << desktopFile;
    QString fullDesktopName = desktopFullFilename(desktopFile);
    if (fullDesktopName.isEmpty()) {
        qWarning() << "Launch app failed, the dekstop file is " << desktopFile;
        return false;
    }
    if (!KDesktopFile::isDesktopFile(fullDesktopName)) {
        return false;
    }
    qDebug() << "fullDesktopName" << fullDesktopName;

#if 1 //crashed: corrupted double-linked list. mybe a bug
    QScopedPointer<KDesktopFile> file(new KDesktopFile(fullDesktopName));
    if (!file->hasApplicationType() && !file->tryExec()) {
        return false;
    }
#endif

    // not full path file name
    QString desktopFilename = fullDesktopName.split("/").last();

    bool launchResult = false;
    d.fork = 0;
    QStringList startUpArgs = args;
    QPair<QString, QStringList> execArgs;
    // m_desktopExec保存全路径下的desktop名称
    if (m_appInfoManager.desktopFilesExec().contains(fullDesktopName)) {
        execArgs = m_appInfoManager.desktopFilesExec().value(fullDesktopName);
    } else {
        execArgs = desktopFileExec(desktopFile);
    }
    if (args.isEmpty()) {
        startUpArgs = execArgs.second;
    }
    // 文件打开方式选择kmre应用打开时，只能启动应用，不能打开文件
    if (m_appInfoManager.isKmreApp(desktopFile)) {
        startUpArgs = execArgs.second;
        for (int i=0; i<args.count(); ++i) {
            startUpArgs.push_back(args.at(i));
        }
    }
    // 打开的应用exec包含sh -c的需要将整个exec字段作为一个字符串传入，而不是单个参数传入
    if (execArgs.second.isEmpty()) {
        qWarning() << "ExecArgs.second is Empty";
    }
    if (execArgs.second.count() > 1) {
        if (execArgs.second[0] == ("sh") && execArgs.second[1] == ("-c")) {
            QStringList execList;
            QStringList execStr;
            execList << "sh" << "-c";
            for (int i=2; i<execArgs.second.count(); ++i) {
                execStr << execArgs.second.at(i);
            }
            execStr << args.join(" ");
            execList << execStr.join(" ");
            startUpArgs = execList;
        }
    }
    if (!startUpArgs.startsWith(execArgs.first)) {
        startUpArgs.prepend(execArgs.first);
    }
    QString exec = execArgs.first;
    if (!hasWindow(desktopFile) || kFreeApps.contains(desktopFilename)) {
        return launchDesktopApp(fullDesktopName, args, exec, startUpArgs);
    }

    qDebug() << exec << startUpArgs;
    // kmre应用以app的名称作为唯一标识，比如com.sina.weibo
    if (m_appInfoManager.isKmreApp(desktopFile)) {
        launchResult = launchDesktopApp(fullDesktopName, args, exec, startUpArgs);
        if (startUpArgs.size() < 2) {
            qWarning() << "Kmre app desktop args error, " << startUpArgs;
            return false;
        }
        return launchResult;
    }

    if (!EventWatcher::isTabletMode()) {
        launchResult = launchDesktopApp(fullDesktopName, args, exec, startUpArgs);
    } else {
        appinfo::RunningStatus runningStatus;
        QString instName;
        bool timeout;
        std::tie(runningStatus, instName, timeout) = m_appInfoManager.runningStatus(fullDesktopName, args);
        switch (runningStatus) {
        case appinfo::None: {
            launchResult = launchDesktopApp(fullDesktopName, args, exec, startUpArgs);
            break;
        }
        case appinfo::Launching: {
            if (kNeedingPolkitWindowApps.contains(desktopFilename)) {
                launchResult = activePolkitWindow();
                if (!launchResult) {
                    launchResult = launchDesktopApp(fullDesktopName, args, exec, startUpArgs);
                }
            }
            if (timeout) {
                launchResult = launchDesktopApp(fullDesktopName, args, exec, startUpArgs);
            }
            break;
        }
        case appinfo::Running: {
            if (kNeedingPolkitWindowApps.contains(desktopFilename)) {
                launchResult = activePolkitWindow();
                if (!launchResult) {
                    qWarning() << "active PolkitWindow failed!" << desktopFilename << startUpArgs;
                    launchResult = launchDesktopApp(fullDesktopName, args, exec, startUpArgs);
                    break;
                }
            }
            auto wids = m_appInfoManager.wids(fullDesktopName, instName);
            auto pids = m_appInfoManager.pids(fullDesktopName, instName);
            qDebug() << "---> " << "app is running " << wids << pids;
            if (wids.isEmpty() || pids.isEmpty() || !activeAppWindow(wids.last(), pids)) {
                launchResult = launchDesktopApp(fullDesktopName, args, exec, startUpArgs);
            } else {
                launchResult = true;
            }
            break;
        }
        default: break;
        }
    }

    return launchResult;
}

bool AppLauncher::activeAppWindow(quint32 wid, QList<int> pids)
{
    if (Policy::isWaylandPlatform()) {
        // kysdk的activateWindow接口没有返回值
        kdk::WindowManager::activateWindow(wid);
        return true;
    }

    for (auto &pid : qAsConst(pids)) {
        bool ret = m_kwinInterface->activeWindowByPid(pid);
        qDebug() << "avtive window ret " <<ret;
        if (!ret) {
            return false;
        }
    }
    return true;
}

bool AppLauncher::activePolkitWindow()
{
    if (Policy::isWaylandPlatform()) {
        kdk::WindowManager::activateWindow(m_polkitWindowId);
        return true;
    } else {
        return m_kwinInterface->activeWindowByPid(m_polkitWindowPid);
    }
}

bool AppLauncher::launch(const QString &desktopFile, const QString &execPath, const QStringList &args)
{
    int dummyFd[2];
    if (pipe(d.fd) < 0 || pipe(dummyFd) < 0) {
        qWarning() << "pipe() failed," << strerror(errno);
        d.fork = 0;
        return false;
    }

    {
        if (d.argv == nullptr) {
            d.argv = new char*[args.count() + 1];
            for (int i=0; i<args.count(); ++i) {
                QByteArray *arg = new QByteArray;
                *arg = args.at(i).toLocal8Bit();
                d.argv[i] = arg->data();
            }
            d.argv[args.count()] = nullptr;
        }
    }

    d.fork = fork();
    switch (d.fork) {
    case -1: {
        qWarning() << execPath << "fork failed, " << strerror(errno);
        close(d.fd[0]);
        close(d.fd[1]);
        d.fork = 0;
        return false;
    }
    // child process
    case 0: {
        close(d.fd[0]);
        close(dummyFd[1]);
        char dummy;
        read(dummyFd[0], &dummy, 1);
        close(dummyFd[0]);

        d.result = 2; // Try execing
        write(d.fd[1], &d.result, 1);

        // We set the close on exec flag.
        // Closing of d.fd[1] indicates that the execvp succeeded!
        fcntl(d.fd[1], F_SETFD, FD_CLOEXEC);

        QByteArray executable;
        executable.append(execPath);
        if (!executable.isEmpty()) {
            execvp(executable.constData(), d.argv);
            delete [] d.argv;
            d.argv = nullptr;
        }
        qWarning() << "Launch error" << strerror(errno);
        d.result = 1; // Error
        write(d.fd[1], &d.result, 1);
        close(d.fd[1]);
        exit(255);
        break;
    }

    default: {
        appinfo::AppType type = m_appInfoManager.isKmreApp(desktopFile) ? appinfo::Kmre : appinfo::Normal;
        m_appInfoManager.newAppInstance(desktopFile, args, QList<int>() << d.fork, type);
        close(d.fd[1]);
        close(dummyFd[0]);

        char dummy = 1;
        write(dummyFd[1], &dummy, 1);
        close(dummyFd[1]);

        bool exec = false;
        for (;;) {
            d.n = read(d.fd[0], &d.result, 1);
            if (d.n == 1) {
                if (d.result == 2) {
                    exec = true;
                    continue;
                }
                if (d.result == 3) {
                    int l = 0;
                    d.n = read(d.fd[0], &l, sizeof(int));
                    if (d.n == sizeof(int)) {
                        QByteArray tmp;
                        tmp.resize(l + 1);
                        d.n = read(d.fd[0], tmp.data(), l);
                        tmp[l] = 0;
                        if (d.n == l) {
                            d.errorMsg = tmp;
                        }
                    }
                }
                qWarning() << "process finished" << errno << "str error " << strerror(errno);
                // Finished
                break;
            }
            if (d.n == -1) {
                if (errno == ECHILD) {  // a child died.
                    continue;
                }
                if (errno == EINTR || errno == EAGAIN) { // interrupted or more to read
                    continue;
                }
            }
            if (d.n == 0) {
                if (exec) {
                    d.result = 0;
                } else {
                    perror("Pipe closed unexpectedly");
                    d.result = 1; // Error
                }
                qWarning() << "process finished1" << errno << "str error " << strerror(errno);;
                break;
            }
            qWarning() << "Error reading from pipe";
            d.result = 1; // Error
        }
        close(d.fd[0]);
        break;
    }
    }

    delete [] d.argv;
    d.argv = nullptr;
    return true;
}

bool AppLauncher::launchDesktopApp(const QString &desktopFile,
                                   const QStringList &args,
                                   const QString &execPath,
                                   const QStringList &execArgs)
{
    qDebug() << current_func << desktopFile;
    GDesktopAppInfo *appInfo = g_desktop_app_info_new_from_filename(desktopFile.toUtf8().constData());
    if (!appInfo) {
        qWarning() << __FUNCTION__ << "appInfo is null";
        return false;
    }

    GList *l = nullptr;
    if (!args.isEmpty()) {
        QString filePath = execArgs.join(" ").contains("sh -c") ? execArgs.join(" ") : args.join(" ");
        QUrl url(filePath);
        QFile file(filePath);

        if (url.isValid() && url.scheme().isEmpty()) {
            if (file.exists() && url.isRelative()) {
                QString fileUriPath = "file://" + filePath;
                char *uri = g_strdup(fileUriPath.toUtf8().constData());
                l = g_list_prepend(l, uri);
            } else {
                qDebug() << filePath << "is not localFile, maybe is command line!";
                return launch(desktopFile, execPath, execArgs);
            }
        } else {
            qDebug() << filePath << " is url type !";
            for (auto uri : qAsConst(args)) {
                l = g_list_prepend(l, g_strdup(uri.toUtf8().constData()));
            }
        }
    }
    gboolean ret = g_desktop_app_info_launch_uris_as_manager(appInfo,
                                                             l,
                                                             nullptr,
                                                             G_SPAWN_DEFAULT,
                                                             nullptr,
                                                             nullptr,
                                                             childProcessCallback,
                                                             (gpointer)&execArgs,
                                                             nullptr);
    g_list_free(l);
    if (ret == FALSE) {
        return launch(desktopFile, execPath, execArgs);
    }
    return true;
}

void AppLauncher::childProcessCallback(GDesktopAppInfo *appInfo, GPid pid, gpointer userData)
{
    QString desktopFile = g_desktop_app_info_get_filename(appInfo);
    if (desktopFile.isEmpty() || !desktopFile.endsWith(".desktop")) {
        return;
    }
    QStringList strList = desktopFile.split("/");
    QString desktopFileName = strList.isEmpty() ? desktopFile : strList.constLast();
    qDebug() << "gio launched pid: " << pid;
    QStringList *args = (QStringList *)userData;
    appinfo::AppType type = AppInfoManager::isKmreApp(desktopFile) ? appinfo::Kmre : appinfo::Normal;
    common::Singleton<AppInfoManager>::GetInstance().newAppInstance(desktopFile, *args, QList<int>() << pid, type);
}
