/*
 * Cisco router simulation platform.
 * Copyright (c) 2006 Christophe Fillot (cf@utc.fr)
 *
 * Frame-Relay switch.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <errno.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>

#include "utils.h"
#include "mempool.h"
#include "registry.h"
#include "net_io.h"
#include "frame_relay.h"

#define DEBUG_FRSW  0

extern FILE *log_file;

/* ANSI LMI packet header */   
static const m_uint8_t lmi_ansi_hdr[] = { 
   0x00, 0x01, 0x03, 0x08, 0x00, 0x75, 0x95,
};

/* DLCI hash function */
static inline u_int frsw_dlci_hash(u_int dlci)
{
   return((dlci ^ (dlci >> 8)) & (FRSW_HASH_SIZE-1));
}

/* DLCI lookup */
frsw_conn_t *frsw_dlci_lookup(frsw_table_t *t,netio_desc_t *input,u_int dlci)
{
   frsw_conn_t *vc;
   
   for(vc=t->dlci_table[frsw_dlci_hash(dlci)];vc;vc=vc->hash_next)
      if ((vc->input == input) && (vc->dlci_in == dlci))
         return vc;

   return NULL;
}

/* Handle a ANSI LMI packet */
ssize_t frsw_handle_lmi_ansi_pkt(frsw_table_t *t,netio_desc_t *input,
                                 m_uint8_t *pkt,ssize_t len)
{
   m_uint8_t resp[FR_MAX_PKT_SIZE],*pres,*preq;
   m_uint8_t itype,isize;
   int msg_type,seq_ok;
   ssize_t rlen;
   frsw_conn_t *sc;
   u_int dlci;

   if ((len <= sizeof(lmi_ansi_hdr)) || 
       memcmp(pkt,lmi_ansi_hdr,sizeof(lmi_ansi_hdr)))
      return(-1);

#if DEBUG_FRSW
   m_log(input->name,"received an ANSI LMI packet:\n");
   mem_dump(log_file,pkt,len);
#endif

   /* Prepare response packet */
   memcpy(resp,lmi_ansi_hdr,sizeof(lmi_ansi_hdr));
   resp[FR_LMI_ANSI_STATUS_OFFSET] = FR_LMI_ANSI_STATUS;

   preq = &pkt[sizeof(lmi_ansi_hdr)];
   pres = &resp[sizeof(lmi_ansi_hdr)];
   
   msg_type = -1;
   seq_ok = FALSE;

   while((preq + 2) < (pkt + len)) {
      /* get item type and size */
      itype = preq[0];
      isize = preq[1];

      /* check packet boundary */
      if ((preq + isize + 2) > (pkt + len)) {
         m_log(input->name,"invalid LMI packet:\n");
         mem_dump(log_file,pkt,len);
         return(-1);
      }

      switch(itype) {
         case 0x01:   /* report information element */
            if (isize != 1) {
               m_log(input->name,"invalid LMI item size.\n");
               return(-1);
            }

            if ((msg_type = preq[2]) > 1) {
               m_log(input->name,"unknown LMI report type 0x%x.\n",msg_type);
               return(-1);
            }

            pres[0] = 0x01;
            pres[1] = 0x01;
            pres[2] = msg_type;
            pres += 3;
            break;

         case 0x03:   /* sequences */
            if (isize != 2) {
               m_log(input->name,"invalid LMI item size.\n");
               return(-1);
            }

            pres[0] = 0x03;
            pres[1] = 0x02;

            if (input->fr_lmi_seq != preq[3]) {
               m_log(input->name,"resynchronization with LMI sequence...\n");
               input->fr_lmi_seq = preq[3];
            } 

            input->fr_lmi_seq++;
            if (!input->fr_lmi_seq) input->fr_lmi_seq++;
            pres[2] = input->fr_lmi_seq;
            pres[3] = preq[2];

#if DEBUG_FRSW
            m_log(input->name,"iSSN=0x%x, iRSN=0x%x, oSSN=0x%x, oRSN=0x%x\n",
                  preq[2],preq[3],pres[2],pres[3]);
#endif
            pres += 4;
            seq_ok = TRUE;
            break;
            
         default:
            m_log(input->name,"unknown LMI item type %u\n",itype);
            goto done;
      }

      /* proceed next item */
      preq += isize + 2;
   }

 done:
   if ((msg_type == -1) || !seq_ok) {
       m_log(input->name,"incomplete LMI packet.\n");
       return(-1);
   }

   /* full status, send DLCI info */
   if (msg_type == 0) {
#if DEBUG_FRSW
      m_log(input->name,"LMI full status, advertising DLCIs\n");
#endif
      for(sc=input->fr_conn_list;sc;sc=sc->next) {
         dlci = sc->dlci_in;
#if DEBUG_FRSW
         m_log(input->name,"sending LMI adv for DLCI %u\n",dlci);
#endif
         pres[0] = 0x07;
         pres[1] = 0x03;
         pres[2] = dlci >> 4;
         pres[3] = 0x80 | ((dlci & 0x0f) << 3);
         pres[4] = 0x82;
         pres += 5;
      }
   }

   rlen = pres - resp;

#if DEBUG_FRSW
   m_log(input->name,"sending ANSI LMI packet:\n");
   mem_dump(log_file,resp,rlen);
#endif

   netio_send(input,resp,rlen);
   return(0);
}

/* DLCI switching */
void frsw_dlci_switch(frsw_conn_t *vc,m_uint8_t *pkt)
{
   pkt[0] = (pkt[0] & 0x03) | ((vc->dlci_out >> 4) << 2);
   pkt[1] = (pkt[1] & 0x0f) | ((vc->dlci_out & 0x0f) << 4);

   /* update the statistics counter */
   vc->count++;
}

/* Handle a Frame-Relay packet */
ssize_t frsw_handle_pkt(frsw_table_t *t,netio_desc_t *input,
                        m_uint8_t *pkt,ssize_t len)
{
   netio_desc_t *output = NULL;
   frsw_conn_t *vc;
   m_uint32_t dlci;
   ssize_t slen;

   /* Extract DLCI information */
   dlci =  ((pkt[0] & 0xfc) >> 2) << 4;
   dlci |= (pkt[1] & 0xf0) >> 4;

#if DEBUG_FRSW
   m_log(input->name,"Trying to switch packet with input DLCI %u.\n",dlci);
   mem_dump(log_file,pkt,len);
#endif

   /* LMI ? */
   if (dlci == FR_DLCI_LMI_ANSI)
      return(frsw_handle_lmi_ansi_pkt(t,input,pkt,len));

   /* DLCI switching */
   if ((vc = frsw_dlci_lookup(t,input,dlci)) != NULL) {
      frsw_dlci_switch(vc,pkt);
      output = vc->output;
   } 

#if DEBUG_FRSW
   if (output) {
      m_log(input->name,"Switching packet to interface %s.\n",output->name);
   } else {
      m_log(input->name,"Unable to switch packet.\n");
   }
#endif

   /* Send the packet on output interface */
   slen = netio_send(output,pkt,len);

   if (len != slen) {
      t->drop++;
      return(-1);
   }

   return(0);
}

/* Receive a Frame-Relay packet */
static int frsw_recv_pkt(netio_desc_t *nio,u_char *pkt,ssize_t pkt_len,
                         frsw_table_t *t)
{
   int res;

   FRSW_LOCK(t);
   res = frsw_handle_pkt(t,nio,pkt,pkt_len);
   FRSW_UNLOCK(t);
   return(res);
}

/* Acquire a reference to a Frame-Relay switch (increment reference count) */
frsw_table_t *frsw_acquire(char *name)
{
   return(registry_find(name,OBJ_TYPE_FRSW));
}

/* Release a Frame-Relay switch (decrement reference count) */
int frsw_release(char *name)
{
   return(registry_unref(name,OBJ_TYPE_FRSW));
}

/* Create a virtual switch table */
frsw_table_t *frsw_create_table(char *name)
{
   frsw_table_t *t;

   /* Allocate a new switch structure */
   if (!(t = malloc(sizeof(*t))))
      return NULL;

   memset(t,0,sizeof(*t));
   pthread_mutex_init(&t->lock,NULL);
   mp_create_fixed_pool(&t->mp,"Frame-Relay Switch");

   if (!(t->name = mp_strdup(&t->mp,name)))
      goto err_name;

   /* Record this object in registry */
   if (registry_add(t->name,OBJ_TYPE_FRSW,t) == -1) {
      fprintf(stderr,"frsw_create_table: unable to create switch '%s'\n",name);
      goto err_reg;
   }

   return t;

 err_reg:
 err_name:
   mp_free_pool(&t->mp);
   free(t);
   return NULL;
}

/* Unlink a VC */
static void frsw_unlink_vc(frsw_conn_t *vc)
{
   if (vc) {
      if (vc->next)
         vc->next->pprev = vc->pprev;

      if (vc->pprev)
         *(vc->pprev) = vc->next;
   }
}

/* Free resources used by a VC */
static void frsw_release_vc(frsw_conn_t *vc)
{
   if (vc) {
      /* release input NIO */
      if (vc->input) {
         netio_rxl_remove(vc->input);
         netio_release(vc->input->name);
      }

      /* release output NIO */
      if (vc->output) 
         netio_release(vc->output->name);
   }
}

/* Free resources used by a Frame-Relay switch */
static int frsw_free(void *data,void *arg)
{
   frsw_table_t *t = data;
   frsw_conn_t *vc;
   int i;

   for(i=0;i<FRSW_HASH_SIZE;i++)
      for(vc=t->dlci_table[i];vc;vc=vc->hash_next)
         frsw_release_vc(vc);

   mp_free_pool(&t->mp);
   free(t);
   return(TRUE);
}

/* Delete a Frame-Relay switch */
int frsw_delete(char *name)
{
   return(registry_delete_if_unused(name,OBJ_TYPE_FRSW,frsw_free,NULL));
}

/* Delete all Frame-Relay switches */
int frsw_delete_all(void)
{
   return(registry_delete_type(OBJ_TYPE_FRSW,frsw_free,NULL));
}

/* Create a switch connection */
int frsw_create_vc(frsw_table_t *t,char *nio_input,u_int dlci_in,
                   char *nio_output,u_int dlci_out)
{
   frsw_conn_t *vc,**p;
   u_int hbucket;

   FRSW_LOCK(t);

   /* Allocate a new VC */
   if (!(vc = mp_alloc(&t->mp,sizeof(*vc)))) {
      FRSW_UNLOCK(t);
      return(-1);
   }   

   vc->input    = netio_acquire(nio_input);
   vc->output   = netio_acquire(nio_output);
   vc->dlci_in  = dlci_in;
   vc->dlci_out = dlci_out;
   
   /* Check these NIOs are valid and the input VC does not exists */
   if (!vc->input || !vc->output)
      goto error;

   if (frsw_dlci_lookup(t,vc->input,dlci_in)) {
      fprintf(stderr,"FRSW %s: switching for VC %u on IF %s "
              "already defined.\n",t->name,dlci_in,vc->input->name);
      goto error;
   }

   /* Add as a RX listener */
   if (netio_rxl_add(vc->input,(netio_rx_handler_t)frsw_recv_pkt,t,NULL) == -1)
      goto error;

   hbucket = frsw_dlci_hash(dlci_in);
   vc->hash_next = t->dlci_table[hbucket];
   t->dlci_table[hbucket] = vc;

   for(p=(frsw_conn_t **)&vc->input->fr_conn_list;*p;p=&(*p)->next)
      if ((*p)->dlci_in > dlci_in)
         break;

   vc->next = *p;
   if (*p) (*p)->pprev = &vc->next;
   vc->pprev = p;
   *p = vc;

   FRSW_UNLOCK(t);
   return(0);

 error:
   FRSW_UNLOCK(t);
   frsw_release_vc(vc);
   mp_free(vc);
   return(-1);
}

/* Remove a switch connection */
int frsw_delete_vc(frsw_table_t *t,char *nio_input,u_int dlci_in,
                   char *nio_output,u_int dlci_out)
{
   netio_desc_t *input,*output;
   frsw_conn_t **vc,*p;
   u_int hbucket;

   FRSW_LOCK(t);

   input = registry_exists(nio_input,OBJ_TYPE_NIO);
   output = registry_exists(nio_output,OBJ_TYPE_NIO);

   if (!input || !output) {
      FRSW_UNLOCK(t);
      return(-1);
   }

   hbucket = frsw_dlci_hash(dlci_in);
   for(vc=&t->dlci_table[hbucket];*vc;vc=&(*vc)->hash_next) 
   {
      p = *vc;

      if ((p->input == input) && (p->output == output) &&
          (p->dlci_in == dlci_in) && (p->dlci_out == dlci_out))
      {
         /* Found a matching VC, remove it */
         *vc = (*vc)->hash_next;
         frsw_unlink_vc(p);
         FRSW_UNLOCK(t);

         /* Release NIOs */
         frsw_release_vc(p);
         mp_free(p);
         return(0);
      }
   }

   FRSW_UNLOCK(t);
   return(-1);
}

/* Save the configuration of a Frame-Relay switch */
void frsw_save_config(frsw_table_t *t,FILE *fd)
{
   frsw_conn_t *vc;
   int i;

   fprintf(fd,"frsw create %s\n",t->name);

   FRSW_LOCK(t);

   for(i=0;i<FRSW_HASH_SIZE;i++) {
      for(vc=t->dlci_table[i];vc;vc=vc->next) {
         fprintf(fd,"frsw create_vc %s %s %u %s %u\n",
                 t->name,vc->input->name,vc->dlci_in,
                 vc->output->name,vc->dlci_out);
      }
   }

   FRSW_UNLOCK(t);

   fprintf(fd,"\n");
}

/* Save configurations of all Frame-Relay switches */
static void frsw_reg_save_config(registry_entry_t *entry,void *opt,int *err)
{
   frsw_save_config((frsw_table_t *)entry->data,(FILE *)opt);
}

void frsw_save_config_all(FILE *fd)
{
   registry_foreach_type(OBJ_TYPE_FRSW,frsw_reg_save_config,fd,NULL);
}

/* Create a new interface */
int frsw_cfg_create_if(frsw_table_t *t,char **tokens,int count)
{
   netio_desc_t *nio = NULL;
   int nio_type;

   /* at least: IF, interface name, NetIO type */
   if (count < 3) {
      fprintf(stderr,"frsw_cfg_create_if: invalid interface description\n");
      return(-1);
   }

   nio_type = netio_get_type(tokens[2]);
   switch(nio_type) {
      case NETIO_TYPE_UNIX:
         if (count != 5) {
            fprintf(stderr,"FRSW: invalid number of arguments "
                    "for UNIX NIO '%s'\n",tokens[1]);
            break;
         }

         nio = netio_desc_create_unix(tokens[1],tokens[3],tokens[4]);
         break;

      case NETIO_TYPE_UDP:
         if (count != 6) {
            fprintf(stderr,"FRSW: invalid number of arguments "
                    "for UDP NIO '%s'\n",tokens[1]);
            break;
         }

         nio = netio_desc_create_udp(tokens[1],atoi(tokens[3]),
                                     tokens[4],atoi(tokens[5]));
         break;

      case NETIO_TYPE_MCAST:
         if (count != 5) {
            fprintf(stderr,"FRSW: invalid number of arguments "
                    "for UDP NIO '%s'\n",tokens[1]);
            break;
         }

         nio = netio_desc_create_mcast(tokens[1],tokens[3],atoi(tokens[4]));
         break;

      case NETIO_TYPE_TCP_CLI:
         if (count != 5) {
            fprintf(stderr,"FRSW: invalid number of arguments "
                    "for TCP CLI NIO '%s'\n",tokens[1]);
            break;
         }

         nio = netio_desc_create_tcp_cli(tokens[1],tokens[3],tokens[4]);
         break;

      case NETIO_TYPE_TCP_SER:
         if (count != 4) {
            fprintf(stderr,"FRSW: invalid number of arguments "
                    "for TCP SER NIO '%s'\n",tokens[1]);
            break;
         }

         nio = netio_desc_create_tcp_ser(tokens[1],tokens[3]);
         break;

      default:
         fprintf(stderr,"FRSW: unknown/invalid NETIO type '%s'\n",
                 tokens[2]);
   }

   if (!nio) {
      fprintf(stderr,"FRSW: unable to create NETIO descriptor of "
              "interface %s\n",tokens[1]);
      return(-1);
   }

   netio_release(nio->name);
   return(0);
}

/* Create a new virtual circuit */
int frsw_cfg_create_vc(frsw_table_t *t,char **tokens,int count)
{
   /* 5 parameters: "VC", InputIF, InDLCI, OutputIF, OutDLCI */
   if (count != 5) {
      fprintf(stderr,"FRSW: invalid VPC descriptor.\n");
      return(-1);
   }

   return(frsw_create_vc(t,tokens[1],atoi(tokens[2]),
                         tokens[3],atoi(tokens[4])));
}

#define FRSW_MAX_TOKENS  16

/* Handle a FRSW configuration line */
int frsw_handle_cfg_line(frsw_table_t *t,char *str)
{  
   char *tokens[FRSW_MAX_TOKENS];
   int count;

   if ((count = m_strsplit(str,':',tokens,FRSW_MAX_TOKENS)) <= 1)
      return(-1);

   if (!strcmp(tokens[0],"IF"))
      return(frsw_cfg_create_if(t,tokens,count));
   else if (!strcmp(tokens[0],"VC"))
      return(frsw_cfg_create_vc(t,tokens,count));

   fprintf(stderr,"FRSW: Unknown statement \"%s\" (allowed: IF,VC)\n",
           tokens[0]);
   return(-1);
}

/* Read a FRSW configuration file */
int frsw_read_cfg_file(frsw_table_t *t,char *filename)
{
   char buffer[1024],*ptr;
   FILE *fd;

   if (!(fd = fopen(filename,"r"))) {
      perror("fopen");
      return(-1);
   }
   
   while(!feof(fd)) {
      if (!fgets(buffer,sizeof(buffer),fd))
         break;
      
      /* skip comments and end of line */
      if ((ptr = strpbrk(buffer,"#\r\n")) != NULL)
         *ptr = 0;

      /* analyze non-empty lines */
      if (strchr(buffer,':'))
         frsw_handle_cfg_line(t,buffer);
   }
   
   fclose(fd);
   return(0);
}

/* Start a virtual Frame-Relay switch */
int frsw_start(char *filename)
{
   frsw_table_t *t;

   if (!(t = frsw_create_table("default"))) {
      fprintf(stderr,"FRSW: unable to create virtual fabric table.\n");
      return(-1);
   }

   if (frsw_read_cfg_file(t,filename) == -1) {
      fprintf(stderr,"FRSW: unable to parse configuration file.\n");
      return(-1);
   }
   
   frsw_release("default");
   return(0);
}
