/*
 * nsh_md2_ioam.c - NSH iOAM functions for MD type 2
 *
 * Copyright (c) 2017 Cisco and/or its affiliates.
 * Licensed 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 <vnet/vnet.h>
#include <vnet/plugin/plugin.h>
#include <nsh/nsh.h>
#include <nsh/nsh_packet.h>
#include <vnet/ip/ip.h>
#include <nsh/nsh-md2-ioam/nsh_md2_ioam.h>

#include <vlibapi/api.h>
#include <vlibmemory/api.h>

#include <vnet/fib/ip6_fib.h>
#include <vnet/fib/ip4_fib.h>
#include <vnet/fib/fib_entry.h>

/* define message structures */
#define vl_typedefs
#include <nsh/nsh.api.h>
#undef vl_typedefs

/* define generated endian-swappers */
#define vl_endianfun
#include <nsh/nsh.api.h>
#undef vl_endianfun

nsh_md2_ioam_main_t nsh_md2_ioam_main;

static void
nsh_md2_ioam_set_clear_output_feature_on_intf (vlib_main_t * vm,
						    u32 sw_if_index0,
						    u8 is_add)
{



  vnet_feature_enable_disable ("ip4-output",
			       "nsh-md2-ioam-encap-transit",
			       sw_if_index0, is_add,
			       0 /* void *feature_config */ ,
			       0 /* u32 n_feature_config_bytes */ );
  return;
}

void
nsh_md2_ioam_clear_output_feature_on_all_intfs (vlib_main_t * vm)
{
  vnet_sw_interface_t *si = 0;
  vnet_main_t *vnm = vnet_get_main ();
  vnet_interface_main_t *im = &vnm->interface_main;

  pool_foreach (si, im->sw_interfaces, (
					 {
					 nsh_md2_ioam_set_clear_output_feature_on_intf
					 (vm, si->sw_if_index, 0);
					 }));
  return;
}


extern fib_forward_chain_type_t
fib_entry_get_default_chain_type (const fib_entry_t * fib_entry);

int
nsh_md2_ioam_enable_disable_for_dest (vlib_main_t * vm,
					   ip46_address_t dst_addr,
					   u32 outer_fib_index,
					   u8 is_ipv4, u8 is_add)
{
  nsh_md2_ioam_main_t *hm = &nsh_md2_ioam_main;
  u32 fib_index0 = 0;

  fib_node_index_t fei = ~0;
  u32 *sw_if_index0 = NULL;
#if 0
  fib_entry_t *fib_entry;
  u32 adj_index0;
  ip_adjacency_t *adj0;
  load_balance_t *lb_m, *lb_b;
  const dpo_id_t *dpo0, *dpo1;
  u32 i, j, k;
#endif
  u32 *intf_list = NULL;
  fib_prefix_t fib_prefix;

  if (is_ipv4)
    {
      clib_memset (&fib_prefix, 0, sizeof (fib_prefix_t));
      fib_prefix.fp_len = 32;
      fib_prefix.fp_proto = FIB_PROTOCOL_IP4;
#define  TRANSIT_UNIT_TEST_HACK 1
#ifdef TRANSIT_UNIT_TEST_HACK
      clib_memset(&dst_addr, 0, sizeof(dst_addr));
      dst_addr.ip4.as_u32 = clib_net_to_host_u32(0x14020102);
#endif
      fib_prefix.fp_addr = dst_addr;
    }
  else
    {
      return 0;
    }

  fei = fib_table_lookup (fib_index0, &fib_prefix);
#if 0
  fib_entry = fib_entry_get (fei);


  if (!dpo_id_is_valid (&fib_entry->fe_lb))
    {
      return (-1);
    }

  lb_m = load_balance_get (fib_entry->fe_lb.dpoi_index);

  for (i = 0; i < lb_m->lb_n_buckets; i++)
    {
      dpo0 = load_balance_get_bucket_i (lb_m, i);

      if (dpo0->dpoi_type == DPO_LOAD_BALANCE ||
	  dpo0->dpoi_type == DPO_ADJACENCY)
	{
	  if (dpo0->dpoi_type == DPO_ADJACENCY)
	    {
	      k = 1;
	    }
	  else
	    {
	      lb_b = load_balance_get (dpo0->dpoi_index);
	      k = lb_b->lb_n_buckets;
	    }

	  for (j = 0; j < k; j++)
	    {
	      if (dpo0->dpoi_type == DPO_ADJACENCY)
		{
		  dpo1 = dpo0;
		}
	      else
		{
		  dpo1 = load_balance_get_bucket_i (lb_b, j);
		}

	      if (dpo1->dpoi_type == DPO_ADJACENCY)
		{
		  adj_index0 = dpo1->dpoi_index;

		  if (ADJ_INDEX_INVALID == adj_index0)
		    {
		      continue;
		    }

		  adj0 =
		    ip_get_adjacency (&(ip4_main.lookup_main), adj_index0);
		  sw_if_index0 = adj0->rewrite_header.sw_if_index;

		  if (~0 == sw_if_index0)
		    {
		      continue;
		    }


		  if (is_add)
		    {
		      vnet_feature_enable_disable ("ip4-output",
						   "nsh-md2-ioam-encap-transit",
						   sw_if_index0, is_add, 0,
						   /* void *feature_config */
						   0	/* u32 n_feature_config_bytes */
			);

		      vec_validate_init_empty (hm->bool_ref_by_sw_if_index,
					       sw_if_index0, ~0);
		      hm->bool_ref_by_sw_if_index[sw_if_index0] = 1;
		    }
		  else
		    {
		      hm->bool_ref_by_sw_if_index[sw_if_index0] = ~0;
		    }
		}
	    }
	}
    }
#else

u32 fib_path_get_resolving_interface (fib_node_index_t path_index);
    vec_add1(intf_list, fib_path_get_resolving_interface(fei));
    vec_foreach(sw_if_index0, intf_list)
    if (is_add)
      {
        vnet_feature_enable_disable ("ip4-output",
                         "nsh-md2-ioam-encap-transit",
                         *sw_if_index0, is_add, 0,
                         /* void *feature_config */
                         0    /* u32 n_feature_config_bytes */
          );

        vec_validate_init_empty (hm->bool_ref_by_sw_if_index,
                         *sw_if_index0, ~0);
        hm->bool_ref_by_sw_if_index[*sw_if_index0] = 1;
      }
    else
      {
        hm->bool_ref_by_sw_if_index[*sw_if_index0] = ~0;
      }

#endif

  if (is_ipv4)
    {
      uword *t = NULL;
      nsh_md2_ioam_dest_tunnels_t *t1;
      fib_prefix_t key4, *key4_copy;
      hash_pair_t *hp;
      clib_memset (&key4, 0, sizeof (key4));
      key4.fp_proto = FIB_PROTOCOL_IP4;
      key4.fp_addr.ip4.as_u32 = fib_prefix.fp_addr.ip4.as_u32;
      t = hash_get_mem (hm->dst_by_ip4, &key4);
      if (is_add)
	{
	  if (t)
	    {
	      return 0;
	    }
	  pool_get_aligned (hm->dst_tunnels, t1, CLIB_CACHE_LINE_BYTES);
	  clib_memset (t1, 0, sizeof (*t1));
	  t1->fp_proto = FIB_PROTOCOL_IP4;
	  t1->dst_addr.ip4.as_u32 = fib_prefix.fp_addr.ip4.as_u32;
	  key4_copy = clib_mem_alloc (sizeof (*key4_copy));
          clib_memset(key4_copy, 0, sizeof(*key4_copy));
	  clib_memcpy_fast (key4_copy, &key4, sizeof (*key4_copy));
	  hash_set_mem (hm->dst_by_ip4, key4_copy, t1 - hm->dst_tunnels);
	  /*
	   * Attach to the FIB entry for the VxLAN-GPE destination
	   * and become its child. The dest route will invoke a callback
	   * when the fib entry changes, it can be used to
	   * re-program the output feature on the egress interface.
	   */

	  const fib_prefix_t tun_dst_pfx = {
	    .fp_len = 32,
	    .fp_proto = FIB_PROTOCOL_IP4,
	    .fp_addr = {.ip4 = t1->dst_addr.ip4,}
	  };

	  t1->fib_entry_index =
	    fib_table_entry_special_add (outer_fib_index,
					 &tun_dst_pfx,
					 FIB_SOURCE_RR,
					 FIB_ENTRY_FLAG_NONE);
	  t1->sibling_index =
	    fib_entry_child_add (t1->fib_entry_index,
				 hm->fib_entry_type, t1 - hm->dst_tunnels);
	  t1->outer_fib_index = outer_fib_index;

	}
      else
	{
	  if (!t)
	    {
	      return 0;
	    }
	  t1 = pool_elt_at_index (hm->dst_tunnels, t[0]);
	  hp = hash_get_pair (hm->dst_by_ip4, &key4);
	  key4_copy = (void *) (hp->key);
	  hash_unset_mem (hm->dst_by_ip4, &key4);
	  clib_mem_free (key4_copy);
	  pool_put (hm->dst_tunnels, t1);
	}
    }
  else
    {
      // TBD for IPv6
    }

  return 0;
}

void
nsh_md2_ioam_refresh_output_feature_on_all_dest (void)
{
  nsh_md2_ioam_main_t *hm = &nsh_md2_ioam_main;
  nsh_main_t *gm = &nsh_main;
  nsh_md2_ioam_dest_tunnels_t *t;
  u32 i;

  if (pool_elts (hm->dst_tunnels) == 0)
    return;

  nsh_md2_ioam_clear_output_feature_on_all_intfs (gm->vlib_main);
  i = vec_len (hm->bool_ref_by_sw_if_index);
  vec_free (hm->bool_ref_by_sw_if_index);
  vec_validate_init_empty (hm->bool_ref_by_sw_if_index, i, ~0);
  pool_foreach (t, hm->dst_tunnels, (
				      {
				      nsh_md2_ioam_enable_disable_for_dest
				      (gm->vlib_main,
				       t->dst_addr,
				       t->outer_fib_index,
				       (t->fp_proto == FIB_PROTOCOL_IP4), 1
				       /* is_add */
				      );
				      }
		));
  return;
}

void
nsh_md2_ioam_clear_output_feature_on_select_intfs (void)
{
  nsh_md2_ioam_main_t *hm = &nsh_md2_ioam_main;
  nsh_main_t *gm = &nsh_main;

  u32 sw_if_index0 = 0;
  for (sw_if_index0 = 0;
       sw_if_index0 < vec_len (hm->bool_ref_by_sw_if_index); sw_if_index0++)
    {
      if (hm->bool_ref_by_sw_if_index[sw_if_index0] == 0xFF)
	{
	  nsh_md2_ioam_set_clear_output_feature_on_intf
	    (gm->vlib_main, sw_if_index0, 0);
	}
    }

  return;
}




clib_error_t *
nsh_md2_ioam_enable_disable (int has_trace_option, int has_pot_option,
				  int has_ppc_option)
{
  nsh_md2_ioam_main_t *hm = &nsh_md2_ioam_main;

  hm->has_trace_option = has_trace_option;
  hm->has_pot_option = has_pot_option;
  hm->has_ppc_option = has_ppc_option;

  if (hm->has_trace_option)
    {
      nsh_md2_ioam_trace_profile_setup ();
    }
  else if (!hm->has_trace_option)
    {
      nsh_md2_ioam_trace_profile_cleanup ();
    }

  return 0;
}


int nsh_md2_ioam_disable_for_dest
  (vlib_main_t * vm, ip46_address_t dst_addr, u32 outer_fib_index,
   u8 ipv4_set)
{
  nsh_md2_ioam_dest_tunnels_t *t;
  nsh_md2_ioam_main_t *hm = &nsh_md2_ioam_main;
  nsh_main_t *gm = &nsh_main;

  nsh_md2_ioam_enable_disable_for_dest (gm->vlib_main,
					     dst_addr, outer_fib_index,
					     ipv4_set, 0);
  if (pool_elts (hm->dst_tunnels) == 0)
    {
      nsh_md2_ioam_clear_output_feature_on_select_intfs ();
      return 0;
    }

  pool_foreach (t, hm->dst_tunnels, (
				      {
				      nsh_md2_ioam_enable_disable_for_dest
				      (gm->vlib_main,
				       t->dst_addr,
				       t->outer_fib_index,
				       (t->fp_proto ==
					FIB_PROTOCOL_IP4), 1 /* is_add */ );
				      }
		));
  nsh_md2_ioam_clear_output_feature_on_select_intfs ();
  return (0);

}

static clib_error_t *nsh_md2_ioam_set_transit_rewrite_command_fn
  (vlib_main_t * vm, unformat_input_t * input, vlib_cli_command_t * cmd)
{
  nsh_main_t *gm = &nsh_main;
  ip46_address_t dst_addr;
  u8 dst_addr_set = 0;
  u8 ipv4_set = 0;
  u8 ipv6_set = 0;
  u8 disable = 0;
  clib_error_t *rv = 0;
  u32 outer_fib_index = 0;
  while (unformat_check_input (input) != UNFORMAT_END_OF_INPUT)
    {
      if (unformat (input, "dst-ip %U", unformat_ip4_address, &dst_addr.ip4))
	{
	  dst_addr_set = 1;
	  ipv4_set = 1;
	}
      else
	if (unformat
	    (input, "dst-ip %U", unformat_ip6_address, &dst_addr.ip6))
	{
	  dst_addr_set = 1;
	  ipv6_set = 1;
	}
      else if (unformat (input, "outer-fib-index %d", &outer_fib_index))
	{
	}

      else if (unformat (input, "disable"))
	disable = 1;
      else
	break;
    }

  if (dst_addr_set == 0)
    return clib_error_return (0,
			      "LISP-GPE Tunnel destination address not specified");
  if (ipv4_set && ipv6_set)
    return clib_error_return (0, "both IPv4 and IPv6 addresses specified");
  if (!disable)
    {
      nsh_md2_ioam_enable_disable_for_dest (gm->vlib_main,
						 dst_addr, outer_fib_index,
						 ipv4_set, 1);
    }
  else
    {
      nsh_md2_ioam_disable_for_dest
	(vm, dst_addr, outer_fib_index, ipv4_set);
    }
  return rv;
}

/* *INDENT-OFF* */
VLIB_CLI_COMMAND (nsh_md2_ioam_set_transit_rewrite_cmd, static) = {
  .path = "set nsh-md2-ioam-transit",
  .short_help = "set nsh-ioam-lisp-gpe-transit dst-ip <dst_ip> [outer-fib-index <outer_fib_index>] [disable]",
  .function = nsh_md2_ioam_set_transit_rewrite_command_fn,
};

/**
 * Function definition to backwalk a FIB node
 */
static fib_node_back_walk_rc_t
nsh_md2_ioam_back_walk (fib_node_t * node, fib_node_back_walk_ctx_t * ctx)
{
  nsh_md2_ioam_refresh_output_feature_on_all_dest ();
  return (FIB_NODE_BACK_WALK_CONTINUE);
}

/**
 * Function definition to get a FIB node from its index
 */
static fib_node_t *
nsh_md2_ioam_fib_node_get (fib_node_index_t index)
{
  nsh_md2_ioam_main_t *hm = &nsh_md2_ioam_main;
  return (&hm->node);
}

/**
 * Function definition to inform the FIB node that its last lock has gone.
 */
static void
nsh_md2_ioam_last_lock_gone (fib_node_t * node)
{
  ASSERT (0);
}


/*
 * Virtual function table registered by MPLS GRE tunnels
 * for participation in the FIB object graph.
 */
const static fib_node_vft_t nsh_md2_ioam_vft = {
  .fnv_get = nsh_md2_ioam_fib_node_get,
  .fnv_last_lock = nsh_md2_ioam_last_lock_gone,
  .fnv_back_walk = nsh_md2_ioam_back_walk,
};

void
nsh_md2_ioam_interface_init (void)
{
  nsh_md2_ioam_main_t *hm = &nsh_md2_ioam_main;
  hm->fib_entry_type = fib_node_register_new_type (&nsh_md2_ioam_vft);
  return;
}