/*
 * Copyright (C) 2019 Koen Zandberg
 *
 * This file is subject to the terms and conditions of the GNU Lesser
 * General Public License v2.1. See the file LICENSE in the top level
 * directory for more details.
 */

/**
 * @ingroup usbus_cdc_ecm
 * @{
 * @file Netdev implementation for ethernet control model
 *
 * @author  Koen Zandberg <koen@bergzand.net>
 * @}
 */

#define USB_H_USER_IS_RIOT_INTERNAL

#include <assert.h>
#include <string.h>

#include "kernel_defines.h"
#include "iolist.h"
#include "luid.h"
#include "mutex.h"
#include "net/ethernet.h"
#include "net/eui48.h"
#include "net/netdev.h"
#include "net/netdev/eth.h"
#include "usb/usbus/cdc/ecm.h"

#define ENABLE_DEBUG 0
#include "debug.h"

static const netdev_driver_t netdev_driver_cdcecm;

static void _signal_rx_flush(usbus_cdcecm_device_t *cdcecm)
{
    usbus_event_post(cdcecm->usbus, &cdcecm->rx_flush);
}

static void _signal_tx_xmit(usbus_cdcecm_device_t *cdcecm)
{
    usbus_event_post(cdcecm->usbus, &cdcecm->tx_xmit);
}

static usbus_cdcecm_device_t *_netdev_to_cdcecm(netdev_t *netdev)
{
    return container_of(netdev, usbus_cdcecm_device_t, netdev);
}

void cdcecm_netdev_setup(usbus_cdcecm_device_t *cdcecm)
{
    cdcecm->netdev.driver = &netdev_driver_cdcecm;
}

static int _send(netdev_t *netdev, const iolist_t *iolist)
{
    assert(iolist);
    usbus_cdcecm_device_t *cdcecm = _netdev_to_cdcecm(netdev);
    uint8_t *buf = cdcecm->ep_in->ep->buf;
    const iolist_t *iolist_start = iolist;
    size_t len = iolist_size(iolist);
    /* interface with alternative function ID 1 is the interface containing the
     * data endpoints, no sense trying to transmit data if it is not active */
    if (cdcecm->active_iface != 1) {
        return -ENOTCONN;
    }
    DEBUG("CDC_ECM_netdev: sending %u bytes\n", len);
    /* load packet data into FIFO */
    size_t iol_offset = 0;
    size_t usb_offset = 0;
    size_t usb_remain = cdcecm->ep_in->ep->len;
    DEBUG("CDC_ECM_netdev: cur iol: %d\n", iolist->iol_len);
    while (len) {
        mutex_lock(&cdcecm->out_lock);
        if (iolist->iol_len - iol_offset > usb_remain) {
            /* Only part of the iolist can be copied, usb_remain bytes */
            memcpy(buf + usb_offset, (uint8_t *)iolist->iol_base + iol_offset,
                   usb_remain);

            usb_offset =  cdcecm->ep_in->maxpacketsize;
            len -= usb_remain;
            iol_offset += usb_remain;
            usb_remain = 0;
        }
        else {
            size_t bytes_copied = iolist->iol_len - iol_offset;
            /* Full iolist can be copied */
            memcpy(buf + usb_offset, (uint8_t *)iolist->iol_base + iol_offset,
                   bytes_copied);
            len -= bytes_copied;
            usb_offset += bytes_copied;
            usb_remain -= bytes_copied;
            iol_offset = iolist->iol_len;
        }
        if (iol_offset == iolist->iol_len) {
            /* Current iolist exhausted */
            iolist = iolist->iol_next;
            if (iolist) {
                DEBUG("CDC_ECM: cur iol: %d\n", iolist->iol_len);
            }
            iol_offset = 0;
        }
        if (usb_remain == 0 || !len) {
            cdcecm->tx_len = usb_offset;
            /* USB frame full or last frame, flush! */

            DEBUG("CDC_ECM_NETDEV: triggering xmit with len %d\n",
                  cdcecm->tx_len);
            _signal_tx_xmit(cdcecm);
            usb_remain = cdcecm->ep_in->maxpacketsize;
            usb_offset = 0;
        }
        else {
            mutex_unlock(&cdcecm->out_lock);
        }
    }
    /* Zero length USB packet required */
    if ((iolist_size(iolist_start) % cdcecm->ep_in->maxpacketsize) == 0) {
        mutex_lock(&cdcecm->out_lock);
        DEBUG("CDC ECM netdev: Zero length USB packet required\n");
        cdcecm->tx_len = 0;
        _signal_tx_xmit(cdcecm);
    }
    return len;
}

static int _recv(netdev_t *netdev, void *buf, size_t max_len, void *info)
{
    (void)info;
    usbus_cdcecm_device_t *cdcecm = _netdev_to_cdcecm(netdev);

    size_t pktlen = cdcecm->len;

    if (max_len == 0 && buf == NULL) {
        return pktlen;
    }
    if (max_len && buf == NULL) {
        _signal_rx_flush(cdcecm);
        return pktlen;
    }
    if (pktlen <= max_len) {
        memcpy(buf, cdcecm->in_buf, pktlen);
    }
    _signal_rx_flush(cdcecm);
    return (pktlen <= max_len) ? (int)pktlen : -ENOBUFS;
}

static int _init(netdev_t *netdev)
{
    usbus_cdcecm_device_t *cdcecm = _netdev_to_cdcecm(netdev);

    luid_get_eui48((eui48_t*)cdcecm->mac_netdev);
    return 0;
}

static int _get(netdev_t *netdev, netopt_t opt, void *value, size_t max_len)
{
    usbus_cdcecm_device_t *cdcecm = _netdev_to_cdcecm(netdev);

    (void)max_len;

    switch (opt) {
        case NETOPT_ADDRESS:
            assert(max_len >= ETHERNET_ADDR_LEN);
            memcpy(value, cdcecm->mac_netdev, ETHERNET_ADDR_LEN);
            return ETHERNET_ADDR_LEN;
        default:
            return netdev_eth_get(netdev, opt, value, max_len);
    }
}

static int _set(netdev_t *netdev, netopt_t opt, const void *value,
                size_t value_len)
{
    usbus_cdcecm_device_t *cdcecm = _netdev_to_cdcecm(netdev);

    (void)cdcecm;

    switch (opt) {
        case NETOPT_ADDRESS:
            assert(value_len == ETHERNET_ADDR_LEN);
            memcpy(cdcecm->mac_netdev, value, ETHERNET_ADDR_LEN);
            return ETHERNET_ADDR_LEN;
        default:
            return netdev_eth_set(netdev, opt, value, value_len);
    }
}

static void _isr(netdev_t *dev)
{
    usbus_cdcecm_device_t *cdcecm = _netdev_to_cdcecm(dev);

    if (cdcecm->len) {
        cdcecm->netdev.event_callback(&cdcecm->netdev,
                                      NETDEV_EVENT_RX_COMPLETE);
    }
}

static const netdev_driver_t netdev_driver_cdcecm = {
    .send = _send,
    .recv = _recv,
    .init = _init,
    .isr = _isr,
    .get = _get,
    .set = _set,
};
