/*
 * This file is part of RTRlib.
 *
 * This file is subject to the terms and conditions of the MIT license.
 * See the file LICENSE in the top level directory for more details.
 *
 * Website: http://rtrlib.realmv6.org/
 */

#include "trie-pfx.h"

#include "rtrlib/lib/alloc_utils_private.h"
#include "rtrlib/lib/ip_private.h"
#include "rtrlib/pfx/pfx_private.h"
#include "rtrlib/pfx/trie/trie_private.h"
#include "rtrlib/rtrlib_export_private.h"

#include <assert.h>
#include <pthread.h>
#include <stdbool.h>
#include <stdlib.h>

struct data_elem {
	uint32_t asn;
	uint8_t max_len;
	const struct rtr_socket *socket;
};

struct node_data {
	unsigned int len;
	struct data_elem *ary;
};

struct copy_cb_args {
	struct pfx_table *pfx_table;
	const struct rtr_socket *socket;
	bool error;
};

struct notify_diff_cb_args {
	struct pfx_table *old_table;
	struct pfx_table *new_table;
	const struct rtr_socket *socket;
	pfx_update_fp pfx_update_fp;
	bool added;
};

static struct trie_node *pfx_table_get_root(const struct pfx_table *pfx_table, const enum lrtr_ip_version ver);
static int pfx_table_del_elem(struct node_data *data, const unsigned int index);
static int pfx_table_create_node(struct trie_node **node, const struct pfx_record *record);
static int pfx_table_append_elem(struct node_data *data, const struct pfx_record *record);
static struct data_elem *pfx_table_find_elem(const struct node_data *data, const struct pfx_record *record,
					     unsigned int *index);
static bool pfx_table_elem_matches(struct node_data *data, const uint32_t asn, const uint8_t prefix_len);
static void pfx_table_notify_clients(struct pfx_table *pfx_table, const struct pfx_record *record, const bool added);
static int pfx_table_remove_id(struct pfx_table *pfx_table, struct trie_node **root, struct trie_node *node,
			       const struct rtr_socket *socket, const unsigned int level);
static int pfx_table_node2pfx_record(struct trie_node *node, struct pfx_record records[], const unsigned int ary_len);
static void pfx_table_free_reason(struct pfx_record **reason, unsigned int *reason_len);

void pfx_table_notify_clients(struct pfx_table *pfx_table, const struct pfx_record *record, const bool added)
{
	if (pfx_table->update_fp)
		pfx_table->update_fp(pfx_table, *record, added);
}

RTRLIB_EXPORT void pfx_table_init(struct pfx_table *pfx_table, pfx_update_fp update_fp)
{
	pfx_table->ipv4 = NULL;
	pfx_table->ipv6 = NULL;
	pfx_table->update_fp = update_fp;
	pthread_rwlock_init(&(pfx_table->lock), NULL);
}

void pfx_table_free_without_notify(struct pfx_table *pfx_table)
{
	pfx_table->update_fp = NULL;
	pfx_table_free(pfx_table);
}

RTRLIB_EXPORT void pfx_table_free(struct pfx_table *pfx_table)
{
	for (int i = 0; i < 2; i++) {
		struct trie_node *root = (i == 0 ? pfx_table->ipv4 : pfx_table->ipv6);

		if (root) {
			struct trie_node *rm_node;

			pthread_rwlock_wrlock(&(pfx_table->lock));
			do {
				struct node_data *data = (struct node_data *)(root->data);

				for (unsigned int j = 0; j < data->len; j++) {
					struct pfx_record record = {data->ary[j].asn, (root->prefix), root->len,
								    data->ary[j].max_len, data->ary[j].socket};
					pfx_table_notify_clients(pfx_table, &record, false);
				}
				rm_node = (trie_remove(root, &(root->prefix), root->len, 0));
				assert(rm_node);
				lrtr_free(((struct node_data *)rm_node->data)->ary);
				lrtr_free(rm_node->data);
				lrtr_free(rm_node);
			} while (rm_node != root);
			if (i == 0)
				pfx_table->ipv4 = NULL;
			else
				pfx_table->ipv6 = NULL;

			pthread_rwlock_unlock(&(pfx_table->lock));
		}
	}
	pthread_rwlock_destroy(&(pfx_table->lock));
}

int pfx_table_append_elem(struct node_data *data, const struct pfx_record *record)
{
	struct data_elem *tmp = lrtr_realloc(data->ary, sizeof(struct data_elem) * ((data->len) + 1));

	if (!tmp)
		return PFX_ERROR;
	data->len++;
	data->ary = tmp;
	data->ary[data->len - 1].asn = record->asn;
	data->ary[data->len - 1].max_len = record->max_len;
	data->ary[data->len - 1].socket = record->socket;
	return PFX_SUCCESS;
}

int pfx_table_create_node(struct trie_node **node, const struct pfx_record *record)
{
	int err;

	*node = lrtr_malloc(sizeof(struct trie_node));
	if (!*node)
		return PFX_ERROR;

	(*node)->prefix = record->prefix;
	(*node)->len = record->min_len;
	(*node)->lchild = NULL;
	(*node)->rchild = NULL;
	(*node)->parent = NULL;

	(*node)->data = lrtr_malloc(sizeof(struct node_data));
	if (!(*node)->data) {
		err = PFX_ERROR;
		goto free_node;
	}

	((struct node_data *)(*node)->data)->len = 0;
	((struct node_data *)(*node)->data)->ary = NULL;

	err = pfx_table_append_elem(((struct node_data *)(*node)->data), record);
	if (err)
		goto free_node_data;

	return PFX_SUCCESS;

free_node_data:
	lrtr_free((*node)->data);
free_node:
	lrtr_free(*node);

	return err;
}

struct data_elem *pfx_table_find_elem(const struct node_data *data, const struct pfx_record *record,
				      unsigned int *index)
{
	for (unsigned int i = 0; i < data->len; i++) {
		if (data->ary[i].asn == record->asn && data->ary[i].max_len == record->max_len &&
		    data->ary[i].socket == record->socket) {
			if (index)
				*index = i;
			return &(data->ary[i]);
		}
	}
	return NULL;
}

// returns pfx_table->ipv4 if record version is LRTR_IPV4 else pfx_table->ipv6
inline struct trie_node *pfx_table_get_root(const struct pfx_table *pfx_table, const enum lrtr_ip_version ver)
{
	return ver == LRTR_IPV4 ? pfx_table->ipv4 : pfx_table->ipv6;
}

int pfx_table_del_elem(struct node_data *data, const unsigned int index)
{
	struct data_elem *tmp;
	struct data_elem deleted_elem = data->ary[index];

	// if index is not the last elem in the list, move all other elems backwards in the array
	if (index != data->len - 1) {
		for (unsigned int i = index; i < data->len - 1; i++)
			data->ary[i] = data->ary[i + 1];
	}

	data->len--;
	if (!data->len) {
		lrtr_free(data->ary);
		data->ary = NULL;
		return PFX_SUCCESS;
	}

	tmp = lrtr_realloc(data->ary, sizeof(struct data_elem) * data->len);
	if (!tmp) {
		data->ary[data->len] = deleted_elem;
		data->len++;
		return PFX_ERROR;
	}

	data->ary = tmp;

	return PFX_SUCCESS;
}

RTRLIB_EXPORT int pfx_table_add(struct pfx_table *pfx_table, const struct pfx_record *record)
{
	pthread_rwlock_wrlock(&(pfx_table->lock));

	struct trie_node *root = pfx_table_get_root(pfx_table, record->prefix.ver);
	unsigned int lvl = 0;

	if (root) {
		bool found;
		struct trie_node *node = trie_lookup_exact(root, &(record->prefix), record->min_len, &lvl, &found);

		if (found) { // node with prefix exists
			if (pfx_table_find_elem(node->data, record, NULL)) {
				pthread_rwlock_unlock(&pfx_table->lock);
				return PFX_DUPLICATE_RECORD;
			}
			// append record to note_data array
			int rtval = pfx_table_append_elem(node->data, record);

			pthread_rwlock_unlock(&pfx_table->lock);
			if (rtval == PFX_SUCCESS)
				pfx_table_notify_clients(pfx_table, record, true);
			return rtval;
		}

		// no node with same prefix and prefix_len found
		struct trie_node *new_node = NULL;

		if (pfx_table_create_node(&new_node, record) == PFX_ERROR) {
			pthread_rwlock_unlock(&pfx_table->lock);
			return PFX_ERROR;
		}
		trie_insert(node, new_node, lvl);
		pthread_rwlock_unlock(&pfx_table->lock);
		pfx_table_notify_clients(pfx_table, record, true);
		return PFX_SUCCESS;
	}

	// tree is empty, record will be the root_node
	struct trie_node *new_node = NULL;

	if (pfx_table_create_node(&new_node, record) == PFX_ERROR) {
		pthread_rwlock_unlock(&pfx_table->lock);
		return PFX_ERROR;
	}
	if (record->prefix.ver == LRTR_IPV4)
		pfx_table->ipv4 = new_node;
	else
		pfx_table->ipv6 = new_node;

	pthread_rwlock_unlock(&pfx_table->lock);
	pfx_table_notify_clients(pfx_table, record, true);
	return PFX_SUCCESS;
}

RTRLIB_EXPORT int pfx_table_remove(struct pfx_table *pfx_table, const struct pfx_record *record)
{
	pthread_rwlock_wrlock(&(pfx_table->lock));
	struct trie_node *root = pfx_table_get_root(pfx_table, record->prefix.ver);

	unsigned int lvl = 0; // tree depth were node was found
	bool found;
	struct trie_node *node = trie_lookup_exact(root, &(record->prefix), record->min_len, &lvl, &found);

	if (!found) {
		pthread_rwlock_unlock(&pfx_table->lock);
		return PFX_RECORD_NOT_FOUND;
	}

	unsigned int index;
	struct data_elem *elem = pfx_table_find_elem(node->data, record, &index);

	if (!elem) {
		pthread_rwlock_unlock(&pfx_table->lock);
		return PFX_RECORD_NOT_FOUND;
	}

	struct node_data *ndata = (struct node_data *)node->data;

	if (pfx_table_del_elem(ndata, index) == PFX_ERROR) {
		pthread_rwlock_unlock(&pfx_table->lock);
		return PFX_ERROR;
	}

	if (ndata->len == 0) {
		node = trie_remove(node, &(record->prefix), record->min_len, lvl);
		assert(node);

		if (node == root) {
			if (record->prefix.ver == LRTR_IPV4)
				pfx_table->ipv4 = NULL;
			else
				pfx_table->ipv6 = NULL;
		}
		assert(((struct node_data *)node->data)->len == 0);
		lrtr_free(node->data);
		lrtr_free(node);
	}
	pthread_rwlock_unlock(&pfx_table->lock);

	pfx_table_notify_clients(pfx_table, record, false);

	return PFX_SUCCESS;
}

bool pfx_table_elem_matches(struct node_data *data, const uint32_t asn, const uint8_t prefix_len)
{
	for (unsigned int i = 0; i < data->len; i++) {
		if (data->ary[i].asn != 0 && data->ary[i].asn == asn && prefix_len <= data->ary[i].max_len)
			return true;
	}
	return false;
}

int pfx_table_node2pfx_record(struct trie_node *node, struct pfx_record *records, const unsigned int ary_len)
{
	struct node_data *data = node->data;

	if (ary_len < data->len)
		return PFX_ERROR;

	for (unsigned int i = 0; i < data->len; i++) {
		records[i].asn = data->ary[i].asn;
		records[i].prefix = node->prefix;
		records[i].min_len = node->len;
		records[i].max_len = data->ary[i].max_len;
		records[i].socket = data->ary[i].socket;
	}
	return data->len;
}

inline void pfx_table_free_reason(struct pfx_record **reason, unsigned int *reason_len)
{
	if (reason) {
		lrtr_free(*reason);
		*reason = NULL;
	}
	if (reason_len)
		*reason_len = 0;
}

RTRLIB_EXPORT int pfx_table_validate_r(struct pfx_table *pfx_table, struct pfx_record **reason,
				       unsigned int *reason_len, const uint32_t asn, const struct lrtr_ip_addr *prefix,
				       const uint8_t prefix_len, enum pfxv_state *result)
{
	// assert(reason_len == NULL || *reason_len  == 0);
	// assert(reason == NULL || *reason == NULL);

	pthread_rwlock_rdlock(&(pfx_table->lock));
	struct trie_node *root = pfx_table_get_root(pfx_table, prefix->ver);

	if (!root) {
		pthread_rwlock_unlock(&pfx_table->lock);
		*result = BGP_PFXV_STATE_NOT_FOUND;
		pfx_table_free_reason(reason, reason_len);
		return PFX_SUCCESS;
	}

	unsigned int lvl = 0;
	struct trie_node *node = trie_lookup(root, prefix, prefix_len, &lvl);

	if (!node) {
		pthread_rwlock_unlock(&pfx_table->lock);
		*result = BGP_PFXV_STATE_NOT_FOUND;
		pfx_table_free_reason(reason, reason_len);
		return PFX_SUCCESS;
	}

	if (reason_len && reason) {
		*reason_len = ((struct node_data *)node->data)->len;
		*reason = lrtr_realloc(*reason, *reason_len * sizeof(struct pfx_record));
		if (!*reason) {
			pthread_rwlock_unlock(&pfx_table->lock);
			pfx_table_free_reason(reason, reason_len);
			return PFX_ERROR;
		}
		if (pfx_table_node2pfx_record(node, *reason, *reason_len) == PFX_ERROR) {
			pthread_rwlock_unlock(&pfx_table->lock);
			pfx_table_free_reason(reason, reason_len);
			return PFX_ERROR;
		}
	}

	while (!pfx_table_elem_matches(node->data, asn, prefix_len)) {
		if (lrtr_ip_addr_is_zero(lrtr_ip_addr_get_bits(
			    prefix, lvl++,
			    1))) //post-incr lvl, trie_lookup is performed on child_nodes => parent lvl + 1
			node = trie_lookup(node->lchild, prefix, prefix_len, &lvl);
		else
			node = trie_lookup(node->rchild, prefix, prefix_len, &lvl);

		if (!node) {
			pthread_rwlock_unlock(&pfx_table->lock);
			*result = BGP_PFXV_STATE_INVALID;
			return PFX_SUCCESS;
		}

		if (reason_len && reason) {
			unsigned int r_len_old = *reason_len;
			*reason_len += ((struct node_data *)node->data)->len;
			*reason = lrtr_realloc(*reason, *reason_len * sizeof(struct pfx_record));
			struct pfx_record *start = *reason + r_len_old;

			if (!*reason) {
				pthread_rwlock_unlock(&pfx_table->lock);
				pfx_table_free_reason(reason, reason_len);
				return PFX_ERROR;
			}
			if (pfx_table_node2pfx_record(node, start, ((struct node_data *)node->data)->len) ==
			    PFX_ERROR) {
				pthread_rwlock_unlock(&pfx_table->lock);
				pfx_table_free_reason(reason, reason_len);
				return PFX_ERROR;
			}
		}
	}

	pthread_rwlock_unlock(&pfx_table->lock);
	*result = BGP_PFXV_STATE_VALID;
	return PFX_SUCCESS;
}

RTRLIB_EXPORT int pfx_table_validate(struct pfx_table *pfx_table, const uint32_t asn, const struct lrtr_ip_addr *prefix,
				     const uint8_t prefix_len, enum pfxv_state *result)
{
	return pfx_table_validate_r(pfx_table, NULL, NULL, asn, prefix, prefix_len, result);
}

RTRLIB_EXPORT int pfx_table_src_remove(struct pfx_table *pfx_table, const struct rtr_socket *socket)
{
	for (unsigned int i = 0; i < 2; i++) {
		struct trie_node **root = (i == 0 ? &(pfx_table->ipv4) : &(pfx_table->ipv6));

		pthread_rwlock_wrlock(&(pfx_table->lock));
		if (*root) {
			int rtval = pfx_table_remove_id(pfx_table, root, *root, socket, 0);

			if (rtval == PFX_ERROR) {
				pthread_rwlock_unlock(&pfx_table->lock);
				return PFX_ERROR;
			}
		}
		pthread_rwlock_unlock(&pfx_table->lock);
	}
	return PFX_SUCCESS;
}

int pfx_table_remove_id(struct pfx_table *pfx_table, struct trie_node **root, struct trie_node *node,
			const struct rtr_socket *socket, const unsigned int level)
{
	assert(node);
	assert(root);
	assert(*root);
	bool check_node = true;

	while (check_node) {
		// data from removed node are replaced from data from child nodes (if children exists),
		// same node must be checked again if it was replaced with previous child node data
		struct node_data *data = node->data;

		for (unsigned int i = 0; i < data->len; i++) {
			while (data->len > i && data->ary[i].socket == socket) {
				struct pfx_record record = {data->ary[i].asn, node->prefix, node->len,
							    data->ary[i].max_len, data->ary[i].socket};
				if (pfx_table_del_elem(data, i) == PFX_ERROR)
					return PFX_ERROR;
				pfx_table_notify_clients(pfx_table, &record, false);
			}
		}
		if (data->len == 0) {
			struct trie_node *rm_node = trie_remove(node, &(node->prefix), node->len, level);

			assert(rm_node);
			assert(((struct node_data *)rm_node->data)->len == 0);
			lrtr_free(((struct node_data *)rm_node->data));
			lrtr_free(rm_node);

			if (rm_node == *root) {
				*root = NULL;
				return PFX_SUCCESS;
			} else if (rm_node == node) {
				return PFX_SUCCESS;
			}
		} else {
			check_node = false;
		}
	}

	if (node->lchild) {
		if (pfx_table_remove_id(pfx_table, root, node->lchild, socket, level + 1) == PFX_ERROR)
			return PFX_ERROR;
	}
	if (node->rchild)
		return pfx_table_remove_id(pfx_table, root, node->rchild, socket, level + 1);
	return PFX_SUCCESS;
}

static void pfx_table_for_each_rec(struct trie_node *n, pfx_for_each_fp fp, void *data)
{
	struct pfx_record pfxr;
	struct node_data *nd;

	assert(n);
	assert(fp);

	nd = (struct node_data *)n->data;
	assert(nd);

	if (n->lchild)
		pfx_table_for_each_rec(n->lchild, fp, data);

	for (unsigned int i = 0; i < nd->len; i++) {
		pfxr.asn = nd->ary[i].asn;
		pfxr.prefix = n->prefix;
		pfxr.min_len = n->len;
		pfxr.max_len = nd->ary[i].max_len;
		pfxr.socket = nd->ary[i].socket;
		fp(&pfxr, data);
	}

	if (n->rchild)
		pfx_table_for_each_rec(n->rchild, fp, data);
}

RTRLIB_EXPORT void pfx_table_for_each_ipv4_record(struct pfx_table *pfx_table, pfx_for_each_fp fp, void *data)
{
	assert(pfx_table);

	if (!pfx_table->ipv4)
		return;

	pthread_rwlock_rdlock(&(pfx_table->lock));
	pfx_table_for_each_rec(pfx_table->ipv4, fp, data);
	pthread_rwlock_unlock(&pfx_table->lock);
}

RTRLIB_EXPORT void pfx_table_for_each_ipv6_record(struct pfx_table *pfx_table, pfx_for_each_fp fp, void *data)
{
	assert(pfx_table);

	if (!pfx_table->ipv6)
		return;

	pthread_rwlock_rdlock(&(pfx_table->lock));
	pfx_table_for_each_rec(pfx_table->ipv6, fp, data);
	pthread_rwlock_unlock(&pfx_table->lock);
}

static void pfx_table_copy_cb(const struct pfx_record *record, void *data)
{
	struct copy_cb_args *args = data;

	if (record->socket != args->socket) {
		if (pfx_table_add(args->pfx_table, record) != PFX_SUCCESS)
			args->error = true;
	}
}

int pfx_table_copy_except_socket(struct pfx_table *src_table, struct pfx_table *dst_table,
				 const struct rtr_socket *socket)
{
	struct copy_cb_args args = {dst_table, socket, false};

	pfx_table_for_each_ipv4_record(src_table, pfx_table_copy_cb, &args);
	if (args.error)
		return PFX_ERROR;

	pfx_table_for_each_ipv6_record(src_table, pfx_table_copy_cb, &args);
	if (args.error)
		return PFX_ERROR;

	return PFX_SUCCESS;
}

void pfx_table_swap(struct pfx_table *a, struct pfx_table *b)
{
	struct trie_node *ipv4_tmp;
	struct trie_node *ipv6_tmp;

	pthread_rwlock_wrlock(&(a->lock));
	pthread_rwlock_wrlock(&(b->lock));

	ipv4_tmp = a->ipv4;
	ipv6_tmp = a->ipv6;

	a->ipv4 = b->ipv4;
	a->ipv6 = b->ipv6;

	b->ipv4 = ipv4_tmp;
	b->ipv6 = ipv6_tmp;

	pthread_rwlock_unlock(&(b->lock));
	pthread_rwlock_unlock(&(a->lock));
}

static void pfx_table_notify_diff_cb(const struct pfx_record *record, void *data)
{
	struct notify_diff_cb_args *args = data;

	if (args->socket == record->socket && args->added) {
		if (pfx_table_remove(args->old_table, record) != PFX_SUCCESS)
			pfx_table_notify_clients(args->new_table, record, args->added);
	} else if (args->socket == record->socket && !args->added) {
		pfx_table_notify_clients(args->new_table, record, args->added);
	}
}

void pfx_table_notify_diff(struct pfx_table *new_table, struct pfx_table *old_table, const struct rtr_socket *socket)
{
	pfx_update_fp old_table_fp;
	struct notify_diff_cb_args args = {old_table, new_table, socket, new_table->update_fp, true};

	// Disable update callback for old_table table
	old_table_fp = old_table->update_fp;
	old_table->update_fp = NULL;

	// Iterate new_table and try to delete every prefix from the given socket in old_table
	// If the prefix could not be removed it was added in new_table and the update cb must be called
	pfx_table_for_each_ipv4_record(new_table, pfx_table_notify_diff_cb, &args);
	pfx_table_for_each_ipv6_record(new_table, pfx_table_notify_diff_cb, &args);

	// Iterate over old_table table and search and remove remaining prefixes from the socket
	// issue a remove notification for every one of them, because they are not present in new_table.
	args.added = false;
	pfx_table_for_each_ipv4_record(old_table, pfx_table_notify_diff_cb, &args);
	pfx_table_for_each_ipv6_record(old_table, pfx_table_notify_diff_cb, &args);

	// Restore original state of old_tables update_fp
	old_table->update_fp = old_table_fp;
}
