/*
 * 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 "appstatusmanager.h"
#include <QDebug>
#include "appinfomanager.h"
#include "eventwatcher.h"
#include <unistd.h>

AppStatusManager::AppStatusManager(AppInfoManager *parent)
    : QObject(parent)
    , m_timerWheel(new timerwheel::TimerWheel(this))
    , m_appInfoManager(parent)
    , m_appCGroup(new AppCGroup(this))
    , m_policy(new Policy(this))
{
    initConnections();
}

QString AppStatusManager::newAppInstance(const QString &appId, const QList<int> &pids)
{
    return m_appCGroup->createProcessCGroup(appId, pids);
}

bool AppStatusManager::deleteAppCGroup(const QString &cgroupName)
{
    bool ret = m_appCGroup->deleteProcessCGroup(cgroupName);
    if (ret) {
        m_cgroupInfo.remove(cgroupName);
        auto it = m_appInfoManager->appInfos.constBegin();
        while (it != m_appInfoManager->appInfos.constEnd()) {
            auto instances = it.value().instances();
            for (auto const &instance : qAsConst(instances)) {
                if (instance.cgroupName == cgroupName) {
                    m_timerWheel->deleteTimer(instance.timerId);
                    m_appInfoManager->appInfos[it.key()].setInstanceTimerId(cgroupName, 0);
                    break;
                }
            }
            ++ it;
        }
    }
    return ret;
}

void AppStatusManager::appFinished(const QString &appId, const QString &instName)
{
    QString cgroupName = instName;
    m_timerWheel->deleteTimer(m_appInfoManager->appInfos[appId].instanceTimerId(instName));
    if (deleteAppCGroup(cgroupName)) {
        m_appInfoManager->appInfos[appId].removeAppInstance(instName);
    }
}

void AppStatusManager::activeAppChanged(const QString &currentAppId,
                                        const QString &currentInstName,
                                        const QString &preAppId,
                                        const QString &preInstName,
                                        bool preAppMinimized)
{
    if (m_appInfoManager->appInfos.contains(currentAppId)) {
        auto &currentAppInfo = m_appInfoManager->appInfos[currentAppId];
        m_timerWheel->deleteTimer(currentAppInfo.instanceTimerId(currentInstName));
        auto currentAppStatus = currentAppInfo.instanceStatus(currentInstName);
        if (currentAppStatus == m_policy->suspendStatus() || currentAppStatus == m_policy->frozenStatus()) {
            setProcessResourceValue(currentAppId, currentAppInfo.desktopFile(), currentInstName, CGROUP_FREEZE, 0);
        }

        if (m_cgroupInfo.contains(currentInstName)) {
            auto values = m_cgroupInfo.value(currentInstName);
            auto it = values.constBegin();
            while (it != values.constEnd()) {
                int defaultValue = m_policy->defaultCGroupVale(it.key());
                if (it.value() != defaultValue) {
                    setProcessResourceValue(currentAppId,
                                            currentAppInfo.desktopFile(),
                                            currentInstName,
                                            it.key(), defaultValue);
                    m_cgroupInfo[currentInstName][it.key()] = defaultValue;
                }
                ++ it;
            }
        }
        currentAppInfo.setInstanceStatus(currentInstName, m_policy->appStatus(true, false));
    }

    if (m_appInfoManager->appInfos.contains(preAppId)) {
        bool isTabletMode = EventWatcher::isTabletMode();
        auto &preAppInfo = m_appInfoManager->appInfos[preAppId];
        auto preAppStatus = m_policy->appStatus(false, preAppMinimized);
        preAppInfo.setInstanceStatus(preInstName, preAppStatus);
        timerwheel::AppData appData { preAppId, preInstName, m_policy->nextAppStatus(preAppStatus) };
        auto timerId = m_timerWheel->addTimer(m_policy->statusSwitchInterval(), appData,
                                              std::bind(&AppStatusManager::appStatusTimeOutCallback,
                                                        this, std::placeholders::_1));
        preAppInfo.setInstanceTimerId(preInstName, timerId);
        if (!isTabletMode || preAppInfo.mprisDbus(preInstName).isEmpty()) {
            return;
        }
        callMprisDbus(preAppInfo.mprisDbus(preInstName));
    }
}

void AppStatusManager::appWidsIsEmpty(const QString &appId, const QString &instance)
{
    QString cgroupName = instance;
    m_timerWheel->deleteTimer(m_appInfoManager->appInfos[appId].instanceTimerId(instance));
    // 删除失败时说明只是窗口退出了，进程并没有退出
    if (deleteAppCGroup(cgroupName)) {
        m_appInfoManager->appInfos[appId].removeAppInstance(instance);
    }
}

void AppStatusManager::appStatusTimeOutCallback(timerwheel::AppData appData)
{
    qDebug() << current_func << appData.appId << appData.dstStatus << appData.instName;
    if (!m_appInfoManager->appInfos.contains(appData.appId)) {
        qWarning() << current_func <<  "Does not contain the app info, app id " << appData.appId;
        return;
    }

    auto &appInfo = m_appInfoManager->appInfos[appData.appId];
    bool isTabletMode = EventWatcher::isTabletMode();
    appInfo.setInstanceStatus(appData.instName, appData.dstStatus);
    if (isTabletMode && appData.dstStatus == m_policy->frozenStatus()) {
        setProcessResourceValue(appData.appId, appInfo.desktopFile(), appData.instName, CGROUP_FREEZE, 1);
    }

    if (appData.dstStatus != m_policy->lastStatus()) {
        timerwheel::AppData nextAppData { appData.appId, appData.instName, m_policy->nextAppStatus(appData.dstStatus) };
        auto timerId = m_timerWheel->addTimer(m_policy->statusSwitchInterval(), nextAppData,
                                              std::bind(&AppStatusManager::appStatusTimeOutCallback,
                                                        this, std::placeholders::_1));
        appInfo.setInstanceTimerId(appData.instName, timerId);
    }
}

void AppStatusManager::tabletModeChanged(bool isTablet)
{
    int freeze = isTablet ? 1 : 0;
    for (auto &appInfo : m_appInfoManager->appInfos) {
        auto instances = appInfo.instances();
        for (auto const &instance : qAsConst(instances)) {
            if (instance.appStatus == m_policy->frozenStatus()) {
                setProcessResourceValue(appInfo.appId(), appInfo.desktopFile(),
                                        instance.cgroupName, CGROUP_FREEZE, freeze);
            }
        }
    }
}

QString AppStatusManager::cgroupWithPid(int pid)
{
    return m_appCGroup->cgroupNameWithPid(pid);
}

bool AppStatusManager::thawApp(const QString &appId, const QString &cgroupName)
{
    if (!m_appInfoManager->appInfos.contains(appId)) {
        qWarning() << current_func << "appInfos dose not contains " << appId << cgroupName;
        return false;
    }
    QString desktopFile = m_appInfoManager->appInfos.value(appId).desktopFile();
    QStringList strList = desktopFile.split("/");
    QString desktop = strList.isEmpty() ? desktopFile : strList.constLast();
    if (m_whiteListManager.isExists(desktop, "AppManager")) {
        return true;
    }
    return setProcessResourceValue(appId, desktopFile, cgroupName, CGROUP_FREEZE, 0);
}

void AppStatusManager::aboutToQuitApp()
{
    qWarning() << "kylin-app-manager about to quit.";
    bool isTabletMode = EventWatcher::isTabletMode();
    auto it = m_appInfoManager->appInfos.constBegin();
    while (it != m_appInfoManager->appInfos.constEnd()) {
        for (auto &instance : it.value().instances()) {
            if (instance.appStatus == Policy::CachedAppAdj && isTabletMode) {
                setProcessResourceValue(it->appId(), it->desktopFile(),
                                        instance.cgroupName,
                                        CGROUP_FREEZE, 0);
            }
        }
        ++ it;
    }
}

void AppStatusManager::initConnections()
{
    connect(&m_whiteListManager, &WhiteListManager::addedToWhitelist,
            this, &AppStatusManager::updateWhitelistAppStatus);
}

bool AppStatusManager::ThawApps()
{
    qWarning() << "shut down and unfreeze all apps";
    auto it = m_appInfoManager->appInfos.constBegin();
    while (it != m_appInfoManager->appInfos.constEnd()) {
        for (auto &instance : it.value().instances()) {
            if (instance.appStatus == Policy::CachedAppAdj || instance.appStatus == Policy::SuspendedAppAdj) {
                setProcessResourceValue(it->appId(), it->desktopFile(),
                                        instance.cgroupName,
                                        CGROUP_FREEZE, 0);
            }
        }
        ++ it;
    }
    return true;
}

void AppStatusManager::updateWhitelistAppStatus(QString desktopName)
{
    if (desktopName.isEmpty()) {
        qWarning() << current_func << "desktopName is empty!";
        return;
    }
    QString fullDesktopFile = m_appInfoManager->desktopFullFilename(desktopName);
    QString appId = m_appInfoManager->appIdByDesktopFile(fullDesktopFile);
    if (!m_appInfoManager->appInfos.contains(appId)) {
        qWarning() << current_func << "appInfos is not contains " << desktopName;
        return;
    }
    auto const &appInfo = m_appInfoManager->appInfos.value(appId);
    auto instances = appInfo.instances();
    for (auto const &instance : qAsConst(instances)) {
        setProcessResourceValue(appId, appInfo.desktopFile(), instance.cgroupName, CGROUP_FREEZE, m_policy->defaultCGroupVale(CGROUP_FREEZE));
        setProcessResourceValue(appId, appInfo.desktopFile(), instance.cgroupName, CPU_WEIGHT, m_policy->defaultCGroupVale(CPU_WEIGHT));
        setProcessResourceValue(appId, appInfo.desktopFile(), instance.cgroupName, IO_WEIGHT, m_policy->defaultCGroupVale(IO_WEIGHT));
    }
}

void AppStatusManager::resourceThresholdWarning(Policy::Feature resource, Policy::ResourceUrgency level)
{
    auto actions = m_policy->actions(resource, level);
    qDebug() << "resourceThresholdWarning1" << resource << level << actions;
    if (actions.isEmpty()) {
        qWarning() << "Cat not find any actions when the system resource warning.";
        return;
    }

    for (auto &action : qAsConst(actions)) {
        if (!action.contains("adjs")) {
            qWarning() << current_func << "Get action error when resource threshold warning." << actions;
            continue;
        }

        Policy::AppStatus appStatus = static_cast<Policy::AppStatus>(action.value("adjs"));
        for (auto &appInfo : m_appInfoManager->appInfos) {
            auto cgroups = appInfo.cgroupNames(appStatus);
            if (cgroups.isEmpty()) {
                continue;
            }
            auto cgroupValues = action;
            for (auto &cgroup : qAsConst(cgroups)) {
                auto it = cgroupValues.constBegin();
                while (it != cgroupValues.constEnd()) {
                    if (it.key() == "adjs") {
                        ++ it;
                        continue;
                    }
                    qDebug() << cgroup << it.key() << it.value();
                    bool ret = setProcessResourceValue(appInfo.appId(), appInfo.desktopFile(), cgroup,
                                                       it.key(), it.value());
                    if (ret) {
                        m_cgroupInfo[cgroup].insert(it.key(), it.value());
                    }
                    if (resource == Policy::Memory) {
                        appInfo.setInstanceStatus(cgroup, m_policy->suspendStatus());
                        m_appCGroup->reclaimProcesses(cgroup);
                    }
                    ++ it;
                }
            }
        }
    }
}

void AppStatusManager::callMprisDbus(const QString &dbusService)
{
    qDebug() << current_func << dbusService;
    if (!QDBusConnection::sessionBus().isConnected()) {
        qWarning() << current_func << "Cannot connect to the D-Bus session bus.";
        return;
    }

    QDBusInterface mprisDbus(dbusService,
                             "/org/mpris/MediaPlayer2",
                             "org.mpris.MediaPlayer2.Player",
                             QDBusConnection::sessionBus());
    if (!mprisDbus.isValid()) {
       qWarning() << current_func << "mprisDbus is not valid!" << dbusService;
       return;
    }

    auto tryPauseReply = mprisDbus.asyncCall("Pause");
    while (!tryPauseReply.isFinished()) {
        qApp->processEvents();
        usleep(10);
    }

    if (!tryPauseReply.isError()) {
        return;
    }

    auto reply = mprisDbus.asyncCall("PlayState");
    while (!reply.isFinished()) {
        qApp->processEvents();
        usleep(10);
    }
    if (reply.isError()) {
        qWarning() << "Call mpris dbus " << dbusService << " error, " << reply.error();
        return;
    }

    if (reply.reply().arguments().isEmpty()) {
        qWarning() << "Mpris dbus return value is empty, " << dbusService;
        return;
    }
    qDebug() << reply.reply().arguments().first().toInt();
    if (reply.reply().arguments().first().toInt()) {
        mprisDbus.asyncCall("PlayPause");
    }
}

bool AppStatusManager::setProcessResourceValue(const QString &appId,
                                               const QString &desktopFile,
                                               const QString &cgroupName,
                                               const QString &attrName,
                                               int value)
{
    qDebug() << current_func << desktopFile << cgroupName;
    if (m_appInfoManager->isInhibited(appId, cgroupName) &&
            attrName == CGROUP_FREEZE && value == 1) {
        return false;
    }
    QStringList strList = desktopFile.split("/");
    QString desktop = strList.isEmpty() ? desktopFile : strList.constLast();
    if (m_whiteListManager.isExists(desktop, "AppManager")) {
        return false;
    }
    m_appCGroup->setProcessCGroupResourceLimit(cgroupName, attrName, value);
    return true;
}
