/*
 * 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 <stdint.h>
#include <syscfg/syscfg.h>
#include <nimble/hci_common.h>
#include <controller/ble_ll.h>
#include <controller/ble_ll_isoal.h>
#include <controller/ble_ll_iso.h>
#include <controller/ble_ll_tmr.h>

#if MYNEWT_VAL(BLE_LL_ISO)

STAILQ_HEAD(ble_ll_iso_conn_q, ble_ll_iso_conn);
struct ble_ll_iso_conn_q ll_iso_conn_q;

int
ble_ll_iso_setup_iso_data_path(const uint8_t *cmdbuf, uint8_t cmdlen,
                               uint8_t *rspbuf, uint8_t *rsplen)
{
    const struct ble_hci_le_setup_iso_data_path_cp *cmd = (const void *)cmdbuf;
    struct ble_hci_le_setup_iso_data_path_rp *rsp = (void *)rspbuf;
    struct ble_ll_iso_conn *conn;
    uint16_t conn_handle;

    conn_handle = le16toh(cmd->conn_handle);

    conn = ble_ll_iso_conn_find_by_handle(conn_handle);
    if (!conn) {
        return BLE_ERR_UNK_CONN_ID;
    }

    if (conn->mux.bn == 0) {
        return BLE_ERR_UNSUPPORTED;
    }

    if (conn->data_path.enabled) {
        return BLE_ERR_CMD_DISALLOWED;
    }

    /* Only input for now since we only support BIS */
    if (cmd->data_path_dir) {
        return BLE_ERR_CMD_DISALLOWED;
    }

    /* We do not (yet) support any vendor-specific data path */
    if (cmd->data_path_id) {
        return BLE_ERR_CMD_DISALLOWED;
    }

    conn->data_path.enabled = 1;
    conn->data_path.data_path_id = cmd->data_path_id;

    rsp->conn_handle = cmd->conn_handle;
    *rsplen = sizeof(*rsp);

    return 0;
}

int
ble_ll_iso_remove_iso_data_path(const uint8_t *cmdbuf, uint8_t cmdlen,
                                uint8_t *rspbuf, uint8_t *rsplen)
{
    const struct ble_hci_le_remove_iso_data_path_cp *cmd = (const void *)cmdbuf;
    struct ble_hci_le_remove_iso_data_path_rp *rsp = (void *)rspbuf;
    struct ble_ll_iso_conn *conn;
    uint16_t conn_handle;

    conn_handle = le16toh(cmd->conn_handle);

    conn = ble_ll_iso_conn_find_by_handle(conn_handle);
    if (!conn) {
        return BLE_ERR_UNK_CONN_ID;
    }

    /* Only input for now since we only support BIS */
    if (cmd->data_path_dir) {
        return BLE_ERR_CMD_DISALLOWED;
    }

    conn->data_path.enabled = 0;

    rsp->conn_handle = cmd->conn_handle;
    *rsplen = sizeof(*rsp);

    return 0;
}

int
ble_ll_iso_read_tx_sync(const uint8_t *cmdbuf, uint8_t cmdlen,
                        uint8_t *rspbuf, uint8_t *rsplen)
{
    const struct ble_hci_le_read_iso_tx_sync_cp *cmd = (const void *)cmdbuf;
    struct ble_hci_le_read_iso_tx_sync_rp *rsp = (void *)rspbuf;
    struct ble_ll_iso_conn *iso_conn;
    uint16_t handle;

    handle = le16toh(cmd->conn_handle);
    iso_conn = ble_ll_iso_conn_find_by_handle(handle);
    if (!iso_conn) {
        return BLE_ERR_UNK_CONN_ID;
    }

    rsp->conn_handle = cmd->conn_handle;
    rsp->packet_seq_num = htole16(iso_conn->mux.last_tx_packet_seq_num);
    rsp->tx_timestamp = htole32(iso_conn->mux.last_tx_timestamp);
    put_le24(rsp->time_offset, 0);

    *rsplen = sizeof(*rsp);

    return 0;
}

int
ble_ll_iso_transmit_test(const uint8_t *cmdbuf, uint8_t cmdlen, uint8_t *rspbuf, uint8_t *rsplen)
{
    const struct ble_hci_le_iso_transmit_test_cp *cmd = (const void *)cmdbuf;
    struct ble_hci_le_iso_transmit_test_rp *rsp = (void *)rspbuf;
    struct ble_ll_iso_conn *conn;
    uint16_t handle;

    handle = le16toh(cmd->conn_handle);

    conn = ble_ll_iso_conn_find_by_handle(handle);
    if (!conn) {
        return BLE_ERR_UNK_CONN_ID;
    }

    if (conn->mux.bn == 0) {
        return BLE_ERR_UNSUPPORTED;
    }

    if (conn->data_path.enabled) {
        return BLE_ERR_CMD_DISALLOWED;
    }

    if (cmd->payload_type > BLE_HCI_PAYLOAD_TYPE_MAXIMUM_LENGTH) {
        return BLE_ERR_INV_LMP_LL_PARM;
    }

    conn->data_path.enabled = 1;
    conn->data_path.data_path_id = BLE_HCI_ISO_DATA_PATH_ID_HCI;
    conn->test_mode.transmit.enabled = 1;
    conn->test_mode.transmit.payload_type = cmd->payload_type;

    rsp->conn_handle = cmd->conn_handle;

    *rsplen = sizeof(*rsp);

    return 0;
}

int
ble_ll_iso_end_test(const uint8_t *cmdbuf, uint8_t len, uint8_t *rspbuf, uint8_t *rsplen)
{
    const struct ble_hci_le_iso_test_end_cp *cmd = (const void *)cmdbuf;
    struct ble_hci_le_iso_test_end_rp *rsp = (void *)rspbuf;
    struct ble_ll_iso_conn *iso_conn;
    uint16_t handle;

    handle = le16toh(cmd->conn_handle);
    iso_conn = ble_ll_iso_conn_find_by_handle(handle);
    if (!iso_conn) {
        return BLE_ERR_UNK_CONN_ID;
    }

    if (!iso_conn->test_mode.transmit.enabled) {
        return BLE_ERR_UNSUPPORTED;
    }

    iso_conn->data_path.enabled = 0;
    iso_conn->test_mode.transmit.enabled = 0;

    rsp->conn_handle = cmd->conn_handle;
    rsp->received_sdu_count = 0;
    rsp->missed_sdu_count = 0;
    rsp->failed_sdu_count = 0;

    *rsplen = sizeof(*rsp);

    return 0;
}

struct ble_ll_iso_conn *
ble_ll_iso_conn_find_by_handle(uint16_t conn_handle)
{
    struct ble_ll_iso_conn *conn;

    STAILQ_FOREACH(conn, &ll_iso_conn_q, iso_conn_q_next) {
        if (conn_handle == conn->handle) {
            return conn;
        }
    }

    return NULL;
}

void
ble_ll_iso_init(void)
{
    STAILQ_INIT(&ll_iso_conn_q);
    ble_ll_isoal_init();
}

void
ble_ll_iso_reset(void)
{
    STAILQ_INIT(&ll_iso_conn_q);
    ble_ll_isoal_reset();
}

int
ble_ll_iso_data_in(struct os_mbuf *om)
{
    struct ble_hci_iso *hci_iso;
    struct ble_hci_iso_data *hci_iso_data;
    struct ble_ll_iso_conn *conn;
    struct ble_mbuf_hdr *blehdr;
    uint16_t data_hdr_len;
    uint16_t handle;
    uint16_t conn_handle;
    uint16_t length;
    uint16_t pb_flag;
    uint16_t ts_flag;
    uint32_t timestamp = 0;

    hci_iso = (void *)om->om_data;

    handle = le16toh(hci_iso->handle);
    conn_handle = BLE_HCI_ISO_CONN_HANDLE(handle);
    pb_flag = BLE_HCI_ISO_PB_FLAG(handle);
    ts_flag = BLE_HCI_ISO_TS_FLAG(handle);
    length = BLE_HCI_ISO_LENGTH(le16toh(hci_iso->length));

    conn = ble_ll_iso_conn_find_by_handle(conn_handle);
    if (!conn) {
        os_mbuf_free_chain(om);
        return BLE_ERR_UNK_CONN_ID;
    }

    data_hdr_len = 0;
    if ((pb_flag == BLE_HCI_ISO_PB_FIRST) ||
        (pb_flag == BLE_HCI_ISO_PB_COMPLETE)) {
        blehdr = BLE_MBUF_HDR_PTR(om);
        blehdr->txiso.packet_seq_num = ++conn->mux.sdu_counter;
        blehdr->txiso.cpu_timestamp = ble_ll_tmr_get();

        if (ts_flag) {
            timestamp = get_le32(om->om_data + sizeof(*hci_iso));
            data_hdr_len += sizeof(uint32_t);
        }
        blehdr->txiso.hci_timestamp = timestamp;

        hci_iso_data = (void *)(om->om_data + sizeof(*hci_iso) + data_hdr_len);
        data_hdr_len += sizeof(*hci_iso_data);
    }
    os_mbuf_adj(om, sizeof(*hci_iso) + data_hdr_len);

    if (OS_MBUF_PKTLEN(om) != length - data_hdr_len) {
        os_mbuf_free_chain(om);
        return BLE_ERR_MEM_CAPACITY;
    }

    switch (pb_flag) {
    case BLE_HCI_ISO_PB_FIRST:
        BLE_LL_ASSERT(!conn->frag);
        conn->frag = om;
        om = NULL;
        break;
    case BLE_HCI_ISO_PB_CONTINUATION:
        BLE_LL_ASSERT(conn->frag);
        os_mbuf_concat(conn->frag, om);
        om = NULL;
        break;
    case BLE_HCI_ISO_PB_COMPLETE:
        BLE_LL_ASSERT(!conn->frag);
        break;
    case BLE_HCI_ISO_PB_LAST:
        BLE_LL_ASSERT(conn->frag);
        os_mbuf_concat(conn->frag, om);
        om = conn->frag;
        conn->frag = NULL;
        break;
    default:
        BLE_LL_ASSERT(0);
        break;
    }

    if (om) {
        ble_ll_isoal_mux_sdu_enqueue(&conn->mux, om);
    }

    return 0;
}

static int
ble_ll_iso_test_pdu_get(struct ble_ll_iso_conn *conn, uint8_t idx, uint32_t pkt_counter, uint8_t *llid, uint8_t *dptr)
{
    uint32_t payload_len;
    uint16_t rem_len;
    uint8_t sdu_idx;
    uint8_t pdu_idx;
    int pdu_len;

    BLE_LL_ASSERT(!conn->mux.framed);

    sdu_idx = idx / conn->mux.pdu_per_sdu;
    pdu_idx = idx - sdu_idx * conn->mux.pdu_per_sdu;

    switch (conn->test_mode.transmit.payload_type) {
    case BLE_HCI_PAYLOAD_TYPE_ZERO_LENGTH:
        *llid = 0b00;
        pdu_len = 0;
        break;
    case BLE_HCI_PAYLOAD_TYPE_VARIABLE_LENGTH:
        payload_len = max(conn->test_mode.transmit.rand + (sdu_idx * pdu_idx), 4);

        rem_len = payload_len - pdu_idx * conn->mux.max_pdu;
        if (rem_len == 0) {
            *llid = 0b01;
            pdu_len = 0;
        } else {
            *llid = rem_len > conn->mux.max_pdu;
            pdu_len = min(conn->mux.max_pdu, rem_len);
        }

        memset(dptr, 0, pdu_len);

        if (payload_len == rem_len) {
            put_le32(dptr, pkt_counter);
        }

        break;
    case BLE_HCI_PAYLOAD_TYPE_MAXIMUM_LENGTH:
        payload_len = conn->max_sdu;

        rem_len = payload_len - pdu_idx * conn->mux.max_pdu;
        if (rem_len == 0) {
            *llid = 0b01;
            pdu_len = 0;
        } else {
            *llid = rem_len > conn->mux.max_pdu;
            pdu_len = min(conn->mux.max_pdu, rem_len);
        }

        memset(dptr, 0, pdu_len);

        if (payload_len == rem_len) {
            put_le32(dptr, pkt_counter);
        }

        break;
    default:
        BLE_LL_ASSERT(0);
    }

    return pdu_len;
}

int
ble_ll_iso_pdu_get(struct ble_ll_iso_conn *conn, uint8_t idx, uint32_t pkt_counter, uint8_t *llid, void *dptr)
{
    if (conn->test_mode.transmit.enabled) {
        return ble_ll_iso_test_pdu_get(conn, idx, pkt_counter, llid, dptr);
    }

    return ble_ll_isoal_mux_pdu_get(&conn->mux, idx, llid, dptr);
}

void
ble_ll_iso_conn_init(struct ble_ll_iso_conn *conn, struct ble_ll_iso_conn_init_param *param)
{
    os_sr_t sr;

    memset(conn, 0, sizeof(*conn));

    conn->handle = param->conn_handle;
    conn->max_sdu = param->max_sdu;

    ble_ll_isoal_mux_init(&conn->mux, param->max_pdu, param->iso_interval_us, param->sdu_interval_us,
                          param->bn, param->pte, BLE_LL_ISOAL_MUX_IS_FRAMED(param->framing),
                          param->framing == BLE_HCI_ISO_FRAMING_FRAMED_UNSEGMENTED);

    OS_ENTER_CRITICAL(sr);
    STAILQ_INSERT_TAIL(&ll_iso_conn_q, conn, iso_conn_q_next);
    OS_EXIT_CRITICAL(sr);
}

void
ble_ll_iso_conn_free(struct ble_ll_iso_conn *conn)
{
    os_sr_t sr;

    OS_ENTER_CRITICAL(sr);
    STAILQ_REMOVE(&ll_iso_conn_q, conn, ble_ll_iso_conn, iso_conn_q_next);
    OS_EXIT_CRITICAL(sr);

    ble_ll_isoal_mux_free(&conn->mux);
}

int
ble_ll_iso_conn_event_start(struct ble_ll_iso_conn *conn, uint32_t timestamp)
{
    if (conn->test_mode.transmit.enabled) {
        conn->test_mode.transmit.rand = ble_ll_rand() % conn->max_sdu;
    }

    ble_ll_isoal_mux_event_start(&conn->mux, timestamp);

    return 0;
}

int
ble_ll_iso_conn_event_done(struct ble_ll_iso_conn *conn)
{
    conn->num_completed_pkt += ble_ll_isoal_mux_event_done(&conn->mux);

    return conn->num_completed_pkt;
}

#endif /* BLE_LL_ISO */
