// 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 NetworkManager
    \brief Represents the main network-manager instance.
    \inmodule nymea-networkmanager
    \ingroup networkmanager



*/

#include "networkmanager.h"
#include "networkconnection.h"

#include <QUuid>
#include <QDebug>
#include <QTimer>
#include <QMetaEnum>
#include <QRegularExpression>

#include <arpa/inet.h>

/*! Constructs a new \l{NetworkManager} object with the given \a parent. */
NetworkManager::NetworkManager(QObject *parent) :
    QObject(parent)
{
    NetworkConnection::registerTypes();

    // Get notification when network-manager appears/disappears on DBus
    m_serviceWatcher = new QDBusServiceWatcher(NetworkManagerUtils::networkManagerServiceString(), QDBusConnection::systemBus(), QDBusServiceWatcher::WatchForRegistration | QDBusServiceWatcher::WatchForUnregistration, this);
    connect(m_serviceWatcher, &QDBusServiceWatcher::serviceRegistered, this, &NetworkManager::onServiceRegistered);
    connect(m_serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, this, &NetworkManager::onServiceUnregistered);
}

NetworkManager::~NetworkManager()
{
    qCDebug(dcNetworkManager()) << "Destroy network manager.";
}

/*! Returns true if the network-manager is available on this system. */
bool NetworkManager::available() const
{
    return m_available;
}

/*! Returns true if wifi is available on this system. */
bool NetworkManager::wirelessAvailable() const
{
    return !m_wirelessNetworkDevices.isEmpty();
}

/*! Returns the list of \l{NetworkDevice}{NetworkDevices} from this \l{NetworkManager}. */
QList<NetworkDevice *> NetworkManager::networkDevices() const
{
    return m_networkDevices.values();
}

QList<WirelessNetworkDevice *> NetworkManager::wirelessNetworkDevices() const
{
    return m_wirelessNetworkDevices.values();
}

/*! Returns the list of \l{WiredNetworkDevice}{WiredNetworkDevices} from this \l{NetworkManager}. */
QList<WiredNetworkDevice *> NetworkManager::wiredNetworkDevices() const
{
    return m_wiredNetworkDevices.values();
}

/*! Returns the \l{NetworkSettings} from this \l{NetworkManager}. */
NetworkSettings *NetworkManager::networkSettings() const
{
    return m_networkSettings;
}

/*! Returns the \l{NetworkDevice} with the given \a interface from this \l{NetworkManager}. If there is no such \a interface returns nullptr. */
NetworkDevice *NetworkManager::getNetworkDevice(const QString &interface)
{
    foreach (NetworkDevice *device, m_networkDevices.values()) {
        if (device->interface() == interface)
            return device;
    }
    return nullptr;
}

/*! Returns the version of the running \l{NetworkManager}. */
QString NetworkManager::version() const
{
    return m_version;
}

/*! Returns the state of this \l{NetworkManager}. \sa NetworkManagerState, */
NetworkManager::NetworkManagerState NetworkManager::state() const
{
    return m_state;
}

/*! Returns the human readable string of the current state of this \l{NetworkManager}. \sa NetworkManagerState, */
QString NetworkManager::stateString() const
{
    return networkManagerStateToString(m_state);
}

/*! Returns the current connectivity state of this \l{NetworkManager}. \sa NetworkManagerConnectivityState, */
NetworkManager::NetworkManagerConnectivityState NetworkManager::connectivityState() const
{
    return m_connectivityState;
}

/*! Connect the given \a interface to a wifi network with the given \a ssid and \a password. Returns the \l{NetworkManagerError} to inform about the result. \sa NetworkManagerError, */
NetworkManager::NetworkManagerError NetworkManager::connectWifi(const QString &interface, const QString &ssid, const QString &password, AuthAlgorithm authAlgorithm, KeyManagement keyManagement, bool hidden)
{
    // Check interface
    if (!getNetworkDevice(interface))
        return NetworkManagerErrorNetworkInterfaceNotFound;

    // Get wirelessNetworkDevice
    WirelessNetworkDevice *wirelessNetworkDevice = nullptr;
    foreach (WirelessNetworkDevice *networkDevice, wirelessNetworkDevices()) {
        if (networkDevice->interface() == interface) {
            wirelessNetworkDevice = networkDevice;
        }
    }

    if (!wirelessNetworkDevice)
        return NetworkManagerErrorInvalidNetworkDeviceType;

    if (!hidden) {
        // Get the access point object path
        WirelessAccessPoint *accessPoint = wirelessNetworkDevice->getAccessPoint(ssid);
        if (!accessPoint) {
            return NetworkManagerErrorAccessPointNotFound;
        }

        qCDebug(dcNetworkManager()) << "Connecting to" << accessPoint;
    } else {
        qCDebug(dcNetworkManager()) << "Connecting to hidden WiFi:" << ssid;
    }

    // Note: https://developer.gnome.org/NetworkManager/stable/ref-settings.html


    // Create network settings for this wifi
    QVariantMap connectionSettings;
    connectionSettings.insert("autoconnect", true);
    connectionSettings.insert("id", ssid);
    connectionSettings.insert("uuid", QUuid::createUuid().toString().remove("{").remove("}"));
    connectionSettings.insert("type", "802-11-wireless");
    connectionSettings.insert("autoconnect-retries", 0); // 0 = forever, -1 = default (4 retries)


    QVariantMap wirelessSettings;
    wirelessSettings.insert("ssid", ssid.toUtf8());
    wirelessSettings.insert("mode", "infrastructure");

    // Note: disable power save mode
    wirelessSettings.insert("powersave", 2);

    if (hidden) {
        wirelessSettings.insert("hidden", true);
    }

    QVariantMap wirelessSecuritySettings;
    switch (authAlgorithm) {
    case AuthAlgorithmOpen:
        wirelessSecuritySettings.insert("auth-alg", "open");
        break;
    }

    switch (keyManagement) {
    case KeyManagementWpaPsk:
        wirelessSecuritySettings.insert("key-mgmt", "wpa-psk");
        break;
    }

    wirelessSecuritySettings.insert("psk", password);

    QVariantMap ipv4Settings;
    ipv4Settings.insert("method", "auto");

    QVariantMap ipv6Settings;
    ipv6Settings.insert("method", "auto");

    // Build connection object
    ConnectionSettings settings;
    settings.insert("connection", connectionSettings);
    settings.insert("802-11-wireless", wirelessSettings);
    settings.insert("ipv4", ipv4Settings);
    settings.insert("ipv6", ipv6Settings);

    if (!password.isEmpty()) {
        settings.insert("802-11-wireless-security", wirelessSecuritySettings);
    }

    // Remove old configuration (if there is any)
    foreach (NetworkConnection *connection, m_networkSettings->connections()) {
        if (connection->id() == connectionSettings.value("id")) {
            connection->deleteConnection();
        }
    }

    // Add connection
    QDBusObjectPath connectionObjectPath = m_networkSettings->addConnection(settings);
    if (connectionObjectPath.path().isEmpty())
        return NetworkManagerErrorWirelessConnectionFailed;

    // Activate connection
    QDBusMessage query = m_networkManagerInterface->call("ActivateConnection", QVariant::fromValue(connectionObjectPath), QVariant::fromValue(wirelessNetworkDevice->objectPath()), QVariant::fromValue(QDBusObjectPath("/")));
    if (query.type() != QDBusMessage::ReplyMessage) {
        qCWarning(dcNetworkManager()) << query.errorName() << query.errorMessage();
        return NetworkManagerErrorWirelessConnectionFailed;
    }

    return NetworkManagerErrorNoError;
}

NetworkManager::NetworkManagerError NetworkManager::startAccessPoint(const QString &interface, const QString &ssid, const QString &password)
{
    qCDebug(dcNetworkManager()) << "Starting access point for" << interface << "SSID:" <<  ssid << "password:" << password;

    // Check interface
    if (!getNetworkDevice(interface))
        return NetworkManagerErrorNetworkInterfaceNotFound;

    // Get wirelessNetworkDevice
    WirelessNetworkDevice *wirelessNetworkDevice = nullptr;
    foreach (WirelessNetworkDevice *networkDevice, wirelessNetworkDevices()) {
        if (networkDevice->interface() == interface) {
            wirelessNetworkDevice = networkDevice;
        }
    }

    if (!wirelessNetworkDevice)
        return NetworkManagerErrorInvalidNetworkDeviceType;


    if (!wirelessNetworkDevice->wirelessCapabilities().testFlag(WirelessNetworkDevice::WirelessCapabilityAP))
        return NetworkManagerErrorUnsupportedFeature;

    // Note: https://developer.gnome.org/NetworkManager/stable/ref-settings.html

    // Create network settings for access point
    QVariantMap connectionSettings;
    connectionSettings.insert("id", ssid);
    connectionSettings.insert("autoconnect", true);
    connectionSettings.insert("uuid", QUuid::createUuid().toString().remove("{").remove("}"));
    connectionSettings.insert("type", "802-11-wireless");

    QVariantMap wirelessSettings;
    wirelessSettings.insert("band", "bg");
    wirelessSettings.insert("mode", "ap");
    wirelessSettings.insert("ssid", ssid.toUtf8());    
    wirelessSettings.insert("security", "802-11-wireless-security");
    // Note: disable power save mode
    wirelessSettings.insert("powersave", 2);

    QVariantMap wirelessSecuritySettings;
    wirelessSecuritySettings.insert("key-mgmt", "wpa-psk");
    wirelessSecuritySettings.insert("psk", password);
    wirelessSecuritySettings.insert("group", QStringList() << "ccmp");
    wirelessSecuritySettings.insert("pairwise", QStringList() << "ccmp");
    wirelessSecuritySettings.insert("proto", QStringList() << "rsn"); // Force WPA2

    QVariantMap ipv4Settings;
    ipv4Settings.insert("method", "shared");

    QVariantMap ipv6Settings;
    ipv6Settings.insert("method", "auto");

    // Build connection object
    ConnectionSettings settings;
    settings.insert("connection", connectionSettings);
    settings.insert("802-11-wireless", wirelessSettings);
    settings.insert("ipv4", ipv4Settings);
    settings.insert("ipv6", ipv6Settings);
    settings.insert("802-11-wireless-security", wirelessSecuritySettings);

    // Remove old configuration (if there is any)
    foreach (NetworkConnection *connection, m_networkSettings->connections()) {
        if (connection->id() == connectionSettings.value("id")) {
            connection->deleteConnection();
        }
    }

    // Add connection
    QDBusObjectPath connectionObjectPath = m_networkSettings->addConnection(settings);
    if (connectionObjectPath.path().isEmpty())
        return NetworkManagerErrorWirelessConnectionFailed;


    qCDebug(dcNetworkManager()) << "Connection added" << connectionObjectPath.path();

    // Activate connection
    QDBusMessage query = m_networkManagerInterface->call("ActivateConnection",
                                                         QVariant::fromValue(connectionObjectPath),
                                                         QVariant::fromValue(wirelessNetworkDevice->objectPath()),
                                                         QVariant::fromValue(QDBusObjectPath("/")));
    if (query.type() != QDBusMessage::ReplyMessage) {
        qCWarning(dcNetworkManager()) << query.errorName() << query.errorMessage();
        return NetworkManagerErrorWirelessConnectionFailed;
    }

    return NetworkManagerErrorNoError;
}

NetworkManager::NetworkManagerError NetworkManager::createWiredAutoConnection(const QString &interface)
{
    qCDebug(dcNetworkManager()) << "Creating auto connection for" << interface;

    NetworkDevice *networkDevice = getNetworkDevice(interface);
    if (!networkDevice) {
        return NetworkManagerErrorNetworkInterfaceNotFound;
    }

    QVariantMap ethernetMode {
        {"duplex", "full"}
    };

    QVariantMap connectionSettings {
        {"id", "auto"},
        {"autoconnect", true},
        {"uuid", QUuid::createUuid().toString().remove(QRegularExpression("[{}]"))},
        {"type", "802-3-ethernet"}
    };

    QVariantMap ipv4Settings {
        {"method", "auto"}
    };

    QVariantMap ipv6Settings {
        {"method", "auto"}
    };

    ConnectionSettings settings;
    settings.insert("connection", connectionSettings);
    settings.insert("ipv4", ipv4Settings);
    settings.insert("ipv6", ipv6Settings);
    settings.insert("802-3-ethernet", ethernetMode);

    // Remove old configuration (if there is any)
    foreach (NetworkConnection *connection, m_networkSettings->connections()) {
        if (connection->id() == connectionSettings.value("id")) {
            connection->deleteConnection();
        }
    }

    // Add connection
    QDBusObjectPath connectionObjectPath = m_networkSettings->addConnection(settings);
    if (connectionObjectPath.path().isEmpty())
        return NetworkManagerErrorUnknownError;


    qCDebug(dcNetworkManager()) << "Connection added" << connectionObjectPath.path();

    // Activate connection
    QDBusMessage query = m_networkManagerInterface->call("ActivateConnection",
                                                         QVariant::fromValue(connectionObjectPath),
                                                         QVariant::fromValue(networkDevice->objectPath()),
                                                         QVariant::fromValue(QDBusObjectPath("/")));
    if (query.type() != QDBusMessage::ReplyMessage) {
        qCWarning(dcNetworkManager()) << query.errorName() << query.errorMessage();
        return NetworkManagerErrorUnknownError;
    }

    return NetworkManagerErrorNoError;
}

NetworkManager::NetworkManagerError NetworkManager::createWiredManualConnection(const QString &interface, const QHostAddress &ip, quint8 prefix, const QHostAddress &gateway, const QHostAddress &dns)
{
    qCDebug(dcNetworkManager()) << "Creating manual connection for" << interface << ip << prefix << gateway << dns;

    NetworkDevice *networkDevice = getNetworkDevice(interface);
    if (!networkDevice) {
        return NetworkManagerErrorNetworkInterfaceNotFound;
    }
    if (ip.isNull() || prefix < 8) {
        return NetworkManagerErrorInvalidConfiguration;
    }

    QVariantMap ethernetMode {
        {"duplex", "full"}
    };

    QVariantMap connectionSettings {
        {"id", "manual"},
        {"autoconnect", true},
        {"uuid", QUuid::createUuid().toString().remove(QRegularExpression("[{}]"))},
        {"type", "802-3-ethernet"}
    };

    NMIntListList addresses;
    QList<uint> address;
    address << inet_addr(ip.toString().toLocal8Bit().data());
    address << prefix;
    address << inet_addr(gateway.toString().toLocal8Bit().data());
    addresses << address;

    NMVariantMapList addressData;
    addressData.append({{"address", ip.toString()}, {"prefix", prefix}});

    QVariantMap ipv4Settings {
        {"method", "manual"},
        {"addresses", QVariant::fromValue(addresses)}, // Deprecated but for our supported platforms still required
        {"address-data", QVariant::fromValue(addressData)} // New style, but ignored by NM as long as addresses is still supported and given
    };
    if (!dns.isNull()) {
        ipv4Settings.insert("dns", QVariant::fromValue(NMIntList{inet_addr(dns.toString().toLocal8Bit().data())}));
    }

    // This is ignored if addresses is given, but it's required once the deprecated addresses entry is removed
    if (!gateway.isNull()) {
        ipv4Settings.insert("gateway", gateway.toString());
    }

    QVariantMap ipv6Settings {
        {"method", "auto"}
    };

    ConnectionSettings settings;
    settings.insert("connection", connectionSettings);
    settings.insert("ipv4", ipv4Settings);
    settings.insert("ipv6", ipv6Settings);
    settings.insert("802-3-ethernet", ethernetMode);

    // Remove old configuration (if there is any)
    foreach (NetworkConnection *connection, m_networkSettings->connections()) {
        if (connection->id() == connectionSettings.value("id")) {
            connection->deleteConnection();
        }
    }

    // Add connection
    QDBusObjectPath connectionObjectPath = m_networkSettings->addConnection(settings);
    if (connectionObjectPath.path().isEmpty())
        return NetworkManagerErrorUnknownError;


    qCDebug(dcNetworkManager()) << "Connection added" << connectionObjectPath.path();

    // Activate connection
    QDBusMessage query = m_networkManagerInterface->call("ActivateConnection",
                                                         QVariant::fromValue(connectionObjectPath),
                                                         QVariant::fromValue(networkDevice->objectPath()),
                                                         QVariant::fromValue(QDBusObjectPath("/")));
    if (query.type() != QDBusMessage::ReplyMessage) {
        qCWarning(dcNetworkManager()) << query.errorName() << query.errorMessage();
        return NetworkManagerErrorUnknownError;
    }

    return NetworkManagerErrorNoError;

}

NetworkManager::NetworkManagerError NetworkManager::createSharedConnection(const QString &interface, const QHostAddress &ip, quint8 prefix)
{
    qCDebug(dcNetworkManager()) << "Starting shared connection for" << interface;

    NetworkDevice *networkDevice = getNetworkDevice(interface);
    if (!networkDevice) {
        return NetworkManagerErrorNetworkInterfaceNotFound;
    }

    QVariantMap connectionSettings;
    connectionSettings.insert("id", "shared");
    connectionSettings.insert("autoconnect", true);
    connectionSettings.insert("uuid", QUuid::createUuid().toString().remove("{").remove("}"));
    connectionSettings.insert("type", "802-3-ethernet");

    NMIntListList addresses;
    QList<uint> address;
    address << inet_addr(ip.toString().toLocal8Bit().data());
    address << prefix;
    address << 0;
    addresses << address;

    NMVariantMapList addressData;
    addressData.append({{"address", ip.toString()}, {"prefix", prefix}});

    QVariantMap ipv4Settings {
        {"method", "shared"}
    };
    if (!ip.isNull()) {
        ipv4Settings.insert("addresses", QVariant::fromValue(addresses)); // Deprecated but for our supported platforms still required
        ipv4Settings.insert("address-data", QVariant::fromValue(addressData)); // New style, but ignored by NM as long as addresses is still supported and given
    }

    QVariantMap ipv6Settings {
        {"method", "auto"}
    };

    // Build connection object
    ConnectionSettings settings;
    settings.insert("connection", connectionSettings);
    settings.insert("ipv4", ipv4Settings);
    settings.insert("ipv6", ipv6Settings);

    // Remove old configuration (if there is any)
    foreach (NetworkConnection *connection, m_networkSettings->connections()) {
        if (connection->id() == connectionSettings.value("id")) {
            connection->deleteConnection();
        }
    }

    // Add connection
    QDBusObjectPath connectionObjectPath = m_networkSettings->addConnection(settings);
    if (connectionObjectPath.path().isEmpty())
        return NetworkManagerErrorUnknownError;


    qCDebug(dcNetworkManager()) << "Connection added" << connectionObjectPath.path();

    // Activate connection
    QDBusMessage query = m_networkManagerInterface->call("ActivateConnection",
                                                         QVariant::fromValue(connectionObjectPath),
                                                         QVariant::fromValue(networkDevice->objectPath()),
                                                         QVariant::fromValue(QDBusObjectPath("/")));
    if (query.type() != QDBusMessage::ReplyMessage) {
        qCWarning(dcNetworkManager()) << query.errorName() << query.errorMessage();
        return NetworkManagerErrorUnknownError;
    }

    return NetworkManagerErrorNoError;
}

/*! Returns true if the networking of this \l{NetworkManager} is enabled. */
bool NetworkManager::networkingEnabled() const
{
    return m_networkingEnabled;
}

/*! Returns true if the networking of this \l{NetworkManager} could be \a enabled. */
bool NetworkManager::enableNetworking(bool enabled)
{
    if (m_networkingEnabled == enabled)
        return true;

    QDBusMessage query = m_networkManagerInterface->call("Enable", enabled);
    if(query.type() != QDBusMessage::ReplyMessage) {
        qCWarning(dcNetworkManager()) << query.errorName() << query.errorMessage();
        return false;
    }
    return true;
}

/*! Sets the networking of this \l{NetworkManager} to \a enabled. */
void NetworkManager::setNetworkingEnabled(bool enabled)
{
    if (m_networkingEnabled == enabled)
        return;

    qCDebug(dcNetworkManager()) << "Networking" << (enabled ? "enabled" : "disabled");

    m_networkingEnabled = enabled;
    emit networkingEnabledChanged(m_networkingEnabled);
}

/*! Returns true if the wireless networking of this \l{NetworkManager} is enabled. */
bool NetworkManager::wirelessEnabled() const
{
    return m_wirelessEnabled;
}

/*! Returns true if the wireless networking of this \l{NetworkManager} could be set to \a enabled. */
bool NetworkManager::enableWireless(bool enabled)
{
    if (m_wirelessEnabled == enabled)
        return true;

    return m_networkManagerInterface->setProperty("WirelessEnabled", enabled);
}

void NetworkManager::checkConnectivity()
{
    // Get network devices
    qCDebug(dcNetworkManager()) << "Checking connectivity ...";
    QDBusMessage query = m_networkManagerInterface->call("CheckConnectivity");
    if(query.type() != QDBusMessage::ReplyMessage) {
        qCWarning(dcNetworkManager()) << query.errorName() << query.errorMessage();
        return;
    }

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

    NetworkManagerConnectivityState state = static_cast<NetworkManagerConnectivityState>(query.arguments().at(0).toUInt());
    qCDebug(dcNetworkManager()) << "Checked connectevitiy state successfully:" << query.arguments().at(0).toUInt() << state;
    setConnectivityState(state);
}

void NetworkManager::init()
{
    qCDebug(dcNetworkManager()) << "Initializing network manager";
    // Check DBus connection
    if (!QDBusConnection::systemBus().isConnected()) {
        qCWarning(dcNetworkManager()) << "System DBus not connected. NetworkManager not available.";
        setAvailable(false);
        return;
    }

    // Create interface
    m_networkManagerInterface = new QDBusInterface(NetworkManagerUtils::networkManagerServiceString(), NetworkManagerUtils::networkManagerPathString(), NetworkManagerUtils::networkManagerServiceString(), QDBusConnection::systemBus(), this);
    if(!m_networkManagerInterface->isValid()) {
        qCWarning(dcNetworkManager()) << "Invalid DBus network manager interface. The NetworkManager is not available.";
        delete m_networkManagerInterface;
        m_networkManagerInterface = nullptr;
        setAvailable(false);
        return;
    }

    qCDebug(dcNetworkManager()) << "DBus interface created successfully" << NetworkManagerUtils::networkManagerPathString();

    // Init properties
    qCDebug(dcNetworkManager()) << "Reading initial properties...";
    setVersion(m_networkManagerInterface->property("Version").toString());
    setState(static_cast<NetworkManagerState>(m_networkManagerInterface->property("State").toUInt()));
    setConnectivityState(static_cast<NetworkManagerConnectivityState>(m_networkManagerInterface->property("Connectivity").toUInt()));
    setNetworkingEnabled(m_networkManagerInterface->property("NetworkingEnabled").toBool());
    setWirelessEnabled(m_networkManagerInterface->property("WirelessEnabled").toBool());

    if (m_version.isEmpty()) {
        qCWarning(dcNetworkManager()) << "Could not read initial properties. The NetworkManager might not be initialized yet. Reinitializing in 2 seconds...";
        delete m_networkManagerInterface;
        m_networkManagerInterface = nullptr;
        setAvailable(false);
        QTimer::singleShot(2000, this, &NetworkManager::init);
        return;
    }

    // Connect signals
    QDBusConnection::systemBus().connect(NetworkManagerUtils::networkManagerServiceString(), NetworkManagerUtils::networkManagerPathString(), NetworkManagerUtils::networkManagerServiceString(), "StateChanged", this, SLOT(onStateChanged(uint)));
    QDBusConnection::systemBus().connect(NetworkManagerUtils::networkManagerServiceString(), NetworkManagerUtils::networkManagerPathString(), NetworkManagerUtils::networkManagerServiceString(), "DeviceAdded", this, SLOT(onDeviceAdded(QDBusObjectPath)));
    QDBusConnection::systemBus().connect(NetworkManagerUtils::networkManagerServiceString(), NetworkManagerUtils::networkManagerPathString(), NetworkManagerUtils::networkManagerServiceString(), "DeviceRemoved", this, SLOT(onDeviceRemoved(QDBusObjectPath)));

    // Networkmanager < 1.2.0 uses custom signal instead of the standard D-Bus properties changed signal
    QDBusConnection::systemBus().connect(NetworkManagerUtils::networkManagerServiceString(), NetworkManagerUtils::networkManagerPathString(), NetworkManagerUtils::networkManagerServiceString(), "PropertiesChanged", this, SLOT(processProperties(QVariantMap)));
    // Networkmanager >= 1.2.0 uses standard D-Bus properties changed signal
    QDBusConnection::systemBus().connect(NetworkManagerUtils::networkManagerServiceString(), NetworkManagerUtils::networkManagerPathString(),  "org.freedesktop.DBus.Properties", "PropertiesChanged", this, SLOT(onPropertiesChanged(QString,QVariantMap,QStringList)));

    // Load network devices
    loadDevices();

    // Create settings
    m_networkSettings = new NetworkSettings(this);

    setAvailable(true);
    qCDebug(dcNetworkManager()) << "NetworkManager initialized successfully.";
    qCDebug(dcNetworkManager()) << "Starting initial wireless network scan...";
    foreach (WirelessNetworkDevice *wirelessDevice, m_wirelessNetworkDevices.values()) {
        wirelessDevice->scanWirelessNetworks();
    }
}

void NetworkManager::deinit()
{
    foreach (NetworkDevice *device, m_networkDevices) {
        onDeviceRemoved(device->objectPath());
    }

    m_wiredNetworkDevices.clear();
    m_wirelessNetworkDevices.clear();

    if (m_networkSettings) {
        delete m_networkSettings;
        m_networkSettings  = nullptr;
    }

    if (m_networkManagerInterface) {
        delete m_networkManagerInterface;
        m_networkManagerInterface  = nullptr;
    }

    setVersion(QString());
    setState(NetworkManagerStateUnknown);
    setConnectivityState(NetworkManagerConnectivityStateUnknown);
    setNetworkingEnabled(false);
    setWirelessEnabled(false);
    setAvailable(false);

    qCDebug(dcNetworkManager()) << "NetworkManager deinitialized successfully.";
}

void NetworkManager::loadDevices()
{
    // Get network devices
    qCDebug(dcNetworkManager()) << "Get available devices";
    QDBusMessage query = m_networkManagerInterface->call("GetDevices");
    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 deviceObjectPath = qdbus_cast<QDBusObjectPath>(argument);
        onDeviceAdded(deviceObjectPath);
    }
    argument.endArray();
}

QString NetworkManager::networkManagerStateToString(const NetworkManager::NetworkManagerState &state)
{
    QMetaObject metaObject = NetworkManager::staticMetaObject;
    int enumIndex = metaObject.indexOfEnumerator(QString("NetworkManagerState").toLatin1().data());
    QMetaEnum metaEnum = metaObject.enumerator(enumIndex);
    return QString(metaEnum.valueToKey(state));
}

QString NetworkManager::networkManagerConnectivityStateToString(const NetworkManager::NetworkManagerConnectivityState &state)
{
    QMetaObject metaObject = NetworkManager::staticMetaObject;
    int enumIndex = metaObject.indexOfEnumerator(QString("NetworkManagerConnectivityState").toLatin1().data());
    QMetaEnum metaEnum = metaObject.enumerator(enumIndex);
    return QString(metaEnum.valueToKey(state)).remove("NetworkManagerConnectivityState");
}

void NetworkManager::setAvailable(bool available)
{
    if (m_available == available)
        return;

    qCDebug(dcNetworkManager()) << "The NetworkManager is now" << (available ? "available" : "unavailable");
    m_available = available;
    emit availableChanged(m_available);
}

void NetworkManager::setVersion(const QString &version)
{
    if (m_version == version)
        return;

    qCDebug(dcNetworkManager()) << "Version:" << version;
    m_version = version;
    emit versionChanged(m_version);
}

void NetworkManager::setWirelessEnabled(bool enabled)
{
    if (m_wirelessEnabled == enabled)
        return;

    qCDebug(dcNetworkManager()) << "Wireless networking" << (enabled ? "enabled" : "disabled");
    m_wirelessEnabled = enabled;
    emit wirelessEnabledChanged(m_wirelessEnabled);
}

void NetworkManager::setConnectivityState(const NetworkManager::NetworkManagerConnectivityState &connectivityState)
{
    if (m_connectivityState == connectivityState)
        return;

    qCDebug(dcNetworkManager()) << "Connectivity state changed:" << networkManagerConnectivityStateToString(connectivityState);
    m_connectivityState = connectivityState;
    emit connectivityStateChanged(m_connectivityState);
}

void NetworkManager::setState(const NetworkManager::NetworkManagerState &state)
{
    if (m_state == state)
        return;

    qCDebug(dcNetworkManager()) << "State changed:" << networkManagerStateToString(state);
    m_state = state;
    emit stateChanged(m_state);
    checkConnectivity();
}

void NetworkManager::onServiceRegistered()
{
    qCDebug(dcNetworkManager()) << "DBus service registered and available.";
    init();
}

void NetworkManager::onServiceUnregistered()
{
    qCWarning(dcNetworkManager()) << "DBus service unregistered.";
    deinit();
}

void NetworkManager::onStateChanged(uint state)
{
    setState(static_cast<NetworkManagerState>(state));
}

void NetworkManager::onDeviceAdded(const QDBusObjectPath &deviceObjectPath)
{
    if (m_networkDevices.keys().contains(deviceObjectPath)) {
        qCWarning(dcNetworkManager()) << "Device" << deviceObjectPath.path() << "already added.";
        return;
    }

    // Get device Type
    QDBusInterface networkDeviceInterface(NetworkManagerUtils::networkManagerServiceString(), deviceObjectPath.path(), NetworkManagerUtils::deviceInterfaceString(), QDBusConnection::systemBus());
    if(!networkDeviceInterface.isValid()) {
        qCWarning(dcNetworkManager()) << "NetworkDevice: Invalid DBus device interface" << deviceObjectPath.path();
        return;
    }

    // Create object
    NetworkDevice::NetworkDeviceType deviceType = NetworkDevice::NetworkDeviceType(networkDeviceInterface.property("DeviceType").toUInt());
    switch (deviceType) {
    case NetworkDevice::NetworkDeviceTypeWifi: {
        WirelessNetworkDevice *wirelessNetworkDevice = new WirelessNetworkDevice(deviceObjectPath, this);
        qCDebug(dcNetworkManager()) << "[+]" << wirelessNetworkDevice;
        m_networkDevices.insert(deviceObjectPath, wirelessNetworkDevice);
        m_wirelessNetworkDevices.insert(deviceObjectPath, wirelessNetworkDevice);
        connect(wirelessNetworkDevice, &WirelessNetworkDevice::deviceChanged, this, &NetworkManager::onWirelessDeviceChanged);
        emit wirelessDeviceAdded(wirelessNetworkDevice);
        break;
    }
    case NetworkDevice::NetworkDeviceTypeEthernet: {
        WiredNetworkDevice *wiredNetworkDevice = new WiredNetworkDevice(deviceObjectPath, this);
        qCDebug(dcNetworkManager()) << "[+]" << wiredNetworkDevice;
        m_networkDevices.insert(deviceObjectPath, wiredNetworkDevice);
        m_wiredNetworkDevices.insert(deviceObjectPath, wiredNetworkDevice);

        connect(wiredNetworkDevice, &WiredNetworkDevice::deviceChanged, this, &NetworkManager::onWiredDeviceChanged);
        emit wiredDeviceAdded(wiredNetworkDevice);
        break;
    }
    default:
        NetworkDevice *networkDevice = new NetworkDevice(deviceObjectPath, this);
        qCDebug(dcNetworkManager()) << "[+]" << networkDevice;
        m_networkDevices.insert(deviceObjectPath, networkDevice);
        break;
    }
}

void NetworkManager::onDeviceRemoved(const QDBusObjectPath &deviceObjectPath)
{
    if (!m_networkDevices.keys().contains(deviceObjectPath)) {
        qCWarning(dcNetworkManager()) << "Unknown network device removed:" << deviceObjectPath.path();
        return;
    }

    NetworkDevice *networkDevice = m_networkDevices.take(deviceObjectPath);

    if (m_wiredNetworkDevices.contains(deviceObjectPath)) {
        qCDebug(dcNetworkManager()) << "[-]" << m_wiredNetworkDevices.value(deviceObjectPath);
        m_wiredNetworkDevices.remove(deviceObjectPath);
        emit wiredDeviceRemoved(networkDevice->interface());
    } else if (m_wirelessNetworkDevices.contains(deviceObjectPath)) {
        qCDebug(dcNetworkManager()) << "[-]" << m_wirelessNetworkDevices.value(deviceObjectPath);
        m_wirelessNetworkDevices.remove(deviceObjectPath);
        if (!wirelessAvailable())
            emit wirelessAvailableChanged(wirelessAvailable());

        emit wirelessDeviceRemoved(networkDevice->interface());
    } else {
        qCDebug(dcNetworkManager()) << "[-]" << networkDevice;
    }

    networkDevice->deleteLater();
}

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

void NetworkManager::processProperties(const QVariantMap &properties)
{
    if (properties.contains("Version"))
        setVersion(properties.value("Version").toString());

    if (properties.contains("State"))
        setState(static_cast<NetworkManagerState>(properties.value("State").toUInt()));

    if (properties.contains("Connectivity"))
        setConnectivityState(NetworkManagerConnectivityState(properties.value("Connectivity").toUInt()));

    if (properties.contains("NetworkingEnabled"))
        setNetworkingEnabled(properties.value("NetworkingEnabled").toBool());

    if (properties.contains("WirelessEnabled"))
        setWirelessEnabled(properties.value("WirelessEnabled").toBool());

}

void NetworkManager::onWirelessDeviceChanged()
{
    WirelessNetworkDevice *networkDevice = qobject_cast<WirelessNetworkDevice *>(sender());
    emit wirelessDeviceChanged(networkDevice);
}

void NetworkManager::onWiredDeviceChanged()
{
    WiredNetworkDevice *networkDevice = qobject_cast<WiredNetworkDevice *>(sender());
    emit wiredDeviceChanged(networkDevice);
}

void NetworkManager::start()
{
    if (m_available) {
        qCDebug(dcNetworkManager()) << "NetworkManager already running.";
        return;
    }
    qCDebug(dcNetworkManager()) << "Starting the NetworkManager.";

    init();
}

void NetworkManager::stop()
{
    qCDebug(dcNetworkManager()) << "Stop the NetworkManager.";
    if (!m_available) {
        qCDebug(dcNetworkManager()) << "NetworkManager already stopped.";
        return;
    }

    deinit();
}

