//
// Copyright 2017 Ettus Research (National Instruments)
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// 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 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 .
//
#include "netd_impl.hpp"
#include <../device3/device3_impl.hpp>
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace uhd;
netd_mboard_impl::netd_mboard_impl(const std::string& addr)
    : rpc(addr, MPM_RPC_PORT)
{
    std::map _dev_info =
        rpc.call("get_device_info");
    device_info =
        dict(_dev_info.begin(), _dev_info.end());
    // Get initial claim on mboard
    _rpc_token = rpc.call("claim", "UHD - Session 01"); // make this configurable with device_addr?
    if (_rpc_token.empty()){
        throw uhd::value_error("netd device claiming failed!");
    }
    _claimer_task = task::make([this] {
        if (not this->claim()) {
            throw uhd::value_error("netd device reclaiming loop failed!");
        };
        boost::this_thread::sleep_for(boost::chrono::milliseconds(1000));
    });
    // std::vector data_ifaces =
    //     rpc.call>("get_interfaces", _rpc_token);
    // discover path to device and tell MPM our MAC address seen at the data
    // interfaces
    // move this into make_transport
    //for (const auto& iface : data_ifaces) {
        //std::vector addrs = rpc.call>(
            //"get_interface_addrs", _rpc_token, iface);
        //for (const auto& iface_addr : addrs) {
            //if (rpc_client(iface_addr, MPM_RPC_PORT)
                    //.call("probe_interface", _rpc_token)) {
                //data_interfaces.emplace(iface, iface_addr);
                //break;
            //}
        //}
    //}
}
uhd::sid_t netd_mboard_impl::allocate_sid(const uint16_t port,
                                          const uhd::sid_t address,
                                          const uint32_t xbar_src_addr,
                                          const uint32_t xbar_src_port){
    const uint32_t sid = rpc.call("allocate_sid", _rpc_token, port,
                                            address.get(), xbar_src_addr, xbar_src_port);
    return sid;
}
netd_mboard_impl::~netd_mboard_impl() {}
netd_mboard_impl::uptr netd_mboard_impl::make(const std::string& addr)
{
    netd_mboard_impl::uptr mb =
        netd_mboard_impl::uptr(new netd_mboard_impl(addr));
    // implicit move
    return mb;
}
bool netd_mboard_impl::claim() { return rpc.call("reclaim", _rpc_token); }
netd_impl::netd_impl(const device_addr_t& device_addr) :
    usrp::device3_impl(), _sid_framer(0)
{
    UHD_LOGGER_INFO("NETD") << "NETD initialization sequence...";
    _tree->create("/name").set("NETD - Series device");
    const device_addrs_t device_args = separate_device_addr(device_addr);
    _mb.reserve(device_args.size());
    for (size_t mb_i = 0; mb_i < device_args.size(); ++mb_i) {
        _mb.push_back(setup_mb(mb_i, device_args[mb_i]));
    }
    try {
        enumerate_rfnoc_blocks(
          0,
          1,
          3, /* base port */
          uhd::sid_t(0x0200),
          device_addr
        );
    } catch (...) {
        printf("%s Derp\n", __func__);
    }
}
netd_impl::~netd_impl() {}
netd_mboard_impl::uptr netd_impl::setup_mb(const size_t mb_i,
                                           const uhd::device_addr_t& dev_addr)
{
    const fs_path mb_path = "/mboards/" + std::to_string(mb_i);
    netd_mboard_impl::uptr mb = netd_mboard_impl::make(dev_addr["addr"]);
    mb->initialization_done = false;
    std::vector addrs;
    const std::string eth0_addr = dev_addr["addr"];
    _tree->create(mb_path / "name")
        .set(mb->device_info.get("type", ""));
    _tree->create(mb_path / "serial")
        .set(mb->device_info.get("serial", ""));
    _tree->create(mb_path / "connection")
        .set(mb->device_info.get("connection", "remote"));
    for (const std::string& key : dev_addr.keys()) {
        if (key.find("recv") != std::string::npos)
            mb->recv_args[key] = dev_addr[key];
        if (key.find("send") != std::string::npos)
            mb->send_args[key] = dev_addr[key];
    }
    // Do real MTU discovery (something similar like X300 but with MPM)
    _tree->create(mb_path / "mtu/recv").set(1500);
    _tree->create(mb_path / "mtu/send").set(1500);
    _tree->create(mb_path / "link_max_rate").set(1e9 / 8);
    // query more information about FPGA/MPM
    // Call init on periph_manager, this will init the dboards/mboard, maybe
    // even selfcal and everything
    // Query time/clock sources on mboards/dboards
    // Throw rpc calls with boost bind into the property tree?
    // Query rfnoc blocks on the device (MPM may know about them?)
    // call enumerate rfnoc_blocks on the device
    // configure radio?
    // implicit move
    return mb;
}
// frame_size_t determine_max_frame_size(const std::string &addr,
//                                       const frame_size_t &user_frame_size){
//     transport::udp_simple::sptr udp =
//     transport::udp_simple::make_connected(addr,
//                                                                             std::to_string(MPM_DISCOVERY_PORT));
//     std::vector buffer(std::max(user_frame_size.rec))
// }
// Everything fake below here
both_xports_t netd_impl::make_transport(const sid_t& address,
                                        usrp::device3_impl::xport_type_t xport_type,
                                        const uhd::device_addr_t& args)
{
    //const size_t mb_index = address.get_dst_addr();
    size_t mb_index = 0;
    both_xports_t xports;
    xports.endianness = uhd::ENDIANNESS_BIG;
    const uhd::device_addr_t& xport_args = (xport_type == CTRL) ? uhd::device_addr_t() : args;
    transport::zero_copy_xport_params default_buff_args;
    /*
    std::cout << address << std::endl;
    std::cout << address.get_src_addr() << std::endl;
    std::cout << address.get_dst_addr() << std::endl;
    */
    std::string interface_addr = "192.168.10.2";
    const uint32_t xbar_src_addr = address.get_src_addr();
    const uint32_t xbar_src_dst = 0;
    default_buff_args.send_frame_size = 8000;
    default_buff_args.recv_frame_size = 8000;
    default_buff_args.num_recv_frames = 32;
    default_buff_args.num_send_frames = 32;
    // hardcode frame size for now
    transport::udp_zero_copy::buff_params buff_params;
    auto recv = transport::udp_zero_copy::make(
        interface_addr,
        BOOST_STRINGIZE(49153),
        default_buff_args,
        buff_params,
        xport_args);
    uint16_t port  = recv->get_local_port();
    xports.send_sid = _mb[mb_index]->allocate_sid(port, address, xbar_src_addr, xbar_src_dst);
    xports.recv_sid = xports.send_sid.reversed();
    xports.recv_buff_size = buff_params.recv_buff_size;
    xports.send_buff_size = buff_params.send_buff_size;
    xports.recv = recv;
    xports.send = xports.recv;
    return xports;
}
device_addrs_t netd_find_with_addr(const device_addr_t& hint_)
{
    transport::udp_simple::sptr comm = transport::udp_simple::make_broadcast(
        hint_["addr"], std::to_string(MPM_DISCOVERY_PORT));
    comm->send(
        boost::asio::buffer(&MPM_DISCOVERY_CMD, sizeof(MPM_DISCOVERY_CMD)));
    device_addrs_t addrs;
    while (true) {
        char buff[4096] = {};
        const size_t nbytes = comm->recv(boost::asio::buffer(buff), 0.050);
        if (nbytes == 0) {
            break;
        }
        const char* reply = (const char*)buff;
        std::string reply_string = std::string(reply);
        std::vector result;
        boost::algorithm::split(result, reply_string,
                                [](const char& in) { return in == ';'; },
                                boost::token_compress_on);
        if (result.empty()) {
            continue;
        }
        // who else is reposending to our request !?
        if (result[0] != "USRP-MPM") {
            continue;
        }
        const std::string recv_addr = comm->get_recv_addr();
        // remove external iface addrs if executed directly on device
        bool external_iface = false;
        for (const auto& addr : transport::get_if_addrs()) {
            if ((addr.inet == comm->get_recv_addr()) &&
                recv_addr !=
                    boost::asio::ip::address_v4::loopback().to_string()) {
                external_iface = true;
            }
        }
        if (external_iface) {
            continue;
        }
        device_addr_t new_addr;
        new_addr["addr"] = recv_addr;
        new_addr["type"] = "netd"; // hwd will overwrite this
        // remove ident string and put other informations into device_addr dict
        result.erase(result.begin());
        // parse key-value pairs in the discovery string and add them to the
        // device_addr
        for (const auto& el : result) {
            std::vector value;
            boost::algorithm::split(value, el,
                                    [](const char& in) { return in == '='; },
                                    boost::token_compress_on);
            new_addr[value[0]] = value[1];
        }
        addrs.push_back(new_addr);
    }
    return addrs;
};
device_addrs_t netd_find(const device_addr_t& hint_)
{
    // handle cases:
    //
    //  - empty hint
    //  - multiple addrs
    //  - single addr
    device_addrs_t hints = separate_device_addr(hint_);
    // either hints has:
    // multiple entries
    //   -> search for multiple devices and join them back into one
    //   device_addr_t
    // one entry with addr:
    //   -> search for one device with this addr
    // one
    // multiple addrs
    if (hints.size() > 1) {
        device_addrs_t found_devices;
        found_devices.reserve(hints.size());
        for (const auto& hint : hints) {
            if (not hint.has_key("addr")) { // maybe allow other attributes as well
                return device_addrs_t();
            }
            device_addrs_t reply_addrs = netd_find_with_addr(hint);
            if (reply_addrs.size() > 1) {
                throw uhd::value_error(
                    str(boost::format("Could not resolve device hint \"%s\" to "
                                      "a single device.") %
                        hint.to_string()));
            } else if (reply_addrs.empty()) {
                return device_addrs_t();
            }
            found_devices.push_back(reply_addrs[0]);
        }
        return device_addrs_t(1, combine_device_addrs(found_devices));
    }
    hints.resize(1);
    device_addr_t hint = hints[0];
    device_addrs_t addrs;
    if (hint.has_key("addr")) {
        // is this safe?
        return netd_find_with_addr(hint);
    }
    for (const transport::if_addrs_t& if_addr : transport::get_if_addrs()) {
        device_addr_t new_hint = hint;
        new_hint["addr"] = if_addr.bcast;
        device_addrs_t reply_addrs = netd_find_with_addr(new_hint);
        addrs.insert(addrs.begin(), reply_addrs.begin(), reply_addrs.end());
    }
    return addrs;
}
static device::sptr netd_make(const device_addr_t& device_addr)
{
    return device::sptr(boost::make_shared(device_addr));
}
UHD_STATIC_BLOCK(register_netd_device)
{
    device::register_device(&netd_find, &netd_make, device::USRP);
}
// vim: sw=4 expandtab: