/****************************************************************************
 * net/ipfrag/ipv4_frag.c
 * Handling incoming IPv4 fragment input
 *
 * 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.
 *
 ****************************************************************************/

/****************************************************************************
 * Included Files
 ****************************************************************************/

#include <nuttx/config.h>

#if defined(CONFIG_NET_IPv4) && defined (CONFIG_NET_IPFRAG)

#include <sys/ioctl.h>
#include <stdint.h>
#include <stdlib.h>
#include <debug.h>
#include <string.h>
#include <errno.h>
#include <netinet/in.h>
#include <net/if.h>

#include <nuttx/kmalloc.h>
#include <nuttx/net/netconfig.h>
#include <nuttx/net/netdev.h>
#include <nuttx/net/netstats.h>
#include <nuttx/net/ip.h>

#include "netdev/netdev.h"
#include "inet/inet.h"
#include "utils/utils.h"
#include "ipfrag.h"

/****************************************************************************
 * Private Function Prototypes
 ****************************************************************************/

static inline int32_t
ipv4_fragin_getinfo(FAR struct iob_s *iob,
                    FAR struct ip_fraglink_s *fraglink);
static uint32_t ipv4_fragin_reassemble(FAR struct ip_fragsnode_s *node);
static inline void
ipv4_fragout_buildipv4header(FAR struct ipv4_hdr_s *ref,
                             FAR struct ipv4_hdr_s *ipv4,
                             uint16_t len, uint16_t ipoff);

/****************************************************************************
 * Private Functions
 ****************************************************************************/

/****************************************************************************
 * Name: ipv4_fragin_getinfo
 *
 * Description:
 *   Polulate fragment information from the input ipv4 packet data.
 *
 * Input Parameters:
 *   iob      - An IPv4 fragment
 *   fraglink - node of the lower-level linked list, it maintains information
 *              of one fragment
 *
 * Returned Value:
 *   None
 *
 ****************************************************************************/

static inline int32_t
ipv4_fragin_getinfo(FAR struct iob_s *iob,
                    FAR struct ip_fraglink_s *fraglink)
{
  FAR struct ipv4_hdr_s *ipv4 = (FAR struct ipv4_hdr_s *)
                                (iob->io_data + iob->io_offset);
  uint16_t offset;

  fraglink->flink     = NULL;
  fraglink->fragsnode = NULL;
  fraglink->isipv4    = true;

  offset = (ipv4->ipoffset[0] << 8) + ipv4->ipoffset[1];
  fraglink->morefrags = offset & IP_FLAG_MOREFRAGS;
  fraglink->fragoff   = ((offset & 0x1fff) << 3);

  fraglink->fraglen   = (ipv4->len[0] << 8) + ipv4->len[1] - IPv4_HDRLEN;
  fraglink->ipid      = (ipv4->ipid[0] << 8) + ipv4->ipid[1];
  fraglink->frag      = iob;

  return OK;
}

/****************************************************************************
 * Name: ipv4_fragin_reassemble
 *
 * Description:
 *   Reassemble all ipv4 fragments to build an IP frame.
 *
 * Input Parameters:
 *   node - node of the upper-level linked list, it maintains
 *          information about all fragments belonging to an IP datagram
 *
 * Returned Value:
 *   The length of the reassembled IP frame
 *
 ****************************************************************************/

static uint32_t ipv4_fragin_reassemble(FAR struct ip_fragsnode_s *node)
{
  FAR struct iob_s *head = NULL;
  FAR struct ipv4_hdr_s *ipv4;
  FAR struct ip_fraglink_s *fraglink;

  /* Loop to walk through the fragment list and reassemble those fragments,
   * the fraglink list was ordered by fragment offset value
   */

  fraglink    = node->frags;
  node->frags = NULL;

  while (fraglink != NULL)
    {
      FAR struct ip_fraglink_s *linknext;
      FAR struct iob_s *iob = fraglink->frag;

      if (fraglink->fragoff != 0)
        {
          uint16_t iphdrlen;

          /* Get IPv4 header length from IPv4 header (it may carry some
           * IPv4 options)
           */

          ipv4 = (FAR struct ipv4_hdr_s *)(head->io_data + head->io_offset);
          iphdrlen = (ipv4->vhl & IPv4_HLMASK) << 2;

          /* Just modify the offset and length of all none zero fragments */

          iob->io_offset += iphdrlen;
          iob->io_len    -= iphdrlen;
          iob->io_pktlen -= iphdrlen;

          /* Concatenate this iob to the reassembly chain */

          iob_concat(head, iob);
        }
      else
        {
          /* Remember the head iob */

          head = iob;
        }

      linknext = fraglink->flink;
      kmm_free(fraglink);

      fraglink = linknext;
    }

  /* Remember the reassembled outgoing IP frame */

  node->outgoframe = head;

  /* Get pointer of the new IPv4 header */

  ipv4 = (FAR struct ipv4_hdr_s *)(head->io_data + head->io_offset);

  /* Update the length value in the IP Header */

  ipv4->len[0] = head->io_pktlen >> 8;
  ipv4->len[1] = head->io_pktlen & 0xff;

  /* Set ipoffset to zero */

  ipv4->ipoffset[0] = 0;
  ipv4->ipoffset[1] = 0;

  /* Calculate IP checksum. */

  ipv4->ipchksum    = 0;
  ipv4->ipchksum    = ~(ipv4_chksum(ipv4));

  return head->io_pktlen;
}

/****************************************************************************
 * Name: ipv4_fragout_buildipv4header
 *
 * Description:
 *   Build IPv4 header for an IPv4 fragment.
 *
 * Input Parameters:
 *   ref    - The reference IPv4 Header
 *   ipv4   - The pointer of the newly generated IPv4 Header
 *   len    - Total Length of this IP frame
 *   ipoff  - Fragment offset
 *
 * Returned Value:
 *   None
 *
 ****************************************************************************/

static inline void
ipv4_fragout_buildipv4header(FAR struct ipv4_hdr_s *ref,
                             FAR struct ipv4_hdr_s *ipv4,
                             uint16_t len, uint16_t ipoff)
{
  if (ref != ipv4)
    {
      uint32_t iphdrlen = (ref->vhl & IPv4_HLMASK) << 2;
      memcpy(ipv4, ref, iphdrlen);
    }

  ipv4->len[0]      = len >> 8;
  ipv4->len[1]      = len & 0xff;

  ipv4->ipoffset[0] = ipoff >> 8;
  ipv4->ipoffset[1] = ipoff & 0xff;

  /* Calculate IP checksum. */

  ipv4->ipchksum    = 0;
  ipv4->ipchksum    = ~(ipv4_chksum(ipv4));
}

/****************************************************************************
 * Public Functions
 ****************************************************************************/

/****************************************************************************
 * Name: ipv4_fragin
 *
 * Description:
 *   Handling incoming IPv4 and IPv6 fragment input, the input data
 *   (dev->d_iob) can be an I/O buffer chain
 *
 * Input Parameters:
 *   dev    - The NIC device that the fragmented data comes from
 *
 * Returned Value:
 *   ENOMEM - No memory
 *   OK     - The input fragment is processed as expected
 *
 ****************************************************************************/

int32_t ipv4_fragin(FAR struct net_driver_s *dev)
{
  FAR struct ip_fragsnode_s *node;
  FAR struct ip_fraglink_s *fraginfo;
  bool restartwdog;

  if (dev->d_len != dev->d_iob->io_pktlen)
    {
      nerr("ERROR: Parameters error.\n");
      return -EINVAL;
    }

  fraginfo = kmm_malloc(sizeof(struct ip_fraglink_s));
  if (fraginfo == NULL)
    {
      nerr("ERROR: Failed to allocate buffer.\n");
      return -ENOMEM;
    }

  /* Polulate fragment information from input packet data */

  ipv4_fragin_getinfo(dev->d_iob, fraginfo);

  nxmutex_lock(&g_ipfrag_lock);

  /* Need to restart reassembly worker if the original linked list is empty */

  restartwdog = ip_fragin_enqueue(dev, fraginfo);

  node = fraginfo->fragsnode;

  if (node->verifyflag & IP_FRAGVERIFY_RECVDALLFRAGS)
    {
      /* Well, all fragments of an IP frame have been received, remove
       * node from link list first, then reassemble and dispatch to the
       * stack.
       */

      ip_frag_remnode(node);

      /* All fragments belonging to one IP frame have been separated
       * from the fragment processing module, unlocks mutex as soon
       * as possible
       */

      nxmutex_unlock(&g_ipfrag_lock);

      /* Reassemble fragments to one IP frame and set the resulting
       * IP frame to dev->d_iob
       */

      ipv4_fragin_reassemble(node);
      netdev_iob_replace(dev, node->outgoframe);

      /* Free the memory of node */

      kmm_free(node);

      return ipv4_input(dev);
    }

  nxmutex_unlock(&g_ipfrag_lock);

  if (restartwdog)
    {
      /* Restart the work queue for fragment processing */

      ip_frag_startwdog();
    }

  return OK;
}

/****************************************************************************
 * Name: ipv4_fragout
 *
 * Description:
 *   Execute the ipv4 fragment function. After this work is done, all
 *   fragments are maintained by dev->d_fragout. In order to reduce the
 *   cyclomatic complexity and facilitate maintenance, fragmentation is
 *   performed in two steps:
 *   1. Reconstruct I/O Buffer according to MTU, which will reserve
 *      the space for the L3 header;
 *   2. Fill the L3 header into the reserved space.
 *
 * Input Parameters:
 *   dev    - The NIC device
 *   mtu    - The MTU of current NIC
 *
 * Returned Value:
 *   0 if success or a negative value if fail.
 *
 * Assumptions:
 *   Data length(dev->d_iob->io_pktlen) is grater than the MTU of
 *   current NIC
 *
 ****************************************************************************/

int32_t ipv4_fragout(FAR struct net_driver_s *dev, uint16_t mtu)
{
  uint32_t iter;
  uint32_t nfrags;
  uint16_t offset = 0;
  uint16_t hdrlen;
  FAR struct iob_s *frag;
  FAR struct ipv4_hdr_s *ref = NULL;
  struct iob_queue_s fragq =
    {
      NULL, NULL
    };

  /* Get the total length of L3 Header(if IPv4 options are present, then this
   * length includes the size of all the IPv4 options)
   */

  hdrlen = (IPv4BUF->vhl & IPv4_HLMASK) << 2;

  /* Reconstruct I/O Buffer according to MTU, which will reserve
   * the space for the L3 header
   */

  nfrags = ip_fragout_slice(dev->d_iob, PF_INET, mtu, hdrlen, &fragq);
  assert(nfrags > 1);
  netdev_iob_clear(dev);

  /* Fill the L3 header into the reserved space */

  for (iter = 0; iter < nfrags; iter++)
    {
      frag = iob_remove_queue(&fragq);

      if (iter == 0)
        {
          ref = (FAR struct ipv4_hdr_s *)(frag->io_data + frag->io_offset);

          /* Update the IPv4 header of the first fragment */

          ipv4_fragout_buildipv4header(ref, ref, frag->io_pktlen,
                                       IP_FLAG_MOREFRAGS);
        }
      else
        {
          uint16_t ipoff = (offset - iter * hdrlen) >> 3;

          if (iter < nfrags - 1)
            {
              ipoff |= IP_FLAG_MOREFRAGS;
            }

          /* Refer to the zero fragment ipv4 header to construct the ipv4
           * header of non-zero fragment
           */

          ipv4_fragout_buildipv4header(ref,
                  (FAR struct ipv4_hdr_s *)(frag->io_data + frag->io_offset),
                  frag->io_pktlen, ipoff);
        }

      /* Enqueue this fragment to dev->d_fragout */

      if (iob_tryadd_queue(frag, &dev->d_fragout) < 0)
        {
          goto fail;
        }

      offset += frag->io_pktlen;
    }

#ifdef CONFIG_NET_STATISTICS
  g_netstats.ipv4.sent += nfrags - 1;
#endif

  netdev_txnotify_dev(dev);

  return OK;

fail:
  netdev_iob_release(dev);
  iob_free_chain(frag);
  iob_free_queue(&fragq);
  iob_free_queue(&dev->d_fragout);
  return -ENOMEM;
}

#endif /* CONFIG_NET_IPv4 && CONFIG_NET_IPFRAG */