/*
 * 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 <plugins/abf/abf_itf_attach.h>
#include <vnet/fib/fib_path_list.h>
#include <plugins/acl/exports.h>

/**
 * Forward declarations;
 */
extern vlib_node_registration_t abf_ip4_node;
extern vlib_node_registration_t abf_ip6_node;

/**
 * FIB node registered type for the bonds
 */
static fib_node_type_t abf_itf_attach_fib_node_type;

/**
 * Pool of ABF interface attachment objects
 */
abf_itf_attach_t *abf_itf_attach_pool;

/**
 * A per interface vector of attached policies. used in the data-plane
 */
static u32 **abf_per_itf[FIB_PROTOCOL_MAX];

/**
 * Per interface values of ACL lookup context IDs. used in the data-plane
 */
static u32 *abf_alctx_per_itf[FIB_PROTOCOL_MAX];

/**
 * ABF ACL module user id returned during the initialization
 */
static u32 abf_acl_user_id;
/*
 * ACL plugin method vtable
 */

static acl_plugin_methods_t acl_plugin;

/**
 * A DB of attachments; key={abf_index,sw_if_index}
 */
static uword *abf_itf_attach_db;

static u64
abf_itf_attach_mk_key (u32 abf_index, u32 sw_if_index)
{
  u64 key;

  key = abf_index;
  key = key << 32;
  key |= sw_if_index;

  return (key);
}

static abf_itf_attach_t *
abf_itf_attach_db_find (u32 abf_index, u32 sw_if_index)
{
  uword *p;
  u64 key;

  key = abf_itf_attach_mk_key (abf_index, sw_if_index);

  p = hash_get (abf_itf_attach_db, key);

  if (NULL != p)
    return (pool_elt_at_index (abf_itf_attach_pool, p[0]));

  return (NULL);
}

static void
abf_itf_attach_db_add (u32 abf_index, u32 sw_if_index, abf_itf_attach_t * aia)
{
  u64 key;

  key = abf_itf_attach_mk_key (abf_index, sw_if_index);

  hash_set (abf_itf_attach_db, key, aia - abf_itf_attach_pool);
}

static void
abf_itf_attach_db_del (u32 abf_index, u32 sw_if_index)
{
  u64 key;

  key = abf_itf_attach_mk_key (abf_index, sw_if_index);

  hash_unset (abf_itf_attach_db, key);
}

static void
abf_itf_attach_stack (abf_itf_attach_t * aia)
{
  /*
   * stack the DPO on the forwarding contributed by the path-list
   */
  dpo_id_t via_dpo = DPO_INVALID;
  abf_policy_t *ap;

  ap = abf_policy_get (aia->aia_abf);

  fib_path_list_contribute_forwarding (ap->ap_pl,
				       (FIB_PROTOCOL_IP4 == aia->aia_proto ?
					FIB_FORW_CHAIN_TYPE_UNICAST_IP4 :
					FIB_FORW_CHAIN_TYPE_UNICAST_IP6),
				       FIB_PATH_LIST_FWD_FLAG_COLLAPSE,
				       &via_dpo);

  dpo_stack_from_node ((FIB_PROTOCOL_IP4 == aia->aia_proto ?
			abf_ip4_node.index :
			abf_ip6_node.index), &aia->aia_dpo, &via_dpo);
  dpo_reset (&via_dpo);
}

static int
abf_cmp_attach_for_sort (void *v1, void *v2)
{
  const abf_itf_attach_t *aia1;
  const abf_itf_attach_t *aia2;

  aia1 = abf_itf_attach_get (*(u32 *) v1);
  aia2 = abf_itf_attach_get (*(u32 *) v2);

  return (aia1->aia_prio - aia2->aia_prio);
}

void
abf_setup_acl_lc (fib_protocol_t fproto, u32 sw_if_index)
{
  u32 *acl_vec = 0;
  u32 *aiai;
  abf_itf_attach_t *aia;

  if (~0 == abf_alctx_per_itf[fproto][sw_if_index])
    return;

  vec_foreach (aiai, abf_per_itf[fproto][sw_if_index])
  {
    aia = abf_itf_attach_get (*aiai);
    vec_add1 (acl_vec, aia->aia_acl);
  }
  acl_plugin.set_acl_vec_for_context (abf_alctx_per_itf[fproto][sw_if_index],
				      acl_vec);
  vec_free (acl_vec);
}

int
abf_itf_attach (fib_protocol_t fproto,
		u32 policy_id, u32 priority, u32 sw_if_index)
{
  abf_itf_attach_t *aia;
  abf_policy_t *ap;
  u32 api, aiai;

  api = abf_policy_find (policy_id);

  ASSERT (INDEX_INVALID != api);
  ap = abf_policy_get (api);

  /*
   * check this is not a duplicate
   */
  aia = abf_itf_attach_db_find (policy_id, sw_if_index);

  if (NULL != aia)
    return (VNET_API_ERROR_ENTRY_ALREADY_EXISTS);

  /*
   * construct a new attachment object
   */
  pool_get (abf_itf_attach_pool, aia);

  fib_node_init (&aia->aia_node, abf_itf_attach_fib_node_type);
  aia->aia_prio = priority;
  aia->aia_proto = fproto;
  aia->aia_acl = ap->ap_acl;
  aia->aia_abf = api;
  aia->aia_sw_if_index = sw_if_index;
  aiai = aia - abf_itf_attach_pool;
  abf_itf_attach_db_add (policy_id, sw_if_index, aia);

  /*
   * stack the DPO on the forwarding contributed by the path-list
   */
  abf_itf_attach_stack (aia);

  /*
   * Insert the policy on the interfaces list.
   */
  vec_validate_init_empty (abf_per_itf[fproto], sw_if_index, NULL);
  vec_add1 (abf_per_itf[fproto][sw_if_index], aia - abf_itf_attach_pool);
  if (1 == vec_len (abf_per_itf[fproto][sw_if_index]))
    {
      /*
       * when enabling the first ABF policy on the interface
       * we need to enable the interface input feature
       */
      vnet_feature_enable_disable ((FIB_PROTOCOL_IP4 == fproto ?
				    "ip4-unicast" :
				    "ip6-unicast"),
				   (FIB_PROTOCOL_IP4 == fproto ?
				    "abf-input-ip4" :
				    "abf-input-ip6"),
				   sw_if_index, 1, NULL, 0);

      /* if this is the first ABF policy, we need to acquire an ACL lookup context */
      vec_validate_init_empty (abf_alctx_per_itf[fproto], sw_if_index, ~0);
      abf_alctx_per_itf[fproto][sw_if_index] =
	acl_plugin.get_lookup_context_index (abf_acl_user_id, sw_if_index, 0);
    }
  else
    {
      vec_sort_with_function (abf_per_itf[fproto][sw_if_index],
			      abf_cmp_attach_for_sort);
    }

  /* Prepare and set the list of ACLs for lookup within the context */
  abf_setup_acl_lc (fproto, sw_if_index);

  /*
   * become a child of the ABF policy so we are notified when
   * its forwarding changes.
   */
  aia->aia_sibling = fib_node_child_add (abf_policy_fib_node_type,
					 api,
					 abf_itf_attach_fib_node_type, aiai);

  return (0);
}

int
abf_itf_detach (fib_protocol_t fproto, u32 policy_id, u32 sw_if_index)
{
  abf_itf_attach_t *aia;
  u32 index;

  /*
   * check this is a valid attachment
   */
  aia = abf_itf_attach_db_find (policy_id, sw_if_index);

  if (NULL == aia)
    return (VNET_API_ERROR_NO_SUCH_ENTRY);

  /*
   * first remove from the interface's vector
   */
  ASSERT (abf_per_itf[fproto]);
  ASSERT (abf_per_itf[fproto][sw_if_index]);

  index = vec_search (abf_per_itf[fproto][sw_if_index],
		      aia - abf_itf_attach_pool);

  ASSERT (index != ~0);
  vec_del1 (abf_per_itf[fproto][sw_if_index], index);

  if (0 == vec_len (abf_per_itf[fproto][sw_if_index]))
    {
      /*
       * when deleting the last ABF policy on the interface
       * we need to disable the interface input feature
       */
      vnet_feature_enable_disable ((FIB_PROTOCOL_IP4 == fproto ?
				    "ip4-unicast" :
				    "ip6-unicast"),
				   (FIB_PROTOCOL_IP4 == fproto ?
				    "abf-input-ip4" :
				    "abf-input-ip6"),
				   sw_if_index, 0, NULL, 0);

      /* Return the lookup context, invalidate its id in our records */
      acl_plugin.put_lookup_context_index (abf_alctx_per_itf[fproto]
					   [sw_if_index]);
      abf_alctx_per_itf[fproto][sw_if_index] = ~0;
    }
  else
    {
      vec_sort_with_function (abf_per_itf[fproto][sw_if_index],
			      abf_cmp_attach_for_sort);
    }

  /* Prepare and set the list of ACLs for lookup within the context */
  abf_setup_acl_lc (fproto, sw_if_index);

  /*
   * remove the dependency on the policy
   */
  fib_node_child_remove (abf_policy_fib_node_type,
			 aia->aia_abf, aia->aia_sibling);

  /*
   * remove the attachment from the DB
   */
  abf_itf_attach_db_del (policy_id, sw_if_index);

  /*
   * release our locks on FIB forwarding data
   */
  dpo_reset (&aia->aia_dpo);

  /*
   * return the object
   */
  pool_put (abf_itf_attach_pool, aia);

  return (0);
}

static u8 *
format_abf_intf_attach (u8 * s, va_list * args)
{
  abf_itf_attach_t *aia = va_arg (*args, abf_itf_attach_t *);
  abf_policy_t *ap;

  ap = abf_policy_get (aia->aia_abf);
  s = format (s, "abf-interface-attach: policy:%d priority:%d",
	      ap->ap_id, aia->aia_prio);
  s = format (s, "\n  %U", format_dpo_id, &aia->aia_dpo, 2);

  return (s);
}

static clib_error_t *
abf_itf_attach_cmd (vlib_main_t * vm,
		    unformat_input_t * input, vlib_cli_command_t * cmd)
{
  u32 policy_id, sw_if_index;
  fib_protocol_t fproto;
  u32 is_del, priority;
  vnet_main_t *vnm;

  is_del = 0;
  sw_if_index = policy_id = ~0;
  vnm = vnet_get_main ();
  fproto = FIB_PROTOCOL_MAX;
  priority = 0;

  while (unformat_check_input (input) != UNFORMAT_END_OF_INPUT)
    {
      if (unformat (input, "del"))
	is_del = 1;
      else if (unformat (input, "add"))
	is_del = 0;
      else if (unformat (input, "ip4"))
	fproto = FIB_PROTOCOL_IP4;
      else if (unformat (input, "ip6"))
	fproto = FIB_PROTOCOL_IP6;
      else if (unformat (input, "policy %d", &policy_id))
	;
      else if (unformat (input, "priority %d", &priority))
	;
      else if (unformat (input, "%U",
			 unformat_vnet_sw_interface, vnm, &sw_if_index))
	;
      else
	return (clib_error_return (0, "unknown input '%U'",
				   format_unformat_error, input));
    }

  if (~0 == policy_id)
    {
      return (clib_error_return (0, "invalid policy ID:%d", policy_id));
    }
  if (~0 == sw_if_index)
    {
      return (clib_error_return (0, "invalid interface name"));
    }
  if (FIB_PROTOCOL_MAX == fproto)
    {
      return (clib_error_return (0, "Specify either ip4 or ip6"));
    }

  if (~0 == abf_policy_find (policy_id))
    return (clib_error_return (0, "invalid policy ID:%d", policy_id));

  if (is_del)
    abf_itf_detach (fproto, policy_id, sw_if_index);
  else
    abf_itf_attach (fproto, policy_id, priority, sw_if_index);

  return (NULL);
}

/* *INDENT-OFF* */
/**
 * Attach an ABF policy to an interface.
 */
VLIB_CLI_COMMAND (abf_itf_attach_cmd_node, static) = {
  .path = "abf attach",
  .function = abf_itf_attach_cmd,
  .short_help = "abf attach <ip4|ip6> [del] policy <value> <interface>",
  // this is not MP safe
};
/* *INDENT-ON* */

static clib_error_t *
abf_show_attach_cmd (vlib_main_t * vm,
		     unformat_input_t * input, vlib_cli_command_t * cmd)
{
  const abf_itf_attach_t *aia;
  u32 sw_if_index, *aiai;
  fib_protocol_t fproto;
  vnet_main_t *vnm;

  sw_if_index = ~0;
  vnm = vnet_get_main ();

  while (unformat_check_input (input) != UNFORMAT_END_OF_INPUT)
    {
      if (unformat (input, "%U",
		    unformat_vnet_sw_interface, vnm, &sw_if_index))
	;
      else
	return (clib_error_return (0, "unknown input '%U'",
				   format_unformat_error, input));
    }

  if (~0 == sw_if_index)
    {
      vlib_cli_output (vm, "specify an interface");
    }

  /* *INDENT-OFF* */
  FOR_EACH_FIB_IP_PROTOCOL(fproto)
  {
    if (sw_if_index < vec_len(abf_per_itf[fproto]))
      {
        if (vec_len(abf_per_itf[fproto][sw_if_index]))
          vlib_cli_output(vm, "%U:", format_fib_protocol, fproto);

        vec_foreach(aiai, abf_per_itf[fproto][sw_if_index])
          {
            aia = pool_elt_at_index(abf_itf_attach_pool, *aiai);
            vlib_cli_output(vm, " %U", format_abf_intf_attach, aia);
          }
      }
  }
  /* *INDENT-ON* */
  return (NULL);
}

/* *INDENT-OFF* */
VLIB_CLI_COMMAND (abf_show_attach_cmd_node, static) = {
  .path = "show abf attach",
  .function = abf_show_attach_cmd,
  .short_help = "show abf attach <interface>",
  .is_mp_safe = 1,
};
/* *INDENT-ON* */

void
abf_itf_attach_walk (abf_itf_attach_walk_cb_t cb, void *ctx)
{
  u32 aii;

  /* *INDENT-OFF* */
  pool_foreach_index (aii, abf_itf_attach_pool)
   {
    if (!cb(aii, ctx))
      break;
  }
  /* *INDENT-ON* */
}

typedef enum abf_next_t_
{
  ABF_NEXT_DROP,
  ABF_N_NEXT,
} abf_next_t;

typedef struct abf_input_trace_t_
{
  abf_next_t next;
  index_t index;
} abf_input_trace_t;

typedef enum
{
#define abf_error(n,s) ABF_ERROR_##n,
#include "abf_error.def"
#undef abf_error
  ABF_N_ERROR,
} abf_error_t;

always_inline uword
abf_input_inline (vlib_main_t * vm,
		  vlib_node_runtime_t * node,
		  vlib_frame_t * frame, fib_protocol_t fproto)
{
  u32 n_left_from, *from, *to_next, next_index, matches, misses;

  from = vlib_frame_vector_args (frame);
  n_left_from = frame->n_vectors;
  next_index = node->cached_next_index;
  matches = misses = 0;

  while (n_left_from > 0)
    {
      u32 n_left_to_next;

      vlib_get_next_frame (vm, node, next_index, to_next, n_left_to_next);

      while (n_left_from > 0 && n_left_to_next > 0)
	{
	  const u32 *attachments0;
	  const abf_itf_attach_t *aia0;
	  abf_next_t next0 = ABF_NEXT_DROP;
	  vlib_buffer_t *b0;
	  u32 bi0, sw_if_index0;
	  fa_5tuple_opaque_t fa_5tuple0;
	  u32 match_acl_index = ~0;
	  u32 match_acl_pos = ~0;
	  u32 match_rule_index = ~0;
	  u32 trace_bitmap = 0;
	  u32 lc_index;
	  u8 action;

	  bi0 = from[0];
	  to_next[0] = bi0;
	  from += 1;
	  to_next += 1;
	  n_left_from -= 1;
	  n_left_to_next -= 1;

	  b0 = vlib_get_buffer (vm, bi0);
	  sw_if_index0 = vnet_buffer (b0)->sw_if_index[VLIB_RX];

	  ASSERT (vec_len (abf_per_itf[fproto]) > sw_if_index0);
	  attachments0 = abf_per_itf[fproto][sw_if_index0];

	  ASSERT (vec_len (abf_alctx_per_itf[fproto]) > sw_if_index0);
	  /*
	   * check if any of the policies attached to this interface matches.
	   */
	  lc_index = abf_alctx_per_itf[fproto][sw_if_index0];

	  /*
	     A non-inline version looks like this:

	     acl_plugin.fill_5tuple (lc_index, b0, (FIB_PROTOCOL_IP6 == fproto),
	     1, 0, &fa_5tuple0);
	     if (acl_plugin.match_5tuple
	     (lc_index, &fa_5tuple0, (FIB_PROTOCOL_IP6 == fproto), &action,
	     &match_acl_pos, &match_acl_index, &match_rule_index,
	     &trace_bitmap))
	     . . .
	   */
	  acl_plugin_fill_5tuple_inline (acl_plugin.p_acl_main, lc_index, b0,
					 (FIB_PROTOCOL_IP6 == fproto), 1, 0,
					 &fa_5tuple0);

	  if (acl_plugin_match_5tuple_inline
	      (acl_plugin.p_acl_main, lc_index, &fa_5tuple0,
	       (FIB_PROTOCOL_IP6 == fproto), &action, &match_acl_pos,
	       &match_acl_index, &match_rule_index, &trace_bitmap))
	    {
	      /*
	       * match:
	       *  follow the DPO chain
	       */
	      aia0 = abf_itf_attach_get (attachments0[match_acl_pos]);

	      next0 = aia0->aia_dpo.dpoi_next_node;
	      vnet_buffer (b0)->ip.adj_index[VLIB_TX] =
		aia0->aia_dpo.dpoi_index;
	      matches++;
	    }
	  else
	    {
	      /*
	       * miss:
	       *  move on down the feature arc
	       */
	      vnet_feature_next (&next0, b0);
	      misses++;
	    }

	  if (PREDICT_FALSE (b0->flags & VLIB_BUFFER_IS_TRACED))
	    {
	      abf_input_trace_t *tr;

	      tr = vlib_add_trace (vm, node, b0, sizeof (*tr));
	      tr->next = next0;
	      tr->index = vnet_buffer (b0)->ip.adj_index[VLIB_TX];
	    }

	  /* verify speculative enqueue, maybe switch current next frame */
	  vlib_validate_buffer_enqueue_x1 (vm, node, next_index,
					   to_next, n_left_to_next, bi0,
					   next0);
	}

      vlib_put_next_frame (vm, node, next_index, n_left_to_next);
    }

  vlib_node_increment_counter (vm,
			       (fproto = FIB_PROTOCOL_IP6 ?
				abf_ip4_node.index :
				abf_ip6_node.index),
			       ABF_ERROR_MATCHED, matches);
  vlib_node_increment_counter (vm,
			       (fproto = FIB_PROTOCOL_IP6 ?
				abf_ip4_node.index :
				abf_ip6_node.index),
			       ABF_ERROR_MISSED, misses);

  return frame->n_vectors;
}

static uword
abf_input_ip4 (vlib_main_t * vm,
	       vlib_node_runtime_t * node, vlib_frame_t * frame)
{
  return abf_input_inline (vm, node, frame, FIB_PROTOCOL_IP4);
}

static uword
abf_input_ip6 (vlib_main_t * vm,
	       vlib_node_runtime_t * node, vlib_frame_t * frame)
{
  return abf_input_inline (vm, node, frame, FIB_PROTOCOL_IP6);
}

static u8 *
format_abf_input_trace (u8 * s, va_list * args)
{
  CLIB_UNUSED (vlib_main_t * vm) = va_arg (*args, vlib_main_t *);
  CLIB_UNUSED (vlib_node_t * node) = va_arg (*args, vlib_node_t *);
  abf_input_trace_t *t = va_arg (*args, abf_input_trace_t *);

  s = format (s, " next %d index %d", t->next, t->index);
  return s;
}

static char *abf_error_strings[] = {
#define abf_error(n,s) s,
#include "abf_error.def"
#undef abf_error
};

/* *INDENT-OFF* */
VLIB_REGISTER_NODE (abf_ip4_node) =
{
  .function = abf_input_ip4,
  .name = "abf-input-ip4",
  .vector_size = sizeof (u32),
  .format_trace = format_abf_input_trace,
  .type = VLIB_NODE_TYPE_INTERNAL,
  .n_errors = ABF_N_ERROR,
  .error_strings = abf_error_strings,
  .n_next_nodes = ABF_N_NEXT,
  .next_nodes =
  {
    [ABF_NEXT_DROP] = "error-drop",
  }
};

VLIB_REGISTER_NODE (abf_ip6_node) =
{
  .function = abf_input_ip6,
  .name = "abf-input-ip6",
  .vector_size = sizeof (u32),
  .format_trace = format_abf_input_trace,
  .type = VLIB_NODE_TYPE_INTERNAL,
  .n_errors = 0,
  .n_next_nodes = ABF_N_NEXT,

  .next_nodes =
  {
    [ABF_NEXT_DROP] = "error-drop",
  }
};

VNET_FEATURE_INIT (abf_ip4_feat, static) =
{
  .arc_name = "ip4-unicast",
  .node_name = "abf-input-ip4",
  .runs_after = VNET_FEATURES ("acl-plugin-in-ip4-fa"),
};

VNET_FEATURE_INIT (abf_ip6_feat, static) =
{
  .arc_name = "ip6-unicast",
  .node_name = "abf-input-ip6",
  .runs_after = VNET_FEATURES ("acl-plugin-in-ip6-fa"),
};
/* *INDENT-ON* */

static fib_node_t *
abf_itf_attach_get_node (fib_node_index_t index)
{
  abf_itf_attach_t *aia = abf_itf_attach_get (index);
  return (&(aia->aia_node));
}

static abf_itf_attach_t *
abf_itf_attach_get_from_node (fib_node_t * node)
{
  return ((abf_itf_attach_t *) (((char *) node) -
				STRUCT_OFFSET_OF (abf_itf_attach_t,
						  aia_node)));
}

static void
abf_itf_attach_last_lock_gone (fib_node_t * node)
{
  /*
   * ABF interface attachments are leaves on the graph.
   * we do not manage locks from children.
   */
}

/*
 * abf_itf_attach_back_walk_notify
 *
 * A back walk has reached this BIER fmask
 */
static fib_node_back_walk_rc_t
abf_itf_attach_back_walk_notify (fib_node_t * node,
				 fib_node_back_walk_ctx_t * ctx)
{
  /*
   * re-stack the fmask on the n-eos of the via
   */
  abf_itf_attach_t *aia = abf_itf_attach_get_from_node (node);

  abf_itf_attach_stack (aia);

  return (FIB_NODE_BACK_WALK_CONTINUE);
}

/*
 * The BIER fmask's graph node virtual function table
 */
static const fib_node_vft_t abf_itf_attach_vft = {
  .fnv_get = abf_itf_attach_get_node,
  .fnv_last_lock = abf_itf_attach_last_lock_gone,
  .fnv_back_walk = abf_itf_attach_back_walk_notify,
};

static clib_error_t *
abf_itf_bond_init (vlib_main_t * vm)
{
  abf_itf_attach_fib_node_type =
    fib_node_register_new_type ("abf-attach", &abf_itf_attach_vft);
  clib_error_t *acl_init_res = acl_plugin_exports_init (&acl_plugin);
  if (acl_init_res)
    return (acl_init_res);

  abf_acl_user_id =
    acl_plugin.register_user_module ("ABF plugin", "sw_if_index", NULL);

  return (NULL);
}

/* *INDENT-OFF* */
VLIB_INIT_FUNCTION (abf_itf_bond_init) =
{
  .runs_after = VLIB_INITS("acl_init"),
};
/* *INDENT-ON* */

/*
 * fd.io coding-style-patch-verification: ON
 *
 * Local Variables:
 * eval: (c-set-style "gnu")
 * End:
 */