/*
 * Copyright © 2013 Canonical Ltd.
 *
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License version 3,
 * as published by the Free Software Foundation.
 *
 * This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Authors:
 *     Antti Kaijanmäki <antti.kaijanmaki@canonical.com>
 */

#include <nmofono/wifi/wifi-link-impl.h>
#include <nmofono/wifi/access-point-impl.h>
#include <nmofono/wifi/grouped-access-point.h>
#include <url-dispatcher-cpp/url-dispatcher.h>
#include <cassert>

#include <NetworkManagerDeviceWirelessInterface.h>
#include <NetworkManagerSettingsConnectionInterface.h>
#include <PropertiesInterface.h>

#include <NetworkManager.h>
#include <iostream>
#include <QUrlQuery>

using namespace std;

namespace nmofono
{
namespace wifi
{

struct WifiLinkImpl::Private: public QObject
{
    Q_OBJECT

public:
    Private(WifiLinkImpl& parent,
            shared_ptr<OrgFreedesktopNetworkManagerDeviceInterface> dev,
            shared_ptr<OrgFreedesktopNetworkManagerInterface> nm,
            WifiToggle::SPtr wifiToggle)
       : p(parent),
         m_dev(dev),
         m_wireless(NM_DBUS_SERVICE, dev->path(), dev->connection()),
         m_nm(nm),
         m_wifiToggle(wifiToggle),
         m_lastState(NM_STATE_UNKNOWN),
         m_connecting(false)
    {
    }

    WifiLinkImpl& p;

    uint32_t m_characteristics = Link::Characteristics::empty;
    Link::Status m_status = Status::disabled;
    bool m_isManaged = true;
    QSet<AccessPointImpl::Ptr> m_rawAccessPoints;
    QSet<AccessPoint::Ptr> m_groupedAccessPoints;
    AccessPoint::Ptr m_activeAccessPoint;
    Signal m_signal = Signal::disconnected;

    shared_ptr<OrgFreedesktopNetworkManagerDeviceInterface> m_dev;
    OrgFreedesktopNetworkManagerDeviceWirelessInterface m_wireless;
    shared_ptr<OrgFreedesktopNetworkManagerInterface> m_nm;
    connection::ActiveConnectionManager::SPtr m_activeConnectionManager;

    WifiToggle::SPtr m_wifiToggle;

    map<AccessPointImpl::Key, shared_ptr<GroupedAccessPoint>> m_grouper;
    uint32_t m_lastState = 0;
    QString m_name;
    connection::ActiveConnection::SPtr m_activeConnection;
    unique_ptr<QMetaObject::Connection> m_signalStrengthConnection;
    bool m_connecting = false;
    bool m_disconnectWifi = false;

    void setStatus(Status status)
    {
        if (m_status == status)
        {
            return;
        }

        m_status = status;
        Q_EMIT p.statusUpdated(m_status);
    }

    void setIsManaged(bool value)
    {
        if (m_isManaged == value)
        {
            return;
        }

        m_isManaged = value;
        Q_EMIT p.isManagedChanged(m_isManaged);
    }

    void updateDeviceState(uint new_state)
    {
        m_lastState = new_state;

        if (new_state == NM_DEVICE_STATE_UNMANAGED) {
            // Mark unmanaged adapters as such.
            setIsManaged(false);
        }

        switch (new_state){
        case NM_DEVICE_STATE_DISCONNECTED:
        case NM_DEVICE_STATE_DEACTIVATING:
            if (m_connecting) {
                // ignore these while doing connect_to()
                break;
            }
            /* fallthrough */
        case NM_DEVICE_STATE_UNKNOWN:
        case NM_DEVICE_STATE_UNMANAGED:
        case NM_DEVICE_STATE_UNAVAILABLE:
        case NM_DEVICE_STATE_FAILED:
        {
            // make sure to set activeConnection before changing the status
            updateActiveConnection(connection::ActiveConnection::SPtr());

            switch(m_wifiToggle->state()) {
            case WifiToggle::State::hard_blocked:
            case WifiToggle::State::soft_blocked:
                setStatus(Status::disabled);
                break;
            case WifiToggle::State::not_available:
            case WifiToggle::State::unblocked:
                setStatus(Status::offline);
            }
            break;
        }
        case NM_DEVICE_STATE_PREPARE:
        case NM_DEVICE_STATE_CONFIG:
        case NM_DEVICE_STATE_NEED_AUTH:
        case NM_DEVICE_STATE_IP_CONFIG:
        case NM_DEVICE_STATE_IP_CHECK:
        {
            // make sure to set activeConnection before changing the status
            QDBusObjectPath path = m_dev->activeConnection();
            // for some reason the path is not always set on these
            // states. Let's not clear the active connection as obviously
            // we have one.
            if (path != QDBusObjectPath("/"))
            {
                updateActiveConnection(m_activeConnectionManager->connection(path));
            }
            setStatus(Status::connecting);
            break;
        }
        case NM_DEVICE_STATE_SECONDARIES:
        {
            // make sure to set activeConnection before changing the status
            updateActiveConnection(m_activeConnectionManager->connection(m_dev->activeConnection()));
            setStatus(Status::connected);
            break;
        }
        case NM_DEVICE_STATE_ACTIVATED:
        {
            // make sure to set activeConnection before changing the status
            updateActiveConnection(m_activeConnectionManager->connection(m_dev->activeConnection()));
            setStatus(Status::online);
            break;
        }}

    }

    void disconnectSignalStengthConnection()
    {
        if (m_signalStrengthConnection)
        {
            disconnect(*m_signalStrengthConnection);
            m_signalStrengthConnection.reset();
        }
    }

    void updateActiveConnection(connection::ActiveConnection::SPtr activeConnection)
    {
        // clear the one we have.
        if (!activeConnection) {
            m_activeAccessPoint.reset();
            Q_EMIT p.activeAccessPointUpdated(m_activeAccessPoint);
            m_activeConnection.reset();
            disconnectSignalStengthConnection();
            strengthUpdated();
            return;
        }

        // already up-to-date
        if (m_activeConnection && m_activeConnection->path() == activeConnection->path())
        {
            return;
        }

        m_activeConnection = activeConnection;
        auto state = m_activeConnection->state();
        switch (state) {
        case connection::ActiveConnection::State::unknown:
        case connection::ActiveConnection::State::activating:
        case connection::ActiveConnection::State::activated:
        case connection::ActiveConnection::State::deactivating:
        case connection::ActiveConnection::State::deactivated:
            // for Wi-Fi devices specific_object is the AccessPoint object.
            QDBusObjectPath ap_path = m_activeConnection->specificObject();
            for (auto &ap : m_groupedAccessPoints) {
                auto shap =  dynamic_pointer_cast<GroupedAccessPoint>(ap);
                if (shap->has_object(ap_path)) {
                    m_activeAccessPoint = ap;
                    disconnectSignalStengthConnection();
                    m_signalStrengthConnection = make_unique<
                            QMetaObject::Connection>(
                            connect(m_activeAccessPoint.get(),
                                    &AccessPoint::strengthUpdated, this,
                                    &Private::strengthUpdated));
                    Q_EMIT p.activeAccessPointUpdated(m_activeAccessPoint);
                    strengthUpdated();
                    break;
                }
            }
        }
}


public Q_SLOTS:
    void ap_added(const QDBusObjectPath &path)
    {
        try {
            for (auto ap : m_rawAccessPoints) {
                if (dynamic_pointer_cast<AccessPoint>(ap)->object_path() == path) {
                    // already in the list
                    return;
                }
            }

            AccessPointImpl::Ptr shap;
            try {
                auto ap = make_shared<
                        OrgFreedesktopNetworkManagerAccessPointInterface>(
                        NM_DBUS_SERVICE, path.path(), m_dev->connection());
                auto ap_prop = make_shared<
                        OrgFreedesktopDBusPropertiesInterface>(
                        NM_DBUS_SERVICE, path.path(), m_dev->connection());
                shap = make_shared<AccessPointImpl>(ap, ap_prop);
            } catch(const exception &e) {
                qWarning() << "Failed to create AccessPoint proxy for "<< path.path() << ": ";
                qWarning() << "\t" << QString::fromStdString(e.what());
                qWarning() << "\tIgnoring.";
                return;
            }

            m_rawAccessPoints.insert(shap);

            auto k = AccessPointImpl::Key(shap);
            if(m_grouper.find(k) != m_grouper.end()) {
                m_grouper[k]->add_ap(shap);
            } else {
                m_grouper[k] = make_shared<GroupedAccessPoint>(shap);
            }
            update_grouped_access_points();
        } catch(const exception &e) {
            /// @bug dbus-cpp internal logic exploded
            // If this happens, lomiri-indicator-network is in an unknown state with no clear way of
            // recovering. The only reasonable way out is a graceful exit.
            cerr << " Failed to run dbus service: " << e.what() << endl;
        }
    }

    void ap_removed(const QDBusObjectPath &path)
    {
        AccessPointImpl::Ptr shap;

        auto list = m_rawAccessPoints;
        for (const auto &ap : list) {
            if (ap->object_path() == path) {
                shap = ap;
                list.remove(ap);
                break;
            }
        }
        if (!shap) {
            qWarning() << "Tried to remove access point " << path.path() << " that has not been added.";
            return;
        }
        m_rawAccessPoints = list;

        AccessPointImpl::Key k(shap);
        auto it = m_grouper.find(k);
        if (it != m_grouper.end())
        {
            it->second->remove_ap(shap);
            if (it->second->num_aps() == 0)
            {
                m_grouper.erase(it);
            }
        }
        update_grouped_access_points();
    }

    void update_grouped_access_points()
    {
        m_groupedAccessPoints.clear();
        for (auto &i : m_grouper)
        {
            m_groupedAccessPoints.insert(i.second);
        }

        if (m_disconnectWifi)
        {
            Q_EMIT p.accessPointsUpdated(QSet<AccessPoint::Ptr>());
        }
        else
        {
            Q_EMIT p.accessPointsUpdated(m_groupedAccessPoints);
        }
    }

    void state_changed(uint new_state, uint, uint)
    {
        updateDeviceState(new_state);
    }

    void wifiToggleChanged()
    {
        updateDeviceState(m_lastState);
    }

    void strengthUpdated()
    {
        Signal signal = Signal::disconnected;

        if (m_activeAccessPoint && !m_disconnectWifi)
        {
            double strength = m_activeAccessPoint->strength();
            bool secured  = m_activeAccessPoint->secured();

            if (strength >= 80.0)
            {
                signal = secured ? Signal::signal_100_secure : Signal::signal_100;
            }
            else if (strength >= 60.0)
            {
                signal = secured ? Signal::signal_75_secure : Signal::signal_75;
            }
            else if (strength >= 40.0)
            {
                signal = secured ? Signal::signal_50_secure : Signal::signal_50;
            }
            else if (strength >= 20.0)
            {
                signal = secured ? Signal::signal_25_secure : Signal::signal_25;
            }
            else
            {
                signal = secured ? Signal::signal_0_secure : Signal::signal_0;
            }
        }

        if (m_signal == signal)
        {
            return;
        }

        m_signal = signal;
        Q_EMIT p.signalUpdated(m_signal);
    }
};

WifiLinkImpl::WifiLinkImpl(shared_ptr<OrgFreedesktopNetworkManagerDeviceInterface> dev,
           shared_ptr<OrgFreedesktopNetworkManagerInterface> nm,
           WifiToggle::SPtr wifiToggle,
           connection::ActiveConnectionManager::SPtr activeConnectionManager)
    : d(new Private(*this, dev, nm, wifiToggle)) {
    d->m_name = d->m_dev->interface();
    d->m_activeConnectionManager = activeConnectionManager;

    connect(&d->m_wireless, &OrgFreedesktopNetworkManagerDeviceWirelessInterface::AccessPointAdded, d.get(), &Private::ap_added);
    connect(&d->m_wireless, &OrgFreedesktopNetworkManagerDeviceWirelessInterface::AccessPointRemoved, d.get(), &Private::ap_removed);
    QList<QDBusObjectPath> aps = d->m_wireless.GetAccessPoints();
    for (const auto& path : aps) {
        d->ap_added(path);
    }

    connect(d->m_dev.get(), &OrgFreedesktopNetworkManagerDeviceInterface::StateChanged, d.get(), &Private::state_changed);
    d->updateDeviceState(d->m_dev->state());

    connect(d->m_wifiToggle.get(), &WifiToggle::stateChanged, d.get(), &Private::wifiToggleChanged);

    d->strengthUpdated();
}

WifiLinkImpl::~WifiLinkImpl()
{}

Link::Type
WifiLinkImpl::type() const
{
    return Type::wifi;
}

uint32_t
WifiLinkImpl::characteristics() const
{
    return d->m_characteristics;
}

Link::Status
WifiLinkImpl::status() const
{
    return d->m_status;
}

bool
WifiLinkImpl::isManaged() const
{
    return d->m_isManaged;
}

Link::Id
WifiLinkImpl::id() const
{
    return 0;
}

QString
WifiLinkImpl::name() const
{
    return d->m_name;
}

WifiLink::Mode WifiLinkImpl::mode() const
{
    static const map<uint, WifiLink::Mode> modeMap
    {
        {NM_802_11_MODE_UNKNOWN, Mode::unknown},
        {NM_802_11_MODE_ADHOC, Mode::adhoc},
        {NM_802_11_MODE_INFRA, Mode::infra},
        {NM_802_11_MODE_AP, Mode::ap}
    };
    return modeMap.find(d->m_wireless.mode())->second;
}

WifiLink::Signal WifiLinkImpl::signal() const
{
    return d->m_signal;
}

QSet<AccessPoint::Ptr>
WifiLinkImpl::accessPoints() const {
    if (d->m_disconnectWifi)
    {
        return QSet<AccessPoint::Ptr>();
    }

    return d->m_groupedAccessPoints;
}

void
WifiLinkImpl::connect_to(AccessPoint::Ptr accessPoint)
{
    qDebug() << "Connecting to:" << accessPoint->ssid();

    d->m_connecting = true;
    QByteArray ssid = accessPoint->raw_ssid();

    shared_ptr<OrgFreedesktopNetworkManagerSettingsConnectionInterface> found;
    QList<QDBusObjectPath> connections = d->m_dev->availableConnections();
    for (auto &path : connections) {
        auto con = make_shared<OrgFreedesktopNetworkManagerSettingsConnectionInterface>(
                NM_DBUS_SERVICE, path.path(), d->m_dev->connection());
        QVariantDictMap settings = con->GetSettings();
        auto wirelessIt = settings.find("802-11-wireless");
        if (wirelessIt != settings.cend())
        {
            auto ssidIt = wirelessIt->find("ssid");
            if (ssidIt != wirelessIt->cend())
            {
                if (ssidIt->toByteArray() == ssid)
                {
                    found = con;
                    break;
                }
            }
        }
    }

    /// @todo check the timestamps as there might be multiple ones that are suitable.
    /// @todo oh, and check more parameters than just the ssid

    connection::ActiveConnection::SPtr ac;
    if (found) {
        qDebug() << "Connecting to known access point";
        ac = d->m_activeConnectionManager->activate(QDBusObjectPath(found->path()),
                                       QDBusObjectPath(d->m_dev->path()),
                                       accessPoint->object_path());
    } else {
        if (accessPoint->enterprise()) {
            qDebug() << "New connection to enterprise access point";
            // activate system settings URI
            QUrlQuery q;
            q.addQueryItem("ssid", accessPoint->raw_ssid());
            q.addQueryItem("bssid", accessPoint->bssid());
            QString url = "settings:///system/wifi?" + q.query(QUrl::FullyEncoded);

            UrlDispatcher::send(url.toStdString(), [](string url, bool success) {
                if (!success) {
                    cerr << "URL Dispatcher failed on " << url << endl;
                }
            });
        } else {
            qDebug() << "New connection to regular access point";
            QVariantDictMap conf;

            /// @todo getting the ssid multiple times over dbus is stupid.

            QVariantMap wireless_conf;
            wireless_conf["ssid"] = ssid;

            conf["802-11-wireless"] = wireless_conf;
            ac = d->m_activeConnectionManager->addAndActivate(conf, QDBusObjectPath(d->m_dev->path()), accessPoint->object_path());
        }
    }
    if (!ac)
    {
        qWarning() << " Failed to activate connection";
        return;
    }

    // For enterprise access points, the system settings app will perform the connection
    if (!accessPoint->enterprise()) {
        d->updateActiveConnection(ac);
    }
    d->m_connecting = false;
}

AccessPoint::Ptr
WifiLinkImpl::activeAccessPoint()
{
    return d->m_activeAccessPoint;
}

QDBusObjectPath
WifiLinkImpl::device_path() const {
    return QDBusObjectPath(d->m_dev->path());
}

void
WifiLinkImpl::setDisconnectWifi(bool disconnect)
{
    if (disconnect == d->m_disconnectWifi)
    {
        return;
    }

    d->m_disconnectWifi = disconnect;

    d->m_dev->setAutoconnect(!disconnect);
    if (disconnect && d->m_activeConnection)
    {
        // Disconnect from the current network
        d->m_nm->DeactivateConnection(
                QDBusObjectPath(d->m_activeConnection->path()));
    }

    d->update_grouped_access_points();
    d->strengthUpdated();
}

}
}

#include "wifi-link-impl.moc"
