// SPDX-FileCopyrightText: 2024 - 2026 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: GPL-3.0-or-later

#include "fcitx5configproxy.h"
#include "private/fcitx5configproxy_p.h"

#include <dbusprovider.h>
#include <varianthelper.h>

#include <QDBusPendingCallWatcher>
#include <QLoggingCategory>

Q_LOGGING_CATEGORY(proxy, "fcitx5.configtool.proxy")

using namespace deepin::fcitx5configtool;

Fcitx5ConfigProxyPrivate::Fcitx5ConfigProxyPrivate(Fcitx5ConfigProxy *parent,
                                                 fcitx::kcm::DBusProvider *dbus,
                                                 const QString &path)
    : q(parent), dbusprovider(dbus), path(path)
{
    qCDebug(proxy) << "Entering Fcitx5ConfigProxyPrivate constructor for path:" << path;
    timer = new QTimer(q);
    timer->setInterval(1000);
    timer->setSingleShot(true);
    connect(timer, &QTimer::timeout, q, &Fcitx5ConfigProxy::save);
    qCDebug(proxy) << "Exiting Fcitx5ConfigProxyPrivate constructor";
}

QStringList Fcitx5ConfigProxyPrivate::formatKey(const QString &shortcut) {
    qCDebug(proxy) << "Formatting key from shortcut:" << shortcut;
    QStringList list;
    for (const auto &key : shortcut.split("+")) {
        if (key.trimmed().toLower() == "control")
            list << "Ctrl";
        else if (key.trimmed().toLower() == "super")
            list << "Meta";
        else if (key.trimmed().toLower() == "backspace")
            list << "Backspace";
        else if (key.trimmed().toLower() == "space")
            list << "Space";
        else
            list << key.trimmed();
    }
    // Remove the present side variant (left takes priority). When the shortcut
    // also carries Meta, surface Meta at the front so it reads "Meta+...+key".
    auto removeSide = [&list](const QString &base) {
        const QString left = base + "_L";
        const QString right = base + "_R";
        if (list.contains(left)) {
            list.removeAll(left);
        } else if (list.contains(right)) {
            list.removeAll(right);
        } else {
            return;
        }
        if (list.contains("Meta")) {
            list.removeAll("Meta");
            list.prepend("Meta");
        }
    };
    if (list.size() == 3 && list.contains("Ctrl") && list.contains("Meta")) {
        removeSide("Control");
    } else if (list.size() == 3 && list.contains("Shift") && list.contains("Meta")) {
        removeSide("Shift");
    } else if (list.size() == 3 && list.contains("Alt") && list.contains("Meta")) {
        removeSide("Alt");
    } else if (list.size() == 2 && list.contains("Ctrl")) {
        removeSide("Control");
    } else if (list.size() == 2 && list.contains("Shift")) {
        removeSide("Shift");
    } else if (list.size() == 2 && list.contains("Alt")) {
        removeSide("Alt");
    }
    qCDebug(proxy) << "Formatted key list:" << list;
    return list;
}

QString Fcitx5ConfigProxyPrivate::formatKeys(const QStringList &keys) {
    qCDebug(proxy) << "Formatting keys from list:" << keys;
    QStringList list;
    for (const auto &key : keys) {
        if (key.trimmed().toLower() == "ctrl")
            list << "Control";
        else if (key.trimmed().toLower() == "meta")
            list << "Super";
        else if (key.trimmed().toLower() == "backspace")
            list << "BackSpace";
        else if (key.trimmed().toLower() == "space")
            list << "space";
        else
            list << key.trimmed();
    }
    // size==2 with Super: normalize to "<Modifier>+Super+<_L>" order regardless
    // of input order, then append the left side variant. size==1 has no Super,
    // just append the left side variant.
    auto appendLeftSide = [&list](const QString &base) {
        if (list.contains("Super")) {
            list.clear();
            list << base << "Super";
        }
        list.append(base + "_L");
    };
    if (list.size() == 2 && list.contains("Shift") && list.contains("Super")) {
        appendLeftSide("Shift");
    } else if (list.size() == 2 && list.contains("Control") && list.contains("Super")) {
        appendLeftSide("Control");
    } else if (list.size() == 2 && list.contains("Alt") && list.contains("Super")) {
        appendLeftSide("Alt");
    } else if (list.size() == 1 && list.contains("Shift")) {
        appendLeftSide("Shift");
    } else if (list.size() == 1 && list.contains("Control")) {
        appendLeftSide("Control");
    } else if (list.size() == 1 && list.contains("Alt")) {
        appendLeftSide("Alt");
    }

    return list.join("+");
}

QVariant Fcitx5ConfigProxyPrivate::readDBusValue(const QVariant &value) {
    if (value.canConvert<QDBusArgument>()) {
        auto argument = qvariant_cast<QDBusArgument>(value);
        QVariantMap map;
        argument >> map;

        QVariantMap resultMap;
        for (auto iter = map.begin(); iter != map.end(); ++iter) {
            resultMap[iter.key()] = readDBusValue(iter.value());
        }
        return resultMap;
    }

#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
    if (value.typeId() == QMetaType::QVariantMap) {
#else
    if (value.type() == QVariant::Map) {
#endif
        QVariantMap resultMap;
        const auto map = value.toMap();
        for (auto iter = map.begin(); iter != map.end(); ++iter) {
            resultMap[iter.key()] = readDBusValue(iter.value());
        }
        return resultMap;
    }

    return value;
}

bool Fcitx5ConfigProxy::saveTriggered() const
{
    return m_saveTriggered;
}

Fcitx5ConfigProxy::Fcitx5ConfigProxy(fcitx::kcm::DBusProvider *dbus, const QString &path, QObject *parent)
    : QObject(parent), d(new Fcitx5ConfigProxyPrivate(this, dbus, path))
{
    qCDebug(proxy) << "Entering Fcitx5ConfigProxy constructor for path:" << path;
}

Fcitx5ConfigProxy::~Fcitx5ConfigProxy() = default;

void Fcitx5ConfigProxy::clear()
{
    qCDebug(proxy) << "Entering clear";
    d->configValue.clear();
    d->configTypes.clear();
    Q_EMIT requestConfigFinished();
    qCDebug(proxy) << "Exiting clear";
}

QVariantList Fcitx5ConfigProxy::globalConfigTypes() const
{
    qCDebug(proxy) << "Entering globalConfigTypes";
    QVariantList list;
    for (const auto &type : d->configTypes) {
        if (type.name() == "GlobalConfig") {
            for (const auto &option : type.options()) {
                QVariantMap item;
                item["name"] = option.name();
                item["description"] = option.description();
                list.append(item);
            }
            break;
        }
    }
    qCDebug(proxy) << "Exiting globalConfigTypes with" << list.size() << "items";
    return list;
}

QVariantList Fcitx5ConfigProxy::globalConfigOptions(const QString &type, bool all) const
{
    qCDebug(proxy) << "Entering globalConfigOptions for type" << type << "all:" << all;
    QVariantList list;
    QString currentType = type+"$"+type+"Config";
    for (const auto &configType : d->configTypes) {
        if (configType.name() != currentType)
            continue;
        for (const auto &option : configType.options()) {
            // Don't display TriggerKeys and EnumerateForwardKeys
            if (!all && (option.name() == "TriggerKeys" || option.name() == "EnumerateForwardKeys")) {
                continue;
            }

            QVariantMap item;
            item["name"] = option.name();
            item["type"] = option.type();
            item["description"] = option.description();
            auto variant = value(type+"/"+option.name());
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
            if (variant.typeId() == QMetaType::QVariantMap) {
#else
            if (variant.type() == QVariant::Map) {
#endif
                QVariantMap map = variant.toMap();
                if (map.contains("0")) {
                    item["value"] = d->formatKey(map["0"].toString());
                }
            } else {
                item["value"] = variant;
            }

            QVariantMap properties = option.properties();
            if (!properties.isEmpty()) {
                auto iterator = properties.begin();
                while (iterator != properties.constEnd()) {
                    if (iterator.key() == "Enum") {
                        auto argument = qvariant_cast<QDBusArgument>(iterator.value());
                        QVariantMap map;
                        argument >> map;
                        QVariantList enumStrings = map.values().toList();
                        if (!enumStrings.isEmpty()) {
                            item["properties"] = enumStrings;
                        }
                        break;
                    }
                    ++iterator;
                }
                iterator = properties.begin();
                while (iterator != properties.constEnd()) {
                    if (iterator.key() == "EnumI18n") {
                        auto argument = qvariant_cast<QDBusArgument>(iterator.value());
                        QVariantMap map;
                        argument >> map;
                        QVariantList enumStrings = map.values().toList();
                        if (!enumStrings.isEmpty()) {
                            item["propertiesI18n"] = enumStrings;
                        }
                        break;
                    }
                    ++iterator;
                }
                if (option.type() == "Integer") {
                    bool ok = false;
                    int maxVal = properties.contains("IntMax") ? properties["IntMax"].toInt(&ok) : 9999;
                    item["intMax"] = ok ? maxVal : 9999;
                    int minVal = properties.contains("IntMin") ? properties["IntMin"].toInt(&ok) : 0;
                    item["intMin"] = ok ? minVal : 0;
                }
            }
            list.append(item);
        }
        break;
    }
    qCDebug(proxy) << "Exiting globalConfigOptions with" << list.size() << "items";
    return list;
}

QVariant Fcitx5ConfigProxy::globalConfigOption(const QString &type, const QString &optionName) const
{
    qCDebug(proxy) << "Entering globalConfigOption for type" << type << "option:" << optionName;
    QVariantMap item;
    QString currentType = type+"$"+type+"Config";
    for (const auto &configType : d->configTypes) {
        if (configType.name() != currentType)
            continue;
        for (const auto &option : configType.options()) {
            // Don't display TriggerKeys and EnumerateForwardKeys
            if (option.name() != optionName) {
                continue;
            }

            item["name"] = option.name();
            item["type"] = option.type();
            item["description"] = option.description();
            auto variant = value(type+"/"+option.name());
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
            if (variant.typeId() == QMetaType::QVariantMap) {
#else
            if (variant.type() == QVariant::Map) {
#endif
                QVariantMap map = variant.toMap();
                if (map.contains("0")) {
                    item["value"] = d->formatKey(map["0"].toString());
                }
            } else {
                item["value"] = variant;
            }

            QVariantMap properties = option.properties();
            if (!properties.isEmpty()) {
                auto iterator = properties.begin();
                while (iterator != properties.constEnd()) {
                    if (iterator.key() == "Enum") {
                        auto argument = qvariant_cast<QDBusArgument>(iterator.value());
                        QVariantMap map;
                        argument >> map;
                        QVariantList enumStrings = map.values().toList();
                        if (!enumStrings.isEmpty()) {
                            item["properties"] = enumStrings;
                        }
                        break;
                    }
                    ++iterator;
                }
                iterator = properties.begin();
                while (iterator != properties.constEnd()) {
                    if (iterator.key() == "EnumI18n") {
                        auto argument = qvariant_cast<QDBusArgument>(iterator.value());
                        QVariantMap map;
                        argument >> map;
                        QVariantList enumStrings = map.values().toList();
                        if (!enumStrings.isEmpty()) {
                            item["propertiesI18n"] = enumStrings;
                        }
                        break;
                    }
                    ++iterator;
                }
            }
            break;
        }
        break;
    }
    qCDebug(proxy) << "Exiting globalConfigOption with item" << item;
    return item;
}

void Fcitx5ConfigProxy::restoreDefault(const QString &type)
{
    qCDebug(proxy) << "Entering restoreDefault for type" << type;
    QString currentType = type+"$"+type+"Config";
    for (const auto &configType : d->configTypes) {
        if (configType.name() != currentType)
            continue;
        for (const auto &option : configType.options()) {
            auto defaultValue = option.defaultValue();
            setValue(type+"/"+option.name(), d->readDBusValue(defaultValue.variant()));
        }
        break;
    }
    qCDebug(proxy) << "Exiting restoreDefault";
}

void Fcitx5ConfigProxy::requestConfig(bool sync, bool fromSave)
{
    qCDebug(proxy) << "Entering requestConfig for path" << d->path << "with sync" << sync << "fromSave" << fromSave;
    if (!d->dbusprovider->controller()) {
        qCWarning(proxy) << "DBus controller not available for requestConfig";
        return;
    }
    auto call = d->dbusprovider->controller()->GetConfig(d->path);
    auto watcher = new QDBusPendingCallWatcher(call, this);
    if (fromSave) {
        watcher->setProperty("saveTriggered", true);
    }
    connect(watcher,
            &QDBusPendingCallWatcher::finished,
            this,
            &Fcitx5ConfigProxy::onRequestConfigFinished);
    if (sync) {
        qCDebug(proxy) << "Waiting for requestConfig to finish";
        watcher->waitForFinished();
    }
    qCDebug(proxy) << "Exiting requestConfig";
}

void Fcitx5ConfigProxy::onRequestConfigFinished(QDBusPendingCallWatcher *watcher)
{
    qCDebug(proxy) << "Entering onRequestConfigFinished for path" << d->path;
    watcher->deleteLater();
    QDBusPendingReply<QDBusVariant, fcitx::FcitxQtConfigTypeList> reply = *watcher;
    if (reply.isError()) {
        qCWarning(proxy) << "Failed to get config for path" << d->path << "Error:" << reply.error();
        return;
    }
    qCInfo(proxy) << "Successfully received config for path:" << d->path;
    d->configTypes = reply.argumentAt<1>();

    auto value = reply.argumentAt<0>().variant();
    QVariantMap allMap;
    allMap = d->readDBusValue(value).toMap();
    std::swap(d->configValue, allMap);

    bool wasSaveTriggered = watcher->property("saveTriggered").toBool();
    if (wasSaveTriggered) {
        m_saveTriggered = true;
        Q_EMIT saveTriggeredChanged();
    }
    Q_EMIT requestConfigFinished();
    if (wasSaveTriggered) {
        m_saveTriggered = false;
        Q_EMIT saveTriggeredChanged();
    }
    qCDebug(proxy) << "Exiting onRequestConfigFinished";
}

QVariant Fcitx5ConfigProxy::value(const QString &path) const
{
    qCDebug(proxy) << "Entering value for path:" << path;
    return fcitx::kcm::readVariant(d->configValue, path);
}

void Fcitx5ConfigProxy::setValue(const QString &path, const QVariant &value, bool isKey)
{
    qCDebug(proxy) << "Setting value for config path:" << path << "isKey:" << isKey;
    if (value == this->value(path)) {
        qCDebug(proxy) << "Value unchanged, skipping update for path:" << path;
        return;
    }
    if (isKey) {
        qCDebug(proxy) << "Formatting keys for path:" << path;
        auto keys = d->formatKeys(value.toStringList());
        fcitx::kcm::writeVariant(d->configValue, path, keys);
    } else {
        qCDebug(proxy) << "Writing value for path:" << path;
        fcitx::kcm::writeVariant(d->configValue, path, value);
    }
    d->timer->start();
    qCDebug(proxy) << "Exiting setValue";
}

void Fcitx5ConfigProxy::save()
{
    qCDebug(proxy) << "Entering save for path:" << d->path;
    if (!d->dbusprovider->controller()) {
        qCWarning(proxy) << "DBus controller not available for save";
        return;
    }

    QDBusVariant var(d->configValue);
    d->dbusprovider->controller()->SetConfig(d->path, var);
    requestConfig(false, true);
    qCDebug(proxy) << "Exiting save";
}
