/*
 * 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 "os/mynewt.h"

#if MYNEWT_VAL(SHELL_MGMT)
#include <stddef.h>
#include "base64/base64.h"
#include "crc/crc16.h"
#include "console/console.h"
#include "mgmt/mgmt.h"
#include "shell/shell.h"
#include "shell_priv.h"

/* NLIP packets sent over serial are fragmented into frames of 127 bytes or
 * fewer. This 127-byte maximum applies to the entire frame, including header,
 * CRC, and terminating newline.
 */
#define MGMT_NLIP_MAX_FRAME     127

static shell_nlip_input_func_t g_shell_nlip_in_func;
static void *g_shell_nlip_in_arg;
static struct os_mqueue g_shell_nlip_mq;
static struct os_mbuf *g_nlip_mbuf;
static uint16_t g_nlip_expected_len;

void
shell_nlip_clear_pkt(void)
{
    if (g_nlip_mbuf) {
        os_mbuf_free_chain(g_nlip_mbuf);
        g_nlip_mbuf = NULL;
    }
    g_nlip_expected_len = 0;
}

int
shell_nlip_process(char *data, int len)
{
    uint16_t copy_len;
    int rc;
    struct os_mbuf *m;
    uint16_t crc;

    /* Trim trailing newline. */
    if (len >= 1 && data[len - 1] == '\n') {
        data[len - 1] = '\0';
        len--;
    }

    rc = base64_decode(data, data);
    if (rc < 0) {
        goto err;
    }
    len = rc;

    if (g_nlip_mbuf == NULL) {
        if (len < 2) {
            rc = -1;
            goto err;
        }

        g_nlip_expected_len = ntohs(*(uint16_t *) data);
        g_nlip_mbuf = os_msys_get_pkthdr(g_nlip_expected_len, 0);
        if (!g_nlip_mbuf) {
            rc = -1;
            goto err;
        }

        data += sizeof(uint16_t);
        len -= sizeof(uint16_t);
    }

    copy_len = min(g_nlip_expected_len - OS_MBUF_PKTHDR(g_nlip_mbuf)->omp_len,
            len);

    rc = os_mbuf_copyinto(g_nlip_mbuf, OS_MBUF_PKTHDR(g_nlip_mbuf)->omp_len,
            data, copy_len);
    if (rc != 0) {
        goto err;
    }

    if (OS_MBUF_PKTHDR(g_nlip_mbuf)->omp_len == g_nlip_expected_len) {
        if (g_shell_nlip_in_func) {
            crc = CRC16_INITIAL_CRC;
            for (m = g_nlip_mbuf; m; m = SLIST_NEXT(m, om_next)) {
                crc = crc16_ccitt(crc, m->om_data, m->om_len);
            }
            if (crc == 0 && g_nlip_expected_len >= sizeof(crc)) {
                os_mbuf_adj(g_nlip_mbuf, -sizeof(crc));
                g_shell_nlip_in_func(g_nlip_mbuf, g_shell_nlip_in_arg);
            } else {
                os_mbuf_free_chain(g_nlip_mbuf);
            }
        } else {
            os_mbuf_free_chain(g_nlip_mbuf);
        }
        g_nlip_mbuf = NULL;
        g_nlip_expected_len = 0;
    }

    return (0);
err:
    return (rc);
}

static int
shell_nlip_mtx(struct os_mbuf *m)
{
#define SHELL_NLIP_MTX_BUF_SIZE (12)
    uint8_t readbuf[SHELL_NLIP_MTX_BUF_SIZE];
    char encodebuf[BASE64_ENCODE_SIZE(SHELL_NLIP_MTX_BUF_SIZE)];
    char pkt_seq[3] = { '\n', SHELL_NLIP_PKT_START1, SHELL_NLIP_PKT_START2 };
    char esc_seq[2] = { SHELL_NLIP_DATA_START1, SHELL_NLIP_DATA_START2 };
    uint16_t totlen;
    uint16_t dlen;
    uint16_t off;
    uint16_t crc;
    int rb_off;
    int elen;
    uint16_t bodylen;
    uint16_t newbodylen;
    int rc;
    struct os_mbuf *tmp;
    void *ptr;

    /* Convert the mbuf into a packet.
     *
     * starts with 06 09
     * base64 encode:
     *  - total packet length (uint16_t)
     *  - data
     *  - crc
     * base64 encoded data must be less than 122 bytes per line to
     * avoid overflows and adhere to convention.
     *
     * continuation packets are preceded by 04 20 until the entire
     * buffer has been sent.
     */
    crc = CRC16_INITIAL_CRC;
    for (tmp = m; tmp; tmp = SLIST_NEXT(tmp, om_next)) {
        crc = crc16_ccitt(crc, tmp->om_data, tmp->om_len);
    }
    crc = htons(crc);
    ptr = os_mbuf_extend(m, sizeof(crc));
    if (!ptr) {
        rc = -1;
        goto err;
    }
    memcpy(ptr, &crc, sizeof(crc));

    totlen = OS_MBUF_PKTHDR(m)->omp_len;
    off = 0;
    bodylen = 0;
    rb_off = 0;

    rc = console_lock(OS_TICKS_PER_SEC);
    if (rc != OS_OK) {
        goto err;
    }

    /* Start a packet */
    console_write(pkt_seq, sizeof(pkt_seq));

    /* Encode the packet length. */
    dlen = htons(totlen);
    memcpy(readbuf, &dlen, sizeof(dlen));
    rb_off += 2;

    while (totlen > 0) {
        /* Calculate number of source bytes to include in this chunk. */
        dlen = min(SHELL_NLIP_MTX_BUF_SIZE - rb_off, totlen);

        /* If appending this chunk to the current line causes the line to be
         * too long, terminate the line before emitting the chunk.
         *
         * Total line length:
         *     2                       +    // Frame header.
         *     bodylen                 +    // Body bytes already emitted.
         *     BASE64_ENCODE_SIZE(...) +    // Next chunk.
         *     1                            // Terminating newline.
         */
        newbodylen = 2 + bodylen + BASE64_ENCODE_SIZE(dlen + rb_off) + 1;
        if (newbodylen > MGMT_NLIP_MAX_FRAME) {
            /* The chunk won't fit.  Terminate the current line. */
            elen = base64_pad(encodebuf, bodylen);
            console_write(encodebuf, elen);
            console_write("\n", 1);

            /* Begin the next frame. */
            console_write(esc_seq, sizeof(esc_seq));
            bodylen = 0;
        } else {
            /* The chunk will fit.  Emit it. */
            rc = os_mbuf_copydata(m, off, dlen, readbuf + rb_off);
            if (rc != 0) {
                goto end;
            }

            elen = base64_encode(readbuf, dlen + rb_off, encodebuf, 0);
            console_write(encodebuf, elen);
            bodylen += elen;
            totlen -= dlen;
            off += dlen;
        }

        rb_off = 0;
    }

    /* The final line was never terminated.  Terminate it now. */ 
    elen = base64_pad(encodebuf, bodylen);
    console_write(encodebuf, elen);
    console_write("\n", 1);

end:
    (void)console_unlock();

err:
    return (rc);
}

int
shell_nlip_input_register(shell_nlip_input_func_t nf, void *arg)
{
    g_shell_nlip_in_func = nf;
    g_shell_nlip_in_arg = arg;

    return (0);
}

int
shell_nlip_output(struct os_mbuf *m)
{
    return os_mqueue_put(&g_shell_nlip_mq, os_eventq_dflt_get(), m);
}

static void
shell_event_data_in(struct os_event *ev)
{
    struct os_mbuf *m;

    /* Copy data out of the mbuf 12 bytes at a time and write it to
     * the console.
     */
    while (1) {
        m = os_mqueue_get(&g_shell_nlip_mq);
        if (!m) {
            break;
        }

        (void) shell_nlip_mtx(m);

        os_mbuf_free_chain(m);
    }
}

void
shell_nlip_init(void)
{
    os_mqueue_init(&g_shell_nlip_mq, shell_event_data_in, NULL);
}
#endif
