// SPDX-License-Identifier: ISC
/*	$OpenBSD$ */

/*
 * Copyright (c) 2013, 2015 Renato Westphal <renato@openbsd.org>
 * Copyright (c) 2009 Michele Marchetto <michele@openbsd.org>
 * Copyright (c) 2005 Claudio Jeker <claudio@openbsd.org>
 * Copyright (c) 2004, 2005, 2008 Esben Norby <norby@openbsd.org>
 */

#include <zebra.h>

#include "ldpd.h"
#include "ldpe.h"
#include "log.h"

static __inline int adj_compare(const struct adj *, const struct adj *);
static void adj_itimer(struct event *);
static __inline int tnbr_compare(const struct tnbr *, const struct tnbr *);
static void	 tnbr_del(struct ldpd_conf *, struct tnbr *);
static void	 tnbr_start(struct tnbr *);
static void	 tnbr_stop(struct tnbr *);
static void tnbr_hello_timer(struct event *);
static void	 tnbr_start_hello_timer(struct tnbr *);
static void	 tnbr_stop_hello_timer(struct tnbr *);

RB_GENERATE(global_adj_head, adj, global_entry, adj_compare)
RB_GENERATE(nbr_adj_head, adj, nbr_entry, adj_compare)
RB_GENERATE(ia_adj_head, adj, ia_entry, adj_compare)
RB_GENERATE(tnbr_head, tnbr, entry, tnbr_compare)

static __inline int
adj_compare(const struct adj *a, const struct adj *b)
{
	if (adj_get_af(a) < adj_get_af(b))
		return (-1);
	if (adj_get_af(a) > adj_get_af(b))
		return (1);

	if (ntohl(a->lsr_id.s_addr) < ntohl(b->lsr_id.s_addr))
		return (-1);
	if (ntohl(a->lsr_id.s_addr) > ntohl(b->lsr_id.s_addr))
		return (1);

	if (a->source.type < b->source.type)
		return (-1);
	if (a->source.type > b->source.type)
		return (1);

	switch (a->source.type) {
	case HELLO_LINK:
		if (if_cmp_name_func(a->source.link.ia->iface->name,
				     b->source.link.ia->iface->name) < 0)
			return (-1);
		if (if_cmp_name_func(a->source.link.ia->iface->name,
				     b->source.link.ia->iface->name) > 0)
			return (1);
		return (ldp_addrcmp(a->source.link.ia->af,
		    &a->source.link.src_addr, &b->source.link.src_addr));
	case HELLO_TARGETED:
		return (ldp_addrcmp(a->source.target->af,
		    &a->source.target->addr, &b->source.target->addr));
	default:
		fatalx("adj_compare: unknown hello type");
	}

	return (0);
}

struct adj *
adj_new(struct in_addr lsr_id, struct hello_source *source,
    union ldpd_addr *addr)
{
	struct adj	*adj;

	log_debug("%s: lsr-id %pI4, %s", __func__, &lsr_id,
	    log_hello_src(source));

	if ((adj = calloc(1, sizeof(*adj))) == NULL)
		fatal(__func__);

	adj->lsr_id = lsr_id;
	adj->nbr = NULL;
	adj->source = *source;
	adj->trans_addr = *addr;

	RB_INSERT(global_adj_head, &global.adj_tree, adj);

	switch (source->type) {
	case HELLO_LINK:
		RB_INSERT(ia_adj_head, &source->link.ia->adj_tree, adj);
		break;
	case HELLO_TARGETED:
		source->target->adj = adj;
		break;
	}

	return (adj);
}

void
adj_del(struct adj *adj, uint32_t notif_status)
{
	struct nbr	*nbr = adj->nbr;

	log_debug("%s: lsr-id %pI4, %s (%s)", __func__, &adj->lsr_id,
	    log_hello_src(&adj->source), af_name(adj_get_af(adj)));

	adj_stop_itimer(adj);

	RB_REMOVE(global_adj_head, &global.adj_tree, adj);
	if (nbr)
		RB_REMOVE(nbr_adj_head, &nbr->adj_tree, adj);
	switch (adj->source.type) {
	case HELLO_LINK:
		RB_REMOVE(ia_adj_head, &adj->source.link.ia->adj_tree, adj);

		if (nbr)
			ldp_sync_fsm_adj_event(adj, LDP_SYNC_EVT_ADJ_DEL);
		break;
	case HELLO_TARGETED:
		adj->source.target->adj = NULL;
		break;
	}

	free(adj);

	/*
	 * If the neighbor still exists but none of its remaining
	 * adjacencies (if any) are from the preferred address-family,
	 * then delete it.
	 */
	if (nbr && nbr_adj_count(nbr, nbr->af) == 0) {
		session_shutdown(nbr, notif_status, 0, 0);
		nbr_del(nbr);
	}
}

struct adj *
adj_find(struct in_addr lsr_id, struct hello_source *source)
{
	struct adj	 adj;
	adj.lsr_id = lsr_id;
	adj.source = *source;
	return (RB_FIND(global_adj_head, &global.adj_tree, &adj));
}

int
adj_get_af(const struct adj *adj)
{
	switch (adj->source.type) {
	case HELLO_LINK:
		return (adj->source.link.ia->af);
	case HELLO_TARGETED:
		return (adj->source.target->af);
	default:
		fatalx("adj_get_af: unknown hello type");
	}
}

/* adjacency timers */

/* ARGSUSED */
static void adj_itimer(struct event *thread)
{
	struct adj *adj = EVENT_ARG(thread);

	adj->inactivity_timer = NULL;

	log_debug("%s: lsr-id %pI4", __func__, &adj->lsr_id);

	if (adj->source.type == HELLO_TARGETED) {
		if (!CHECK_FLAG(adj->source.target->flags, F_TNBR_CONFIGURED) &&
		    adj->source.target->pw_count == 0 &&
		    adj->source.target->rlfa_count == 0) {
			/* remove dynamic targeted neighbor */
			tnbr_del(leconf, adj->source.target);
			return;
		}
	}

	adj_del(adj, S_HOLDTIME_EXP);
}

void
adj_start_itimer(struct adj *adj)
{
	EVENT_OFF(adj->inactivity_timer);
	adj->inactivity_timer = NULL;
	event_add_timer(master, adj_itimer, adj, adj->holdtime,
			&adj->inactivity_timer);
}

void
adj_stop_itimer(struct adj *adj)
{
	EVENT_OFF(adj->inactivity_timer);
}

/* targeted neighbors */

static __inline int
tnbr_compare(const struct tnbr *a, const struct tnbr *b)
{
	if (a->af < b->af)
		return (-1);
	if (a->af > b->af)
		return (1);

	return (ldp_addrcmp(a->af, &a->addr, &b->addr));
}

struct tnbr *
tnbr_new(int af, union ldpd_addr *addr)
{
	struct tnbr		*tnbr;

	if ((tnbr = calloc(1, sizeof(*tnbr))) == NULL)
		fatal(__func__);

	tnbr->af = af;
	tnbr->addr = *addr;
	tnbr->state = TNBR_STA_DOWN;

	return (tnbr);
}

static void
tnbr_del(struct ldpd_conf *xconf, struct tnbr *tnbr)
{
	tnbr_stop(tnbr);
	RB_REMOVE(tnbr_head, &xconf->tnbr_tree, tnbr);
	free(tnbr);
}

struct tnbr *
tnbr_find(struct ldpd_conf *xconf, int af, union ldpd_addr *addr)
{
	struct tnbr	 tnbr;
	tnbr.af = af;
	tnbr.addr = *addr;
	return (RB_FIND(tnbr_head, &xconf->tnbr_tree, &tnbr));
}

struct tnbr *
tnbr_check(struct ldpd_conf *xconf, struct tnbr *tnbr)
{
	if (!CHECK_FLAG(tnbr->flags, (F_TNBR_CONFIGURED|F_TNBR_DYNAMIC)) &&
	    tnbr->pw_count == 0 && tnbr->rlfa_count == 0) {
		tnbr_del(xconf, tnbr);
		return (NULL);
	}

	return (tnbr);
}

static void
tnbr_start(struct tnbr *tnbr)
{
	send_hello(HELLO_TARGETED, NULL, tnbr);
	tnbr_start_hello_timer(tnbr);
	tnbr->state = TNBR_STA_ACTIVE;
}

static void
tnbr_stop(struct tnbr *tnbr)
{
	tnbr_stop_hello_timer(tnbr);
	if (tnbr->adj)
		adj_del(tnbr->adj, S_SHUTDOWN);
	tnbr->state = TNBR_STA_DOWN;
}

void
tnbr_update(struct tnbr *tnbr)
{
	int			 socket_ok, rtr_id_ok;

	if ((ldp_af_global_get(&global, tnbr->af))->ldp_edisc_socket != -1)
		socket_ok = 1;
	else
		socket_ok = 0;

	if (ldp_rtr_id_get(leconf) != INADDR_ANY)
		rtr_id_ok = 1;
	else
		rtr_id_ok = 0;

	if (tnbr->state == TNBR_STA_DOWN) {
		if (!socket_ok || !rtr_id_ok)
			return;

		tnbr_start(tnbr);
	} else if (tnbr->state == TNBR_STA_ACTIVE) {
		if (socket_ok && rtr_id_ok)
			return;

		tnbr_stop(tnbr);
	}
}

void
tnbr_update_all(int af)
{
	struct tnbr		*tnbr;

	/* update targeted neighbors */
	RB_FOREACH(tnbr, tnbr_head, &leconf->tnbr_tree)
		if (tnbr->af == af || af == AF_UNSPEC)
			tnbr_update(tnbr);
}

uint16_t
tnbr_get_hello_holdtime(struct tnbr *tnbr)
{
	if ((ldp_af_conf_get(leconf, tnbr->af))->thello_holdtime != 0)
		return ((ldp_af_conf_get(leconf, tnbr->af))->thello_holdtime);

	return (leconf->thello_holdtime);
}

uint16_t
tnbr_get_hello_interval(struct tnbr *tnbr)
{
	if ((ldp_af_conf_get(leconf, tnbr->af))->thello_interval != 0)
		return ((ldp_af_conf_get(leconf, tnbr->af))->thello_interval);

	return (leconf->thello_interval);
}

/* target neighbors timers */

/* ARGSUSED */
static void tnbr_hello_timer(struct event *thread)
{
	struct tnbr *tnbr = EVENT_ARG(thread);

	tnbr->hello_timer = NULL;
	send_hello(HELLO_TARGETED, NULL, tnbr);
	tnbr_start_hello_timer(tnbr);
}

static void
tnbr_start_hello_timer(struct tnbr *tnbr)
{
	EVENT_OFF(tnbr->hello_timer);
	tnbr->hello_timer = NULL;
	event_add_timer(master, tnbr_hello_timer, tnbr,
			tnbr_get_hello_interval(tnbr), &tnbr->hello_timer);
}

static void
tnbr_stop_hello_timer(struct tnbr *tnbr)
{
	EVENT_OFF(tnbr->hello_timer);
}

struct ctl_adj *
adj_to_ctl(struct adj *adj)
{
	static struct ctl_adj	 actl;

	actl.af = adj_get_af(adj);
	actl.id = adj->lsr_id;
	actl.type = adj->source.type;
	switch (adj->source.type) {
	case HELLO_LINK:
		memcpy(actl.ifname, adj->source.link.ia->iface->name,
		    sizeof(actl.ifname));
		actl.src_addr = adj->source.link.src_addr;
		break;
	case HELLO_TARGETED:
		actl.src_addr = adj->source.target->addr;
		break;
	}
	actl.holdtime = adj->holdtime;
	actl.holdtime_remaining =
		event_timer_remain_second(adj->inactivity_timer);
	actl.trans_addr = adj->trans_addr;
	actl.ds_tlv = adj->ds_tlv;

	return (&actl);
}
