/*
 * Copyright (C) 2021 Freie Universität Berlin
 *
 * 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 fido2_ctap_transport_hid
 * @{
 * @file
 *
 * @author      Nils Ollrogge <nils.ollrogge@fu-berlin.de>
 * @}
 */

#include <string.h>

#include "xtimer.h"
#include "usb/usbus.h"
#include "usb/usbus/hid_io.h"

#include "fido2/ctap.h"
#include "fido2/ctap/transport/hid/ctap_hid.h"
#include "fido2/ctap/ctap_utils.h"

#define ENABLE_DEBUG    (0)
#include "debug.h"

/**
 * @brief CTAP HID report descriptor
 *
 * CTAP specification (version 20190130) section 8.1.8.2
 */
const uint8_t _hid_report_desc[] = {
    0x06, 0xD0, 0xF1,   /**< HID_UsagePage ( FIDO_USAGE_PAGE ) */
    0x09, 0x01,         /**< HID_Usage ( FIDO_USAGE_CTAPHID ) */
    0xA1, 0x01,         /**< HID_Collection ( HID_Application ) */
    0x09, 0x20,         /**< HID_Usage ( FIDO_USAGE_DATA_IN ) */
    0x15, 0x00,         /**< HID_LogicalMin ( 0 ) */
    0x26, 0xFF, 0x00,   /**< HID_LogicalMaxS ( 0xff ) */
    0x75, 0x08,         /**< HID_ReportSize ( 8 ) */
    0x95, 0x40,         /**< HID_ReportCount ( HID_INPUT_REPORT_BYTES ) */
    0x81, 0x02,         /**< HID_Input ( HID_Data | HID_Absolute | HID_Variable ) */
    0x09, 0x21,         /**< HID_Usage ( FIDO_USAGE_DATA_OUT ) */
    0x15, 0x00,         /**< HID_LogicalMin ( 0 ) */
    0x26, 0xFF, 0x00,   /**< HID_LogicalMaxS ( 0xff ) */
    0x75, 0x08,         /**< HID_ReportSize ( 8 ) */
    0x95, 0x40,         /**< HID_ReportCount ( HID_OUTPUT_REPORT_BYTES ) */
    0x91, 0x02,         /**< HID_Output ( HID_Data | HID_Absolute | HID_Variable ) */
    0xC0,               /**< HID_EndCollection */
};

/**
 * @brief CTAP_HID buffer struct
 *
 */
typedef struct {
    uint32_t cid;                           /**< channel identifier */
    uint8_t cmd;                            /**< CTAP_HID command */
    uint8_t buffer[CTAP_HID_BUFFER_SIZE];   /**< data buffer */
    uint16_t offset;                        /**< current offset into data buffer */
    int16_t seq;                            /**< current sequence number */
    uint16_t bcnt;                          /**< expected amount of bytes to be received */
    uint8_t err;                            /**< error type if error  */
    bool is_locked;                         /**< buffer is locked by transaction */
    bool should_cancel;                     /**< flag if current transaction should be cancelled */
} ctap_hid_state_t;

/**
 * @brief Serialize data and transmit it via USB HID layer
 */
static void _ctap_hid_write(uint8_t cmd, uint32_t cid, const void *_data, size_t size);

/**
 * @brief CTAPHID_CBOR command
 *
 * CTAP specification (version 20190130) section 8.1.9.1.2
 */
static void _handle_cbor_packet(uint8_t cmd, uint32_t cid, uint8_t *buf, uint16_t bcnt);

/**
 * @brief CTAPHID_INIT command
 *
 * CTAP specification (version 20190130) section 8.1.9.1.3
 */
static uint32_t _handle_init_packet(uint32_t cid, uint16_t bcnt,
                                    const uint8_t *nonce);

/**
 * @brief CTAPHID_WINK command
 *
 * CTAP specification (version 20190130) section 8.1.9.2.1
 */
static void _wink(uint32_t cid, uint8_t cmd);

/**
 * @brief Encode response to CTAPHID_INIT command
 */
static void _send_init_response(uint32_t cid_old, uint32_t cid_new,
                                const uint8_t *nonce);

/**
 * @brief Clear the CTAP packet buffer
 */
static void _clear_ctap_buffer(void);

/**
 * @brief Buffer packet belonging to currently processed transaction
 */
static uint8_t _buffer_pkt(const ctap_hid_pkt_t *pkt);

/**
 * @brief Send error code to cid
 */
static void _send_error_response(uint32_t cid, uint8_t err);

/**
 * @brief Refresh the last_used timestamp for this cid
 */
static int8_t _refresh_cid(uint32_t cid);

/**
 * @brief Allocate a new logical channel
 */
static int8_t _add_cid(uint32_t cid);

/**
 * @brief Delete logical channel
 */
static int8_t _delete_cid(uint32_t cid);

/**
 * @brief Check if a logical channel with cid exists
 */
static bool _cid_exists(uint32_t cid);

/**
 * @brief Parse packet length from pkt
 */
static inline uint16_t _get_packet_len(const ctap_hid_pkt_t *pkt);

/**
 * @brief Process CTAPHID transaction
 */
static void _process_transaction(event_t *arg);

/**
 * @brief Check if packet is an initialization packet
 */
static inline bool _is_init_type_pkt(const ctap_hid_pkt_t *pkt);

/* usbus functionality */

/**
 * @brief USB stack
 */
static char _usb_stack[USBUS_STACKSIZE];

/**
 * @brief USBUS context
 */
static usbus_t _usbus;

/**
 * @brief Indicate if authenticator is busy processing a transactions
 *
 * Transactions are atomic, therefore only 1 transaction can be processed at
 * once
 */
static bool _is_busy = false;

/**
 * @brief State for handling transactions
 */
static ctap_hid_state_t _state;

/**
 * @brief Logical CTAPHID channels
 */
static ctap_hid_cid_t g_cids[CTAP_HID_CIDS_MAX];

/**
 * @brief Incremental channel ids
 *
 * channel id 0 is reserved
 */
static uint32_t _cid = 1;

/**
 * @brief CTAP transport layer event queue
 */
static event_queue_t *_queue;

/**
 * @brief CTAPHID event
 */
static event_t _ctap_hid_event = { .handler = _process_transaction };

/**
 * @brief USBUS context
 */
static usbus_t _usbus;

static void _usb_cb(void *arg)
{
    (void)arg;

    uint8_t buffer[CONFIG_USBUS_HID_INTERRUPT_EP_SIZE];
    int read;

    read = usb_hid_io_read(buffer, CONFIG_USBUS_HID_INTERRUPT_EP_SIZE);

    if (read == CONFIG_USBUS_HID_INTERRUPT_EP_SIZE) {
        fido2_ctap_transport_hid_handle_packet(buffer);
    }
}

void fido2_ctap_transport_hid_init(event_queue_t *queue)
{
    _queue = queue;
    usbdev_t *usbdev = usbdev_get_ctx(0);

    assert(usbdev);
    usbus_init(&_usbus, usbdev);
    usb_hid_io_init(&_usbus, _hid_report_desc, sizeof(_hid_report_desc));
    usb_hid_io_set_rx_cb(_usb_cb, NULL);
    usbus_create(_usb_stack, sizeof(_usb_stack), USBUS_PRIO, USBUS_TNAME, &_usbus);
}

void fido2_ctap_transport_hid_handle_packet(void *pkt_raw)
{
    ctap_hid_pkt_t *pkt = (ctap_hid_pkt_t *)pkt_raw;
    uint32_t cid = pkt->cid;
    uint8_t status = CTAP_HID_BUFFER_STATUS_BUFFERING;

    if (cid == 0x00) {
        /* cid = 0x00 always invalid */
        _send_error_response(cid, CTAP_HID_ERR_INVALID_CHANNEL);
        return;
    }
    else if (_is_busy) {
        if (_state.cid == cid) {
            /* CTAP specification (version 20190130) section 8.1.5.3 */
            if (_is_init_type_pkt(pkt)) {
                if (pkt->init.cmd == CTAP_HID_COMMAND_INIT) {
                    /* abort */
                    _clear_ctap_buffer();
                    status = _buffer_pkt(pkt);
                }
                else if (_state.is_locked && pkt->init.cmd ==
                         CTAP_HID_COMMAND_CANCEL) {
                    _state.should_cancel = true;
                }
                /* random init type pkt. invalid sequence of pkts */
                else {
                    _send_error_response(cid, CTAP_HID_ERR_INVALID_SEQ);
                    return;
                }
            }
            /* packet for this cid is currently being worked */
            else if (_state.is_locked) {
                _send_error_response(cid, CTAP_HID_ERR_CHANNEL_BUSY);
                return;
            }
            else {
                /* buffer cont packets */
                status = _buffer_pkt(pkt);
            }
        }
        /* transactions are atomic. Deny all other cids if busy with one cid */
        else {
            _send_error_response(cid, CTAP_HID_ERR_CHANNEL_BUSY);
            return;
        }
    }
    else {
        /* first init packet received starts a transaction */
        if (_is_init_type_pkt(pkt)) {
            _is_busy = true;
            status = _buffer_pkt(pkt);
        }
        /* ignore rest */
    }

    if (status == CTAP_HID_BUFFER_STATUS_ERROR) {
        _send_error_response(cid, _state.err);
        _delete_cid(cid);
        _clear_ctap_buffer();
        _is_busy = false;
        return;
    }

    /* pkt->init.bcnt bytes have been received. Transaction can now be processed */
    if (status == CTAP_HID_BUFFER_STATUS_DONE) {
        _state.is_locked = 1;
        event_post(_queue, &_ctap_hid_event);
        _is_busy = false;
    }
    else {
        /* refresh timestamp of cid that is being buffered */
        _refresh_cid(_state.cid);
    }
}

static uint8_t _buffer_pkt(const ctap_hid_pkt_t *pkt)
{
    if (_is_init_type_pkt(pkt)) {
        /**
         * broadcast cid only allowed for CTAP_HID_COMMAND_INIT
         */
        if (pkt->cid == CTAP_HID_BROADCAST_CID &&
            pkt->init.cmd != CTAP_HID_COMMAND_INIT) {
            _send_error_response(pkt->cid, CTAP_HID_ERR_INVALID_CHANNEL);
        }

        /**
         * received CTAP_HID_COMMAND_CANCEL while buffering packet.
         * Cancel request.
         */
        if (pkt->init.cmd == CTAP_HID_COMMAND_CANCEL && !_state.is_locked &&
            pkt->cid == _state.cid) {

            _state.err = CTAP2_ERR_KEEPALIVE_CANCEL;
            return CTAP_HID_BUFFER_STATUS_ERROR;
        }

        _state.bcnt = _get_packet_len(pkt);

        /* check for init transaction size described in CTAP specification
           (version 20190130) section 8.1.9.1.3 */
        if (pkt->init.cmd == CTAP_HID_COMMAND_INIT && _state.bcnt != 8) {
            _state.err = CTAP_HID_ERR_INVALID_LEN;
            return CTAP_HID_BUFFER_STATUS_ERROR;
        }

        /* don't allow transactions bigger than max buffer size */
        if (_state.bcnt > CTAP_HID_BUFFER_SIZE) {
            _state.err = CTAP_HID_ERR_INVALID_LEN;
            return CTAP_HID_BUFFER_STATUS_ERROR;
        }

        uint16_t size = (_state.bcnt < CTAP_HID_INIT_PAYLOAD_SIZE) ?
                        _state.bcnt : CTAP_HID_INIT_PAYLOAD_SIZE;
        _state.cmd = pkt->init.cmd;
        _state.cid = pkt->cid;
        _state.seq = -1;
        memcpy(_state.buffer, pkt->init.payload, size);
        _state.offset = size;
    }
    else {
        int left = _state.bcnt - _state.offset;
        int diff = left - CTAP_HID_CONT_PAYLOAD_SIZE;
        _state.seq++;

        /* seqs have to increase sequentially */
        if (pkt->cont.seq != _state.seq) {
            _state.err = CTAP_HID_ERR_INVALID_SEQ;
            return CTAP_HID_BUFFER_STATUS_ERROR;
        }

        /* check for potential buffer overflow */
        if (_state.offset + CTAP_HID_CONT_PAYLOAD_SIZE > CTAP_HID_BUFFER_SIZE) {
            _state.err = CTAP_HID_ERR_INVALID_LEN;
            return CTAP_HID_BUFFER_STATUS_ERROR;
        }

        if (diff <= 0) {
            memcpy(_state.buffer + _state.offset, pkt->cont.payload, left);
            _state.offset += left;
        }
        else {
            memcpy(_state.buffer + _state.offset, pkt->cont.payload,
                   CTAP_HID_CONT_PAYLOAD_SIZE);
            _state.offset += CTAP_HID_CONT_PAYLOAD_SIZE;
        }
    }

    return _state.offset == _state.bcnt ?
           CTAP_HID_BUFFER_STATUS_DONE : CTAP_HID_BUFFER_STATUS_BUFFERING;
}

static void _process_transaction(event_t *arg)
{
    (void)arg;
    uint8_t *buf = (uint8_t *)&_state.buffer;
    uint32_t cid = _state.cid;
    uint16_t bcnt = _state.bcnt;
    uint8_t cmd = _state.cmd;

    if (cmd == CTAP_HID_COMMAND_INIT) {
        _handle_init_packet(cid, bcnt, buf);
    }
    else {
        /* readding deleted cid */
        if (!_cid_exists(cid) && _add_cid(cid) == -1) {
            _send_error_response(cid, CTAP_HID_ERR_CHANNEL_BUSY);
        }
        else {
            switch (cmd) {
            case CTAP_HID_COMMAND_MSG:
                /* not implemented as of now */
                DEBUG("CTAP_HID: MSG COMMAND \n");
                _send_error_response(cid, CTAP_HID_ERR_INVALID_CMD);
                break;
            case CTAP_HID_COMMAND_CBOR:
                DEBUG("CTAP_HID: CBOR COMMAND \n");
                _handle_cbor_packet(cmd, cid, buf, bcnt);
                break;
            case CTAP_HID_COMMAND_WINK:
                DEBUG("CTAP_HID: wink \n");
                _wink(cid, cmd);
                break;
            case CTAP_HID_COMMAND_PING:
                DEBUG("CTAP_HID: PING \n");
                _ctap_hid_write(cmd, cid, buf, bcnt);
                break;
            case CTAP_HID_COMMAND_CANCEL:
                /*
                 * no transaction is currently being processed,
                 * no reason to send cancel
                 */
                break;
            default:
                _send_error_response(cid, CTAP_HID_ERR_INVALID_CMD);
                DEBUG("Ctaphid: unknown command %u \n", cmd);
            }
        }
    }

    /* transaction done, cleanup */
    _clear_ctap_buffer();
}

static uint32_t _handle_init_packet(uint32_t cid, uint16_t bcnt,
                                    const uint8_t *nonce)
{
    uint32_t cid_new = 0;

    /* cid 0 is reserved */
    if (cid == 0) {
        _send_error_response(cid, CTAP_HID_ERR_INVALID_CHANNEL);
        return 0;
    }
    /* check for len described in standard */
    if (bcnt != 8) {
        _send_error_response(cid, CTAP_HID_ERR_INVALID_LEN);
        return 0;
    }
    /* create new channel */
    if (cid == CTAP_HID_BROADCAST_CID) {
        cid_new = _cid++;

        if (_add_cid(cid_new) == -1) {
            _send_error_response(cid, CTAP_HID_ERR_CHANNEL_BUSY);
            return 0;
        }
        _send_init_response(cid, cid_new, nonce);
    }
    /* synchronize channel */
    else {
        cid_new = cid;
        if (!_cid_exists(cid)) {
            if (_add_cid(cid) == -1) {
                /* reached cid limit */
                _send_error_response(cid, CTAP_HID_ERR_CHANNEL_BUSY);
                return 0;
            }
        }
        _send_init_response(cid, cid, nonce);
    }

    return cid_new;
}

static void _handle_cbor_packet(uint8_t cmd, uint32_t cid, uint8_t *buf, uint16_t bcnt)
{
    ctap_resp_t resp;
    uint8_t err;
    size_t size;

    if (bcnt == 0) {
        err = CTAP_HID_ERR_INVALID_LEN;
        cmd = CTAP_HID_COMMAND_ERROR;
        _ctap_hid_write(cmd, cid, &err, sizeof(err));
        return;
    }

    memset(&resp, 0, sizeof(ctap_resp_t));

    ctap_req_t req;

    req.method = *buf;
    req.buf = buf + 1;
    req.len = bcnt - 1;

    size = fido2_ctap_handle_request(&req, &resp);

    /* transaction done, clear should_cancel flag */
    _state.should_cancel = false;

    if (resp.status == CTAP2_OK && size > 0) {
        /* status + data */
        _ctap_hid_write(cmd, cid, &resp, size + sizeof(resp.status));
    }
    else {
        /* status only */
        _ctap_hid_write(cmd, cid, &resp.status, sizeof(resp.status));
    }
}

static inline bool _is_init_type_pkt(const ctap_hid_pkt_t *pkt)
{
    return ((pkt->init.cmd & CTAP_HID_INIT_PACKET) == CTAP_HID_INIT_PACKET);
}

static void _clear_ctap_buffer(void)
{
    memset(&_state, 0, sizeof(_state));
}

bool fido2_ctap_transport_hid_should_cancel(void)
{
    return _state.should_cancel;
}

void fido2_ctap_transport_hid_check_timeouts(void)
{
    uint64_t now = xtimer_now_usec64();

    for (uint8_t i = 0; i < CTAP_HID_CIDS_MAX; i++) {
        /* transaction timed out because cont packets didn't arrive in time */
        if (_is_busy && g_cids[i].taken &&
            (now - g_cids[i].last_used) >= CTAP_HID_TRANSACTION_TIMEOUT &&
            _state.cid == g_cids[i].cid && !_state.is_locked) {

            _send_error_response(g_cids[i].cid, CTAP_HID_ERR_MSG_TIMEOUT);
            _delete_cid(g_cids[i].cid);
            _clear_ctap_buffer();

            _is_busy = false;
        }
    }
}

static int8_t _add_cid(uint32_t cid)
{
    uint64_t oldest = xtimer_now_usec64();
    int8_t index_oldest = -1;

    for (int i = 0; i < CTAP_HID_CIDS_MAX; i++) {
        if (!g_cids[i].taken) {
            g_cids[i].taken = true;
            g_cids[i].cid = cid;
            g_cids[i].last_used = xtimer_now_usec64();

            return CTAP_HID_OK;
        }

        if (g_cids[i].last_used < oldest) {
            oldest = g_cids[i].last_used;
            index_oldest = i;
        }
    }

    /* remove oldest cid to make place for a new one */
    if (index_oldest > -1) {
        g_cids[index_oldest].taken = true;
        g_cids[index_oldest].cid = cid;
        g_cids[index_oldest].last_used = xtimer_now_usec64();
        return CTAP_HID_OK;
    }

    return CTAP_HID_ERR_OTHER;
}

static int8_t _refresh_cid(uint32_t cid)
{
    for (int i = 0; i < CTAP_HID_CIDS_MAX; i++) {
        if (g_cids[i].cid == cid) {
            g_cids[i].last_used = xtimer_now_usec64();
            return CTAP_HID_OK;
        }
    }
    return CTAP_HID_ERR_OTHER;
}

static int8_t _delete_cid(uint32_t cid)
{
    for (int i = 0; i < CTAP_HID_CIDS_MAX; i++) {
        if (g_cids[i].cid == cid) {
            g_cids[i].taken = false;
            g_cids[i].cid = 0;

            return CTAP_HID_OK;
        }
    }
    return CTAP_HID_ERR_OTHER;
}

static bool _cid_exists(uint32_t cid)
{
    for (int i = 0; i < CTAP_HID_CIDS_MAX; i++) {
        if (g_cids[i].cid == cid) {
            return true;
        }
    }
    return false;
}

static inline uint16_t _get_packet_len(const ctap_hid_pkt_t *pkt)
{
    return (uint16_t)((pkt->init.bcnth << 8) | pkt->init.bcntl);
}

static void _wink(uint32_t cid, uint8_t cmd)
{
#if !IS_ACTIVE(CONFIG_FIDO2_CTAP_DISABLE_LED)
    uint32_t delay = CTAP_HID_WINK_DELAY;
    for (int i = 1; i <= 8; i++) {
#ifdef LED0_TOGGLE
        LED0_TOGGLE;
        xtimer_msleep(delay);
#endif
#ifdef LED1_TOGGLE
        LED1_TOGGLE;
        xtimer_msleep(delay);
#endif
#ifdef LED2_TOGGLE
        LED2_TOGGLE;
        xtimer_msleep(delay);
#endif
#ifdef LED3_TOGGLE
        LED3_TOGGLE;
        xtimer_msleep(delay);
#endif
        delay /= 2;
    }
#endif /* CONFIG_FIDO2_CTAP_DISABLE_LED  */

    _ctap_hid_write(cmd, cid, NULL, 0);
}

static void _send_error_response(uint32_t cid, uint8_t err)
{
    DEBUG("ctap_trans_hid err resp: %02x \n", err);
    _ctap_hid_write(CTAP_HID_COMMAND_ERROR, cid, &err, sizeof(err));
}

static void _send_init_response(uint32_t cid_old, uint32_t cid_new,
                                const uint8_t *nonce)
{
    ctap_hid_init_resp_t resp;

    memset(&resp, 0, sizeof(ctap_hid_init_resp_t));

    resp.cid = cid_new;
    resp.protocol_version = CTAP_HID_PROTOCOL_VERSION;
    resp.version_major = 0;
    resp.version_minor = 0;
    resp.build_version = 0;
    memcpy(resp.nonce, nonce, sizeof(resp.nonce));

    uint8_t cmd = (CTAP_HID_INIT_PACKET | CTAP_HID_COMMAND_INIT);

    resp.capabilities = CTAP_HID_CAPABILITY_CBOR | CTAP_HID_CAPABILITY_WINK
                        | CTAP_HID_CAPABILITY_NMSG;

    _ctap_hid_write(cmd, cid_old, &resp, sizeof(ctap_hid_init_resp_t));
}

void _ctap_hid_write(uint8_t cmd, uint32_t cid, const void *_data, size_t len)
{
    const uint8_t *data = (uint8_t *)_data;
    uint8_t buf[CONFIG_USBUS_HID_INTERRUPT_EP_SIZE] = { 0 };
    uint16_t bytes_written = 0;
    uint8_t seq = 0;
    uint8_t offset = 0;

    memcpy(buf, &cid, sizeof(cid));
    offset += sizeof(cid);
    buf[offset++] = cmd;
    /* high part of payload length first */
    buf[offset++] = (len & 0xff00) >> 8;
    buf[offset++] = (len & 0xff) >> 0;

    if (data == NULL) {
        usb_hid_io_write(buf, CONFIG_USBUS_HID_INTERRUPT_EP_SIZE);
        return;
    }

    for (size_t i = 0; i < len; i++) {
        if (offset == 0) {
            memcpy(buf, &cid, sizeof(cid));
            offset += sizeof(cid);

            /* initialization packet */
            if (bytes_written == 0) {
                buf[offset++] = cmd;
                buf[offset++] = (len & 0xff00) >> 8;
                buf[offset++] = (len & 0xff) >> 0;
            }
            /* continuation packet */
            else {
                buf[offset++] = seq++;
            }
        }

        buf[offset++] = data[i];
        bytes_written++;

        if (offset == CONFIG_USBUS_HID_INTERRUPT_EP_SIZE) {
            usb_hid_io_write(buf, CONFIG_USBUS_HID_INTERRUPT_EP_SIZE);
            offset = 0;
        }
    }

    if (offset > 0) {
        memset(buf + offset, 0, CONFIG_USBUS_HID_INTERRUPT_EP_SIZE - offset);
        usb_hid_io_write(buf, CONFIG_USBUS_HID_INTERRUPT_EP_SIZE);
    }
}
