/*
 * Copyright (C) 2014, 2015 Canonical Ltd.
 *
 * Authors:
 *    Jussi Pakkanen <jussi.pakkanen@canonical.com>
 *    Jonas G. Drange <jonas.drange@canonical.com>
 *
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 3, as published
 * by the Free Software Foundation.
 *
 * This library 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 General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include <nmofono/hotspot-manager.h>
#include <qrepowerd/qrepowerd.h>
#include <NetworkManagerActiveConnectionInterface.h>
#include <NetworkManagerDeviceInterface.h>
#include <NetworkManagerInterface.h>
#include <NetworkManagerSettingsInterface.h>
#include <NetworkManagerSettingsConnectionInterface.h>
#include <URfkillInterface.h>
#include <util/localisation.h>

#include <QStringList>
#include <QDBusReply>
#include <QtDebug>
#include <QDBusInterface>
#include <QDBusMetaType>
#include <QRandomGenerator>
#include <QRegularExpression>
#include <NetworkManager.h>

#ifndef DEFAULT_PASSWORD_LEN
#define DEFAULT_PASSWORD_LEN 12
#endif // !DEFAULT_PASSWORD_LEN

// Interfacing with Mechanicd/Android WiFi HAL
#define WIFI_GET_FW_PATH_STA  0
#define WIFI_GET_FW_PATH_AP 1
#define WIFI_GET_FW_PATH_P2P  2

using namespace std;

namespace {
    // TODO: barely any of these codes are used. Do we really need all of these?
    QString reasonToString (int reason) {
        switch (reason) {
            case 0:
                return _("Unknown error");
            case 1:
                return _("No reason given");
            case 2:
                return _("Device is now managed");
            case 3:
                return _("Device is now unmanaged");
            case 4:
                return _("The device could not be readied for configuration");
            case 5:
                return _("IP configuration could not be reserved (no available address, timeout, etc.)");
            case 6:
                return _("The IP configuration is no longer valid");
            case 7:
                return _("Your authentication details were incorrect");
            case 8:
                return _("802.1X supplicant disconnected");
            case 9:
                return _("802.1X supplicant configuration failed");
            case 10:
                return _("802.1X supplicant failed");
            case 11:
                return _("802.1X supplicant took too long to authenticate");
            case 15:
                return _("DHCP client failed to start");
            case 16:
                return _("DHCP client error");
            case 17:
                return _("DHCP client failed");
            case 18:
                return _("Shared connection service failed to start");
            case 19:
                return _("Shared connection service failed");
            case 35:
                return _("Necessary firmware for the device may be missing");
            case 36:
                return _("The device was removed");
            case 37:
                return _("NetworkManager went to sleep");
            case 38:
                return _("The device's active connection disappeared");
            case 39:
                return _("Device disconnected by user or client");
            case 41:
                return _("The device's existing connection was assumed");
            case 42:
                return _("The supplicant is now available");
            case 43:
                return _("The modem could not be found");
            case 44:
                return _("The Bluetooth connection failed or timed out");
            case 50:
                return _("A dependency of the connection failed");
            case 52:
                return _("ModemManager is unavailable");
            case 53:
                return _("The Wi-Fi network could not be found");
            case 54:
                return _("A secondary connection of the base connection failed");
            default:
                return _("Unknown");
        }
    }
}

namespace nmofono
{

struct ApDevice
{
    ApDevice (const QDBusObjectPath& path, const QString& interface) :
            m_path (path), m_interface (interface)
    {
    }

    QDBusObjectPath m_path;
    QString m_interface;
};

class HotspotManager::Priv: public QObject
{

public:
    Priv(HotspotManager& parent) :
        p(parent)
    {
    }

    void addConnection()
    {
        qDebug() << "Adding new hotspot connection";
        QVariantDictMap connection = createConnectionSettings(m_ssid, m_password,
                                                              m_mode, m_auth);

        auto add_connection_reply = m_settings->AddConnection(connection);
        add_connection_reply.waitForFinished();

        if (add_connection_reply.isError())
        {
            qCritical() << "Failed to add connection: "
                    << add_connection_reply.error().message();
            Q_EMIT p.reportError(0);
            m_hotspot.reset();

            setStored(false);
            return;
        }

        QDBusObjectPath connectionPath(add_connection_reply);

        m_hotspot = make_shared<
                OrgFreedesktopNetworkManagerSettingsConnectionInterface>(
                NM_DBUS_SERVICE, connectionPath.path(), m_manager->connection());

        setStored(true);
    }

    void updateConnection()
    {
        qDebug() << "Updating hotspot connection";
        // Get new settings
        QVariantDictMap new_settings = createConnectionSettings(m_ssid,
                                                                m_password,
                                                                m_mode, m_auth);
        auto updating = m_hotspot->Update(new_settings);
        updating.waitForFinished();
        if (!updating.isValid())
        {
            qCritical()
                    << "Could not update connection:"
                    << updating.error().message();
        }
    }

    bool activateConnection(const QDBusObjectPath& device)
    {
        auto activeConnectionObject = m_activeConnectionManager->activate(QDBusObjectPath(m_hotspot->path()), device);
        if (!activeConnectionObject)
        {
            qCritical() << "Could not activate hotspot connection";
            return false;
        }

        OrgFreedesktopNetworkManagerConnectionActiveInterface activeConnection (
                    NM_DBUS_SERVICE, activeConnectionObject->path().path (),
                    m_manager->connection());

        int count = 0;
        // Wait for connection to activate. Waits 100 ms x 100 = 10 secs.
        while (count < 100 && activeConnection.state() != NM_ACTIVE_CONNECTION_STATE_ACTIVATED)
        {
            if (count % 10 == 0) // Only log once every second
                qDebug() << "Waiting for hotspot to connect";
            QThread::msleep(100);
            ++count;
        }

        return (activeConnection.state() == NM_ACTIVE_CONNECTION_STATE_ACTIVATED);
    }

    /**
     * Enables a hotspot.
     */
    void enable(const QDBusObjectPath& device)
    {
        if (!m_hotspot)
        {
            qWarning() << "Could not find a hotspot setup to enable";
            return;
        }

        qDebug() << "Activating hotspot on device" << device.path();
        bool success = activateConnection(device);
        setEnable(success);
        if (success)
        {
            // If our connection gets booted, reconnect
            connect(m_activeConnectionManager.get(),
                    &connection::ActiveConnectionManager::connectionsUpdated, this,
                    &Priv::reactivateConnection,
                    Qt::QueuedConnection);
        }
    }

    /**
     * Disables a hotspot.
     */
    void disable()
    {
        disconnect(m_activeConnectionManager.get(),
                   &connection::ActiveConnectionManager::connectionsUpdated,
                   this, &Priv::reactivateConnection);

        auto activeConnection = getActiveConnection();
        if (activeConnection)
        {
            m_activeConnectionManager->deactivate(activeConnection);
        }

        setInterfaceFirmware("sta");

        setEnable(false);
    }

    void setStored(bool value)
    {
        if (m_stored != value)
        {
            m_stored = value;
            Q_EMIT p.storedChanged(value);
        }
    }

    void setEnable(bool value)
    {
        if (m_enabled != value)
        {
            m_enabled = value;
            // Request or clear the wakelock, depending on the hotspot state
            if (value)
            {
                m_wakelock = m_powerd->requestSysState(
                        "connectivity-service", QRepowerd::SysPowerState::active);
            }
            else
            {
                m_wakelock.reset();
            }
            Q_EMIT p.enabledChanged(value);
        }
    }

    void updateSettingsFromDbus()
    {
        setEnable(isHotspotActive());
        setDisconnectWifi(m_enabled);

        QVariantDictMap settings = getConnectionSettings(*m_hotspot);
        const char wifi_key[] = "802-11-wireless";
        const char security_key[] = "802-11-wireless-security";

        if (settings.find(wifi_key) != settings.end())
        {
            QByteArray ssid = settings[wifi_key]["ssid"].toByteArray();
            if (!ssid.isEmpty())
            {
                p.setSsid(ssid);
            }

            QString mode = settings[wifi_key]["mode"].toString();
            if (!mode.isEmpty())
            {
                p.setMode(mode);
            }
        }

        QVariantDictMap secrets = getConnectionSecrets(*m_hotspot,
                                                       security_key);

        if (secrets.find(security_key) != secrets.end())
        {
            QString pwd = secrets[security_key]["psk"].toString();
            if (!pwd.isEmpty())
            {
                p.setPassword(pwd);
            }
        } else {
            p.setAuth("none");
        }
    }

    // wpa_supplicant interaction

    QString getTetheringInterface()
    {
        QString program("getprop");
        QStringList arguments;
        arguments << "wifi.tethering.interface";

        QProcess getprop;
        getprop.start(program, arguments);

        if (!getprop.waitForFinished())
        {
            qCritical() << "getprop process failed:" << getprop.errorString();
            return QString();
        }

        QString output = getprop.readAllStandardOutput();
        // Take just the first line
        return output.split("\n").first();
    }

    /**
     * True if changed successfully, or there was no need. Otherwise false.
     * Supported modes are 'p2p', 'sta' and 'ap'.
     */
    bool setInterfaceFirmware(const QString& mode)
    {
    #ifdef ENABLE_MECHANICD
        qint32 mode_i;

        if (mode == "sta")
            mode_i = WIFI_GET_FW_PATH_STA;
        else if (mode == "ap")
            mode_i = WIFI_GET_FW_PATH_AP;
        else if (mode == "p2p")
            mode_i = WIFI_GET_FW_PATH_P2P;
        else
            return false;

        QDBusInterface mechanicdIface(DBusTypes::MECHANICD_DBUS_NAME,
                                 DBusTypes::MECHANICD_DBUS_WIFI_PATH,
                                 DBusTypes::MECHANICD_DBUS_WIFI_INTERFACE,
                                 m_manager->connection());

        QDBusReply<qint32> ret = mechanicdIface.call(
                "requestWifiModeSwitch", mode_i);

        if (!ret.isValid())
        {
            qCritical() << "Failed to change interface firmware:"
                    << ret.error().message();
            return false;
        }

        // Mechanicd returns error code directly from wifi_change_fw_path(),
        // need to also check that.
        auto ret_i = ret.value();
        if (ret_i != 0) {
            qCritical() << "Failed to change interface firmware "
                           "(mechanicd returns" << ret_i << ")";
            return false;
        }

    #else
        Q_UNUSED(mode);
    #endif

        return true;
    }

    void findApDevice()
    {
        m_device.reset();
        QString tetherIface = getTetheringInterface();

        auto devices = QList<QDBusObjectPath>(m_manager->GetDevices());

        // Iterate in reverse to attempt to minimise dbus calls (new device is likely at the end)
        for (auto path = devices.rbegin(); path != devices.rend(); ++path)
        {
            OrgFreedesktopNetworkManagerDeviceInterface device(NM_DBUS_SERVICE, path->path(), m_manager->connection());

            QString interface = device.interface();

            if (!tetherIface.isEmpty())
            {
                if (tetherIface.compare(interface) != 0)
                {
                    continue;
                }
            }

            if (device.deviceType() != NM_DEVICE_TYPE_WIFI)
            {
                continue;
            }

            if (device.state() <= NM_DEVICE_STATE_UNAVAILABLE)
            {
                continue;
            }

            qDebug() << "Using AP interface " << interface;
            m_device = make_unique<ApDevice>(*path, interface);
            break;
        }
    }

    void createApDevice()
    {
        setInterfaceFirmware(m_mode);

        m_device.reset();

        int count = 0;
        // Wait for AP device to appear
        while (count < 20 && !m_device)
        {
            QThread::msleep(100);
            findApDevice();
            qDebug() << "Searching for AP device";
            ++count;
        }
    }

    // wpa_supplicant interaction

    /**
     * Helper that maps QStrings to other QVariantMaps, i.e.
     * QMap<QString, QVariantMap>. QVariantMap is an alias for
     * QMap<QString, QVariant>.
     * See http://doc.qt.io/qt-5/qvariant.html#QVariantMap-typedef and
     * https://developer.gnome.org/NetworkoManager/0.9/spec.html
     *     #type-String_String_Variant_Map_Map
     */
    QVariantDictMap createConnectionSettings(
        const QByteArray &ssid, const QString &password,
        QString mode, QString auth)
    {
        bool autoConnect = false;

        QVariantDictMap connection;

        QString s_ssid = QString::fromLatin1(ssid);

        if (m_uuid.isEmpty()) {
            m_uuid = QUuid().createUuid().toString();
            // Remove {} from the generated uuid.
            m_uuid.remove(0, 1);
            m_uuid.remove(m_uuid.size() - 1, 1);
        }

        QVariantMap wireless;

        if (auth != "none")
        {
            wireless[QStringLiteral("security")] = QVariant(QStringLiteral("802-11-wireless-security"));
        }
        wireless[QStringLiteral("ssid")] = QVariant(ssid);
        wireless[QStringLiteral("mode")] = QVariant(mode);

        connection["802-11-wireless"] = wireless;

        QVariantMap connsettings;
        connsettings[QStringLiteral("autoconnect")] = QVariant(autoConnect);
        connsettings[QStringLiteral("id")] = QVariant(s_ssid);
        connsettings[QStringLiteral("uuid")] = QVariant(m_uuid);
        connsettings[QStringLiteral("type")] = QVariant(QStringLiteral("802-11-wireless"));
        connection["connection"] = connsettings;

        QVariantMap ipv4;
        ipv4[QStringLiteral("method")] = QVariant(QStringLiteral("shared"));
        connection["ipv4"] = ipv4;

        QVariantMap ipv6;
        ipv6[QStringLiteral("method")] = QVariant(QStringLiteral("ignore"));
        connection["ipv6"] = ipv6;

        if (auth != "none")
        {
            QVariantMap security;
            security[QStringLiteral("proto")] = QVariant(QStringList{ "rsn" });
            security[QStringLiteral("pairwise")] = QVariant(QStringList{ "ccmp" });
            security[QStringLiteral("group")] = QVariant(QStringList{ "ccmp" });
            security[QStringLiteral("key-mgmt")] = QVariant(auth);
            security[QStringLiteral("psk")] = QVariant(password);
            connection["802-11-wireless-security"] = security;
        }

        return connection;
    }

    /**
     * Helper that returns a QMap<QString, QVariantMap> given a QDBusObjectPath.
     * See https://developer.gnome.org/NetworkManager/0.9/spec.html
     *     #org.freedesktop.NetworkManager.Settings.Connection.GetSettings
     */
    QVariantDictMap getConnectionSettings (OrgFreedesktopNetworkManagerSettingsConnectionInterface& conn) {
        auto connection_settings = conn.GetSettings();
        connection_settings.waitForFinished();
        return connection_settings.value();
    }


    /**
     * Helper that returns a QMap<QString, QVariantMap> given a QDBusObjectPath.
     * See https://developer.gnome.org/NetworkManager/0.9/spec.html
     *     #org.freedesktop.NetworkManager.Settings.Connection.GetSettings
     */
    QVariantDictMap getConnectionSecrets (OrgFreedesktopNetworkManagerSettingsConnectionInterface& conn,
        const QString key)
    {
        auto connection_secrets = conn.GetSecrets(key);
        connection_secrets.waitForFinished();
        return connection_secrets.value();
    }

    /**
     * Returns a QDBusObjectPath of a hotspot given a mode.
     * Valid modes are 'p2p', 'ap' and 'adhoc'.
     */
    void getHotspot()
    {
        const char wifi_key[] = "802-11-wireless";

        auto listed_connections = m_settings->ListConnections();
        listed_connections.waitForFinished();

        for (const auto &connection : listed_connections.value())
        {
            auto conn = make_shared<OrgFreedesktopNetworkManagerSettingsConnectionInterface>(
                    NM_DBUS_SERVICE, connection.path(),
                    m_manager->connection());

            auto connection_settings = getConnectionSettings(*conn);

            if (connection_settings.find(wifi_key) != connection_settings.end())
            {
                auto wifi_setup = connection_settings[wifi_key];
                QString wifi_mode = wifi_setup["mode"].toString();

                if (wifi_mode == m_mode)
                {
                    m_hotspot = conn;
                    m_uuid = connection_settings["connection"]["uuid"].toString();
                    return;
                }
            }
        }
        m_hotspot.reset();
        m_uuid = QString();
    }

    connection::ActiveConnection::SPtr getActiveConnection()
    {
        connection::ActiveConnection::SPtr activeConnection;

        if (m_hotspot)
        {
            for (const auto &active_connection : m_activeConnectionManager->connections())
            {
                if (active_connection->connectionPath().path() == m_hotspot->path())
                {
                    activeConnection = active_connection;
                    break;
                }
            }
        }

        return activeConnection;
    }

    /**
     * Helper to check if the hotspot on a given QDBusObjectPath is active
     * or not. It checks if the Connection.Active [1] for the given
     * path is in NetworkManager's ActiveConnections property [2].
     * [1] https://developer.gnome.org/NetworkManager/0.9/spec.html
     *     #org.freedesktop.NetworkManager.Connection.Active
     * [2] https://developer.gnome.org/NetworkManager/0.9/spec.html
     *     #org.freedesktop.NetworkManager
     */
    bool isHotspotActive ()
    {
        return bool(getActiveConnection());
    }

    void generatePassword()
    {
        static const QString symbols = QLatin1String("01234567890"
            "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz");
        QString result;
        QRandomGenerator *grng = QRandomGenerator::global();

        for (int i = 0; i < DEFAULT_PASSWORD_LEN; i++)
        {
            result.push_back(symbols[grng->bounded(symbols.length())]);
        }

        m_password = result;
    }

    void setDisconnectWifi(bool disconnect)
    {
        if (m_disconnectWifi == disconnect)
        {
            return;
        }

        m_disconnectWifi = disconnect;
        Q_EMIT p.disconnectWifiChanged(m_disconnectWifi);
    }

public Q_SLOTS:
    void reactivateConnection()
    {
        if (!m_hotspot)
        {
            qWarning() << "Could not find a hotspot setup to enable";
            return;
        }

        auto activeConnection = getActiveConnection();
        if (activeConnection)
        {
            return;
        }


        findApDevice();

        if (m_device)
        {
            qDebug() << "Reactivating hotspot connection on device" << m_device->m_path.path();
            activateConnection(m_device->m_path);
        }
        else
        {
            qWarning() << "Could not get device when reactivating hotspot connection";
        }
    }

    void sendErrorNotification(int reason)
    {
        const QString summary = _("Failed to enable hotspot");
        const QString message = reasonToString(reason);
        const QString icon = QStringLiteral("hotspot-disabled");

        m_notificationManager->notify(summary, message, icon,
            /* actions */ {}, /* hints */ {}, /* timeout */ 5)->show();
    }

public:
    HotspotManager& p;

    QString m_mode = "ap";
    QString m_auth = "wpa-psk";
    bool m_enabled = false;
    bool m_stored = false;
    QString m_password;
    QByteArray m_ssid = "Lomiri";

    unique_ptr<ApDevice> m_device;

    QRepowerd::UPtr m_powerd;
    QRepowerd::RequestSPtr m_wakelock;

    bool m_disconnectWifi = false;

    /**
     * NetworkManager dbus interface proxy we will use to query
     * against NetworkManager. See
     * https://developer.gnome.org/NetworkManager/0.9/spec.html
     *     #org.freedesktop.NetworkManager
     */
    unique_ptr<OrgFreedesktopNetworkManagerInterface> m_manager;

    /**
     * NetworkManager Settings interface proxy we use to get
     * the list of connections, as well as adding connections.
     * See https://developer.gnome.org/NetworkManager/0.9/spec.html
     *     #org.freedesktop.NetworkManager.Settings
     */
    unique_ptr<OrgFreedesktopNetworkManagerSettingsInterface> m_settings;

    shared_ptr<OrgFreedesktopNetworkManagerSettingsConnectionInterface> m_hotspot;

    QString m_uuid;

    connection::ActiveConnectionManager::SPtr m_activeConnectionManager;

    notify::NotificationManager::SPtr m_notificationManager;
};

HotspotManager::HotspotManager(connection::ActiveConnectionManager::SPtr activeConnectionManager,
                               notify::NotificationManager::SPtr notificationManager,
                               const QDBusConnection& connection,
                               QObject *parent) :
        QObject(parent), d(new Priv(*this))
{
    d->m_activeConnectionManager = activeConnectionManager;
    d->m_notificationManager = notificationManager;

    d->m_manager = make_unique<OrgFreedesktopNetworkManagerInterface>(
            NM_DBUS_SERVICE, NM_DBUS_PATH, connection);
    d->m_settings = make_unique<OrgFreedesktopNetworkManagerSettingsInterface>(
            NM_DBUS_SERVICE, NM_DBUS_PATH_SETTINGS, connection);

    d->m_powerd = make_unique<QRepowerd>(connection);

    d->generatePassword();

    // Stored is false if hotspot path is empty.
    d->getHotspot();
    d->setStored(bool(d->m_hotspot));

    if (d->m_stored)
    {
        d->updateSettingsFromDbus();
    }

    QObject::connect(this, &HotspotManager::reportError, d.get(), &Priv::sendErrorNotification);
}

void HotspotManager::setEnabled(bool value)
{
    if (enabled() == value)
    {
        return;
    }

    // We are enabling a hotspot
    if (value)
    {
        // If the SSID is empty, we report an error.
        if (d->m_ssid.isEmpty())
        {
            qWarning() << "  SSID was empty";
            Q_EMIT reportError(1);
            d->setEnable(false);
            return;
        }

        d->setDisconnectWifi(true);

        // We use Hybris to load the new device firmware
        d->createApDevice();

        if (!d->m_device)
        {
            qWarning() << "Failed to create AP device";
            Q_EMIT reportError(1);
            d->setDisconnectWifi(false);
            return;
        }

        if (d->m_stored)
        {
            d->updateConnection();
        }
        else
        {
            d->addConnection();
        }
        d->enable(d->m_device->m_path);
    }

    // We disable the hotspot if requested to be disabled, or if requested to be
    // enabled but we fail to enable it. We can do this as d->enable() is fully
    // synchronous (sigh).
    if (!value || (value && !enabled()))
    {
        d->disable();

        d->setDisconnectWifi(false);

        if (value) {
            // FIXME: is this a good code?
            Q_EMIT reportError(NM_DEVICE_STATE_REASON_CONFIG_FAILED);
        }
    }

}

bool HotspotManager::enabled() const {
    return d->m_enabled;
}

bool HotspotManager::stored() const {
    return d->m_stored;
}

QByteArray HotspotManager::ssid() const {
    return d->m_ssid;
}

void HotspotManager::setSsid(const QByteArray& value) {
    if (d->m_ssid != value)
    {
        d->m_ssid = value;
        Q_EMIT ssidChanged(value);
    }
}

QString HotspotManager::password() const {
    return d->m_password;
}

void HotspotManager::setPassword(const QString& value) {
    if (d->m_password != value)
    {
        d->m_password = value;
        Q_EMIT passwordChanged(value);
    }
}

QString HotspotManager::mode() const {
    return d->m_mode;
}

QString HotspotManager::auth() const {
    return d->m_auth;
}

QString HotspotManager::interface() const
{
    if (enabled() && d->m_device)
        return d->m_device->m_interface;
    else
        return QString();
}

void HotspotManager::setMode(const QString& value) {
    if (d->m_mode != value)
    {
        d->m_mode = value;
        Q_EMIT modeChanged(value);
    }
}

void HotspotManager::setAuth(const QString& value) {
    if (d->m_auth != value)
    {
        d->m_auth = value;
        Q_EMIT authChanged(value);
    }
}

bool HotspotManager::disconnectWifi() const
{
    return d->m_disconnectWifi;
}

}
