// SPDX-License-Identifier: LGPL-3.0-or-later

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright (C) 2013 - 2024, nymea GmbH
* Copyright (C) 2024 - 2025, chargebyte austria GmbH
*
* This file is part of libnymea-networkmanager.
*
* libnymea-networkmanager is free software: you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation, either version 3
* of the License, or (at your option) any later version.
*
* libnymea-networkmanager is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with libnymea-networkmanager. If not, see <https://www.gnu.org/licenses/>.
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/*!
    \class WirelessNetworkDevice
    \brief Represents a wireless network device.
    \inmodule nymea-networkmanager
    \ingroup networkmanager


*/

/*! \enum WirelessNetworkDevice::Mode
    \value ModeUnknown
    \value ModeAdhoc
    \value ModeInfrastructure
    \value ModeAccessPoint

*/

/*! \fn void WirelessNetworkDevice::bitRateChanged(int bitRate);
    This signal will be emitted when the \a bitRate of this \l{WirelessNetworkDevice} has changed.
*/

/*! \fn void WirelessNetworkDevice::modeChanged(Mode mode);
    This signal will be emitted when the current \a mode of this \l{WirelessNetworkDevice} has changed.

    \sa WirelessNetworkDevice::Mode
*/

#include "networkmanagerutils.h"
#include "wirelessnetworkdevice.h"

#include <QUuid>
#include <QDebug>
#include <QMetaEnum>

/*! Constructs a new \l{WirelessNetworkDevice} with the given dbus \a objectPath and \a parent. */
WirelessNetworkDevice::WirelessNetworkDevice(const QDBusObjectPath &objectPath, QObject *parent) :
    NetworkDevice(objectPath, parent),
    m_activeAccessPoint(nullptr)
{
    QDBusConnection systemBus = QDBusConnection::systemBus();
    if (!systemBus.isConnected()) {
        qCWarning(dcNetworkManager()) << "WirelessNetworkDevice: System DBus not connected";
        return;
    }

    m_wirelessInterface = new QDBusInterface(NetworkManagerUtils::networkManagerServiceString(), this->objectPath().path(), NetworkManagerUtils::wirelessInterfaceString(), systemBus, this);
    if (!m_wirelessInterface->isValid()) {
        qCWarning(dcNetworkManager()) << "WirelessNetworkDevice: Invalid wireless dbus interface";
        return;
    }

    QDBusConnection::systemBus().connect(NetworkManagerUtils::networkManagerServiceString(), this->objectPath().path(), NetworkManagerUtils::wirelessInterfaceString(), "AccessPointAdded", this, SLOT(accessPointAdded(QDBusObjectPath)));
    QDBusConnection::systemBus().connect(NetworkManagerUtils::networkManagerServiceString(), this->objectPath().path(), NetworkManagerUtils::wirelessInterfaceString(), "AccessPointRemoved", this, SLOT(accessPointRemoved(QDBusObjectPath)));
    // org.freedesktop.NetworkManager.Device.Wireless.PropertiesChanged(QVariantMap) is used in older versions of NetworkManager instead of the standard D-Bus properties changed signal
    QDBusConnection::systemBus().connect(NetworkManagerUtils::networkManagerServiceString(), this->objectPath().path(), NetworkManagerUtils::wirelessInterfaceString(), "PropertiesChanged", this, SLOT(processProperties(QVariantMap)));
    // Newer versions of NetworkManager dropped the other and switched to the D-Bus standard PropertiesChanged
    QDBusConnection::systemBus().connect(NetworkManagerUtils::networkManagerServiceString(), this->objectPath().path(), "org.freedesktop.DBus.Properties", "PropertiesChanged", this, SLOT(onPropertiesChanged(QString, QVariantMap, QStringList)));

    readAccessPoints();

    m_macAddress = m_wirelessInterface->property("HwAddress").toString();
    m_wirelessCapabilities = static_cast<WirelessCapabilities>(m_wirelessInterface->property("WirelessCapabilities").toUInt());
    m_wirelessMode = static_cast<WirelessMode>(m_wirelessInterface->property("Mode").toUInt());
    m_bitRate = m_wirelessInterface->property("Bitrate").toInt() / 1000;
    setActiveAccessPoint(qdbus_cast<QDBusObjectPath>(m_wirelessInterface->property("ActiveAccessPoint")));
}

/*! Returns the mac address of this \l{WirelessNetworkDevice}. */
QString WirelessNetworkDevice::macAddress() const
{
    return m_macAddress;
}

/*! Returns the bit rate [Mb/s] of this \l{WirelessNetworkDevice}. */
int WirelessNetworkDevice::bitRate() const
{
    return m_bitRate;
}

WirelessNetworkDevice::WirelessCapabilities WirelessNetworkDevice::wirelessCapabilities() const
{
    return m_wirelessCapabilities;
}

WirelessNetworkDevice::WirelessMode WirelessNetworkDevice::wirelessMode() const
{
    return m_wirelessMode;
}

/*! Returns the current active \l{WirelessAccessPoint} of this \l{WirelessNetworkDevice}. */
WirelessAccessPoint *WirelessNetworkDevice::activeAccessPoint()
{
    return m_activeAccessPoint;
}

/*! Perform a wireless network scan on this \l{WirelessNetworkDevice}. */
void WirelessNetworkDevice::scanWirelessNetworks()
{
    qCDebug(dcNetworkManager()) << "Request scan" << this;
    QDBusMessage query = m_wirelessInterface->call("RequestScan", QVariantMap());
    if (query.type() != QDBusMessage::ReplyMessage) {
        qCWarning(dcNetworkManager()) << "Scan error:" << query.errorName() << query.errorMessage();
        return;
    }
}

/*! Returns the list of all \l{WirelessAccessPoint}{WirelessAccessPoints} of this \l{WirelessNetworkDevice}. */
QList<WirelessAccessPoint *> WirelessNetworkDevice::accessPoints()
{
    return m_accessPointsTable.values();
}

/*! Returns the \l{WirelessAccessPoint} with the given \a ssid. If the \l{WirelessAccessPoint} could not be found, return nullptr. */
WirelessAccessPoint *WirelessNetworkDevice::getAccessPoint(const QString &ssid)
{
    foreach (WirelessAccessPoint *accessPoint, m_accessPointsTable.values()) {
        if (accessPoint->ssid() == ssid)
            return accessPoint;
    }
    return nullptr;

}

/*! Returns the \l{WirelessAccessPoint} with the given \a objectPath. If the \l{WirelessAccessPoint} could not be found, return nullptr. */
WirelessAccessPoint *WirelessNetworkDevice::getAccessPoint(const QDBusObjectPath &objectPath)
{
    return m_accessPointsTable.value(objectPath);
}

void WirelessNetworkDevice::readAccessPoints()
{
    QDBusMessage query = m_wirelessInterface->call("GetAccessPoints");
    if(query.type() != QDBusMessage::ReplyMessage) {
        qCWarning(dcNetworkManager()) << query.errorName() << query.errorMessage();
        return;
    }

    if (query.arguments().isEmpty())
        return;

    const QDBusArgument &argument = query.arguments().at(0).value<QDBusArgument>();
    argument.beginArray();
    while (!argument.atEnd()) {
        QDBusObjectPath accessPointObjectPath = qdbus_cast<QDBusObjectPath>(argument);
        accessPointAdded(accessPointObjectPath);
    }
    argument.endArray();
}

void WirelessNetworkDevice::setActiveAccessPoint(const QDBusObjectPath &activeAccessPointObjectPath)
{
    if (m_activeAccessPointObjectPath != activeAccessPointObjectPath) {
        m_activeAccessPointObjectPath = activeAccessPointObjectPath;
        if (m_accessPointsTable.contains(m_activeAccessPointObjectPath)) {
            if (m_activeAccessPoint)
                disconnect(m_activeAccessPoint, &WirelessAccessPoint::signalStrengthChanged, this, &WirelessNetworkDevice::deviceChanged);

            // Set new access point object
            m_activeAccessPoint = m_accessPointsTable.value(activeAccessPointObjectPath);
            // Update the device when the signalstrength changed
            connect(m_activeAccessPoint, &WirelessAccessPoint::signalStrengthChanged, this, &WirelessNetworkDevice::deviceChanged);
        } else {
            m_activeAccessPoint = nullptr;
        }
        emit deviceChanged();
    }
}

void WirelessNetworkDevice::accessPointAdded(const QDBusObjectPath &objectPath)
{
    QDBusInterface accessPointInterface(NetworkManagerUtils::networkManagerServiceString(), objectPath.path(), NetworkManagerUtils::accessPointInterfaceString(), QDBusConnection::systemBus());
    if (!accessPointInterface.isValid()) {
        qCWarning(dcNetworkManager()) << this << "Invalid access point dbus interface";
        return;
    }

    if (m_accessPointsTable.keys().contains(objectPath)) {
        qCWarning(dcNetworkManager()) << this << "Access point already added" << objectPath.path();
        return;
    }

    WirelessAccessPoint *accessPoint = new WirelessAccessPoint(objectPath, this);
    qCDebug(dcNetworkManager()) << interface() << "[+]" << accessPoint;
    m_accessPointsTable.insert(objectPath, accessPoint);
}

void WirelessNetworkDevice::accessPointRemoved(const QDBusObjectPath &objectPath)
{
    if (!m_accessPointsTable.keys().contains(objectPath))
        return;

    WirelessAccessPoint *accessPoint = m_accessPointsTable.take(objectPath);
    if (accessPoint == m_activeAccessPoint)
        m_activeAccessPoint = nullptr;

    qCDebug(dcNetworkManager()) << interface() << "[-]" << accessPoint;
    accessPoint->deleteLater();
}

void WirelessNetworkDevice::processProperties(const QVariantMap &properties)
{
    //qCDebug(dcNetworkManager()) << "WirelessNetworkDevice: Property changed" << properties;

    if (properties.contains("Bitrate")) {
        m_bitRate = properties.value("Bitrate").toInt() / 1000;
        emit bitRateChanged(m_bitRate);
    }

    if (properties.contains("Mode")) {
        m_wirelessMode = static_cast<WirelessMode>(m_wirelessInterface->property("Mode").toUInt());
        emit wirelessModeChanged(m_wirelessMode);
    }

    if (properties.contains("WirelessCapabilities")) {
        m_wirelessCapabilities = static_cast<WirelessCapabilities>(m_wirelessInterface->property("WirelessCapabilities").toUInt());
        emit wirelessCapabilitiesChanged(m_wirelessCapabilities);
    }

    // Note: available since 1.12 (-1 means never scanned)
    if (properties.contains("LastScan")) {
        m_lastScan = m_wirelessInterface->property("LastScan").toInt();
        emit lastScanChanged(m_lastScan);
    }

    if (properties.contains("ActiveAccessPoint")) {
        setActiveAccessPoint(qdbus_cast<QDBusObjectPath>(properties.value("ActiveAccessPoint")));
    }

    emit deviceChanged();
}

void WirelessNetworkDevice::onPropertiesChanged(const QString &interface, const QVariantMap &changedProperties, const QStringList &invalidatedProperties)
{
    Q_UNUSED(interface)
    Q_UNUSED(invalidatedProperties)
    //qCDebug(dcNetworkManager()) << "WirelessNetworkDevice: Properties changed" << interface << changedProperties << invalidatedProperties;
    processProperties(changedProperties);
}

/*! Writes the given \a device to the given to \a debug. \sa WirelessNetworkDevice, */
QDebug operator<<(QDebug debug, WirelessNetworkDevice *device)
{
    debug.nospace() << "WirelessNetworkDevice(" << device->interface() << ", ";
    debug.nospace() << device->macAddress() <<  ", ";
    debug.nospace() << device->wirelessCapabilities() <<  ", ";
    debug.nospace() << device->wirelessMode() <<  ", ";
    debug.nospace() << device->bitRate() <<  " [Mb/s], ";
    debug.nospace() << device->deviceStateString() <<  ") ";
    return debug.space();
}
