// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
// SPDX-FileCopyrightText: 2022 Harald Sitter <sitter@kde.org>

#include "gamesmodel.h"

#include "com.feralinteractive.GameMode.Game.h"
#include "com.feralinteractive.GameMode.h"
#include "org.freedesktop.DBus.Properties.h"

GamesModel::GamesModel(QObject *parent)
: QAbstractListModel(parent)
{
    auto watcher = new QDBusServiceWatcher("com.feralinteractive.GameMode", QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForOwnerChange, this);
    connect(watcher,
            &QDBusServiceWatcher::serviceOwnerChanged,
            this,
            [this](const QString & /*service*/, const QString & /*oldOwner*/, const QString &newOwner) {
                if (!newOwner.isEmpty()) { // this is a registration even not a loss event
                    reload();
                } else {
                    reset();
                }
            });
    reload();
}

QHash<int, QByteArray> GamesModel::roleNames() const
{
    return m_roles;
}

int GamesModel::rowCount(const QModelIndex &parent) const
{
    Q_UNUSED(parent); // this is a flat list we decidedly don't care about the parent
    return m_objects.count();
}

QVariant GamesModel::data(const QModelIndex &index, int role) const
{
    if (!hasIndex(index.row(), index.column())) {
        qDebug() << "no index";
        return {};
    }
    QObject *obj = m_objects.at(index.row());
    if (role == LastRole) {
        return QVariant::fromValue(obj);
    }
    const QByteArray prop = m_objectProperties.value(role);
    if (prop.isEmpty()) {
        qDebug() << "no prop";
        return {};
    }
    qDebug() << "returning" << prop << obj->property(prop.constData());
    return obj->property(prop.constData());
}

int GamesModel::role(const QByteArray &roleName) const
{
    return m_roles.key(roleName, -1);
}

bool GamesModel::valid() const
{
    return m_iface != nullptr;
}

bool GamesModel::waiting() const
{
    return m_getManagedObjectsWatcher != nullptr;
}

void GamesModel::addObject(int pid, const QDBusObjectPath &dbusPath)
{
    const QString path = dbusPath.path();
    qDebug() << pid << dbusPath.path();

    auto properties = new OrgFreedesktopDBusPropertiesInterface("com.feralinteractive.GameMode", path, QDBusConnection::sessionBus(), this);
    auto pendingCall = properties->GetAll("com.feralinteractive.GameMode.Game");
    auto watcher = new QDBusPendingCallWatcher(pendingCall);
    connect(watcher, &QDBusPendingCallWatcher::finished, this, [dbusPath, path, watcher, pendingCall, this] {
        watcher->deleteLater();
        if (pendingCall.isError() || !pendingCall.isValid()) {
            qWarning() << pendingCall.error();
            return;
        }

        int newIndex = 0;
        for (auto m_object : std::as_const(m_objects)) {
            if (m_object->objectName() == path) {
                return; // already tracked
            }
            ++newIndex;
        }

        beginInsertRows(QModelIndex(), newIndex, newIndex);

        // QDBus doesn't manage to map notifiable properties for its generated interface classes
        // so it brings literally nothing to the table for our Device class.
        // Use QObjects with dynamic properties instead to model the remote objects.
        // Property changes are abstracted via the ListModel anyway.
        auto obj = new ComFeralinteractiveGameModeGameInterface("com.feralinteractive.GameMode.Game", path, QDBusConnection::sessionBus(), this);
        m_objects << obj;
        obj->setObjectName(path);
        const auto propertyMap = pendingCall.value();
        for (auto propertyIt = propertyMap.cbegin(); propertyIt != propertyMap.cend(); ++propertyIt) {
            obj->setProperty(qPrintable(propertyIt.key().toLower()), propertyIt.value());
        }

        // Properties aren't mutable in gamemode's case

        if (m_roles.isEmpty()) {
            initRoleNames(obj);
        }

        endInsertRows();
        Q_EMIT rowCountChanged();
    });
}

void GamesModel::removeObject(int pid, const QDBusObjectPath &dbusPath)
{
    const QString path = dbusPath.path();
    auto it = std::find_if(m_objects.begin(), m_objects.end(), [path](const QObject *o) {
        return o->objectName() == path;
    });
    if (it == m_objects.end()) {
        return; // not tracked
    }
    auto index = std::distance(m_objects.begin(), it);
    beginRemoveRows(QModelIndex(), index, index);
    (*it)->deleteLater();
    m_objects.erase(it);
    endRemoveRows();
    Q_EMIT rowCountChanged();
}

void GamesModel::initRoleNames(QObject *object)
{
    m_roles[ObjectRole] = QByteArrayLiteral("object");

    int maxEnumValue = LastRole;
    Q_ASSERT(maxEnumValue != -1);
    auto mo = *object->metaObject();
    for (int i = 0; i < mo.propertyCount(); ++i) {
        QMetaProperty property = mo.property(i);
        QString name(property.name());
        m_roles[++maxEnumValue] = name.toLatin1();
        m_objectProperties.insert(maxEnumValue, property.name());
        if (!property.hasNotifySignal()) {
            continue;
        }
        m_signalIndexToProperties.insert(property.notifySignalIndex(), maxEnumValue);
    }
    const auto dynamicPropertyNames = object->dynamicPropertyNames();
    for (const auto &dynProperty : dynamicPropertyNames) {
        m_roles[++maxEnumValue] = dynProperty;
        m_objectProperties.insert(maxEnumValue, dynProperty);
    }
}

void GamesModel::reset()
{
    beginResetModel();

    m_serviceRegistered = QDBusConnection::sessionBus().interface()->isServiceRegistered("com.feralinteractive.GameMode");
    Q_EMIT serviceRegisteredChanged();

    qDeleteAll(m_objects);
    m_objects.clear();

    if (m_iface) {
        m_iface->disconnect(this);
        m_iface->deleteLater();
        m_iface = nullptr;
        Q_EMIT validChanged();
    }

    if (m_getManagedObjectsWatcher) {
        m_getManagedObjectsWatcher->deleteLater();
        m_getManagedObjectsWatcher = nullptr;
        Q_EMIT waitingChanged();
    }

    endResetModel();
    Q_EMIT rowCountChanged();
}

void GamesModel::reload()
{
    reset();
    qDebug() << Q_FUNC_INFO;

    m_iface = new ComFeralinteractiveGameModeInterface("com.feralinteractive.GameMode", "/com/feralinteractive/GameMode", QDBusConnection::sessionBus(), this);
    connect(m_iface, &ComFeralinteractiveGameModeInterface::GameRegistered, this, &GamesModel::addObject);
    connect(m_iface, &ComFeralinteractiveGameModeInterface::GameUnregistered, this, &GamesModel::removeObject);

    Q_EMIT validChanged();

    // Load existing objects.
    if (m_getManagedObjectsWatcher) {
        // Last reload didn't finish before this one, so throw away the last watcher
        m_getManagedObjectsWatcher->deleteLater();
    }
    auto call = m_iface->ListGames();
    m_getManagedObjectsWatcher = new QDBusPendingCallWatcher(call, this);
    Q_EMIT waitingChanged();
    connect(m_getManagedObjectsWatcher, &QDBusPendingCallWatcher::finished, this, [call, this] {

        qDebug() << Q_FUNC_INFO << "lambda" << call.error();
        auto map = call.value();
        for (const auto &entry : map) {
            addObject(entry.pid, entry.path);
        }
        m_getManagedObjectsWatcher->deleteLater();
        m_getManagedObjectsWatcher = nullptr;
        Q_EMIT waitingChanged();
    });
}
