/*
 * IS-IS Rout(e)ing protocol - Multi Topology Support
 *
 * Copyright (C) 2017 Christian Franke
 *
 * This file is part of FRRouting (FRR)
 *
 * FRR 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.
 *
 * FRR 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 "isisd/isisd.h"
#include "isisd/isis_memory.h"
#include "isisd/isis_circuit.h"
#include "isisd/isis_adjacency.h"
#include "isisd/isis_misc.h"
#include "isisd/isis_lsp.h"
#include "isisd/isis_mt.h"
#include "isisd/isis_tlvs.h"

DEFINE_MTYPE_STATIC(ISISD, MT_AREA_SETTING, "ISIS MT Area Setting")
DEFINE_MTYPE_STATIC(ISISD, MT_CIRCUIT_SETTING, "ISIS MT Circuit Setting")
DEFINE_MTYPE_STATIC(ISISD, MT_ADJ_INFO, "ISIS MT Adjacency Info")

bool isis_area_ipv6_dstsrc_enabled(struct isis_area *area)
{
	struct isis_area_mt_setting *area_mt_setting;
	area_mt_setting = area_lookup_mt_setting(area, ISIS_MT_IPV6_DSTSRC);

	return (area_mt_setting && area_mt_setting->enabled);
}

uint16_t isis_area_ipv6_topology(struct isis_area *area)
{
	struct isis_area_mt_setting *area_mt_setting;
	area_mt_setting = area_lookup_mt_setting(area, ISIS_MT_IPV6_UNICAST);

	if (area_mt_setting && area_mt_setting->enabled)
		return ISIS_MT_IPV6_UNICAST;
	return ISIS_MT_IPV4_UNICAST;
}

/* MT naming api */
const char *isis_mtid2str(uint16_t mtid)
{
	static char buf[sizeof("65535")];

	switch (mtid) {
	case ISIS_MT_IPV4_UNICAST:
		return "ipv4-unicast";
	case ISIS_MT_IPV4_MGMT:
		return "ipv4-mgmt";
	case ISIS_MT_IPV6_UNICAST:
		return "ipv6-unicast";
	case ISIS_MT_IPV4_MULTICAST:
		return "ipv4-multicast";
	case ISIS_MT_IPV6_MULTICAST:
		return "ipv6-multicast";
	case ISIS_MT_IPV6_MGMT:
		return "ipv6-mgmt";
	case ISIS_MT_IPV6_DSTSRC:
		return "ipv6-dstsrc";
	default:
		snprintf(buf, sizeof(buf), "%hu", mtid);
		return buf;
	}
}

uint16_t isis_str2mtid(const char *name)
{
	if (!strcmp(name, "ipv4-unicast"))
		return ISIS_MT_IPV4_UNICAST;
	if (!strcmp(name, "ipv4-mgmt"))
		return ISIS_MT_IPV4_MGMT;
	if (!strcmp(name, "ipv6-unicast"))
		return ISIS_MT_IPV6_UNICAST;
	if (!strcmp(name, "ipv4-multicast"))
		return ISIS_MT_IPV4_MULTICAST;
	if (!strcmp(name, "ipv6-multicast"))
		return ISIS_MT_IPV6_MULTICAST;
	if (!strcmp(name, "ipv6-mgmt"))
		return ISIS_MT_IPV6_MGMT;
	if (!strcmp(name, "ipv6-dstsrc"))
		return ISIS_MT_IPV6_DSTSRC;
	return -1;
}

/* General MT settings api */

struct mt_setting {
	ISIS_MT_INFO_FIELDS;
};

static void *lookup_mt_setting(struct list *mt_list, uint16_t mtid)
{
	struct listnode *node;
	struct mt_setting *setting;

	for (ALL_LIST_ELEMENTS_RO(mt_list, node, setting)) {
		if (setting->mtid == mtid)
			return setting;
	}
	return NULL;
}

static void add_mt_setting(struct list **mt_list, void *setting)
{
	if (!*mt_list)
		*mt_list = list_new();
	listnode_add(*mt_list, setting);
}

/* Area specific MT settings api */

struct isis_area_mt_setting *area_lookup_mt_setting(struct isis_area *area,
						    uint16_t mtid)
{
	return lookup_mt_setting(area->mt_settings, mtid);
}

struct isis_area_mt_setting *area_new_mt_setting(struct isis_area *area,
						 uint16_t mtid)
{
	struct isis_area_mt_setting *setting;

	setting = XCALLOC(MTYPE_MT_AREA_SETTING, sizeof(*setting));
	setting->mtid = mtid;
	return setting;
}

static void area_free_mt_setting(void *setting)
{
	XFREE(MTYPE_MT_AREA_SETTING, setting);
}

void area_add_mt_setting(struct isis_area *area,
			 struct isis_area_mt_setting *setting)
{
	add_mt_setting(&area->mt_settings, setting);
}

void area_mt_init(struct isis_area *area)
{
	struct isis_area_mt_setting *v4_unicast_setting;

	/* MTID 0 is always enabled */
	v4_unicast_setting = area_new_mt_setting(area, ISIS_MT_IPV4_UNICAST);
	v4_unicast_setting->enabled = true;
	add_mt_setting(&area->mt_settings, v4_unicast_setting);
	area->mt_settings->del = area_free_mt_setting;
}

void area_mt_finish(struct isis_area *area)
{
	list_delete(&area->mt_settings);
}

struct isis_area_mt_setting *area_get_mt_setting(struct isis_area *area,
						 uint16_t mtid)
{
	struct isis_area_mt_setting *setting;

	setting = area_lookup_mt_setting(area, mtid);
	if (!setting) {
		setting = area_new_mt_setting(area, mtid);
		area_add_mt_setting(area, setting);
	}
	return setting;
}

int area_write_mt_settings(struct isis_area *area, struct vty *vty)
{
	int written = 0;
	struct listnode *node;
	struct isis_area_mt_setting *setting;

	for (ALL_LIST_ELEMENTS_RO(area->mt_settings, node, setting)) {
		const char *name = isis_mtid2str(setting->mtid);
		if (name && setting->enabled) {
			if (setting->mtid == ISIS_MT_IPV4_UNICAST)
				continue; /* always enabled, no need to write
					     out config */
			vty_out(vty, " topology %s%s\n", name,
				setting->overload ? " overload" : "");
			written++;
		}
	}
	return written;
}

bool area_is_mt(struct isis_area *area)
{
	struct listnode *node, *node2;
	struct isis_area_mt_setting *setting;
	struct isis_circuit *circuit;
	struct isis_circuit_mt_setting *csetting;

	for (ALL_LIST_ELEMENTS_RO(area->mt_settings, node, setting)) {
		if (setting->enabled && setting->mtid != ISIS_MT_IPV4_UNICAST)
			return true;
	}
	for (ALL_LIST_ELEMENTS_RO(area->circuit_list, node, circuit)) {
		for (ALL_LIST_ELEMENTS_RO(circuit->mt_settings, node2,
					  csetting)) {
			if (!csetting->enabled
			    && csetting->mtid == ISIS_MT_IPV4_UNICAST)
				return true;
		}
	}

	return false;
}

struct isis_area_mt_setting **area_mt_settings(struct isis_area *area,
					       unsigned int *mt_count)
{
	static unsigned int size = 0;
	static struct isis_area_mt_setting **rv = NULL;

	unsigned int count = 0;
	struct listnode *node;
	struct isis_area_mt_setting *setting;

	for (ALL_LIST_ELEMENTS_RO(area->mt_settings, node, setting)) {
		if (!setting->enabled)
			continue;

		count++;
		if (count > size) {
			rv = XREALLOC(MTYPE_TMP, rv, count * sizeof(*rv));
			size = count;
		}
		rv[count - 1] = setting;
	}

	*mt_count = count;
	return rv;
}

/* Circuit specific MT settings api */

struct isis_circuit_mt_setting *
circuit_lookup_mt_setting(struct isis_circuit *circuit, uint16_t mtid)
{
	return lookup_mt_setting(circuit->mt_settings, mtid);
}

struct isis_circuit_mt_setting *
circuit_new_mt_setting(struct isis_circuit *circuit, uint16_t mtid)
{
	struct isis_circuit_mt_setting *setting;

	setting = XCALLOC(MTYPE_MT_CIRCUIT_SETTING, sizeof(*setting));
	setting->mtid = mtid;
	setting->enabled = true; /* Enabled is default for circuit */
	return setting;
}

static void circuit_free_mt_setting(void *setting)
{
	XFREE(MTYPE_MT_CIRCUIT_SETTING, setting);
}

void circuit_add_mt_setting(struct isis_circuit *circuit,
			    struct isis_circuit_mt_setting *setting)
{
	add_mt_setting(&circuit->mt_settings, setting);
}

void circuit_mt_init(struct isis_circuit *circuit)
{
	circuit->mt_settings = list_new();
	circuit->mt_settings->del = circuit_free_mt_setting;
}

void circuit_mt_finish(struct isis_circuit *circuit)
{
	list_delete(&circuit->mt_settings);
}

struct isis_circuit_mt_setting *
circuit_get_mt_setting(struct isis_circuit *circuit, uint16_t mtid)
{
	struct isis_circuit_mt_setting *setting;

	setting = circuit_lookup_mt_setting(circuit, mtid);
	if (!setting) {
		setting = circuit_new_mt_setting(circuit, mtid);
		circuit_add_mt_setting(circuit, setting);
	}
	return setting;
}

#ifdef FABRICD
static int circuit_write_mt_settings(struct isis_circuit *circuit,
				     struct vty *vty)
{
	int written = 0;
	struct listnode *node;
	struct isis_circuit_mt_setting *setting;

	for (ALL_LIST_ELEMENTS_RO(circuit->mt_settings, node, setting)) {
		const char *name = isis_mtid2str(setting->mtid);
		if (name && !setting->enabled) {
			vty_out(vty, " no " PROTO_NAME " topology %s\n", name);
			written++;
		}
	}
	return written;
}
#endif

struct isis_circuit_mt_setting **
circuit_mt_settings(struct isis_circuit *circuit, unsigned int *mt_count)
{
	static unsigned int size = 0;
	static struct isis_circuit_mt_setting **rv = NULL;

	struct isis_area_mt_setting **area_settings;
	unsigned int area_count;

	unsigned int count = 0;

	struct listnode *node;
	struct isis_circuit_mt_setting *setting;

	area_settings = area_mt_settings(circuit->area, &area_count);

	for (unsigned int i = 0; i < area_count; i++) {
		for (ALL_LIST_ELEMENTS_RO(circuit->mt_settings, node,
					  setting)) {
			if (setting->mtid != area_settings[i]->mtid)
				continue;
			break;
		}
		if (!setting)
			setting = circuit_get_mt_setting(
				circuit, area_settings[i]->mtid);

		if (!setting->enabled)
			continue;

		count++;
		if (count > size) {
			rv = XREALLOC(MTYPE_TMP, rv, count * sizeof(*rv));
			size = count;
		}
		rv[count - 1] = setting;
	}

	*mt_count = count;
	return rv;
}

/* ADJ specific MT API */
static void adj_mt_set(struct isis_adjacency *adj, unsigned int index,
		       uint16_t mtid)
{
	if (adj->mt_count < index + 1) {
		adj->mt_set = XREALLOC(MTYPE_MT_ADJ_INFO, adj->mt_set,
				       (index + 1) * sizeof(*adj->mt_set));
		adj->mt_count = index + 1;
	}
	adj->mt_set[index] = mtid;
}

bool tlvs_to_adj_mt_set(struct isis_tlvs *tlvs, bool v4_usable, bool v6_usable,
			struct isis_adjacency *adj)
{
	struct isis_circuit_mt_setting **mt_settings;
	unsigned int circuit_mt_count;

	unsigned int intersect_count = 0;

	uint16_t *old_mt_set = NULL;
	unsigned int old_mt_count;

	old_mt_count = adj->mt_count;
	if (old_mt_count) {
		old_mt_set =
			XCALLOC(MTYPE_TMP, old_mt_count * sizeof(*old_mt_set));
		memcpy(old_mt_set, adj->mt_set,
		       old_mt_count * sizeof(*old_mt_set));
	}

	mt_settings = circuit_mt_settings(adj->circuit, &circuit_mt_count);
	for (unsigned int i = 0; i < circuit_mt_count; i++) {
		if (!tlvs->mt_router_info.count
		    && !tlvs->mt_router_info_empty) {
			/* Other end does not have MT enabled */
			if (mt_settings[i]->mtid == ISIS_MT_IPV4_UNICAST
			    && (v4_usable || v6_usable))
				adj_mt_set(adj, intersect_count++,
					   ISIS_MT_IPV4_UNICAST);
		} else {
			struct isis_mt_router_info *info_head;

			info_head = (struct isis_mt_router_info *)
					    tlvs->mt_router_info.head;
			for (struct isis_mt_router_info *info = info_head; info;
			     info = info->next) {
				if (mt_settings[i]->mtid == info->mtid) {
					bool usable;
					switch (info->mtid) {
					case ISIS_MT_IPV4_UNICAST:
					case ISIS_MT_IPV4_MGMT:
					case ISIS_MT_IPV4_MULTICAST:
						usable = v4_usable;
						break;
					case ISIS_MT_IPV6_UNICAST:
					case ISIS_MT_IPV6_MGMT:
					case ISIS_MT_IPV6_MULTICAST:
						usable = v6_usable;
						break;
					default:
						usable = true;
						break;
					}
					if (usable)
						adj_mt_set(adj,
							   intersect_count++,
							   info->mtid);
				}
			}
		}
	}
	adj->mt_count = intersect_count;

	bool changed = false;

	if (adj->mt_count != old_mt_count)
		changed = true;

	if (!changed && old_mt_count
	    && memcmp(adj->mt_set, old_mt_set,
		      old_mt_count * sizeof(*old_mt_set)))
		changed = true;

	if (old_mt_count)
		XFREE(MTYPE_TMP, old_mt_set);

	return changed;
}

bool adj_has_mt(struct isis_adjacency *adj, uint16_t mtid)
{
	for (unsigned int i = 0; i < adj->mt_count; i++)
		if (adj->mt_set[i] == mtid)
			return true;
	return false;
}

void adj_mt_finish(struct isis_adjacency *adj)
{
	XFREE(MTYPE_MT_ADJ_INFO, adj->mt_set);
	adj->mt_count = 0;
}

static void mt_set_add(uint16_t **mt_set, unsigned int *size,
		       unsigned int *index, uint16_t mtid)
{
	for (unsigned int i = 0; i < *index; i++) {
		if ((*mt_set)[i] == mtid)
			return;
	}

	if (*index >= *size) {
		*mt_set = XREALLOC(MTYPE_TMP, *mt_set,
				   sizeof(**mt_set) * ((*index) + 1));
		*size = (*index) + 1;
	}

	(*mt_set)[*index] = mtid;
	*index = (*index) + 1;
}

static uint16_t *circuit_bcast_mt_set(struct isis_circuit *circuit, int level,
				      unsigned int *mt_count)
{
	static uint16_t *rv;
	static unsigned int size;
	struct listnode *node;
	struct isis_adjacency *adj;

	unsigned int count = 0;

	if (circuit->circ_type != CIRCUIT_T_BROADCAST) {
		*mt_count = 0;
		return NULL;
	}

	for (ALL_LIST_ELEMENTS_RO(circuit->u.bc.adjdb[level - 1], node, adj)) {
		if (adj->adj_state != ISIS_ADJ_UP)
			continue;
		for (unsigned int i = 0; i < adj->mt_count; i++)
			mt_set_add(&rv, &size, &count, adj->mt_set[i]);
	}

	*mt_count = count;
	return rv;
}

static void tlvs_add_mt_set(struct isis_area *area, struct isis_tlvs *tlvs,
			    unsigned int mt_count, uint16_t *mt_set,
			    uint8_t *id, uint32_t metric,
			    struct isis_ext_subtlvs *ext)
{
	for (unsigned int i = 0; i < mt_count; i++) {
		uint16_t mtid = mt_set[i];
		if (mt_set[i] == ISIS_MT_IPV4_UNICAST) {
			lsp_debug(
				"ISIS (%s): Adding %s.%02x as te-style neighbor",
				area->area_tag, sysid_print(id),
				LSP_PSEUDO_ID(id));
		} else {
			lsp_debug(
				"ISIS (%s): Adding %s.%02x as mt-style neighbor for %s",
				area->area_tag, sysid_print(id),
				LSP_PSEUDO_ID(id), isis_mtid2str(mtid));
		}
		isis_tlvs_add_extended_reach(tlvs, mtid, id, metric, ext);
	}
}

void tlvs_add_mt_bcast(struct isis_tlvs *tlvs, struct isis_circuit *circuit,
		       int level, uint8_t *id, uint32_t metric)
{
	unsigned int mt_count;
	uint16_t *mt_set = circuit_bcast_mt_set(circuit, level, &mt_count);

	tlvs_add_mt_set(circuit->area, tlvs, mt_count, mt_set, id, metric,
			circuit->ext);
}

void tlvs_add_mt_p2p(struct isis_tlvs *tlvs, struct isis_circuit *circuit,
		     uint8_t *id, uint32_t metric)
{
	struct isis_adjacency *adj = circuit->u.p2p.neighbor;

	tlvs_add_mt_set(circuit->area, tlvs, adj->mt_count, adj->mt_set, id,
			metric, circuit->ext);
}

void mt_init(void)
{
#ifdef FABRICD
	hook_register(isis_circuit_config_write,
		      circuit_write_mt_settings);
#endif
}
