/*
 * 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 "link.h"
#include "access-point.h"

using namespace platform::nmofono::wifi;
namespace networking = com::ubuntu::connectivity::networking;
namespace dbus = core::dbus;
namespace fdo = org::freedesktop;
namespace NM = fdo::NetworkManager;

using core::Property;

Link::Link(NM::Interface::Device dev,
           NM::Interface::NetworkManager nm)
    : m_dev(dev),
      m_wireless(dev.object),
      m_nm(nm),
      m_flightMode(false)
{   
    m_characteristics.set(Link::Characteristics::empty);
    m_status.set(Status::disabled);

    m_name = m_dev.device_interface->get();

    std::set<std::shared_ptr<networking::wifi::AccessPoint>> list;
    for (auto access_point : dev.get_access_points()) {
        list.insert(std::make_shared<AccessPoint>(access_point));
    }
    m_accessPoints.set(list);

    updateDeviceState(m_dev.state->get());
    m_dev.state_changed->connect([this](NM::Interface::Device::Signal::StateChanged::ArgumentType args) {
        // StateChanged ( u: new_state, u: old_state, u: reason )
        std::uint32_t new_state = std::get<0>(args);
        // std::uint32_t old_state = std::get<1>(args);
        // std::uint32_t reason    = std::get<2>(args);
        updateDeviceState(new_state);
    });

    m_wireless.access_point_added->connect([this](const dbus::types::ObjectPath &path) {
        for (auto ap : m_accessPoints.get()) {
            if (std::dynamic_pointer_cast<AccessPoint>(ap)->m_ap.object->path() == path) {
                // already in the list
                return;
            }
        }

        auto list = m_accessPoints.get();
        NM::Interface::AccessPoint ap(m_nm.service->object_for_path(path));
        list.insert(std::make_shared<AccessPoint>(ap));
        m_accessPoints.set(list);
    });
    m_wireless.access_point_removed->connect([this](const dbus::types::ObjectPath &path) {
        auto list = m_accessPoints.get();
        for (auto ap : list) {
            if (std::dynamic_pointer_cast<AccessPoint>(ap)->m_ap.object->path() == path) {
                list.erase(ap);
                m_accessPoints.set(list);
                return;
            }
        }
    });


}

Link::~Link()
{}


void
Link::enable()
{
    std::cout << "Link::enable()" << std::endl;

    /// @todo see comment in disable()
    m_flightMode = false;
    m_nm.wireless_enabled->set(true);
#if 0
    /** @todo:
     * NetworkManager 0.9.9 allows doing this below and letting NM to figure out the best connection:
     *
     * m_nm.activate_connection(dbus::types::ObjectPath("/"),
     *                          m_dev.object->path(),
     *                          core::dbus::typ es::ObjectPath("/"));
     *
     * for now we get somewhat similar result by toggling the Autoconnect property,
     * but this breaks the possible setting we will have for autoconnecting later.
     */
    m_dev.autoconnect->set(true);
    m_disabled = false;
#endif
}

void
Link::disable()
{
    std::cout << "Link::disable()" << std::endl;

    /// @todo for now just disable wireless completely.
    ///        this only works properly when there is one wifi adapter on the system
    m_flightMode = true; /// @todo remove this hack once NM properly detects external rfkill
    m_nm.wireless_enabled->set(false);
#if 0
    /** @todo remove this after NM 0.9.9 is available. check comment in enable() */
    m_dev.autoconnect->set(false);

    m_dev.disconnect();
    m_disabled = true;
#endif
}

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

const Property<std::uint32_t>&
Link::characteristics()
{
    return m_characteristics;
}

const Property<networking::Link::Status>&
Link::status()
{
    return m_status;
}

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

std::string
Link::name() const
{
    return m_name;
}

const Property<std::set<std::shared_ptr<networking::wifi::AccessPoint> > >&
Link::accessPoints()
{
    return m_accessPoints;
}

void
Link::connect_to(std::shared_ptr<networking::wifi::AccessPoint> accessPoint)
{
    std::shared_ptr<AccessPoint> ap = std::dynamic_pointer_cast<AccessPoint>(accessPoint);

    NM::Interface::Connection *found = nullptr;
    auto connections = m_dev.get_available_connections();
    for (auto &con : connections) {
        for (auto map : con.get_settings()) {
            if (map.first == "802-11-wireless") {
                for (auto conf : map.second) {
                    if (conf.first == "ssid") {
                        std::vector<int8_t> value;
                        value = conf.second.as<std::vector<std::int8_t>>();
                        if (value == ap->m_ap.ssid->get()) {
                            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

    core::dbus::types::ObjectPath ac;
    if (found) {
        ac = m_nm.activate_connection(found->object->path(),
                                      m_dev.object->path(),
                                      ap->m_ap.object->path());
    } else {
        std::map<std::string, std::map<std::string, dbus::types::Variant> > conf;

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

        std::map<std::string, dbus::types::Variant> wireless_conf;
        wireless_conf["ssid"] = dbus::types::Variant::encode<std::vector<std::int8_t>>(ap->m_ap.ssid->get());

        conf["802-11-wireless"] = wireless_conf;
        auto ret = m_nm.add_and_activate_connection(conf,
                                                    m_dev.object->path(),
                                                    ap->m_ap.object->path());
        ac = std::get<1>(ret);
    }
    updateActiveConnection(ac);
}

const Property<std::shared_ptr<networking::wifi::AccessPoint> >&
Link::activeAccessPoint()
{
    return m_activeAccessPoint;
}

void
Link::updateDeviceState(std::uint32_t new_state)
{
    m_lastState = new_state;
    switch (new_state){
    case NM_DEVICE_STATE_UNKNOWN:
    case NM_DEVICE_STATE_UNMANAGED:
    case NM_DEVICE_STATE_UNAVAILABLE:
    case NM_DEVICE_STATE_DISCONNECTED:
    case NM_DEVICE_STATE_DEACTIVATING:
    case NM_DEVICE_STATE_FAILED:
    {
        /// @todo need to find a way to actually disable individual devices in NM
        ///       this is just a work-around as NM does not detect external rfkill
        if (m_flightMode)
            m_status.set(Status::disabled);
        else
            m_status.set(Status::offline);

        updateActiveConnection(core::dbus::types::ObjectPath("/"));
        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:
    {
        m_status.set(Status::connecting);

        auto path = m_dev.active_connection->get();
        // 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 != core::dbus::types::ObjectPath("/"))
            updateActiveConnection(path);
        break;
    }
    case NM_DEVICE_STATE_SECONDARIES:
    {
        m_status.set(Status::connected);
        updateActiveConnection(m_dev.active_connection->get());
        break;
    }
    case NM_DEVICE_STATE_ACTIVATED:
    {
        m_status.set(Status::online);
        updateActiveConnection(m_dev.active_connection->get());
        break;
    }}

}

/// '/' path means invalid.
void
Link::updateActiveConnection(const core::dbus::types::ObjectPath &path)
{
    // clear the one we have.
    if (path == core::dbus::types::ObjectPath("/")) {
        m_activeAccessPoint.set(com::ubuntu::connectivity::networking::wifi::AccessPoint::Ptr());
        m_activeConnection.reset();
        return;
    }

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

    try {
        m_activeConnection =
                std::make_shared<NM::Interface::ActiveConnection>(
                    m_dev.service->object_for_path(path));
        auto state = m_activeConnection->get_state();
        switch (state) {
        case NM::Interface::ActiveConnection::State::unknown:
        case NM::Interface::ActiveConnection::State::activating:
        case NM::Interface::ActiveConnection::State::activated:
        case NM::Interface::ActiveConnection::State::deactivating:
        case NM::Interface::ActiveConnection::State::deactivated:
            ;

            // for Wi-Fi devices specific_object is the AccessPoint object.
            auto ap_path = m_activeConnection->specific_object->get();
            for (auto ap : m_accessPoints.get()) {
                auto nm_ap = std::dynamic_pointer_cast<AccessPoint>(ap);
                if (nm_ap->m_ap.object->path() == ap_path) {
                    m_activeAccessPoint.set(ap);
                    break;
                }
            }
        }
    } catch (std::exception &e) {
        std::cerr << "failed to get active connection:" << std::endl
                  << "\tpath: " << path.as_string() << std::endl
                  << "\t" << e.what() << std::endl;
    }
}

void
Link::setFlightMode(bool value)
{
    m_flightMode = value;
    updateDeviceState(m_lastState);
}

