// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * SRC-DEST Routing Table
 *
 * Copyright (C) 2017 by David Lamparter & Christian Franke,
 *                       Open Source Routing / NetDEF Inc.
 *
 * This file is part of FRRouting (FRR)
 */

#include <zebra.h>

#include "srcdest_table.h"

#include "memory.h"
#include "prefix.h"
#include "table.h"
#include "printfrr.h"

DEFINE_MTYPE_STATIC(LIB, ROUTE_SRC_NODE, "Route source node");

/* ----- functions to manage rnodes _with_ srcdest table ----- */
struct srcdest_rnode {
	/* must be first in structure for casting to/from route_node */
	ROUTE_NODE_FIELDS;

	struct route_table *src_table;
};

static struct srcdest_rnode *srcdest_rnode_from_rnode(struct route_node *rn)
{
	assert(rnode_is_dstnode(rn));
	return (struct srcdest_rnode *)rn;
}

static struct route_node *srcdest_rnode_to_rnode(struct srcdest_rnode *srn)
{
	return (struct route_node *)srn;
}

static struct route_node *srcdest_rnode_create(route_table_delegate_t *delegate,
					       struct route_table *table)
{
	struct srcdest_rnode *srn;
	srn = XCALLOC(MTYPE_ROUTE_NODE, sizeof(struct srcdest_rnode));
	return srcdest_rnode_to_rnode(srn);
}

static void srcdest_rnode_destroy(route_table_delegate_t *delegate,
				  struct route_table *table,
				  struct route_node *rn)
{
	struct srcdest_rnode *srn = srcdest_rnode_from_rnode(rn);
	struct route_table *src_table;

	/* Clear route node's src_table here already, otherwise the
	 * deletion of the last node in the src_table will trigger
	 * another call to route_table_finish for the src_table.
	 *
	 * (Compare with srcdest_srcnode_destroy)
	 */
	src_table = srn->src_table;
	srn->src_table = NULL;
	route_table_finish(src_table);
	XFREE(MTYPE_ROUTE_NODE, rn);
}

route_table_delegate_t _srcdest_dstnode_delegate = {
	.create_node = srcdest_rnode_create,
	.destroy_node = srcdest_rnode_destroy};

/* ----- functions to manage rnodes _in_ srcdest table ----- */

/* node creation / deletion for srcdest source prefix nodes.
 * the route_node isn't actually different from the normal route_node,
 * but the cleanup is special to free the table (and possibly the
 * destination prefix's route_node) */

static struct route_node *
srcdest_srcnode_create(route_table_delegate_t *delegate,
		       struct route_table *table)
{
	return XCALLOC(MTYPE_ROUTE_SRC_NODE, sizeof(struct route_node));
}

static void srcdest_srcnode_destroy(route_table_delegate_t *delegate,
				    struct route_table *table,
				    struct route_node *rn)
{
	struct srcdest_rnode *srn;

	XFREE(MTYPE_ROUTE_SRC_NODE, rn);

	srn = route_table_get_info(table);
	if (srn->src_table && route_table_count(srn->src_table) == 0) {
		/* deleting the route_table from inside destroy_node is ONLY
		 * permitted IF table->count is 0!  see lib/table.c
		 * route_node_delete()
		 * for details */
		route_table_finish(srn->src_table);
		srn->src_table = NULL;

		/* drop the ref we're holding in srcdest_node_get().  there
		 * might be
		 * non-srcdest routes, so the route_node may still exist.
		 * hence, it's
		 * important to clear src_table above. */
		route_unlock_node(srcdest_rnode_to_rnode(srn));
	}
}

route_table_delegate_t _srcdest_srcnode_delegate = {
	.create_node = srcdest_srcnode_create,
	.destroy_node = srcdest_srcnode_destroy};

/* NB: read comments in code for refcounting before using! */
static struct route_node *srcdest_srcnode_get(struct route_node *rn,
					      const struct prefix_ipv6 *src_p)
{
	struct srcdest_rnode *srn;

	if (!src_p || src_p->prefixlen == 0)
		return rn;

	srn = srcdest_rnode_from_rnode(rn);
	if (!srn->src_table) {
		/* this won't use srcdest_rnode, we're already on the source
		 * here */
		srn->src_table = route_table_init_with_delegate(
			&_srcdest_srcnode_delegate);
		route_table_set_info(srn->src_table, srn);

		/* there is no route_unlock_node on the original rn here.
		 * The reference is kept for the src_table. */
	} else {
		/* only keep 1 reference for the src_table, makes the
		 * refcounting
		 * more similar to the non-srcdest case.  Either way after
		 * return from
		 * function, the only reference held is the one on the return
		 * value.
		 *
		 * We can safely drop our reference here because src_table is
		 * holding
		 * another reference, so this won't free rn */
		route_unlock_node(rn);
	}

	return route_node_get(srn->src_table, (const struct prefix *)src_p);
}

static struct route_node *srcdest_srcnode_lookup(
	struct route_node *rn,
	const struct prefix_ipv6 *src_p)
{
	struct srcdest_rnode *srn;

	if (!rn || !src_p || src_p->prefixlen == 0)
		return rn;

	/* We got this rn from a lookup, so its refcnt was incremented. As we
	 * won't
	 * return return rn from any point beyond here, we should decrement its
	 * refcnt.
	 */
	route_unlock_node(rn);

	srn = srcdest_rnode_from_rnode(rn);
	if (!srn->src_table)
		return NULL;

	return route_node_lookup(srn->src_table, (const struct prefix *)src_p);
}

/* ----- exported functions ----- */

struct route_table *srcdest_table_init(void)
{
	return route_table_init_with_delegate(&_srcdest_dstnode_delegate);
}

struct route_node *srcdest_route_next(struct route_node *rn)
{
	struct route_node *next, *parent;

	/* For a non src-dest node, just return route_next */
	if (!(rnode_is_dstnode(rn) || rnode_is_srcnode(rn)))
		return route_next(rn);

	if (rnode_is_dstnode(rn)) {
		/* This means the route_node is part of the top hierarchy
		 * and refers to a destination prefix. */
		struct srcdest_rnode *srn = srcdest_rnode_from_rnode(rn);

		if (srn->src_table)
			next = route_top(srn->src_table);
		else
			next = NULL;

		if (next) {
			/* There is a source prefix. Return the node for it */
			route_unlock_node(rn);
			return next;
		} else {
			/* There is no source prefix, just continue as usual */
			return route_next(rn);
		}
	}

	/* This part handles the case of iterating source nodes. */
	parent = route_lock_node(route_table_get_info(rn->table));
	next = route_next(rn);

	if (next) {
		/* There is another source node, continue in the source table */
		route_unlock_node(parent);
		return next;
	} else {
		/* The source table is complete, continue in the parent table */
		return route_next(parent);
	}
}

struct route_node *srcdest_rnode_get(struct route_table *table,
				     union prefixconstptr dst_pu,
				     const struct prefix_ipv6 *src_p)
{
	const struct prefix_ipv6 *dst_p = dst_pu.p6;
	struct route_node *rn;

	rn = route_node_get(table, (const struct prefix *)dst_p);
	return srcdest_srcnode_get(rn, src_p);
}

struct route_node *srcdest_rnode_lookup(struct route_table *table,
					union prefixconstptr dst_pu,
					const struct prefix_ipv6 *src_p)
{
	const struct prefix_ipv6 *dst_p = dst_pu.p6;
	struct route_node *rn;
	struct route_node *srn;

	rn = route_node_lookup_maynull(table, (const struct prefix *)dst_p);
	srn = srcdest_srcnode_lookup(rn, src_p);

	if (rn != NULL && rn == srn && !rn->info) {
		/* Match the behavior of route_node_lookup and don't return an
		 * empty route-node for a dest-route */
		route_unlock_node(rn);
		return NULL;
	}
	return srn;
}

void srcdest_rnode_prefixes(const struct route_node *rn,
			    const struct prefix **p,
			    const struct prefix **src_p)
{
	if (rnode_is_srcnode(rn)) {
		struct route_node *dst_rn = route_table_get_info(rn->table);
		if (p)
			*p = &dst_rn->p;
		if (src_p)
			*src_p = &rn->p;
	} else {
		if (p)
			*p = &rn->p;
		if (src_p)
			*src_p = NULL;
	}
}

const char *srcdest2str(const struct prefix *dst_p,
			const struct prefix_ipv6 *src_p,
			char *str, int size)
{
	char dst_buf[PREFIX_STRLEN], src_buf[PREFIX_STRLEN];

	snprintf(str, size, "%s%s%s",
		 prefix2str(dst_p, dst_buf, sizeof(dst_buf)),
		 (src_p && src_p->prefixlen) ? " from " : "",
		 (src_p && src_p->prefixlen)
			 ? prefix2str(src_p, src_buf, sizeof(src_buf))
			 : "");
	return str;
}

const char *srcdest_rnode2str(const struct route_node *rn, char *str, int size)
{
	const struct prefix *dst_p, *src_p;

	srcdest_rnode_prefixes(rn, &dst_p, &src_p);
	return srcdest2str(dst_p, (const struct prefix_ipv6 *)src_p, str, size);
}

printfrr_ext_autoreg_p("RN", printfrr_rn);
static ssize_t printfrr_rn(struct fbuf *buf, struct printfrr_eargs *ea,
			   const void *ptr)
{
	const struct route_node *rn = ptr;
	const struct prefix *dst_p, *src_p;
	char cbuf[PREFIX_STRLEN * 2 + 6];

	if (!rn)
		return bputs(buf, "(null)");

	srcdest_rnode_prefixes(rn, &dst_p, &src_p);
	srcdest2str(dst_p, (const struct prefix_ipv6 *)src_p,
		    cbuf, sizeof(cbuf));
	return bputs(buf, cbuf);
}

struct route_table *srcdest_srcnode_table(struct route_node *rn)
{
	if (rnode_is_dstnode(rn)) {
		struct srcdest_rnode *srn = srcdest_rnode_from_rnode(rn);

		return srn->src_table;
	}
	return NULL;
}
