/*********************************************************************
   PicoTCP. Copyright (c) 2012 TASS Belgium NV. Some rights reserved.
   See COPYING, LICENSE.GPLv2 and LICENSE.GPLv3 for usage.

   Authors: Laurens Miers, Daniele Lacamera
 *********************************************************************/


#include "pico_config.h"
#ifdef PICO_SUPPORT_IPV6
#include "pico_ipv6.h"
#include "pico_icmp6.h"
#endif
#ifdef PICO_SUPPORT_IPV4
#include "pico_ipv4.h"
#include "pico_icmp4.h"
#endif
#include "pico_stack.h"
#include "pico_eth.h"
#include "pico_udp.h"
#include "pico_tcp.h"
#include "pico_socket.h"
#include "pico_device.h"
#include "pico_tree.h"
#include "pico_constants.h"
#include "pico_fragments.h"

#ifdef DEBUG_FRAG
    #define frag_dbg dbg
#else
    #define frag_dbg(...) do {} while(0)
#endif

#if defined(PICO_SUPPORT_IPV6) && defined(PICO_SUPPORT_IPV6FRAG)
#define IP6_FRAG_OFF(x)         ((x & 0xFFF8u))
#define IP6_FRAG_MORE(x)        ((x & 0x0001))
#define IP6_FRAG_ID(x)          ((uint32_t)(((uint32_t)x->ext.frag.id[0] << 24) + ((uint32_t)x->ext.frag.id[1] << 16) + \
                                            ((uint32_t)x->ext.frag.id[2] << 8) + (uint32_t)x->ext.frag.id[3]))

#else
#define IP6_FRAG_OFF(x)         (0)
#define IP6_FRAG_MORE(x)        (0)
#define IP6_FRAG_ID(x)          (0)
#endif

#if defined(PICO_SUPPORT_IPV4) && defined(PICO_SUPPORT_IPV4FRAG)
#define IP4_FRAG_OFF(frag)      (((uint32_t)frag & PICO_IPV4_FRAG_MASK) << 3ul)
#define IP4_FRAG_MORE(frag)     ((frag & PICO_IPV4_MOREFRAG) ? 1 : 0)
#define IP4_FRAG_ID(hdr)        (hdr->id)
#else
#define IP4_FRAG_OFF(frag)      (0)
#define IP4_FRAG_MORE(frag)     (0)
#define IP4_FRAG_ID(hdr)        (0)
#endif

#define PICO_IPV6_FRAG_TIMEOUT   60000
#define PICO_IPV4_FRAG_TIMEOUT   15000

static void pico_frag_expire(pico_time now, void *arg);
static void pico_fragments_complete(unsigned int bookmark, uint8_t proto, uint8_t net);
static int pico_fragments_check_complete(struct pico_tree *tree, uint8_t proto, uint8_t net);
static int pico_fragments_reassemble(struct pico_tree *tree, unsigned int len, uint8_t proto, uint8_t net);
static int pico_fragments_get_more_flag(struct pico_frame *frame, uint8_t net);
static uint32_t pico_fragments_get_offset(struct pico_frame *frame, uint8_t net);
static void pico_fragments_send_notify(struct pico_frame *first);
static uint16_t pico_fragments_get_header_length(uint8_t net);
static void pico_fragments_empty_tree(struct pico_tree *tree);

#if defined(PICO_SUPPORT_IPV6) && defined(PICO_SUPPORT_IPV6FRAG)
static uint32_t ipv6_cur_frag_id = 0u;
static uint32_t ipv6_fragments_timer = 0u;

static int pico_ipv6_frag_compare(void *ka, void *kb)
{
    struct pico_frame *a = ka, *b = kb;
    if (IP6_FRAG_OFF(a->frag) > IP6_FRAG_OFF(b->frag))
        return 1;

    if (IP6_FRAG_OFF(a->frag) < IP6_FRAG_OFF(b->frag))
        return -1;

    return 0;
}
static PICO_TREE_DECLARE(ipv6_fragments, pico_ipv6_frag_compare);

static void pico_ipv6_fragments_complete(unsigned int len, uint8_t proto)
{
    if (pico_fragments_reassemble(&ipv6_fragments, len, proto, PICO_PROTO_IPV6) == 0)
    {
        pico_timer_cancel(ipv6_fragments_timer);
        ipv6_fragments_timer = 0;
    }
}

static void pico_ipv6_frag_timer_on(void)
{
    ipv6_fragments_timer = pico_timer_add(PICO_IPV6_FRAG_TIMEOUT, pico_frag_expire, &ipv6_fragments);
    if (!ipv6_fragments_timer) {
        frag_dbg("FRAG: Failed to start IPv6 expiration timer\n");
        pico_fragments_empty_tree(&ipv6_fragments);
    }
}

static int pico_ipv6_frag_match(struct pico_frame *a, struct pico_frame *b)
{
    struct pico_ipv6_hdr *ha = NULL, *hb = NULL;
    if (!a || !b)
        return -1;

    ha = (struct pico_ipv6_hdr *)a->net_hdr;
    hb = (struct pico_ipv6_hdr *)b->net_hdr;
    if (!ha || !hb)
        return -2;

    if (memcmp(ha->src.addr, hb->src.addr, PICO_SIZE_IP6) != 0)
        return 1;

    if (memcmp(ha->dst.addr, hb->dst.addr, PICO_SIZE_IP6) != 0)
        return 2;

    return 0;
}
#endif

#if defined(PICO_SUPPORT_IPV4) && defined(PICO_SUPPORT_IPV4FRAG)
static uint32_t ipv4_cur_frag_id = 0u;
static uint32_t ipv4_fragments_timer = 0u;

static int pico_ipv4_frag_compare(void *ka, void *kb)
{
    struct pico_frame *a = ka, *b = kb;
    if (IP4_FRAG_OFF(a->frag) > IP4_FRAG_OFF(b->frag))
        return 1;

    if (IP4_FRAG_OFF(a->frag) < IP4_FRAG_OFF(b->frag))
        return -1;

    return 0;
}
static PICO_TREE_DECLARE(ipv4_fragments, pico_ipv4_frag_compare);

static void pico_ipv4_fragments_complete(unsigned int len, uint8_t proto)
{
    if (pico_fragments_reassemble(&ipv4_fragments, len, proto, PICO_PROTO_IPV4) == 0)
    {
        pico_timer_cancel(ipv4_fragments_timer);
        ipv4_fragments_timer = 0;
    }
}

static void pico_ipv4_frag_timer_on(void)
{
    ipv4_fragments_timer = pico_timer_add( PICO_IPV4_FRAG_TIMEOUT, pico_frag_expire, &ipv4_fragments);
    if (!ipv4_fragments_timer) {
        frag_dbg("FRAG: Failed to start IPv4 expiration timer\n");
        pico_fragments_empty_tree(&ipv4_fragments);
    }
}

static int pico_ipv4_frag_match(struct pico_frame *a, struct pico_frame *b)
{
    struct pico_ipv4_hdr *ha, *hb;
    if (!a || !b)
        return -1;

    ha = (struct pico_ipv4_hdr *)a->net_hdr;
    hb = (struct pico_ipv4_hdr *)b->net_hdr;
    if (!ha || !hb)
        return -2;

    if (memcmp(&(ha->src.addr), &(hb->src.addr), PICO_SIZE_IP4) != 0)
        return 1;

    if (memcmp(&(ha->dst.addr), &(hb->dst.addr), PICO_SIZE_IP4) != 0)
        return 2;

    return 0;
}
#endif

static void pico_fragments_complete(unsigned int bookmark, uint8_t proto, uint8_t net)
{
    if (0) {}

#if defined(PICO_SUPPORT_IPV4) && defined(PICO_SUPPORT_IPV4FRAG)
    else if (net == PICO_PROTO_IPV4)
    {
        pico_ipv4_fragments_complete(bookmark, proto);
    }
#endif
#if defined(PICO_SUPPORT_IPV6) && defined(PICO_SUPPORT_IPV6FRAG)
    else if (net == PICO_PROTO_IPV6)
    {
        pico_ipv6_fragments_complete(bookmark, proto);
    }
#endif
}

static void pico_fragments_empty_tree(struct pico_tree *tree)
{
    struct pico_tree_node *index, *tmp;

    if (!tree)
    {
        return;
    }

    pico_tree_foreach_safe(index, tree, tmp) {
        struct pico_frame * old = index->keyValue;
        pico_tree_delete(tree, old);
        pico_frame_discard(old);
    }

}

static int pico_fragments_check_complete(struct pico_tree *tree, uint8_t proto, uint8_t net)
{
    struct pico_tree_node *index, *temp;
    struct pico_frame *cur;
    unsigned int bookmark = 0;

    if (!tree)
        return 0;

    pico_tree_foreach_safe(index, tree, temp) {
        cur = index->keyValue;
        if (cur) {
            if (pico_fragments_get_offset(cur, net) != bookmark)
                return -1;

            bookmark += cur->transport_len;
            if (!pico_fragments_get_more_flag(cur, net)) {
                pico_fragments_complete(bookmark, proto, net);
                return 0;
            }
        }
    }
    return 1;
}

static void pico_frag_expire(pico_time now, void *arg)
{
    struct pico_tree *tree = (struct pico_tree *) arg;
    struct pico_frame *first = NULL;
    IGNORE_PARAMETER(now);

    if (!tree)
    {
        frag_dbg("Expired packet but no tree supplied!\n");
        return;
    }

    first = pico_tree_first(tree);

    if (!first) {
        frag_dbg("Empty tree - not sending notify\n");
        return;
    }

    pico_fragments_send_notify(first);

    pico_fragments_empty_tree(tree);
}

static void pico_fragments_send_notify(struct pico_frame *first)
{
    uint8_t net = 0;

    if (!first)
    {
        return;
    }

    if (0) {}

#if defined(PICO_SUPPORT_IPV4) && defined(PICO_SUPPORT_IPV4FRAG)
    else if (IS_IPV4(first))
    {
        net = PICO_PROTO_IPV4;
        frag_dbg("Packet expired! ID:%hu\n", ipv4_cur_frag_id);
    }

#endif
#if defined(PICO_SUPPORT_IPV6) && defined(PICO_SUPPORT_IPV6FRAG)
    else if (IS_IPV6(first))
    {
        net = PICO_PROTO_IPV6;
        frag_dbg("Packet expired! ID:%hu\n", ipv6_cur_frag_id);
    }

#endif

    if (((pico_fragments_get_offset(first, net) == 0) && (pico_frame_dst_is_unicast(first))))
    {
        frag_dbg("sending notify\n");
        pico_notify_frag_expired(first);
    }
    else
    {
        frag_dbg("Not first packet or not unicast address, not sending notify");
    }
}

static int pico_fragments_reassemble(struct pico_tree *tree, unsigned int len, uint8_t proto, uint8_t net)
{
    struct pico_tree_node *index, *tmp;
    struct pico_frame *f;
    uint16_t header_length = 0;
    unsigned int bookmark = 0;
    struct pico_frame *full = NULL;
    struct pico_frame *first = NULL;

    if (!tree)
    {
        frag_dbg("Cannot reassemble packet, no tree supplied!\n");
        return -1;
    }

    first = pico_tree_first(tree);

    if (!first)
    {
        frag_dbg("Cannot reassemble packet, empty tree supplied!\n");
        return -2;
    }

    header_length = pico_fragments_get_header_length(net);

    if (!header_length)
    {
        return -3;
    }

    full = pico_frame_alloc((uint16_t)(header_length + len));
    if (full) {
        full->net_hdr = full->buffer;
        full->net_len = header_length;
        memcpy(full->net_hdr, first->net_hdr, full->net_len);
        full->transport_hdr = full->net_hdr + full->net_len;
        full->transport_len = (uint16_t)len;
        full->dev = first->dev;
        pico_tree_foreach_safe(index, tree, tmp) {
            f = index->keyValue;
            memcpy(full->transport_hdr + bookmark, f->transport_hdr, f->transport_len);
            bookmark += f->transport_len;
            pico_tree_delete(tree, f);
            pico_frame_discard(f);
        }
        if (pico_transport_receive(full, proto) == -1)
        {
            pico_frame_discard(full);
        }

        return 0;
    }

    return 1;
}

static uint16_t pico_fragments_get_header_length(uint8_t net)
{
    if (0) {}

#if defined(PICO_SUPPORT_IPV4) && defined(PICO_SUPPORT_IPV4FRAG)
    else if (net == PICO_PROTO_IPV4)
    {
        return PICO_SIZE_IP4HDR;
    }
#endif
#if defined(PICO_SUPPORT_IPV6) && defined(PICO_SUPPORT_IPV6FRAG)
    else if (net == PICO_PROTO_IPV6)
    {
        return PICO_SIZE_IP6HDR;
    }
#endif

    return 0;
}

static int pico_fragments_get_more_flag(struct pico_frame *frame, uint8_t net)
{
    if (!frame)
    {
      frag_dbg("no frame given to determine more flag\n");
      return 0;
    }

    if (0) {}

#if defined(PICO_SUPPORT_IPV4) && defined(PICO_SUPPORT_IPV4FRAG)
    else if (net == PICO_PROTO_IPV4)
    {
      return IP4_FRAG_MORE(frame->frag);
    }
#endif
#if defined(PICO_SUPPORT_IPV6) && defined(PICO_SUPPORT_IPV6FRAG)
    else if (net == PICO_PROTO_IPV6)
    {
      return IP6_FRAG_MORE(frame->frag);
    }
#endif

    return 0;
}

static uint32_t pico_fragments_get_offset(struct pico_frame *frame, uint8_t net)
{
    if (!frame)
    {
      frag_dbg("no frame given to determine offset\n");
      return 0;
    }

    if (0) {}

#if defined(PICO_SUPPORT_IPV4) && defined(PICO_SUPPORT_IPV4FRAG)
    else if (net == PICO_PROTO_IPV4)
    {
      return IP4_FRAG_OFF(frame->frag);
    }
#endif
#if defined(PICO_SUPPORT_IPV6) && defined(PICO_SUPPORT_IPV6FRAG)
    else if (net == PICO_PROTO_IPV6)
    {
      return IP6_FRAG_OFF(frame->frag);
    }
#endif

    return 0;
}

void pico_ipv6_process_frag(struct pico_ipv6_exthdr *frag, struct pico_frame *f, uint8_t proto)
{
#if defined(PICO_SUPPORT_IPV6) && defined(PICO_SUPPORT_IPV6FRAG)
    struct pico_frame *first = NULL;

    if (!f || !frag)
    {
        frag_dbg("Bad arguments provided to pico_ipv6_process_frag\n");
        return;
    }

    first = pico_tree_first(&ipv6_fragments);

    if (first)
    {
        if ((pico_ipv6_frag_match(f, first) == 0 && (IP6_FRAG_ID(frag) == ipv6_cur_frag_id))) {
            struct pico_frame *temp = NULL;

            temp = pico_frame_copy(f);

            if (!temp) {
                frag_dbg("Could not allocate memory to continue reassembly of IPV6 fragmented packet (id: %hu)\n", ipv6_cur_frag_id);
                return;
            }

            if (pico_tree_insert(&ipv6_fragments, temp)) {
                frag_dbg("FRAG: Could not insert picoframe in tree\n");
                pico_frame_discard(temp);
                return;
            }
        }
    }
    else
    {
        struct pico_frame *temp = NULL;

        if (ipv6_cur_frag_id && (IP6_FRAG_ID(frag) == ipv6_cur_frag_id)) {
            /* Discard late arrivals, without firing the timer. */
            frag_dbg("discarded late arrival, exp:%hu found:%hu\n", ipv6_cur_frag_id, IP6_FRAG_ID(frag));
            return;
        }

        temp = pico_frame_copy(f);

        if (!temp) {
            frag_dbg("Could not allocate memory to start reassembly of fragmented packet\n");
            return;
        }

        pico_ipv6_frag_timer_on();
        ipv6_cur_frag_id = IP6_FRAG_ID(frag);
        frag_dbg("Started new reassembly, ID:%hu\n", ipv6_cur_frag_id);

        if (pico_tree_insert(&ipv6_fragments, temp)) {
            frag_dbg("FRAG: Could not insert picoframe in tree\n");
            pico_frame_discard(temp);
            return;
        }
    }

    pico_fragments_check_complete(&ipv6_fragments, proto, PICO_PROTO_IPV6);
#else
    IGNORE_PARAMETER(frag);
    IGNORE_PARAMETER(f);
    IGNORE_PARAMETER(proto);
#endif
}

void pico_ipv4_process_frag(struct pico_ipv4_hdr *hdr, struct pico_frame *f, uint8_t proto)
{
#if defined(PICO_SUPPORT_IPV4) && defined(PICO_SUPPORT_IPV4FRAG)
    struct pico_frame *first = NULL;

    if (!f || !hdr)
    {
        frag_dbg("Bad arguments provided to pico_ipv4_process_frag\n");
        return;
    }

    first = pico_tree_first(&ipv4_fragments);

    if (first)
    {
        /* fragments from old packets still in tree, and new first fragment ? */
        if ((IP4_FRAG_ID(hdr) != ipv4_cur_frag_id) && (IP4_FRAG_OFF(f->frag) == 0)) {
            pico_fragments_empty_tree(&ipv4_fragments);

            first = NULL;
            ipv4_cur_frag_id = 0;
        }

        if ((pico_ipv4_frag_match(f, first) == 0 && (IP4_FRAG_ID(hdr) == ipv4_cur_frag_id))) {
            struct pico_frame *temp = NULL;

            temp = pico_frame_copy(f);

            if (!temp) {
                frag_dbg("Could not allocate memory to continue reassembly of IPV4 fragmented packet (id: %hu)\n", ipv4_cur_frag_id);
                return;
            }

            if (pico_tree_insert(&ipv4_fragments, temp)) {
            	frag_dbg("FRAG: Could not insert picoframe in tree\n");
                pico_frame_discard(temp);
                return;
			}
        }
    }
    else
    {
        struct pico_frame *temp = NULL;

        if (ipv4_cur_frag_id && (IP4_FRAG_ID(hdr) == ipv4_cur_frag_id)) {
            /* Discard late arrivals, without firing the timer */
            return;
        }

        temp = pico_frame_copy(f);

        if (!temp) {
            frag_dbg("Could not allocate memory to start reassembly fragmented packet\n");
            return;
        }

        pico_ipv4_frag_timer_on();
        ipv4_cur_frag_id = IP4_FRAG_ID(hdr);
        frag_dbg("Started new reassembly, ID:%hu\n", ipv4_cur_frag_id);

        if (pico_tree_insert(&ipv4_fragments, temp)) {
            frag_dbg("FRAG: Could not insert picoframe in tree\n");
            pico_frame_discard(temp);
            return;
        }
    }

    pico_fragments_check_complete(&ipv4_fragments, proto, PICO_PROTO_IPV4);
#else
    IGNORE_PARAMETER(hdr);
    IGNORE_PARAMETER(f);
    IGNORE_PARAMETER(proto);
#endif
}

void pico_fragments_deinit(void)
{
#if defined(PICO_SUPPORT_IPV4) && defined(PICO_SUPPORT_IPV4FRAG)
	pico_fragments_empty_tree(&ipv4_fragments);
#endif
#if defined(PICO_SUPPORT_IPV6) && defined(PICO_SUPPORT_IPV6FRAG)
	pico_fragments_empty_tree(&ipv6_fragments);
#endif
}
