#include "rtpengine.h"
#include "rtpengine_hash.h"

#include "../../core/str.h"
#include "../../core/dprint.h"
#include "../../core/mem/shm_mem.h"
#include "../../core/timer.h"


static struct rtpengine_hash_table *rtpengine_hash_table;

/* from sipwise rtpengine */
static unsigned int str_hash(str s)
{
	unsigned int ret = 5381;
	str it = s;

	while(it.len > 0) {
		ret = (ret << 5) + ret + *it.s;
		it.s++;
		it.len--;
	}

	return ret % rtpengine_hash_table->size;
}

/* rtpengine hash API */
int rtpengine_hash_table_init(int size)
{
	int i;
	int hash_table_size;

	// init hash table size
	if(size < 1) {
		hash_table_size = 1;
	} else {
		hash_table_size = size;
	}
	LM_DBG("rtpengine_hash_table size = %d\n", hash_table_size);

	// init hashtable
	rtpengine_hash_table = shm_malloc(sizeof(struct rtpengine_hash_table));
	if(!rtpengine_hash_table) {
		LM_ERR("no shm left to create rtpengine_hash_table\n");
		return 0;
	}
	memset(rtpengine_hash_table, 0, sizeof(struct rtpengine_hash_table));
	rtpengine_hash_table->size = hash_table_size;

	// init hashtable row_locks
	rtpengine_hash_table->row_locks =
			shm_malloc(hash_table_size * sizeof(gen_lock_t));
	if(!rtpengine_hash_table->row_locks) {
		LM_ERR("no shm left to create rtpengine_hash_table->row_locks\n");
		rtpengine_hash_table_destroy();
		return 0;
	}
	memset((void *)rtpengine_hash_table->row_locks, 0,
			hash_table_size * sizeof(gen_lock_t));

	// init hashtable row_entry_list
	rtpengine_hash_table->row_entry_list = shm_malloc(
			rtpengine_hash_table->size * sizeof(struct rtpengine_hash_entry *));
	if(!rtpengine_hash_table->row_entry_list) {
		LM_ERR("no shm left to create rtpengine_hash_table->row_entry_list\n");
		rtpengine_hash_table_destroy();
		return 0;
	}
	memset(rtpengine_hash_table->row_entry_list, 0,
			rtpengine_hash_table->size * sizeof(struct rtpengine_hash_entry *));

	// init hashtable row_totals
	rtpengine_hash_table->row_totals =
			shm_malloc(hash_table_size * sizeof(unsigned int));
	if(!rtpengine_hash_table->row_totals) {
		LM_ERR("no shm left to create rtpengine_hash_table->row_totals\n");
		rtpengine_hash_table_destroy();
		return 0;
	}
	memset(rtpengine_hash_table->row_totals, 0,
			hash_table_size * sizeof(unsigned int));

	// init hashtable row_locks[i]
	for(i = 0; i < hash_table_size; i++) {
		// init hashtable row_locks[i]
		if(!lock_init(&rtpengine_hash_table->row_locks[i])) {
			LM_ERR("fail to init rtpengine_hash_table->row_locks[%d]\n", i);
			rtpengine_hash_table_destroy();
			return 0;
		}
	}

	return 1;
}

int rtpengine_hash_table_destroy()
{
	int i;

	// check rtpengine hashtable
	if(!rtpengine_hash_table) {
		LM_ERR("NULL rtpengine_hash_table\n");
		return 1;
	}

	// check rtpengine hashtable->row_locks
	if(!rtpengine_hash_table->row_locks) {
		LM_ERR("NULL rtpengine_hash_table->row_locks\n");
		shm_free(rtpengine_hash_table);
		rtpengine_hash_table = NULL;
		return 1;
	}

	// destroy hashtable content
	for(i = 0; i < rtpengine_hash_table->size; i++) {
		// check rtpengine hashtable->row_entry_list
		if(rtpengine_hash_table->row_entry_list) {
			// destroy hashtable row_entry_list[i]
			rtpengine_hash_table_free_row_entry_list(
					rtpengine_hash_table->row_entry_list[i]);
			rtpengine_hash_table->row_entry_list[i] = NULL;
		}

		lock_destroy(&rtpengine_hash_table->row_locks[i]);
	}

	// destroy hashtable row_entry_list
	if(!rtpengine_hash_table->row_entry_list) {
		LM_ERR("NULL rtpengine_hash_table->row_entry_list\n");
	} else {
		shm_free(rtpengine_hash_table->row_entry_list);
		rtpengine_hash_table->row_entry_list = NULL;
	}

	// destroy hashtable row_totals
	if(!rtpengine_hash_table->row_totals) {
		LM_ERR("NULL rtpengine_hash_table->row_totals\n");
	} else {
		shm_free(rtpengine_hash_table->row_totals);
		rtpengine_hash_table->row_totals = NULL;
	}

	// destroy hashtable row_locks
	shm_free((void *)rtpengine_hash_table->row_locks);
	rtpengine_hash_table->row_locks = NULL;

	// destroy hashtable
	if(!rtpengine_hash_table) {
		// should not be the case; just for code symmetry
		LM_ERR("NULL rtpengine_hash_table\n");
	} else {
		shm_free(rtpengine_hash_table);
		rtpengine_hash_table = NULL;
	}

	return 1;
}

int rtpengine_hash_table_insert(
		str callid, str viabranch, struct rtpengine_hash_entry *value)
{
	struct rtpengine_hash_entry *entry, **last_next;
	struct rtpengine_hash_entry *new_entry =
			(struct rtpengine_hash_entry *)value;
	unsigned int hash_index;

	// sanity checks
	if(!rtpengine_hash_table_sanity_checks()) {
		LM_ERR("sanity checks failed\n");
		return 0;
	}

	// get entry list
	hash_index = str_hash(callid);

	// lock
	lock_get(&rtpengine_hash_table->row_locks[hash_index]);

	last_next = &rtpengine_hash_table->row_entry_list[hash_index];

	while((entry = *last_next)) {
		// if found, don't add new entry
		if(STR_EQ(entry->callid, new_entry->callid)
				&& STR_EQ(entry->viabranch, new_entry->viabranch)) {
			// unlock
			lock_release(&rtpengine_hash_table->row_locks[hash_index]);

			// entry may have been freed by a concurrent DELETE operation;
			// use function parameters which are guaranteed to be valid
			LM_NOTICE("callid=%.*s, viabranch=%.*s already in hashtable, "
					  "ignore new value\n",
					callid.len, callid.s, viabranch.len, viabranch.s);
			return 0;
		}

		// if expired entry discovered, delete it
		if(entry->tout < get_ticks()) {
			// set pointers; exclude entry
			*last_next = entry->next;

			// free current entry; entry points to unknown
			rtpengine_hash_table_free_entry(entry);

			// update total
			rtpengine_hash_table->row_totals[hash_index]--;

			continue;
		}

		// next entry in the list
		last_next = &entry->next;
	}

	*last_next = new_entry;

	// update total
	rtpengine_hash_table->row_totals[hash_index]++;

	// unlock
	lock_release(&rtpengine_hash_table->row_locks[hash_index]);

	return 1;
}

int rtpengine_hash_table_remove(
		str callid, str viabranch, enum rtpe_operation op)
{
	struct rtpengine_hash_entry *entry, **last_next;
	unsigned int hash_index;
	int found = 0;

	// sanity checks
	if(!rtpengine_hash_table_sanity_checks()) {
		LM_ERR("sanity checks failed\n");
		return 0;
	}

	hash_index = str_hash(callid);

	// lock
	lock_get(&rtpengine_hash_table->row_locks[hash_index]);

	// get first entry from entry list
	last_next = &rtpengine_hash_table->row_entry_list[hash_index];

	while((entry = *last_next)) {
		// if callid found, delete entry
		if((STR_EQ(entry->callid, callid)
				   && STR_EQ(entry->viabranch, viabranch))
				|| (STR_EQ(entry->callid, callid) && viabranch.len == 0
						&& op == OP_DELETE)) {
			// set pointers; exclude entry
			*last_next = entry->next;

			// free current entry; entry points to unknown
			rtpengine_hash_table_free_entry(entry);

			// update total
			rtpengine_hash_table->row_totals[hash_index]--;

			found = 1;

			if(!(viabranch.len == 0 && op == OP_DELETE)) {
				// unlock
				lock_release(&rtpengine_hash_table->row_locks[hash_index]);
				return found;
			}

			// try to also delete other viabranch entries for callid
			continue;
		}

		// if expired entry discovered, delete it
		if(entry->tout < get_ticks()) {
			// set pointers; exclude entry
			*last_next = entry->next;

			// free current entry; entry points to unknown
			rtpengine_hash_table_free_entry(entry);

			// update total
			rtpengine_hash_table->row_totals[hash_index]--;

			continue;
		}

		last_next = &entry->next;
	}

	// unlock
	lock_release(&rtpengine_hash_table->row_locks[hash_index]);

	return found;
}

struct rtpp_node *rtpengine_hash_table_lookup(
		str callid, str viabranch, enum rtpe_operation op)
{
	struct rtpengine_hash_entry *entry, **last_next;
	unsigned int hash_index;
	struct rtpp_node *node;

	// sanity checks
	if(!rtpengine_hash_table_sanity_checks()) {
		LM_ERR("sanity checks failed\n");
		return 0;
	}

	hash_index = str_hash(callid);

	// lock
	lock_get(&rtpengine_hash_table->row_locks[hash_index]);

	// get first entry from entry list
	last_next = &rtpengine_hash_table->row_entry_list[hash_index];

	while((entry = *last_next)) {
		// if callid found, return entry
		if((STR_EQ(entry->callid, callid)
				   && STR_EQ(entry->viabranch, viabranch))
				|| (STR_EQ(entry->callid, callid) && viabranch.len == 0
						&& op == OP_DELETE)) {
			node = entry->node;

			// unlock
			lock_release(&rtpengine_hash_table->row_locks[hash_index]);

			return node;
		}

		// if expired entry discovered, delete it
		if(entry->tout < get_ticks()) {
			// set pointers; exclude entry
			*last_next = entry->next;

			// free current entry; entry points to unknown
			rtpengine_hash_table_free_entry(entry);

			// update total
			rtpengine_hash_table->row_totals[hash_index]--;

			continue;
		}

		last_next = &entry->next;
	}

	// unlock
	lock_release(&rtpengine_hash_table->row_locks[hash_index]);

	return NULL;
}

// print hash table entries while deleting expired entries
void rtpengine_hash_table_print()
{
	int i;
	struct rtpengine_hash_entry *entry, **last_next;

	// sanity checks
	if(!rtpengine_hash_table_sanity_checks()) {
		LM_ERR("sanity checks failed\n");
		return;
	}

	// print hashtable
	for(i = 0; i < rtpengine_hash_table->size; i++) {
		// lock
		lock_get(&rtpengine_hash_table->row_locks[i]);

		last_next = &rtpengine_hash_table->row_entry_list[i];

		while((entry = *last_next)) {
			// if expired entry discovered, delete it
			if(entry->tout < get_ticks()) {
				// set pointers; exclude entry
				*last_next = entry->next;

				// free current entry; entry points to unknown
				rtpengine_hash_table_free_entry(entry);

				// update total
				rtpengine_hash_table->row_totals[i]--;

				continue;
			} else {
				LM_DBG("hash_index=%d callid=%.*s tout=%u\n", i,
						entry->callid.len, entry->callid.s,
						entry->tout - get_ticks());
			}

			last_next = &entry->next;
		}

		// unlock
		lock_release(&rtpengine_hash_table->row_locks[i]);
	}
}

unsigned int rtpengine_hash_table_total()
{
	int i;
	unsigned int total = 0;

	// sanity checks
	if(!rtpengine_hash_table_sanity_checks()) {
		LM_ERR("sanity checks failed\n");
		return 0;
	}

	for(i = 0; i < rtpengine_hash_table->size; i++) {
		total += rtpengine_hash_table->row_totals[i];
	}

	return total;
}

void rtpengine_hash_table_free_entry(struct rtpengine_hash_entry *entry)
{
	if(!entry) {
		LM_ERR("try to free a NULL entry\n");
		return;
	}

	// free callid
	if(entry->callid.s) {
		shm_free(entry->callid.s);
	}

	// free viabranch
	if(entry->viabranch.s) {
		shm_free(entry->viabranch.s);
	}

	// free entry
	shm_free(entry);

	return;
}

void rtpengine_hash_table_free_row_entry_list(
		struct rtpengine_hash_entry *row_entry_list)
{
	struct rtpengine_hash_entry *entry, *last_entry;

	entry = row_entry_list;
	while(entry) {
		last_entry = entry;
		entry = entry->next;
		rtpengine_hash_table_free_entry(last_entry);
		last_entry = NULL;
	}

	return;
}

int rtpengine_hash_table_sanity_checks()
{
	// check rtpengine hashtable
	if(!rtpengine_hash_table) {
		LM_ERR("NULL rtpengine_hash_table\n");
		return 0;
	}

	// check rtpengine hashtable->row_locks
	if(!rtpengine_hash_table->row_locks) {
		LM_ERR("NULL rtpengine_hash_table->row_locks\n");
		return 0;
	}

	// check rtpengine hashtable->row_entry_list
	if(!rtpengine_hash_table->row_entry_list) {
		LM_ERR("NULL rtpengine_hash_table->row_entry_list\n");
		return 0;
	}

	// check rtpengine hashtable->row_totals
	if(!rtpengine_hash_table->row_totals) {
		LM_ERR("NULL rtpengine_hash_table->row_totals\n");
		return 0;
	}

	return 1;
}
