/*
    This file is part of the KDE libraries

    SPDX-FileCopyrightText: 2007 Andreas Hartmetz <ahartmetz@gmail.com>
    SPDX-FileCopyrightText: 2007 Michael Jansen <kde@michael-jansen.biz>

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

#include "kglobalacceld.h"

#include "component.h"
#include "globalshortcut.h"
#include "globalshortcutcontext.h"
#include "globalshortcutsregistry.h"
#include "kglobalaccel.h"
#include "kserviceactioncomponent.h"
#include "logging.h"

#include <QDBusConnection>
#include <QDBusMetaType>
#include <QMetaMethod>
#include <QTimer>

QDBusArgument &operator<<(QDBusArgument &argument, const QSet<QKeySequence> &sequences)
{
    argument.beginArray(QMetaType::fromType<QKeySequence>());
    for (const QKeySequence &sequence : sequences) {
        argument << sequence;
    }
    argument.endArray();
    return argument;
}

const QDBusArgument &operator>>(const QDBusArgument &argument, QSet<QKeySequence> &sequences)
{
    sequences.clear();
    argument.beginArray();
    while (!argument.atEnd()) {
        QKeySequence sequence;
        argument >> sequence;
        sequences.insert(sequence);
    }
    argument.endArray();
    return argument;
}

struct KGlobalAccelDPrivate {
    KGlobalAccelDPrivate(KGlobalAccelD *qq)
        : q(qq)
    {
    }

    GlobalShortcut *findAction(const QStringList &actionId) const;

    /**
     * Find the action @a shortcutUnique in @a componentUnique.
     *
     * @return the action or @c nullptr if doesn't exist
     */
    GlobalShortcut *findAction(const QString &componentUnique, const QString &shortcutUnique) const;

    GlobalShortcut *addAction(const QStringList &actionId);

    void splitComponent(QString &component, QString &context) const
    {
        context = QStringLiteral("default");
        const int index = component.indexOf(QLatin1Char('|'));
        if (index != -1) {
            Q_ASSERT(component.indexOf(QLatin1Char('|'), index + 1) == -1); // Only one '|' character
            context = component.mid(index + 1);
            component.truncate(index);
        }
    }

    //! Timer for delayed writing to kglobalshortcutsrc
    QTimer writeoutTimer;

    //! Our holder
    KGlobalAccelD *q;

    std::unique_ptr<GlobalShortcutsRegistry> m_registry = nullptr;
};

GlobalShortcut *KGlobalAccelDPrivate::findAction(const QStringList &actionId) const
{
    // Check if actionId is valid
    if (actionId.size() != 4) {
        qCDebug(KGLOBALACCELD) << "Invalid! '" << actionId << "'";
        return nullptr;
    }

    return findAction(actionId.at(KGlobalAccel::ComponentUnique), actionId.at(KGlobalAccel::ActionUnique));
}

GlobalShortcut *KGlobalAccelDPrivate::findAction(const QString &_componentUnique, const QString &shortcutUnique) const
{
    QString componentUnique = _componentUnique;

    Component *component;
    QString contextUnique;
    if (componentUnique.indexOf(QLatin1Char('|')) == -1) {
        component = m_registry->getComponent(componentUnique);
        if (component) {
            contextUnique = component->currentContext()->uniqueName();
        }
    } else {
        splitComponent(componentUnique, contextUnique);
        component = m_registry->getComponent(componentUnique);
    }

    if (!component) {
        qCDebug(KGLOBALACCELD) << componentUnique << "not found";
        return nullptr;
    }

    GlobalShortcut *shortcut = component->getShortcutByName(shortcutUnique, contextUnique);

    if (shortcut) {
        qCDebug(KGLOBALACCELD) << componentUnique << contextUnique << shortcut->uniqueName();
    } else {
        qCDebug(KGLOBALACCELD) << "No match for" << shortcutUnique;
    }
    return shortcut;
}

KGlobalAccelInterface * ::KGlobalAccelD::interface() const
{
    return d->m_registry->interface();
}

GlobalShortcut *KGlobalAccelDPrivate::addAction(const QStringList &actionId)
{
    Q_ASSERT(actionId.size() >= 4);

    QString componentUnique = actionId.at(KGlobalAccel::ComponentUnique);
    QString contextUnique;
    splitComponent(componentUnique, contextUnique);

    Component *component = m_registry->getOrCreateComponent(componentUnique, actionId.at(KGlobalAccel::ComponentFriendly));
    if (!component) {
        return nullptr;
    }

    // Create the context if necessary
    if (component->getShortcutContexts().count(contextUnique) == 0) {
        component->createGlobalShortcutContext(contextUnique);
    }

    Q_ASSERT(!component->getShortcutByName(componentUnique, contextUnique));

    return new GlobalShortcut(actionId.at(KGlobalAccel::ActionUnique),
                              actionId.at(KGlobalAccel::ActionFriendly),
                              m_registry->nextSerial(),
                              component->shortcutContext(contextUnique),
                              m_registry.get());
}

Q_DECLARE_METATYPE(QStringList)

KGlobalAccelD::KGlobalAccelD(QObject *parent)
    : QObject(parent)
    , d(new KGlobalAccelDPrivate(this))
{
}

bool KGlobalAccelD::init()
{
    qDBusRegisterMetaType<QKeySequence>();
    qDBusRegisterMetaType<QList<QKeySequence>>();
    qDBusRegisterMetaType<QSet<QKeySequence>>();
    qDBusRegisterMetaType<QList<QDBusObjectPath>>();
    qDBusRegisterMetaType<QList<QStringList>>();
    qDBusRegisterMetaType<QStringList>();
    qDBusRegisterMetaType<KGlobalShortcutInfo>();
    qDBusRegisterMetaType<QList<KGlobalShortcutInfo>>();
    qDBusRegisterMetaType<KGlobalAccel::MatchType>();

    d->m_registry = std::make_unique<GlobalShortcutsRegistry>();
    Q_ASSERT(d->m_registry);
    connect(d->m_registry.get(), &GlobalShortcutsRegistry::needsSave, this, &KGlobalAccelD::scheduleWriteSettings);

    d->writeoutTimer.setSingleShot(true);
    connect(&d->writeoutTimer, &QTimer::timeout, d->m_registry.get(), &GlobalShortcutsRegistry::writeSettings);

    d->m_registry->setDBusPath(QDBusObjectPath("/"));
    d->m_registry->loadSettings();

    if (!QDBusConnection::sessionBus().registerObject(QStringLiteral("/kglobalaccel"), this, QDBusConnection::ExportScriptableContents)) {
        qCWarning(KGLOBALACCELD) << "Failed to register object kglobalaccel in org.kde.kglobalaccel";
        return false;
    }

    if (!QDBusConnection::sessionBus().registerService(QLatin1String("org.kde.kglobalaccel"))) {
        qCWarning(KGLOBALACCELD) << "Failed to register service org.kde.kglobalaccel";
        return false;
    }

    return true;
}

KGlobalAccelD::~KGlobalAccelD()
{
    if (d->writeoutTimer.isActive()) {
        d->writeoutTimer.stop();
        d->m_registry->writeSettings();
    }
    d->m_registry->deactivateShortcuts();
    delete d;
}

QList<QStringList> KGlobalAccelD::allMainComponents() const
{
    return d->m_registry->allComponentNames();
}

QList<QStringList> KGlobalAccelD::allActionsForComponent(const QStringList &actionId) const
{
    // ### Would it be advantageous to sort the actions by unique name?
    QList<QStringList> ret;

    Component *const component = d->m_registry->getComponent(actionId[KGlobalAccel::ComponentUnique]);
    if (!component) {
        return ret;
    }

    QStringList partialId(actionId[KGlobalAccel::ComponentUnique]); // ComponentUnique
    partialId.append(QString()); // ActionUnique
    // Use our internal friendlyName, not the one passed in. We should have the latest data.
    partialId.append(component->friendlyName()); // ComponentFriendly
    partialId.append(QString()); // ActionFriendly

    const auto listShortcuts = component->allShortcuts();
    for (const GlobalShortcut *const shortcut : listShortcuts) {
        if (shortcut->isFresh()) {
            // isFresh is only an intermediate state, not to be reported outside.
            continue;
        }
        QStringList actionId(partialId);
        actionId[KGlobalAccel::ActionUnique] = shortcut->uniqueName();
        actionId[KGlobalAccel::ActionFriendly] = shortcut->friendlyName();
        ret.append(actionId);
    }
    return ret;
}

#if KGLOBALACCELD_BUILD_DEPRECATED_SINCE(5, 90)
QStringList KGlobalAccelD::action(int key) const
{
    return actionList(key);
}
#endif

QStringList KGlobalAccelD::actionList(const QKeySequence &key) const
{
    QList<GlobalShortcut *> shortcuts = d->m_registry->getShortcutsByKey(key);
    if (shortcuts.isEmpty()) {
        return {};
    }

    std::sort(shortcuts.begin(), shortcuts.end(), [](const auto &a, const auto &b) {
        return a->serial() < b->serial();
    });

    return {
        shortcuts[0]->context()->component()->uniqueName(),
        shortcuts[0]->uniqueName(),
        shortcuts[0]->context()->component()->friendlyName(),
        shortcuts[0]->friendlyName(),
    };
}

void KGlobalAccelD::activateGlobalShortcutContext(const QString &component, const QString &uniqueName)
{
    Component *const comp = d->m_registry->getComponent(component);
    if (comp) {
        comp->activateGlobalShortcutContext(uniqueName);
    }
}

QList<QDBusObjectPath> KGlobalAccelD::allComponents() const
{
    return d->m_registry->componentsDbusPaths();
}

void KGlobalAccelD::blockGlobalShortcuts(bool block)
{
    qCDebug(KGLOBALACCELD) << "Block global shortcuts?" << block;
    if (block) {
        d->m_registry->deactivateShortcuts(true);
    } else {
        d->m_registry->activateShortcuts();
    }
}

#if KGLOBALACCELD_BUILD_DEPRECATED_SINCE(5, 90)
QList<int> KGlobalAccelD::shortcut(const QStringList &action) const
{
    GlobalShortcut *shortcut = d->findAction(action);
    if (shortcut) {
        QList<int> ret;
        for (auto i : shortcut->keys()) {
            ret << i[0].toCombined();
        }
        return ret;
    }
    return QList<int>();
}
#endif

QSet<QKeySequence> KGlobalAccelD::shortcutKeys(const QStringList &action) const
{
    GlobalShortcut *shortcut = d->findAction(action);
    if (shortcut) {
        return shortcut->keys();
    }
    return QSet<QKeySequence>();
}

#if KGLOBALACCELD_BUILD_DEPRECATED_SINCE(5, 90)
QList<int> KGlobalAccelD::defaultShortcut(const QStringList &action) const
{
    GlobalShortcut *shortcut = d->findAction(action);
    if (shortcut) {
        QList<int> ret;
        for (auto i : shortcut->keys()) {
            ret << i[0].toCombined();
        }
        return ret;
    }
    return QList<int>();
}
#endif

QSet<QKeySequence> KGlobalAccelD::defaultShortcutKeys(const QStringList &action) const
{
    GlobalShortcut *shortcut = d->findAction(action);
    if (shortcut) {
        return shortcut->defaultKeys();
    }
    return QSet<QKeySequence>();
}

// This method just registers the action. Nothing else. Shortcut has to be set
// later.
void KGlobalAccelD::doRegister(const QStringList &actionId)
{
    qCDebug(KGLOBALACCELD) << actionId;

    // Check because we would not want to add a action for an invalid
    // actionId. findAction returns nullptr in that case.
    if (actionId.size() < 4) {
        return;
    }

    GlobalShortcut *shortcut = d->findAction(actionId);
    if (!shortcut) {
        shortcut = d->addAction(actionId);
    } else {
        // a switch of locales is one common reason for a changing friendlyName
        if ((!actionId[KGlobalAccel::ActionFriendly].isEmpty()) && shortcut->friendlyName() != actionId[KGlobalAccel::ActionFriendly]) {
            shortcut->setFriendlyName(actionId[KGlobalAccel::ActionFriendly]);
            scheduleWriteSettings();
        }
        if ((!actionId[KGlobalAccel::ComponentFriendly].isEmpty())
            && shortcut->context()->component()->friendlyName() != actionId[KGlobalAccel::ComponentFriendly]) {
            shortcut->context()->component()->setFriendlyName(actionId[KGlobalAccel::ComponentFriendly]);
            scheduleWriteSettings();
        }
    }
}

QDBusObjectPath KGlobalAccelD::getComponent(const QString &componentUnique) const
{
    qCDebug(KGLOBALACCELD) << componentUnique;

    Component *component = d->m_registry->getComponent(componentUnique);

    if (component) {
        return component->dbusPath();
    } else {
        sendErrorReply(QStringLiteral("org.kde.kglobalaccel.NoSuchComponent"), QStringLiteral("The component '%1' doesn't exist.").arg(componentUnique));
        return QDBusObjectPath("/");
    }
}

#if KGLOBALACCELD_BUILD_DEPRECATED_SINCE(5, 90)
QList<KGlobalShortcutInfo> KGlobalAccelD::getGlobalShortcutsByKey(int key) const
{
    return globalShortcutsByKey(key, KGlobalAccel::MatchType::Equal);
}
#endif

QList<KGlobalShortcutInfo> KGlobalAccelD::globalShortcutsByKey(const QKeySequence &key, KGlobalAccel::MatchType type) const
{
    qCDebug(KGLOBALACCELD) << key;
    const QList<GlobalShortcut *> shortcuts = d->m_registry->getShortcutsByKey(key, type);

    QList<KGlobalShortcutInfo> rc;
    rc.reserve(shortcuts.size());
    for (const GlobalShortcut *sc : shortcuts) {
        qCDebug(KGLOBALACCELD) << sc->context()->uniqueName() << sc->uniqueName();
        rc.append(static_cast<KGlobalShortcutInfo>(*sc));
    }

    return rc;
}

#if KGLOBALACCELD_BUILD_DEPRECATED_SINCE(5, 90)
bool KGlobalAccelD::isGlobalShortcutAvailable(int shortcut, const QString &component) const
{
    return globalShortcutAvailable(shortcut, component);
}
#endif

bool KGlobalAccelD::globalShortcutAvailable(const QKeySequence &shortcut, const QString &component) const
{
    QString realComponent = component;
    QString context;
    d->splitComponent(realComponent, context);
    return d->m_registry->isShortcutAvailable(shortcut, realComponent, context);
}

void KGlobalAccelD::setInactive(const QStringList &actionId)
{
    qCDebug(KGLOBALACCELD) << actionId;

    GlobalShortcut *shortcut = d->findAction(actionId);
    if (shortcut) {
        shortcut->setIsPresent(false);
    }
}

bool KGlobalAccelD::unregister(const QString &componentUnique, const QString &shortcutUnique)
{
    qCDebug(KGLOBALACCELD) << componentUnique << shortcutUnique;

    // Stop grabbing the key
    GlobalShortcut *shortcut = d->findAction(componentUnique, shortcutUnique);
    if (shortcut) {
        shortcut->unRegister();
        scheduleWriteSettings();
    }

    return shortcut;
}

#if KGLOBALACCELD_BUILD_DEPRECATED_SINCE(4, 3)
void KGlobalAccelD::unRegister(const QStringList &actionId)
{
    qCDebug(KGLOBALACCELD) << actionId;

    // Stop grabbing the key
    GlobalShortcut *shortcut = d->findAction(actionId);
    if (shortcut) {
        shortcut->unRegister();
        scheduleWriteSettings();
    }
}
#endif

#if KGLOBALACCELD_BUILD_DEPRECATED_SINCE(5, 90)
QList<int> KGlobalAccelD::setShortcut(const QStringList &actionId, const QList<int> &keys, uint flags)
{
    QSet<QKeySequence> input;
    input.reserve(keys.size());
    for (auto i : keys) {
        input << i;
    }

    const QSet<QKeySequence> activeKeys = setShortcutKeys(actionId, input, flags);
    QList<int> ret;
    ret.reserve(activeKeys.size());
    for (auto i : activeKeys) {
        ret << i[0].toCombined();
    }
    return ret;
}
#endif

QSet<QKeySequence> KGlobalAccelD::setShortcutKeys(const QStringList &actionId, const QSet<QKeySequence> &keys, uint flags)
{
    // spare the DBus framework some work
    const bool setPresent = (flags & SetPresent);
    const bool isAutoloading = !(flags & NoAutoloading);
    const bool isDefault = (flags & IsDefault);

    GlobalShortcut *shortcut = d->findAction(actionId);
    if (!shortcut) {
        return QSet<QKeySequence>();
    }

    // default shortcuts cannot clash because they don't do anything
    if (isDefault) {
        if (shortcut->defaultKeys() != keys) {
            shortcut->setDefaultKeys(keys);
            scheduleWriteSettings();
        }
        return keys; // doesn't matter
    }

    if (isAutoloading && !shortcut->isFresh()) {
        // the trivial and common case - synchronize the action from our data
        // and exit.
        if (!shortcut->isPresent() && setPresent) {
            shortcut->setIsPresent(true);
        }
        // We are finished here. Return the list of current active keys.
        return shortcut->keys();
    }

    // now we are actually changing the shortcut of the action
    shortcut->setKeys(keys);

    if (setPresent) {
        shortcut->setIsPresent(true);
    }

    // maybe isFresh should really only be set if setPresent, but only two things should use !setPresent:
    //- the global shortcuts KCM: very unlikely to catch KWin/etc.'s actions in isFresh state
    //- KGlobalAccel::stealGlobalShortcutSystemwide(): only applies to actions with shortcuts
    //  which can never be fresh if created the usual way
    shortcut->setIsFresh(false);

    scheduleWriteSettings();

    return shortcut->keys();
}

#if KGLOBALACCELD_BUILD_DEPRECATED_SINCE(5, 90)
void KGlobalAccelD::setForeignShortcut(const QStringList &actionId, const QList<int> &keys)
{
    QSet<QKeySequence> input;
    for (auto i : keys) {
        input << i;
    }
    return setForeignShortcutKeys(actionId, input);
}
#endif

void KGlobalAccelD::setForeignShortcutKeys(const QStringList &actionId, const QSet<QKeySequence> &keys)
{
    qCDebug(KGLOBALACCELD) << actionId;

    GlobalShortcut *shortcut = d->findAction(actionId);
    if (!shortcut) {
        return;
    }

    QSet<QKeySequence> newKeys = setShortcutKeys(actionId, keys, NoAutoloading);

    Q_EMIT yourShortcutsChanged(actionId, newKeys);
}

void KGlobalAccelD::scheduleWriteSettings() const
{
    if (!d->writeoutTimer.isActive()) {
        d->writeoutTimer.start(500);
    }
}

#include "moc_kglobalacceld.cpp"
