/*
  PIM for Quagga: add the ability to configure multicast static routes
  Copyright (C) 2014  Nathan Bahr, ATCorp

  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; either version 2 of the License, or
  (at your option) any later version.

  This program is distributed in the hope that it will be useful, but
  WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; see the file COPYING; if not, write to the
  Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston,
  MA 02110-1301 USA

  $QuaggaId: $Format:%an, %ai, %h$ $
*/

#include "pim_static.h"
#include "pim_time.h"
#include "pim_str.h"
#include "pimd.h"
#include "pim_iface.h"
#include "log.h"
#include "memory.h"
#include "linklist.h"

void pim_static_route_free(struct static_route *s_route)
{
  XFREE(MTYPE_PIM_STATIC_ROUTE, s_route);
}

static struct static_route * static_route_alloc()
{
   struct static_route *s_route;

   s_route = XCALLOC(MTYPE_PIM_STATIC_ROUTE, sizeof(*s_route));
   if (!s_route) {
     zlog_err("PIM XCALLOC(%lu) failure", sizeof(*s_route));
     return 0;
   }
   return s_route;
}

static struct static_route *static_route_new(unsigned int   iif,
                                             unsigned int   oif,
                                             struct in_addr group,
                                             struct in_addr source)
{
  struct static_route * s_route;
  s_route = static_route_alloc();
  if (!s_route) {
     return 0;
  }

  s_route->group             = group;
  s_route->source            = source;
  s_route->iif               = iif;
  s_route->oif_ttls[oif]     = 1;
  s_route->oif_count         = 1;
  s_route->mc.mfcc_origin    = source;
  s_route->mc.mfcc_mcastgrp  = group;
  s_route->mc.mfcc_parent    = iif;
  s_route->mc.mfcc_ttls[oif] = 1;
  s_route->creation[oif] = pim_time_monotonic_sec();

  return s_route;
}


int pim_static_add(struct interface *iif, struct interface *oif, struct in_addr group, struct in_addr source)
{
   struct listnode *node = 0;
   struct static_route *s_route = 0;
   struct static_route *original_s_route = 0;
   struct pim_interface *pim_iif = iif ? iif->info : 0;
   struct pim_interface *pim_oif = oif ? oif->info : 0;
   unsigned int iif_index = pim_iif ? pim_iif->mroute_vif_index : 0;
   unsigned int oif_index = pim_oif ? pim_oif->mroute_vif_index : 0;

   if (!iif_index || !oif_index) {
      zlog_warn("%s %s: Unable to add static route: Invalid interface index(iif=%d,oif=%d)",
               __FILE__, __PRETTY_FUNCTION__,
               iif_index,
               oif_index);
      return -2;
   }

#ifdef PIM_ENFORCE_LOOPFREE_MFC
   if (iif_index == oif_index) {
      /* looped MFC entry */
      zlog_warn("%s %s: Unable to add static route: Looped MFC entry(iif=%d,oif=%d)",
               __FILE__, __PRETTY_FUNCTION__,
               iif_index,
               oif_index);
      return -4;
   }
#endif

   for (ALL_LIST_ELEMENTS_RO(qpim_static_route_list, node, s_route)) {
      if (s_route->group.s_addr == group.s_addr &&
          s_route->source.s_addr == source.s_addr) {
         if (s_route->iif == iif_index &&
             s_route->oif_ttls[oif_index]) {
            char gifaddr_str[100];
            char sifaddr_str[100];
            pim_inet4_dump("<ifaddr?>", group, gifaddr_str, sizeof(gifaddr_str));
            pim_inet4_dump("<ifaddr?>", source, sifaddr_str, sizeof(sifaddr_str));
            zlog_warn("%s %s: Unable to add static route: Route already exists (iif=%d,oif=%d,group=%s,source=%s)",
                     __FILE__, __PRETTY_FUNCTION__,
                     iif_index,
                     oif_index,
                     gifaddr_str,
                     sifaddr_str);
            return -3;
         }

         /* Ok, from here on out we will be making changes to the s_route structure, but if
          * for some reason we fail to commit these changes to the kernel, we want to be able
          * restore the state of the list. So copy the node data and if need be, we can copy
          * back if it fails.
          */
         original_s_route = static_route_alloc();
         if (!original_s_route) {
            return -5;
         }
         memcpy(original_s_route, s_route, sizeof(struct static_route));

         /* Route exists and has the same input interface, but adding a new output interface */
         if (s_route->iif == iif_index) {
            s_route->oif_ttls[oif_index] = 1;
            s_route->mc.mfcc_ttls[oif_index] = 1;
            s_route->creation[oif_index] = pim_time_monotonic_sec();
            ++s_route->oif_count;
         } else {
            /* input interface changed */
            s_route->iif = iif_index;
            s_route->mc.mfcc_parent = iif_index;

#ifdef PIM_ENFORCE_LOOPFREE_MFC
            /* check to make sure the new input was not an old output */
            if (s_route->oif_ttls[iif_index]) {
               s_route->oif_ttls[iif_index] = 0;
               s_route->creation[iif_index] = 0;
               s_route->mc.mfcc_ttls[iif_index] = 0;
               --s_route->oif_count;
            }
#endif

            /* now add the new output, if it is new */
            if (!s_route->oif_ttls[oif_index]) {
               s_route->oif_ttls[oif_index] = 1;
               s_route->creation[oif_index] = pim_time_monotonic_sec();
               s_route->mc.mfcc_ttls[oif_index] = 1;
               ++s_route->oif_count;
            }
         }

         break;
      }
   }

   /* If node is null then we reached the end of the list without finding a match */
   if (!node) {
      s_route = static_route_new(iif_index, oif_index, group, source);
      listnode_add(qpim_static_route_list, s_route);
   }

   if (pim_mroute_add(&(s_route->mc)))
   {
      char gifaddr_str[100];
      char sifaddr_str[100];
      pim_inet4_dump("<ifaddr?>", group, gifaddr_str, sizeof(gifaddr_str));
      pim_inet4_dump("<ifaddr?>", source, sifaddr_str, sizeof(sifaddr_str));
      zlog_warn("%s %s: Unable to add static route(iif=%d,oif=%d,group=%s,source=%s)",
               __FILE__, __PRETTY_FUNCTION__,
               iif_index,
               oif_index,
               gifaddr_str,
               sifaddr_str);

      /* Need to put s_route back to the way it was */
      if (original_s_route) {
         memcpy(s_route, original_s_route, sizeof(struct static_route));
      } else {
         /* we never stored off a copy, so it must have been a fresh new route */
         listnode_delete(qpim_static_route_list, s_route);
         pim_static_route_free(s_route);
      }

      return -1;
   }

   /* Make sure we free the memory for the route copy if used */
   if (original_s_route) {
      pim_static_route_free(original_s_route);
   }

   if (PIM_DEBUG_STATIC) {
     char gifaddr_str[100];
     char sifaddr_str[100];
     pim_inet4_dump("<ifaddr?>", group, gifaddr_str, sizeof(gifaddr_str));
     pim_inet4_dump("<ifaddr?>", source, sifaddr_str, sizeof(sifaddr_str));
     zlog_debug("%s: Static route added(iif=%d,oif=%d,group=%s,source=%s)",
           __PRETTY_FUNCTION__,
           iif_index,
           oif_index,
           gifaddr_str,
           sifaddr_str);
   }

   return 0;
}

int pim_static_del(struct interface *iif, struct interface *oif, struct in_addr group, struct in_addr source)
{
   struct listnode *node = 0;
   struct listnode *nextnode = 0;
   struct static_route *s_route = 0;
   struct pim_interface *pim_iif = iif ? iif->info : 0;
   struct pim_interface *pim_oif = oif ? oif->info : 0;
   unsigned int iif_index = pim_iif ? pim_iif->mroute_vif_index : 0;
   unsigned int oif_index = pim_oif ? pim_oif->mroute_vif_index : 0;

   if (!iif_index || !oif_index) {
      zlog_warn("%s %s: Unable to remove static route: Invalid interface index(iif=%d,oif=%d)",
               __FILE__, __PRETTY_FUNCTION__,
               iif_index,
               oif_index);
      return -2;
   }

   for (ALL_LIST_ELEMENTS(qpim_static_route_list, node, nextnode, s_route)) {
      if (s_route->iif == iif_index &&
          s_route->group.s_addr == group.s_addr &&
          s_route->source.s_addr == source.s_addr &&
          s_route->oif_ttls[oif_index]) {
         s_route->oif_ttls[oif_index] = 0;
         s_route->mc.mfcc_ttls[oif_index] = 0;
         --s_route->oif_count;

         /* If there are no more outputs then delete the whole route, otherwise set the route with the new outputs */
         if (s_route->oif_count <= 0 ? pim_mroute_del(&s_route->mc) : pim_mroute_add(&s_route->mc)) {
            char gifaddr_str[100];
            char sifaddr_str[100];
            pim_inet4_dump("<ifaddr?>", group, gifaddr_str, sizeof(gifaddr_str));
            pim_inet4_dump("<ifaddr?>", source, sifaddr_str, sizeof(sifaddr_str));
            zlog_warn("%s %s: Unable to remove static route(iif=%d,oif=%d,group=%s,source=%s)",
                     __FILE__, __PRETTY_FUNCTION__,
                     iif_index,
                     oif_index,
                     gifaddr_str,
                     sifaddr_str);

            s_route->oif_ttls[oif_index] = 1;
            s_route->mc.mfcc_ttls[oif_index] = 1;
            ++s_route->oif_count;

            return -1;
         }

         s_route->creation[oif_index] = 0;

         if (s_route->oif_count <= 0) {
            listnode_delete(qpim_static_route_list, s_route);
            pim_static_route_free(s_route);
         }

         if (PIM_DEBUG_STATIC) {
           char gifaddr_str[100];
           char sifaddr_str[100];
           pim_inet4_dump("<ifaddr?>", group, gifaddr_str, sizeof(gifaddr_str));
           pim_inet4_dump("<ifaddr?>", source, sifaddr_str, sizeof(sifaddr_str));
           zlog_debug("%s: Static route removed(iif=%d,oif=%d,group=%s,source=%s)",
                 __PRETTY_FUNCTION__,
                 iif_index,
                 oif_index,
                 gifaddr_str,
                 sifaddr_str);
         }

         break;
      }
   }

   if (!node) {
      char gifaddr_str[100];
      char sifaddr_str[100];
      pim_inet4_dump("<ifaddr?>", group, gifaddr_str, sizeof(gifaddr_str));
      pim_inet4_dump("<ifaddr?>", source, sifaddr_str, sizeof(sifaddr_str));
      zlog_warn("%s %s: Unable to remove static route: Route does not exist(iif=%d,oif=%d,group=%s,source=%s)",
               __FILE__, __PRETTY_FUNCTION__,
               iif_index,
               oif_index,
               gifaddr_str,
               sifaddr_str);
      return -3;
   }

   return 0;
}
