/**
 * ospf6_bfd.c: IPv6 OSPF BFD handling routines
 *
 * @copyright Copyright (C) 2015 Cumulus Networks, Inc.
 *
 * This file is part of GNU Zebra.
 *
 * GNU Zebra 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, or (at your option) any
 * later version.
 *
 * GNU Zebra 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
 */

#include <zebra.h>

#include "command.h"
#include "linklist.h"
#include "memory.h"
#include "prefix.h"
#include "thread.h"
#include "buffer.h"
#include "stream.h"
#include "zclient.h"
#include "vty.h"
#include "table.h"
#include "bfd.h"
#include "if.h"
#include "ospf6d.h"
#include "ospf6_message.h"
#include "ospf6_neighbor.h"
#include "ospf6_interface.h"
#include "ospf6_route.h"
#include "ospf6_zebra.h"
#include "ospf6_bfd.h"

extern struct zclient *zclient;

/*
 * ospf6_bfd_trigger_event - Neighbor is registered/deregistered with BFD when
 *                           neighbor state is changed to/from 2way.
 */
void ospf6_bfd_trigger_event(struct ospf6_neighbor *on, int old_state,
			     int state)
{
	int family;
	struct in6_addr src, dst;

	/* Skip sessions without BFD. */
	if (on->bfd_session == NULL)
		return;

	if (old_state < OSPF6_NEIGHBOR_TWOWAY
	    && state >= OSPF6_NEIGHBOR_TWOWAY) {
		/*
		 * Check if neighbor address changed.
		 *
		 * When the neighbor is configured BFD before having an existing
		 * connection, then the destination address will be set to `::`
		 * which will cause session installation failure. This piece of
		 * code updates the address in that case.
		 */
		bfd_sess_addresses(on->bfd_session, &family, &src, &dst);
		if (memcmp(&on->linklocal_addr, &dst, sizeof(dst))) {
			bfd_sess_set_ipv6_addrs(on->bfd_session, &src,
						&on->linklocal_addr);
		}

		bfd_sess_install(on->bfd_session);
	} else if (old_state >= OSPF6_NEIGHBOR_TWOWAY
		   && state < OSPF6_NEIGHBOR_TWOWAY)
		bfd_sess_uninstall(on->bfd_session);
}

/*
 * ospf6_bfd_reg_dereg_all_nbr - Register/Deregister all neighbors associated
 *                               with a interface with BFD through
 *                               zebra for starting/stopping the monitoring of
 *                               the neighbor rechahability.
 */
static void ospf6_bfd_reg_dereg_all_nbr(struct ospf6_interface *oi,
					bool install)
{
	struct ospf6_neighbor *on;
	struct listnode *node;

	for (ALL_LIST_ELEMENTS_RO(oi->neighbor_list, node, on)) {
		/* Remove all sessions. */
		if (!install) {
			bfd_sess_free(&on->bfd_session);
			continue;
		}

		/* Always allocate session data even if not enabled. */
		ospf6_bfd_info_nbr_create(oi, on);

		/*
		 * If not connected yet, don't create any session but defer it
		 * for later. See function `ospf6_bfd_trigger_event`.
		 */
		if (on->state < OSPF6_NEIGHBOR_TWOWAY)
			continue;

		bfd_sess_install(on->bfd_session);
	}
}

static void ospf6_bfd_callback(struct bfd_session_params *bsp,
			       const struct bfd_session_status *bss, void *arg)
{
	struct ospf6_neighbor *on = arg;

	if (bss->state == BFD_STATUS_DOWN
	    && bss->previous_state == BFD_STATUS_UP) {
		THREAD_OFF(on->inactivity_timer);
		thread_add_event(master, inactivity_timer, on, 0, NULL);
	}
}

/*
 * ospf6_bfd_info_nbr_create - Create/update BFD information for a neighbor.
 */
void ospf6_bfd_info_nbr_create(struct ospf6_interface *oi,
			       struct ospf6_neighbor *on)
{
	if (!oi->bfd_config.enabled)
		return;

	if (on->bfd_session == NULL)
		on->bfd_session = bfd_sess_new(ospf6_bfd_callback, on);

	bfd_sess_set_timers(on->bfd_session,
			    oi->bfd_config.detection_multiplier,
			    oi->bfd_config.min_rx, oi->bfd_config.min_tx);
	bfd_sess_set_ipv6_addrs(on->bfd_session, on->ospf6_if->linklocal_addr,
				&on->linklocal_addr);
	bfd_sess_set_interface(on->bfd_session, oi->interface->name);
	bfd_sess_set_vrf(on->bfd_session, oi->interface->vrf->vrf_id);
	bfd_sess_set_profile(on->bfd_session, oi->bfd_config.profile);
}

/*
 * ospf6_bfd_write_config - Write the interface BFD configuration.
 */
void ospf6_bfd_write_config(struct vty *vty, struct ospf6_interface *oi)
{
	if (!oi->bfd_config.enabled)
		return;

#if HAVE_BFDD == 0
	if (oi->bfd_config.detection_multiplier != BFD_DEF_DETECT_MULT
	    || oi->bfd_config.min_rx != BFD_DEF_MIN_RX
	    || oi->bfd_config.min_tx != BFD_DEF_MIN_TX)
		vty_out(vty, " ipv6 ospf6 bfd %d %d %d\n",
			oi->bfd_config.detection_multiplier,
			oi->bfd_config.min_rx, oi->bfd_config.min_tx);
	else
#endif /* ! HAVE_BFDD */
		vty_out(vty, " ipv6 ospf6 bfd\n");

	if (oi->bfd_config.profile)
		vty_out(vty, " ipv6 ospf6 bfd profile %s\n",
			oi->bfd_config.profile);
}

DEFUN(ipv6_ospf6_bfd, ipv6_ospf6_bfd_cmd,
      "ipv6 ospf6 bfd [profile BFDPROF]",
      IP6_STR OSPF6_STR
      "Enables BFD support\n"
      "BFD Profile selection\n"
      "BFD Profile name\n")
{
	VTY_DECLVAR_CONTEXT(interface, ifp);
	struct ospf6_interface *oi;
	int prof_idx = 4;
	assert(ifp);

	oi = (struct ospf6_interface *)ifp->info;
	if (oi == NULL)
		oi = ospf6_interface_create(ifp);
	assert(oi);

	oi->bfd_config.detection_multiplier = BFD_DEF_DETECT_MULT;
	oi->bfd_config.min_rx = BFD_DEF_MIN_RX;
	oi->bfd_config.min_tx = BFD_DEF_MIN_TX;
	oi->bfd_config.enabled = true;
	if (argc > prof_idx) {
		XFREE(MTYPE_TMP, oi->bfd_config.profile);
		oi->bfd_config.profile =
			XSTRDUP(MTYPE_TMP, argv[prof_idx]->arg);
	}

	ospf6_bfd_reg_dereg_all_nbr(oi, true);

	return CMD_SUCCESS;
}

DEFUN(no_ipv6_ospf6_bfd_profile, no_ipv6_ospf6_bfd_profile_cmd,
      "no ipv6 ospf6 bfd profile [BFDPROF]",
      NO_STR IP6_STR OSPF6_STR
      "BFD support\n"
      "BFD Profile selection\n"
      "BFD Profile name\n")
{
	VTY_DECLVAR_CONTEXT(interface, ifp);
	struct ospf6_interface *oi;
	assert(ifp);

	oi = (struct ospf6_interface *)ifp->info;
	if (oi == NULL)
		oi = ospf6_interface_create(ifp);
	assert(oi);

	/* BFD not enabled, nothing to do. */
	if (!oi->bfd_config.enabled)
		return CMD_SUCCESS;

	/* Remove profile and apply new configuration. */
	XFREE(MTYPE_TMP, oi->bfd_config.profile);
	ospf6_bfd_reg_dereg_all_nbr(oi, true);

	return CMD_SUCCESS;
}

#if HAVE_BFDD > 0
DEFUN_HIDDEN(
#else
DEFUN(
#endif /* HAVE_BFDD */
       ipv6_ospf6_bfd_param,
       ipv6_ospf6_bfd_param_cmd,
       "ipv6 ospf6 bfd (2-255) (50-60000) (50-60000)",
       IP6_STR
       OSPF6_STR
       "Enables BFD support\n"
       "Detect Multiplier\n"
       "Required min receive interval\n"
       "Desired min transmit interval\n")
{
	VTY_DECLVAR_CONTEXT(interface, ifp);
	int idx_number = 3;
	int idx_number_2 = 4;
	int idx_number_3 = 5;
	struct ospf6_interface *oi;

	assert(ifp);

	oi = (struct ospf6_interface *)ifp->info;
	if (oi == NULL)
		oi = ospf6_interface_create(ifp);
	assert(oi);

	oi->bfd_config.detection_multiplier =
		strtoul(argv[idx_number]->arg, NULL, 10);
	oi->bfd_config.min_rx = strtoul(argv[idx_number_2]->arg, NULL, 10);
	oi->bfd_config.min_tx = strtoul(argv[idx_number_3]->arg, NULL, 10);
	oi->bfd_config.enabled = true;

	ospf6_bfd_reg_dereg_all_nbr(oi, true);

	return CMD_SUCCESS;
}

DEFUN (no_ipv6_ospf6_bfd,
       no_ipv6_ospf6_bfd_cmd,
       "no ipv6 ospf6 bfd",
       NO_STR
       IP6_STR
       OSPF6_STR
       "Disables BFD support\n"
       )
{
	VTY_DECLVAR_CONTEXT(interface, ifp);
	struct ospf6_interface *oi;
	assert(ifp);

	oi = (struct ospf6_interface *)ifp->info;
	if (oi == NULL)
		oi = ospf6_interface_create(ifp);
	assert(oi);

	oi->bfd_config.enabled = false;
	ospf6_bfd_reg_dereg_all_nbr(oi, false);

	return CMD_SUCCESS;
}

void ospf6_bfd_init(void)
{
	bfd_protocol_integration_init(zclient, master);

	/* Install BFD command */
	install_element(INTERFACE_NODE, &ipv6_ospf6_bfd_cmd);
	install_element(INTERFACE_NODE, &ipv6_ospf6_bfd_param_cmd);
	install_element(INTERFACE_NODE, &no_ipv6_ospf6_bfd_profile_cmd);
	install_element(INTERFACE_NODE, &no_ipv6_ospf6_bfd_cmd);
}
