/*
 * SPDX-FileCopyrightText: 2026 Maciej Sopyło <me@klh.io>
 * SPDX-FileCopyrightText: 2026 ChromiumOS-Guy <chromiumos-guy-opensource@proton.me>
 * SPDX-License-Identifier: GPL-3.0-or-later
 */

#include <QDBusConnection>
#include <QDBusConnectionInterface>
#include <QDBusAbstractAdaptor>
#include <QDBusPendingCall>
#include <QDBusPendingCallWatcher>
#include <QDBusServiceWatcher>
#include <QDBusObjectPath>
#include <QMetaClassInfo>
#include <QMetaMethod>

#include <limits>

#include "ObjectProxy.h"

QString qObjectClassInfoValue(QObject *obj, const char *name)
{
    int index = obj->metaObject()->indexOfClassInfo(name);
    if (index == -1) {
        return QString("");
    }

    return QString(obj->metaObject()->classInfo(index).value());
}

QString ObjectProxy::adaptorObjectPath(QObject *adaptor)
{
    return QString("%1/%2")
            .arg(qObjectClassInfoValue(this, "D-Bus Object Path"))
            .arg(adaptor->metaObject()->className());
}

QString ObjectProxy::adaptorService(QObject *adaptor)
{
    return QString("%1.%2")
            .arg(qObjectClassInfoValue(this, "D-Bus Service Name"))
            .arg(adaptor->metaObject()->className());
}

ObjectProxy::ObjectProxy(QObject *parent) : QDBusVirtualObject(parent) { }

void ObjectProxy::registerVirtualObject()
{
    auto connection = QDBusConnection::sessionBus();
    connection.interface()->registerService(qObjectClassInfoValue(this, "D-Bus Service Name"),
                                            QDBusConnectionInterface::DontQueueService);
    connection.registerVirtualObject(qObjectClassInfoValue(this, "D-Bus Object Path"), this);
}

void ObjectProxy::registerAdaptor(QDBusAbstractAdaptor *adaptor)
{
    if (adaptor->parent()->property("registered").isValid()) {
        return;
    }

    auto objPath = adaptorObjectPath(adaptor);
    auto serviceName = adaptorService(adaptor);
    // Open a new connection - this is necessary to support delayed replies. If we re-used the
    // connection between the virtual object and children Qt would route the calls internally
    // without support for async responses
    // Another option would be to use QDBusServer.
    auto connection = QDBusConnection::connectToBus(QDBusConnection::SessionBus,
                                                    adaptor->metaObject()->className());
    connection.interface()->registerService(serviceName,
                                            QDBusConnectionInterface::DontQueueService);
    connection.registerObject(objPath, adaptor->parent());

    // Connect all adaptor signals to the proxy signal handler - otherwise they are emitted on the
    // wrong service/object path.
    auto meta = adaptor->metaObject();
    for (int i = meta->methodOffset(); i < meta->methodCount(); ++i) {
        QDBusConnection::sessionBus().connect(
                serviceName, objPath, qObjectClassInfoValue(adaptor, "D-Bus Interface"),
                QString(meta->method(i).name()), this, SLOT(onSignal()));
    }
    adaptor->parent()->setProperty("registered", true);
}

void ObjectProxy::onSignal()
{
    auto proxyMsg = QDBusMessage::createSignal(qObjectClassInfoValue(this, "D-Bus Object Path"),
                                               message().interface(), message().member());
    proxyMsg.setArguments(message().arguments());

    connection().send(proxyMsg);
}

bool ObjectProxy::handleMessage(const QDBusMessage &message, const QDBusConnection &connection)
{
    if (message.type() != QDBusMessage::MethodCallMessage) {
        return false;
    }

    for (auto adaptor : this->findChildren<QDBusAbstractAdaptor *>()) {
        auto interface = qObjectClassInfoValue(adaptor, "D-Bus Interface");
        if (interface == message.interface()
            || (message.interface() == "org.freedesktop.DBus.Properties"
                && message.arguments()[0] == interface)) {
            auto proxyMsg = QDBusMessage::createMethodCall(adaptorService(adaptor),
                                                           adaptorObjectPath(adaptor),
                                                           message.interface(), message.member());
            proxyMsg.setArguments(message.arguments());

            if (!message.isReplyRequired()) {
                connection.send(proxyMsg);
                return true;
            }

            // Forward asynchronously and relay the reply when it arrives; unlike the old
            // QDBus::BlockWithGui call handleMessage is non-reentrant, so a portal using
            // setDelayedReply() just replies later and the watcher fires then.
            auto *watcher = new QDBusPendingCallWatcher(
                    connection.asyncCall(proxyMsg, std::numeric_limits<int>::max()), this);
            connect(watcher, &QDBusPendingCallWatcher::finished, this,
                    [connection, message](QDBusPendingCallWatcher *self) {
                        const QDBusMessage response = self->reply();
                        if (response.type() == QDBusMessage::ErrorMessage) {
                            connection.send(message.createErrorReply(response.errorName(),
                                                                     response.errorMessage()));
                        } else {
                            connection.send(message.createReply(response.arguments()));
                        }
                        self->deleteLater();
                    });
                    
            return true;
        }
    }

    return false;
}

QString ObjectProxy::introspect(const QString &path) const
{
    QStringList introspections;
    for (auto adaptor : this->findChildren<QDBusAbstractAdaptor *>()) {
        introspections << qObjectClassInfoValue(adaptor, "D-Bus Introspection");
    }
    return introspections.join("");
}
