/*
 * Copyright (c) 2009-2012, Vsevolod Stakhov
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY AUTHOR ''AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL AUTHOR BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "config.h"
#include "hash.h"

#define HASH_TABLE_MIN_SIZE 19
#define HASH_TABLE_MAX_SIZE 13845163

/*
 * Performs a lookup in the hash table.  Virtually all hash operations
 * will use this function internally.
 */
static inline struct rspamd_hash_node **
rspamd_hash_lookup_node (rspamd_hash_t * hash, gconstpointer key, guint * hash_return)
{
	struct rspamd_hash_node       **node_ptr, *node;
	guint                           hash_value;
	hash_value = (*hash->hash_func) (key);

	if (hash->shared) {
		memory_pool_rlock_rwlock (hash->lock);
	}
	node_ptr = &hash->nodes[hash_value % hash->size];

	if (hash_return)
		*hash_return = hash_value;

	/* Hash table lookup needs to be fast.
	 *  We therefore remove the extra conditional of testing
	 *  whether to call the key_equal_func or not from
	 *  the inner loop.
	 *
	 *  Additional optimisation: first check if our full hash
	 *  values are equal so we can avoid calling the full-blown
	 *  key equality function in most cases.
	 */
	if (hash->key_equal_func) {
		while ((node = *node_ptr)) {
			if (node->key_hash == hash_value && hash->key_equal_func (node->key, key)) {
				break;
			}
			node_ptr = &(*node_ptr)->next;
		}
	}
	else {
		while ((node = *node_ptr)) {
			if (node->key == key) {
				break;
			}
			node_ptr = &(*node_ptr)->next;
		}
	}
	if (hash->shared) {
		memory_pool_runlock_rwlock (hash->lock);
	}
	return node_ptr;
}

/*
 * Removes a node from the hash table and updates the node count.
 * No table resize is performed.
 */
static void
rspamd_hash_remove_node (rspamd_hash_t * hash, struct rspamd_hash_node ***node_ptr_ptr)
{
	struct rspamd_hash_node       **node_ptr, *node;

	if (hash->shared) {
		memory_pool_wlock_rwlock (hash->lock);
	}
	node_ptr = *node_ptr_ptr;
	node = *node_ptr;

	*node_ptr = node->next;

	hash->nnodes--;
	if (hash->shared) {
		memory_pool_wunlock_rwlock (hash->lock);
	}
}

/*
 * Resizes the hash table to the optimal size based on the number of
 * nodes currently held.
 */
static void
rspamd_hash_resize (rspamd_hash_t * hash)
{
	struct rspamd_hash_node       **new_nodes;
	struct rspamd_hash_node        *node, *next;
	guint                           hash_val;
	gint                            new_size, i;

	new_size = g_spaced_primes_closest (hash->nnodes);
	new_size = CLAMP (new_size, HASH_TABLE_MIN_SIZE, HASH_TABLE_MAX_SIZE);

	if (hash->shared) {
		new_nodes = memory_pool_alloc_shared (hash->pool, sizeof (struct rspamd_hash_node *) * new_size);
	}
	else {
		new_nodes = memory_pool_alloc (hash->pool, sizeof (struct rspamd_hash_node *) * new_size);
	}

	if (hash->shared) {
		memory_pool_wlock_rwlock (hash->lock);
	}

	for (i = 0; i < hash->size; i++) {
		for (node = hash->nodes[i]; node; node = next) {
			next = node->next;
			hash_val = node->key_hash % new_size;
			node->next = new_nodes[hash_val];
			new_nodes[hash_val] = node;
		}
	}

	hash->nodes = new_nodes;
	hash->size = new_size;

	if (hash->shared) {
		memory_pool_wunlock_rwlock (hash->lock);
	}
}

/*
 * Resizes the hash table, if needed.
 */
static inline void
rspamd_hash_maybe_resize (rspamd_hash_t * hash)
{
	gint                            nnodes = hash->nnodes;
	gint                            size = hash->size;

	if ((size >= 3 * nnodes && size > HASH_TABLE_MIN_SIZE) || (3 * size <= nnodes && size < HASH_TABLE_MAX_SIZE)) {
		rspamd_hash_resize (hash);
	}
}

/* Create new hash in specified pool */
rspamd_hash_t                  *
rspamd_hash_new (memory_pool_t * pool, GHashFunc hash_func, GEqualFunc key_equal_func)
{
	rspamd_hash_t                  *hash;

	hash = memory_pool_alloc (pool, sizeof (rspamd_hash_t));
	hash->size = HASH_TABLE_MIN_SIZE;
	hash->nnodes = 0;
	hash->hash_func = hash_func ? hash_func : g_direct_hash;
	hash->key_equal_func = key_equal_func;
	hash->nodes = memory_pool_alloc0 (pool, sizeof (struct rspamd_hash_node *) * hash->size);
	hash->shared = 0;
	hash->pool = pool;

	return hash;
}

/* 
 * Create new hash in specified pool using shared memory 
 */
rspamd_hash_t                  *
rspamd_hash_new_shared (memory_pool_t * pool, GHashFunc hash_func, GEqualFunc key_equal_func, gint size)
{
	rspamd_hash_t                  *hash;

	hash = memory_pool_alloc_shared (pool, sizeof (rspamd_hash_t));
	hash->size = size;
	hash->nnodes = 0;
	hash->hash_func = hash_func ? hash_func : g_direct_hash;
	hash->key_equal_func = key_equal_func;
	hash->nodes = memory_pool_alloc0_shared (pool, sizeof (struct rspamd_hash_node *) * hash->size);
	hash->shared = 1;
	/* Get mutex from pool for locking on insert/remove operations */
	hash->lock = memory_pool_get_rwlock (pool);
	hash->pool = pool;

	return hash;
}

/* 
 * Insert item in hash 
 */
void
rspamd_hash_insert (rspamd_hash_t * hash, gpointer key, gpointer value)
{
	struct rspamd_hash_node       **node_ptr, *node;
	guint                           key_hash;

	g_return_if_fail (hash != NULL);
	node_ptr = rspamd_hash_lookup_node (hash, key, &key_hash);

	if (hash->shared) {
		memory_pool_wlock_rwlock (hash->lock);
	}
	if ((node = *node_ptr)) {
		node->key = key;
		node->value = value;
	}
	else {
		if (hash->shared) {
			node = memory_pool_alloc_shared (hash->pool, sizeof (struct rspamd_hash_node));
		}
		else {
			node = memory_pool_alloc (hash->pool, sizeof (struct rspamd_hash_node));
		}

		node->key = key;
		node->value = value;
		node->key_hash = key_hash;
		node->next = NULL;

		*node_ptr = node;
		hash->nnodes++;
	}
	if (hash->shared) {
		memory_pool_wunlock_rwlock (hash->lock);
	}

	if (!hash->shared) {
		rspamd_hash_maybe_resize (hash);
	}
}

/* 
 * Remove item from hash 
 */
gboolean
rspamd_hash_remove (rspamd_hash_t * hash, gpointer key)
{
	struct rspamd_hash_node       **node_ptr;

	g_return_val_if_fail (hash != NULL, FALSE);

	node_ptr = rspamd_hash_lookup_node (hash, key, NULL);
	if (*node_ptr == NULL)
		return FALSE;

	rspamd_hash_remove_node (hash, &node_ptr);
	rspamd_hash_maybe_resize (hash);

	return TRUE;
}

/* 
 * Lookup item from hash 
 */
gpointer
rspamd_hash_lookup (rspamd_hash_t * hash, gpointer key)
{
	struct rspamd_hash_node        *node;
	g_return_val_if_fail (hash != NULL, NULL);

	node = *rspamd_hash_lookup_node (hash, key, NULL);

	return node ? node->value : NULL;
}

/* 
 * Iterate throught hash 
 */
void
rspamd_hash_foreach (rspamd_hash_t * hash, GHFunc func, gpointer user_data)
{
	struct rspamd_hash_node        *node;
	gint                            i;

	g_return_if_fail (hash != NULL);
	g_return_if_fail (func != NULL);

	if (hash->shared) {
		memory_pool_rlock_rwlock (hash->lock);
	}
	for (i = 0; i < hash->size; i++) {
		for (node = hash->nodes[i]; node; node = node->next) {
			(*func) (node->key, node->value, user_data);
		}
	}
	if (hash->shared) {
		memory_pool_runlock_rwlock (hash->lock);
	}
}

/**
 * LRU hashing
 */

static void
rspamd_lru_hash_destroy_node (gpointer v)
{
	rspamd_lru_element_t           *node = v;

	if (node->hash->value_destroy) {
		node->hash->value_destroy (node->data);
	}
	g_queue_delete_link (node->hash->q, node->link);
	g_slice_free1 (sizeof (rspamd_lru_element_t), node);
}

static rspamd_lru_element_t*
rspamd_lru_create_node (rspamd_lru_hash_t *hash, gpointer key, gpointer value, time_t now, guint ttl)
{
	rspamd_lru_element_t           *node;

	node = g_slice_alloc (sizeof (rspamd_lru_element_t));
	node->data = value;
	node->key = key;
	node->store_time = now;
	node->ttl = ttl;
	node->hash = hash;

	return node;
}

/**
 * Create new lru hash with GHashTable as storage
 * @param maxsize maximum elements in a hash
 * @param maxage maximum age of elemnt
 * @param hash_func pointer to hash function
 * @param key_equal_func pointer to function for comparing keys
 * @return new rspamd_hash object
 */
rspamd_lru_hash_t*
rspamd_lru_hash_new (GHashFunc hash_func, GEqualFunc key_equal_func, gint maxsize, gint maxage,
		GDestroyNotify key_destroy, GDestroyNotify value_destroy)
{
	rspamd_lru_hash_t              *new;

	new = g_malloc (sizeof (rspamd_lru_hash_t));
	new->storage = g_hash_table_new_full (hash_func, key_equal_func, key_destroy, rspamd_lru_hash_destroy_node);
	new->maxage = maxage;
	new->maxsize = maxsize;
	new->value_destroy = value_destroy;
	new->key_destroy = NULL;
	new->q = g_queue_new ();
	new->insert_func = (lru_cache_insert_func)g_hash_table_replace;
	new->lookup_func = (lru_cache_lookup_func)g_hash_table_lookup;
	new->delete_func = (lru_cache_delete_func)g_hash_table_remove;
	new->destroy_func = (lru_cache_destroy_func)g_hash_table_destroy;

	return new;
}
/**
 * Create new lru hash with custom storage
 * @param maxsize maximum elements in a hash
 * @param maxage maximum age of elemnt
 * @param hash_func pointer to hash function
 * @param key_equal_func pointer to function for comparing keys
 * @return new rspamd_hash object
 */
rspamd_lru_hash_t*
rspamd_lru_hash_new_full (GHashFunc hash_func, GEqualFunc key_equal_func,
		gint maxsize, gint maxage, GDestroyNotify key_destroy, GDestroyNotify value_destroy,
		gpointer storage, lru_cache_insert_func insert_func, lru_cache_lookup_func lookup_func,
		lru_cache_delete_func delete_func)
{
	rspamd_lru_hash_t              *new;

	new = g_malloc (sizeof (rspamd_lru_hash_t));
	new->storage = storage;
	new->maxage = maxage;
	new->maxsize = maxsize;
	new->value_destroy = value_destroy;
	new->key_destroy = key_destroy;
	new->q = g_queue_new ();
	new->insert_func = insert_func;
	new->lookup_func = lookup_func;
	new->delete_func = delete_func;
	new->destroy_func = NULL;

	return new;
}

/**
 * Lookup item from hash
 * @param hash hash object
 * @param key key to find
 * @return value of key or NULL if key is not found
 */
gpointer
rspamd_lru_hash_lookup (rspamd_lru_hash_t *hash, gpointer key, time_t now)
{
	rspamd_lru_element_t           *res;

	if ((res = hash->lookup_func (hash->storage, key)) != NULL) {
		if (res->ttl != 0) {
			if (now - res->store_time > res->ttl) {
				hash->delete_func (hash->storage, key);
				return NULL;
			}
		}
		if (hash->maxage > 0) {
			if (now - res->store_time > hash->maxage) {
				res = g_queue_peek_tail (hash->q);
				/* Expire elements from queue tail */
				while (res != NULL && now - res->store_time > hash->maxage) {
					hash->delete_func (hash->storage, res->key);
					res = g_queue_peek_tail (hash->q);
				}

				return NULL;
			}
		}
		return res->data;
	}

	return NULL;
}
/**
 * Insert item in hash
 * @param hash hash object
 * @param key key to insert
 * @param value value of key
 */
void
rspamd_lru_hash_insert (rspamd_lru_hash_t *hash, gpointer key, gpointer value,
		time_t now, guint ttl)
{
	rspamd_lru_element_t           *res;
	gint                            removed = 0;

	if ((res = hash->lookup_func (hash->storage, key)) != NULL) {
		hash->delete_func (hash->storage, res->key);
	}
	else {
		if (hash->maxsize > 0 &&
				(gint)g_queue_get_length (hash->q) >= hash->maxsize) {
			/* Expire some elements */
			res = g_queue_peek_tail (hash->q);
			if (hash->maxage > 0) {
				while (res != NULL && now - res->store_time > hash->maxage) {
					if (res->key != NULL) {
						hash->delete_func (hash->storage, res->key);
					}
					else {
						break;
					}
					res = g_queue_peek_tail (hash->q);
					removed ++;
				}
			}
			if (removed == 0) {
				/* Remove explicitly */
				if (res->key != NULL) {
					hash->delete_func (hash->storage, res->key);
				}
			}
		}
	}

	res = rspamd_lru_create_node (hash, key, value, now, ttl);
	hash->insert_func (hash->storage, key, res);
	g_queue_push_head (hash->q, res);
	res->link = g_queue_peek_head_link (hash->q);
}

void
rspamd_lru_hash_destroy (rspamd_lru_hash_t *hash)
{
	if (hash->destroy_func) {
		hash->destroy_func (hash->storage);
	}
	g_queue_free (hash->q);
	g_free (hash);
}

/*
 * vi:ts=4
 */
