/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

#include <assert.h>
#include <string.h>
#include <errno.h>
#include <stdint.h>
#include <sysinit/sysinit.h>
#include <syscfg/syscfg.h>
#include "os/os_mbuf.h"
#include "os/os_mempool.h"
#include "hal/hal_uart.h"
#include "nimble/transport.h"
#include "nimble/transport/hci_h4.h"

#define TX_Q_SIZE   (MYNEWT_VAL(BLE_TRANSPORT_ACL_FROM_LL_COUNT) + \
                     MYNEWT_VAL(BLE_TRANSPORT_EVT_COUNT) + \
                     MYNEWT_VAL(BLE_TRANSPORT_EVT_DISCARDABLE_COUNT))

struct hci_uart_tx {
    uint8_t type;
    uint8_t sent_type;
    uint16_t len;
    uint16_t idx;

    struct os_mbuf *om;
    uint8_t *buf;

    STAILQ_ENTRY(hci_uart_tx) tx_q_next;
};

static STAILQ_HEAD(, hci_uart_tx) tx_q;

static struct os_mempool pool_tx_q;
static uint8_t pool_tx_q_buf[ OS_MEMPOOL_BYTES(TX_Q_SIZE,
                                               sizeof(struct hci_uart_tx)) ];

struct hci_h4_sm hci_uart_h4sm;

static int
hci_uart_frame_cb(uint8_t pkt_type, void *data)
{
    switch (pkt_type) {
    case HCI_H4_CMD:
        return ble_transport_to_ll_cmd(data);
    case HCI_H4_ACL:
        return ble_transport_to_ll_acl(data);
    default:
        assert(0);
        break;
    }

    return -1;
}

static int
hci_uart_rx_char(void *arg, uint8_t data)
{
    hci_h4_sm_rx(&hci_uart_h4sm, &data, 1);

    return 0;
}

static int
hci_uart_tx_char(void *arg)
{
    struct hci_uart_tx *tx;
    uint8_t ch;
    os_sr_t sr;

    OS_ENTER_CRITICAL(sr);
    tx = STAILQ_FIRST(&tx_q);
    OS_EXIT_CRITICAL(sr);
    if (!tx) {
        return -1;
    }

    if (!tx->sent_type) {
        tx->sent_type = 1;
        return tx->type;
    }

    switch (tx->type) {
    case HCI_H4_EVT:
        ch = tx->buf[tx->idx];
        tx->idx++;
        if (tx->idx == tx->len) {
            ble_transport_free(tx->buf);
            OS_ENTER_CRITICAL(sr);
            STAILQ_REMOVE_HEAD(&tx_q, tx_q_next);
            OS_EXIT_CRITICAL(sr);
            os_memblock_put(&pool_tx_q, tx);
        }
        break;
    case HCI_H4_ACL:
        os_mbuf_copydata(tx->om, 0, 1, &ch);
        os_mbuf_adj(tx->om, 1);
        tx->len--;
        if (tx->len == 0) {
            os_mbuf_free_chain(tx->om);
            OS_ENTER_CRITICAL(sr);
            STAILQ_REMOVE_HEAD(&tx_q, tx_q_next);
            OS_EXIT_CRITICAL(sr);
            os_memblock_put(&pool_tx_q, tx);
        }
        break;
    default:
        assert(0);
        OS_ENTER_CRITICAL(sr);
        STAILQ_REMOVE_HEAD(&tx_q, tx_q_next);
        OS_EXIT_CRITICAL(sr);
        os_memblock_put(&pool_tx_q, tx);
    }

    return ch;
}

static int
hci_uart_configure(void)
{
    enum hal_uart_parity parity;
    enum hal_uart_flow_ctl flowctl;
    int rc;

    rc = hal_uart_init_cbs(MYNEWT_VAL(BLE_TRANSPORT_UART_PORT),
                           hci_uart_tx_char, NULL,
                           hci_uart_rx_char, NULL);
    if (rc != 0) {
        return -1;
    }

    if (MYNEWT_VAL_CHOICE(BLE_TRANSPORT_UART_PARITY, odd)) {
        parity = HAL_UART_PARITY_ODD;
    } else if (MYNEWT_VAL_CHOICE(BLE_TRANSPORT_UART_PARITY, even)) {
        parity = HAL_UART_PARITY_EVEN;
    } else {
        parity = HAL_UART_PARITY_NONE;
    }

    if (MYNEWT_VAL_CHOICE(BLE_TRANSPORT_UART_FLOW_CONTROL, rtscts)) {
        flowctl = HAL_UART_FLOW_CTL_RTS_CTS;
    } else {
        flowctl = HAL_UART_FLOW_CTL_NONE;
    }

    rc = hal_uart_config(MYNEWT_VAL(BLE_TRANSPORT_UART_PORT),
                         MYNEWT_VAL(BLE_TRANSPORT_UART_BAUDRATE),
                         MYNEWT_VAL(BLE_TRANSPORT_UART_DATA_BITS),
                         MYNEWT_VAL(BLE_TRANSPORT_UART_STOP_BITS),
                         parity, flowctl);
    if (rc != 0) {
        return -1;
    }

    return 0;
}

int
ble_transport_to_hs_evt_impl(void *buf)
{
    struct hci_uart_tx *txe;
    os_sr_t sr;

    txe = os_memblock_get(&pool_tx_q);
    if (!txe) {
        assert(0);
        return -ENOMEM;
    }

    txe->type = HCI_H4_EVT;
    txe->sent_type = 0;
    txe->len = 2 + ((uint8_t *)buf)[1];
    txe->buf = buf;
    txe->idx = 0;
    txe->om = NULL;

    OS_ENTER_CRITICAL(sr);
    STAILQ_INSERT_TAIL(&tx_q, txe, tx_q_next);
    OS_EXIT_CRITICAL(sr);

    hal_uart_start_tx(MYNEWT_VAL(BLE_TRANSPORT_UART_PORT));

    return 0;
}

int
ble_transport_to_hs_acl_impl(struct os_mbuf *om)
{
    struct hci_uart_tx *txe;
    os_sr_t sr;

    txe = os_memblock_get(&pool_tx_q);
    if (!txe) {
        assert(0);
        return -ENOMEM;
    }

    txe->type = HCI_H4_ACL;
    txe->sent_type = 0;
    txe->len = OS_MBUF_PKTLEN(om);
    txe->idx = 0;
    txe->buf = NULL;
    txe->om = om;

    OS_ENTER_CRITICAL(sr);
    STAILQ_INSERT_TAIL(&tx_q, txe, tx_q_next);
    OS_EXIT_CRITICAL(sr);

    hal_uart_start_tx(MYNEWT_VAL(BLE_TRANSPORT_UART_PORT));

    return 0;
}

void
ble_transport_hs_init(void)
{
    int rc;

    SYSINIT_ASSERT_ACTIVE();

    rc = hci_uart_configure();
    SYSINIT_PANIC_ASSERT(rc == 0);

    rc = os_mempool_init(&pool_tx_q, TX_Q_SIZE, sizeof(struct hci_uart_tx),
                         pool_tx_q_buf, "hci_uart_tx_q");
    SYSINIT_PANIC_ASSERT(rc == 0);

    hci_h4_sm_init(&hci_uart_h4sm, &hci_h4_allocs_from_hs, hci_uart_frame_cb);

    STAILQ_INIT(&tx_q);
}
