/*
    SPDX-FileCopyrightText: 2025 Fushan Wen <qydwhotmail@gmail.com>

    SPDX-License-Identifier: GPL-2.0-or-later
*/

#include "keysrunner.h"

#include <QDBusConnection>
#include <QDBusMessage>
#include <QDBusMetaType>

#include <algorithm>

#include <KLocalizedString>

#include "filteredmodel.h"
#include "globalaccelmodel.h"

using namespace Qt::StringLiterals;

// Don't show actions that are handled by other runners (e.g. Desktop Sessions / Power)
static const QMap<QString, QStringList> ignoredComponentActions = {
    { u"org_kde_powerdevil"_s /* "Power Managment" */, {
        u"Hibernate"_s,
        u"PowerDown"_s,
        u"PowerOff"_s,
        u"Sleep"_s
    }},
    { u"ksmserver"_s /* Session Management */, {
        u"Lock Session"_s,
        u"LogOut"_s,
        u"Log Out Without Confirmation"_s,
        u"Reboot"_s,
        u"Reboot Without Confirmation"_s,
        u"Shut Down"_s,
        u"Halt Without Confirmation"_s
    }}
};

QDBusArgument &operator<<(QDBusArgument &argument, const QKeySequence &sequence)
{
    argument.beginStructure();
    argument.beginArray(qMetaTypeId<int>());
    for (int i = 0; i < 4; i++) {
        argument << (i < sequence.count() ? sequence[i].toCombined() : 0);
    }
    argument.endArray();
    argument.endStructure();
    return argument;
}

const QDBusArgument &operator>>(const QDBusArgument &argument, QKeySequence &sequence)
{
    int s1;
    int s2;
    int s3;
    int s4;
    argument.beginStructure();
    argument.beginArray();
    argument >> s1 >> s2 >> s3 >> s4;
    sequence = QKeySequence(s1, s2, s3, s4);
    argument.endArray();
    argument.endStructure();
    return argument;
}

KeysRunner::KeysRunner(QObject *parent, const KPluginMetaData &metaData)
    : KRunner::AbstractRunner(parent, metaData)
    , m_isKRunner(KLocalizedString::applicationDomain() == QByteArrayView("krunner"))
    , m_isPlasmashell(KLocalizedString::applicationDomain() == QByteArrayView("plasmashell"))
{
    addSyntax(u":q:"_s, i18nc("@info:usagetip", "Finds shortcuts with names or descriptions matching :q: or gets the name of a shortcut"));
}

void KeysRunner::init()
{
    m_globalAccelModel = new GlobalAccelModel(this);
    m_filteredModel = new FilteredShortcutsModel(this);
    m_filteredModel->setShowsLaunchAction(false);
    m_filteredModel->setSourceModel(m_globalAccelModel);
    if (m_globalAccelModel->isValid()) {
        m_globalAccelModel->load();
        connect(
            m_globalAccelModel,
            &QAbstractItemModel::modelReset,
            this,
            [this] {
                qDBusRegisterMetaType<QKeySequence>();
                qDBusRegisterMetaType<QList<QKeySequence>>();
                QDBusConnection::sessionBus().connect(u"org.kde.kglobalaccel"_s,
                                                      u"/kglobalaccel"_s,
                                                      u"org.kde.KGlobalAccel"_s,
                                                      u"yourShortcutsChanged"_s,
                                                      this,
                                                      SLOT(onShortcutsChanged(QStringList, QList<QKeySequence>)));
                m_needsUpdate = false;
            },
            Qt::SingleShotConnection);
    }

    connect(this, &KRunner::AbstractRunner::prepare, this, &KeysRunner::onPrepare);
}

void KeysRunner::match(KRunner::RunnerContext &context)
{
    m_filteredModel->setFilter(context.query());
    for (int i = 0, count = m_filteredModel->rowCount(); i < count; ++i) {
        const QModelIndex componentIdx = m_filteredModel->index(i, 0);
        Q_ASSERT(componentIdx.isValid() && !componentIdx.parent().isValid());
        const QString componentUniqueName = componentIdx.data(BaseModel::ComponentRole).toString();
        if (m_isKRunner && componentUniqueName == u"org.kde.krunner.desktop") {
            continue;
        } else if (m_isPlasmashell && componentUniqueName == u"plasmashell") {
            continue;
        }

        const QString componentFriendlyName = componentIdx.data(Qt::DisplayRole).toString();
        const QString iconName = componentIdx.data(Qt::DecorationRole).toString();

        for (int j = 0, childCount = m_filteredModel->rowCount(componentIdx); j < childCount; ++j) {
            const QModelIndex actionIdx = m_filteredModel->index(j, 0, componentIdx);
            const QString actionName = actionIdx.data(Qt::DisplayRole).toString();
            const QString action = actionIdx.data(BaseModel::ActionRole).toString();
            if (actionName.isEmpty()) {
                continue;
            }

            auto it = ignoredComponentActions.constFind(componentUniqueName);
            if (it != ignoredComponentActions.cend() && it->contains(action)) {
                continue;
            };

            KRunner::QueryMatch match(this);
            match.setText(i18nc("@label action - app", "%1 - %2", actionName, componentFriendlyName));

            auto shortcuts = actionIdx.data(BaseModel::ActiveShortcutsRole).value<QSet<QKeySequence>>();
            if (shortcuts.empty()) {
                shortcuts = actionIdx.data(BaseModel::DefaultShortcutsRole).value<QSet<QKeySequence>>();
            }
            if (shortcuts.empty()) {
                shortcuts = actionIdx.data(BaseModel::CustomShortcutsRole).value<QSet<QKeySequence>>();
            }
            if (!shortcuts.empty()) {
                QString subText = shortcuts.cbegin()->toString(QKeySequence::NativeText);
                subText.reserve(shortcuts.size() * 5);
                for (auto it = std::next(shortcuts.cbegin()); it != shortcuts.cend(); it = std::next(it)) {
                    subText += u", " + it->toString(QKeySequence::NativeText);
                }
                match.setSubtext(subText);
            }

            match.setIconName(iconName);
            match.setData(QStringList{componentUniqueName, action});
            context.addMatch(match);
        }
    }
}

void KeysRunner::run(const KRunner::RunnerContext & /*context*/, const KRunner::QueryMatch &match)
{
    const QStringList data = match.data().toStringList();
    Q_ASSERT(data.length() == 2);
    QString dbusPath = data[0];
    // DBus path can only contain ASCII characters, any non-alphanumeric char should
    // be turned into '_'
    auto isNonAscii = [](QChar ch) {
        const char c = ch.unicode();
        const bool isAscii = c == '_' //
            || (c >= 'A' && c <= 'Z') //
            || (c >= 'a' && c <= 'z') //
            || (c >= '0' && c <= '9');
        return !isAscii;
    };
    std::replace_if(dbusPath.begin(), dbusPath.end(), isNonAscii, QLatin1Char('_'));
    QDBusMessage msg =
        QDBusMessage::createMethodCall(u"org.kde.kglobalaccel"_s, u"/component/" + dbusPath, u"org.kde.kglobalaccel.Component"_s, u"invokeShortcut"_s);
    msg << data[1];
    QDBusConnection::sessionBus().call(msg, QDBus::NoBlock);
}

void KeysRunner::onShortcutsChanged(const QStringList &, const QList<QKeySequence> &)
{
    m_needsUpdate = true;
}

void KeysRunner::onPrepare()
{
    if (!m_needsUpdate) {
        return;
    }
    connect(
        m_globalAccelModel,
        &QAbstractItemModel::modelReset,
        this,
        [this] {
            m_needsUpdate = false;
        },
        Qt::SingleShotConnection);
    m_globalAccelModel->load();
}

K_PLUGIN_CLASS_WITH_JSON(KeysRunner, "plasma-runner-keys.json")

#include "keysrunner.moc"
#include "moc_keysrunner.cpp"
