/*
 * 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 <string.h>
#include <errno.h>
#include "syscfg/syscfg.h"
#include "os/os.h"
#include "host/ble_l2cap.h"
#include "nimble/ble.h"
#include "nimble/hci_common.h"
#include "ble_hs_priv.h"
#include "ble_l2cap_coc_priv.h"

#if NIMBLE_BLE_CONNECT
_Static_assert(sizeof (struct ble_l2cap_hdr) == BLE_L2CAP_HDR_SZ,
               "struct ble_l2cap_hdr must be 4 bytes");

struct os_mempool ble_l2cap_chan_pool;

static os_membuf_t ble_l2cap_chan_mem[
    OS_MEMPOOL_SIZE(MYNEWT_VAL(BLE_L2CAP_MAX_CHANS) +
                    MYNEWT_VAL(BLE_L2CAP_COC_MAX_NUM),
                    sizeof (struct ble_l2cap_chan))
];

STATS_SECT_DECL(ble_l2cap_stats) ble_l2cap_stats;
STATS_NAME_START(ble_l2cap_stats)
    STATS_NAME(ble_l2cap_stats, chan_create)
    STATS_NAME(ble_l2cap_stats, chan_delete)
    STATS_NAME(ble_l2cap_stats, update_init)
    STATS_NAME(ble_l2cap_stats, update_rx)
    STATS_NAME(ble_l2cap_stats, update_fail)
    STATS_NAME(ble_l2cap_stats, proc_timeout)
    STATS_NAME(ble_l2cap_stats, sig_tx)
    STATS_NAME(ble_l2cap_stats, sig_rx)
    STATS_NAME(ble_l2cap_stats, sm_tx)
    STATS_NAME(ble_l2cap_stats, sm_rx)
STATS_NAME_END(ble_l2cap_stats)

struct ble_l2cap_chan *
ble_l2cap_chan_alloc(uint16_t conn_handle)
{
    struct ble_l2cap_chan *chan;

    chan = os_memblock_get(&ble_l2cap_chan_pool);
    if (chan == NULL) {
        return NULL;
    }

    memset(chan, 0, sizeof *chan);
    chan->conn_handle = conn_handle;

    STATS_INC(ble_l2cap_stats, chan_create);

    return chan;
}

void
ble_l2cap_chan_free(struct ble_hs_conn *conn, struct ble_l2cap_chan *chan)
{
    int rc;

    if (chan == NULL) {
        return;
    }

    os_mbuf_free_chain(chan->rx_buf);
    ble_l2cap_coc_cleanup_chan(conn, chan);

#if MYNEWT_VAL(BLE_HS_DEBUG)
    memset(chan, 0xff, sizeof *chan);
#endif
    rc = os_memblock_put(&ble_l2cap_chan_pool, chan);
    BLE_HS_DBG_ASSERT_EVAL(rc == 0);

    STATS_INC(ble_l2cap_stats, chan_delete);
}

bool
ble_l2cap_is_mtu_req_sent(const struct ble_l2cap_chan *chan)
{
    return (chan->flags & BLE_L2CAP_CHAN_F_TXED_MTU);
}

int
ble_l2cap_parse_hdr(struct os_mbuf *om, int off,
                    struct ble_l2cap_hdr *l2cap_hdr)
{
    int rc;

    rc = os_mbuf_copydata(om, off, sizeof *l2cap_hdr, l2cap_hdr);
    if (rc != 0) {
        return BLE_HS_EMSGSIZE;
    }

    l2cap_hdr->len = get_le16(&l2cap_hdr->len);
    l2cap_hdr->cid = get_le16(&l2cap_hdr->cid);

    return 0;
}

struct os_mbuf *
ble_l2cap_prepend_hdr(struct os_mbuf *om, uint16_t cid, uint16_t len)
{
    struct ble_l2cap_hdr hdr;

    put_le16(&hdr.len, len);
    put_le16(&hdr.cid, cid);

    om = os_mbuf_prepend_pullup(om, sizeof hdr);
    if (om == NULL) {
        return NULL;
    }

    memcpy(om->om_data, &hdr, sizeof hdr);

    return om;
}

uint16_t
ble_l2cap_get_conn_handle(struct ble_l2cap_chan *chan)
{
    if (!chan) {
        return BLE_HS_CONN_HANDLE_NONE;
    }

    return chan->conn_handle;
}

int
ble_l2cap_create_server(uint16_t psm, uint16_t mtu,
                        ble_l2cap_event_fn *cb, void *cb_arg)
{
    return ble_l2cap_coc_create_server(psm, mtu, cb, cb_arg);
}

int
ble_l2cap_connect(uint16_t conn_handle, uint16_t psm, uint16_t mtu,
                  struct os_mbuf *sdu_rx, ble_l2cap_event_fn *cb, void *cb_arg)
{
    return ble_l2cap_sig_coc_connect(conn_handle, psm, mtu, sdu_rx, cb, cb_arg);
}

int
ble_l2cap_get_chan_info(struct ble_l2cap_chan *chan, struct ble_l2cap_chan_info *chan_info)
{
    if (!chan || !chan_info) {
        return BLE_HS_EINVAL;
    }

    memset(chan_info, 0, sizeof(*chan_info));
    chan_info->dcid = chan->dcid;
    chan_info->scid = chan->scid;
    chan_info->our_l2cap_mtu = chan->my_mtu;
    chan_info->peer_l2cap_mtu = chan->peer_mtu;

#if MYNEWT_VAL(BLE_L2CAP_COC_MAX_NUM)
    chan_info->psm = chan->psm;
    chan_info->our_coc_mtu = chan->coc_rx.mtu;
    chan_info->peer_coc_mtu = chan->coc_tx.mtu;
#endif

    return 0;
}

int
ble_l2cap_enhanced_connect(uint16_t conn_handle,
                               uint16_t psm, uint16_t mtu,
                               uint8_t num, struct os_mbuf *sdu_rx[],
                               ble_l2cap_event_fn *cb, void *cb_arg)
{
    return ble_l2cap_sig_ecoc_connect(conn_handle, psm, mtu,
                                      num, sdu_rx, cb, cb_arg);
}

int
ble_l2cap_reconfig(struct ble_l2cap_chan *chans[], uint8_t num, uint16_t new_mtu)
{
    int i;
    uint16_t conn_handle;

    if (num == 0 || !chans) {
        return BLE_HS_EINVAL;
    }

    conn_handle = chans[0]->conn_handle;

    for (i = 1; i < num; i++) {
        if (conn_handle != chans[i]->conn_handle) {
            BLE_HS_LOG(ERROR, "All channels should have same conn handle\n");
            return BLE_HS_EINVAL;
        }
    }

    return ble_l2cap_sig_coc_reconfig(conn_handle, chans, num, new_mtu);
}

int
ble_l2cap_disconnect(struct ble_l2cap_chan *chan)
{
    return ble_l2cap_sig_disconnect(chan);
}

/**
 * Transmits a packet over an L2CAP channel.  This function only consumes the
 * supplied mbuf on success.
 */
int
ble_l2cap_send(struct ble_l2cap_chan *chan, struct os_mbuf *sdu)
{
    return ble_l2cap_coc_send(chan, sdu);
}

int
ble_l2cap_recv_ready(struct ble_l2cap_chan *chan, struct os_mbuf *sdu_rx)
{
    return ble_l2cap_coc_recv_ready(chan, sdu_rx);
}

void
ble_l2cap_remove_rx(struct ble_hs_conn *conn, struct ble_l2cap_chan *chan)
{
    conn->bhc_rx_chan = NULL;
    os_mbuf_free_chain(chan->rx_buf);
    chan->rx_buf = NULL;
    chan->rx_len = 0;
}

static void
ble_l2cap_append_rx(struct ble_l2cap_chan *chan, struct os_mbuf *frag)
{
#if MYNEWT_VAL(BLE_L2CAP_JOIN_RX_FRAGS)
    struct os_mbuf *m;

    /* Copy the data from the incoming fragment into the packet in progress. */
    m = os_mbuf_pack_chains(chan->rx_buf, frag);
    assert(m);
#else
    /* Join disabled or append failed due to mbuf shortage.  Just attach the
     * mbuf to the end of the packet.
     */
    os_mbuf_concat(chan->rx_buf, frag);
#endif
}

static int
ble_l2cap_rx_payload(struct ble_hs_conn *conn, struct ble_l2cap_chan *chan,
                     struct os_mbuf *om,
                     ble_l2cap_rx_fn **out_rx_cb)
{
    int len_diff;
    int rc;

    if (chan->rx_buf == NULL) {
        /* First fragment in packet. */
        chan->rx_buf = om;
    } else {
        /* Continuation of packet in progress. */
        ble_l2cap_append_rx(chan, om);
    }

    /* Determine if packet is fully reassembled. */
    len_diff = OS_MBUF_PKTLEN(chan->rx_buf) - chan->rx_len;
    if (len_diff > 0) {
        /* More data than expected; data corruption. */
        ble_l2cap_remove_rx(conn, chan);
        rc = BLE_HS_EBADDATA;
    } else if (len_diff == 0) {
        /* All fragments received. */
        *out_rx_cb = chan->rx_fn;
        rc = 0;
    } else {
        /* More fragments remain. */
#if MYNEWT_VAL(BLE_L2CAP_RX_FRAG_TIMEOUT) != 0
        conn->bhc_rx_timeout =
            ble_npl_time_get() + MYNEWT_VAL(BLE_L2CAP_RX_FRAG_TIMEOUT);

        ble_hs_timer_resched();
#endif
        rc = BLE_HS_EAGAIN;
    }

    return rc;
}

static uint16_t
ble_l2cap_get_mtu(struct ble_l2cap_chan *chan)
{
    if (chan->scid == BLE_L2CAP_CID_ATT) {
        /* In case of ATT chan->my_mtu keeps preferred MTU which is later
         * used during exchange MTU procedure. Helper below will gives us actual
         * MTU on the channel, which is 23 or higher if exchange MTU has been
         * done
         */
        return ble_att_chan_mtu(chan);
    }

    return chan->my_mtu;
}

/**
 * Processes an incoming L2CAP fragment.
 *
 * @param conn                  The connection the L2CAP fragment was sent
 *                                  over.
 * @param hci_hdr               The ACL data header that was at the start of
 *                                  the L2CAP fragment.  This header has been
 *                                  stripped from the mbuf parameter.
 * @param om                    An mbuf containing the L2CAP data.  If this is
 *                                  the first fragment, the L2CAP header is at
 *                                  the start of the mbuf.  For subsequent
 *                                  fragments, the mbuf starts with L2CAP
 *                                  payload data.
 * @param out_rx_cb             If a full L2CAP packet has been received, a
 *                                  pointer to the appropriate handler gets
 *                                  written here.  The caller should pass the
 *                                  receive buffer to this callback.
 * @param out_reject_cid        Indicates whether an L2CAP Command Reject
 *                                  command should be sent.  If this equals -1,
 *                                  no reject should get sent.  Otherwise, the
 *                                  value indicates the CID that the outgoing
 *                                  reject should specify.
 *
 * @return                      0 if a complete L2CAP packet has been received.
 *                              BLE_HS_EAGAIN if a partial L2CAP packet has
 *                                  been received; more fragments are expected.
 *                              Other value on error.
 */
int
ble_l2cap_rx(struct ble_hs_conn *conn,
             struct hci_data_hdr *hci_hdr,
             struct os_mbuf *om,
             ble_l2cap_rx_fn **out_rx_cb,
             int *out_reject_cid)
{
    struct ble_l2cap_chan *chan;
    struct ble_l2cap_hdr l2cap_hdr;
    uint8_t pb;
    int rc;

    *out_reject_cid = -1;

    pb = BLE_HCI_DATA_PB(hci_hdr->hdh_handle_pb_bc);
    switch (pb) {
    case BLE_HCI_PB_FIRST_FLUSH:
        /* First fragment. */
        rc = ble_l2cap_parse_hdr(om, 0, &l2cap_hdr);
        if (rc != 0) {
            goto err;
        }

        /* Strip L2CAP header from the front of the mbuf. */
        os_mbuf_adj(om, BLE_L2CAP_HDR_SZ);

        chan = ble_hs_conn_chan_find_by_scid(conn, l2cap_hdr.cid);
        if (chan == NULL) {
            rc = BLE_HS_ENOENT;

            /* Unsupported channel. If the target CID is the black hole
             * channel, quietly drop the packet.  Otherwise, send an invalid
             * CID response.
             */
            if (l2cap_hdr.cid != BLE_L2CAP_CID_BLACK_HOLE) {
                BLE_HS_LOG(DEBUG, "rx on unknown L2CAP channel: %d\n",
                           l2cap_hdr.cid);
                *out_reject_cid = l2cap_hdr.cid;
            }
            goto err;
        }

        /* For CIDs from dynamic range we check if SDU size isn't larger than MPS */
        if (chan->dcid >= 0x0040 && chan->dcid <= 0x007F && l2cap_hdr.len > chan->my_coc_mps) {
            /* Data exceeds MPS */
            BLE_HS_LOG(ERROR, "error: sdu_len > chan->my_coc_mps (%d>%d)\n",
                       l2cap_hdr.len, chan->my_coc_mps);
            ble_l2cap_disconnect(chan);
            rc = BLE_HS_EBADDATA;
            goto err;
        }

        if (chan->rx_buf != NULL) {
            /* Previous data packet never completed.  Discard old packet. */
            ble_l2cap_remove_rx(conn, chan);
        }

        if (l2cap_hdr.len > ble_l2cap_get_mtu(chan)) {
            /* More data than we expected on the channel.
             * Disconnect peer with invalid behaviour
             */
            rc = BLE_HS_EBADDATA;
            ble_l2cap_disconnect(chan);
            goto err;
        }

        /* Remember channel and length of L2CAP data for reassembly. */
        conn->bhc_rx_chan = chan;
        chan->rx_len = l2cap_hdr.len;
        break;

    case BLE_HCI_PB_MIDDLE:
        chan = conn->bhc_rx_chan;
        if (chan == NULL || chan->rx_buf == NULL) {
            /* Middle fragment without the start.  Discard new packet. */
            rc = BLE_HS_EBADDATA;
            goto err;
        }
        break;

    default:
        rc = BLE_HS_EBADDATA;
        goto err;
    }

    rc = ble_l2cap_rx_payload(conn, chan, om, out_rx_cb);
    om = NULL;
    if (rc != 0) {
        goto err;
    }

    return 0;

err:
    os_mbuf_free_chain(om);
    return rc;
}

/**
 * Transmits the L2CAP payload contained in the specified mbuf.  The supplied
 * mbuf is consumed, regardless of the outcome of the function call.
 *
 * @param chan                  The L2CAP channel to transmit over.
 * @param txom                  The data to transmit.
 *
 * @return                      0 on success; nonzero on error.
 */
int
ble_l2cap_tx(struct ble_hs_conn *conn, struct ble_l2cap_chan *chan,
             struct os_mbuf *txom)
{
    int rc;

    txom = ble_l2cap_prepend_hdr(txom, chan->dcid, OS_MBUF_PKTLEN(txom));
    if (txom == NULL) {
        return BLE_HS_ENOMEM;
    }

    rc = ble_hs_hci_acl_tx(conn, &txom);
    switch (rc) {
    case 0:
        /* Success. */
        return 0;

    case BLE_HS_EAGAIN:
        /* Controller could not accommodate full packet.  Enqueue remainder. */
        STAILQ_INSERT_TAIL(&conn->bhc_tx_q, OS_MBUF_PKTHDR(txom), omp_next);
        return 0;

    default:
        /* Error. */
        return rc;
    }
}

int
ble_l2cap_init(void)
{
    int rc;

    rc = os_mempool_init(&ble_l2cap_chan_pool,
                         MYNEWT_VAL(BLE_L2CAP_MAX_CHANS) +
                         MYNEWT_VAL(BLE_L2CAP_COC_MAX_NUM),
                         sizeof (struct ble_l2cap_chan),
                         ble_l2cap_chan_mem, "ble_l2cap_chan_pool");
    if (rc != 0) {
        return BLE_HS_EOS;
    }

    rc = ble_l2cap_sig_init();
    if (rc != 0) {
        return rc;
    }

    rc = ble_l2cap_coc_init();
    if (rc != 0) {
        return rc;
    }

    rc = ble_sm_init();
    if (rc != 0) {
        return rc;
    }

    rc = stats_init_and_reg(
        STATS_HDR(ble_l2cap_stats), STATS_SIZE_INIT_PARMS(ble_l2cap_stats,
        STATS_SIZE_32), STATS_NAME_INIT_PARMS(ble_l2cap_stats), "ble_l2cap");
    if (rc != 0) {
        return BLE_HS_EOS;
    }

    return 0;
}

#endif
