/* grecs - Gray's Extensible Configuration System
   Copyright (C) 2007-2021 Sergey Poznyakoff

   Grecs 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 3 of the License, or (at your
   option) any later version.

   Grecs 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 Grecs. If not, see <http://www.gnu.org/licenses/>. */

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif
#include <grecs.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <ctype.h>

/* |hash_size| defines a sequence of symbol table sizes. These are prime
   numbers, each of which is approximately twice its predecessor. */

static unsigned int hash_size[] = {
	7, 17, 37, 101, 229, 487, 1009, 2039, 4091, 8191, 16411
};

/* |max_rehash| keeps the number of entries in |hash_size| table. */
static unsigned int max_rehash = sizeof(hash_size) / sizeof(hash_size[0]);

enum symtab_defer_type {
	defer_add,
	defer_del
};

struct grecs_symtab {
	int flags;
	unsigned int hash_num;     /* Index to hash_size table */
	size_t elsize;             /* Size of an element */
	size_t elcount;            /* Number of elements in use */
	struct grecs_syment **tab; /* Table of element pointers */	

	/* Functions that can be overridden when creating the symtab: */
	unsigned (*hash_fun)(void *, unsigned long);
	int (*cmp_fun)(const void *, const void *);
	int (*copy_fun)(void *, void *);
	void *(*syment_alloc_fun)(size_t);
	void (*syment_free_fun) (void *);

	/* Support for deferred operations.  When an insertion or deletion
	   is requested while the symtab is being iterated over, this operation
	   is deferred until the iteration is finished.  */

	/* Iteration level.  Gets increased upon entry to grecs_symtab_foreach,
	   and decreased right before leaving it. */
	unsigned int itr_level;
	/* Two deferment lists keep track of the deferred additions and
	   removals:
	*/
	grecs_list_ptr_t defer_list[2];
};

static void
syment_free(struct grecs_symtab *st, void *ptr)
{
	if (st->syment_free_fun)
		st->syment_free_fun(ptr);
	else
		free(ptr);
}

static struct grecs_syment *
syment_alloc(struct grecs_symtab *st, void *key)
{
	struct grecs_syment *ent;
	
	ent = st->syment_alloc_fun ?
		st->syment_alloc_fun(st->elsize) : malloc(st->elsize);
	if (ent) {
		memset(ent, 0, st->elsize);
		if (st->copy_fun(ent, key)) {
			int ec = errno;
			syment_free(st, ent);
			errno = ec;
			return NULL;
		}
	}
	return ent;
}

static void
symtab_defer_op(struct grecs_symtab *st, enum symtab_defer_type type,
		struct grecs_syment *ent)
{
	if (!st->defer_list[type]) {
		st->defer_list[type] = grecs_list_create();
		st->defer_list[type]->cmp = st->cmp_fun;
	}
	grecs_list_append(st->defer_list[type], ent);
}

unsigned
grecs_hash_string(const char *name, unsigned long hashsize)
{
	unsigned i;
	
	for (i = 0; *name; name++) {
		i <<= 1;
		i ^= *(unsigned char*) name;
	}
	return i % hashsize;
}

unsigned
grecs_hash_string_ci(const char *name, unsigned long hashsize)
{
	unsigned i;
	
	for (i = 0; *name; name++) {
		i <<= 1;
		i ^= tolower (*(unsigned char*) name);
	}
	return i % hashsize;
}

static unsigned
def_hash(void *data, unsigned long hashsize)
{
	struct grecs_syment *sym = data;
	return grecs_hash_string(sym->name, hashsize);
}

static int
def_cmp(const void *a, const void *b)
{
	struct grecs_syment const *syma = a;
	struct grecs_syment const *symb = b;

	return strcmp(syma->name, symb->name);
}

static int
def_copy(void *a, void *b)
{
	struct grecs_syment *syma = a;
	struct grecs_syment *symb = b;

	syma->name = strdup(symb->name);
	return syma->name == NULL;
}

static void
def_free_fun(void *p)
{
	struct grecs_syment *sym = p;
	free(sym->name);
	free(sym);
}
		


static unsigned
symtab_insert_pos(struct grecs_symtab *st, void *elt)
{
	unsigned i;
	unsigned pos = st->hash_fun(elt, hash_size[st->hash_num]);
	
	for (i = pos; st->tab[i];) {
		if (++i >= hash_size[st->hash_num])
			i = 0;
		if (i == pos)
			/* FIXME: Error message? */
			abort();
	}
	return i;
}

int
grecs_symtab_replace(struct grecs_symtab *st, void *ent, void **old_ent)
{
	struct grecs_syment *entry;
	unsigned i, pos = st->hash_fun(ent, hash_size[st->hash_num]);
	for (i = pos; (entry = st->tab[i]);) {
		if (st->cmp_fun(entry, ent) == 0)
			break;
		if (++i >= hash_size[st->hash_num])
			i = 0;
		if (i == pos)
			return ENOENT;
	}
	if (old_ent)
		*old_ent = entry;
	st->tab[i] = ent;
	return 0;
}

static int
symtab_rehash(struct grecs_symtab *st)
{
	struct grecs_syment **old_tab = st->tab;
	struct grecs_syment **new_tab;
	unsigned int i;
	unsigned int hash_num = st->hash_num + 1;
	
	if (hash_num >= max_rehash)
		return E2BIG;

	new_tab = calloc(hash_size[hash_num], sizeof(*new_tab));
	if (!new_tab)
		return ENOMEM;
	st->tab = new_tab;
	if (old_tab) {
		st->hash_num = hash_num;
		for (i = 0; i < hash_size[hash_num-1]; i++) {
			struct grecs_syment *elt = old_tab[i];
			if (elt->name) {
				unsigned n = symtab_insert_pos(st, elt);
				new_tab[n] = elt;
			}
		}
		free(old_tab);
	}
	return 0;
}

const char *
grecs_symtab_strerror(int rc)
{
	switch (rc) {
	case ENOENT:
		return _("element not found in table");
	case E2BIG:
		return _("symbol table is full");
	case ENOMEM:
		return strerror(ENOMEM);
	}
	return strerror(rc);
}

int
grecs_symtab_remove(struct grecs_symtab *st, void *elt)
{
	unsigned int pos, i, j, r;
	struct grecs_syment *entry;
	
	if (st->itr_level) {
		if (grecs_list_remove(st->defer_list[defer_add], elt) == 0)
			return 0;
	}
	
	pos = st->hash_fun(elt, hash_size[st->hash_num]);
	for (i = pos; (entry = st->tab[i]);) {
		if (st->cmp_fun(entry, elt) == 0)
			break;
		if (++i >= hash_size[st->hash_num])
			i = 0;
		if (i == pos)
			return ENOENT;
	}
	
	if (!entry)
		return ENOENT;

	if (st->itr_level) {
		symtab_defer_op(st, defer_del, entry);
		//FIXME: mark the entry as deleted
		return 0;
	}
	
	syment_free(st, entry);
	st->elcount--;

	for (;;) {
		st->tab[i] = NULL;
		j = i;

		do {
			if (++i >= hash_size[st->hash_num])
				i = 0;
			if (!st->tab[i])
				return 0;
			r = st->hash_fun(st->tab[i], hash_size[st->hash_num]);
		}
		while ((j < r && r <= i)
		       || (i < j && j < r) || (r <= i && i < j));
		st->tab[j] = st->tab[i];
	}
	return 0;
}

int
grecs_symtab_get_index(unsigned *idx, struct grecs_symtab *st,
		       void *key, int *install)
{
	int rc;
	unsigned i, pos;
	struct grecs_syment *elem;
  
	if (!st->tab) {
		if (install) {
			rc = symtab_rehash(st);
			if (rc)
				return rc;
		} else
			return ENOENT;
	}

	pos = st->hash_fun(key, hash_size[st->hash_num]);

	for (i = pos; (elem = st->tab[i]);) {
		if (st->cmp_fun(elem, key) == 0) {
			if (install)
				*install = 0;
			*idx = i; 
			return 0;
		}
      
		if (++i >= hash_size[st->hash_num])
			i = 0;
		if (i == pos)
			break;
	}

	if (!install)
		return ENOENT;
  
	if (!elem) {
		*install = 1;
		*idx = i;
		return 0;
	}

	if ((rc = symtab_rehash(st)) != 0)
		return rc;

	return grecs_symtab_get_index(idx, st, key, install);
}

void *
grecs_symtab_lookup_or_install(struct grecs_symtab *st, void *key,
			       int *install)
{
	unsigned i;
	int rc = grecs_symtab_get_index(&i, st, key, install);
	if (rc == 0) {
		if (install && *install == 1) {
			struct grecs_syment *ent = syment_alloc(st, key);
			if (!ent) {
				errno = ENOMEM;
				return NULL;
			}
			if (st->itr_level) {
				symtab_defer_op(st, defer_add, ent);
			} else {
				st->tab[i] = ent;
				st->elcount++;
			}
			return ent;
		} else
			return st->tab[i];
	} else if (rc == ENOENT && st->itr_level) {
		void *ptr = grecs_list_locate(st->defer_list[defer_add], key);
		if (ptr)
			return ptr;
		rc = ENOENT;
	}
	errno = rc;
	return NULL;
}

void
grecs_symtab_clear(struct grecs_symtab *st)
{
	unsigned i, hs;
  
	if (!st || !st->tab)
		return;

	hs = hash_size[st->hash_num];
	for (i = 0; i < hs; i++) {
		struct grecs_syment *elem = st->tab[i];
		if (elem) {
			syment_free(st, elem);
			st->tab[i] = NULL;
		}
	}
	st->elcount = 0;	
}

struct grecs_symtab *
grecs_symtab_create(size_t elsize, 
		    unsigned (*hash_fun)(void *, unsigned long),
		    int (*cmp_fun)(const void *, const void *),
		    int (*copy_fun)(void *, void *),
		    void *(*alloc_fun)(size_t), void (*free_fun)(void *))
{
	struct grecs_symtab *st = malloc(sizeof(*st));
	if (st) {
		memset(st, 0, sizeof(*st));
		st->elsize = elsize;
		st->elcount = 0;
		st->hash_fun = hash_fun ? hash_fun : def_hash;
		st->cmp_fun = cmp_fun ? cmp_fun : def_cmp;
		st->copy_fun = copy_fun ? copy_fun : def_copy;
		st->syment_alloc_fun = alloc_fun;
		if (free_fun)
			st->syment_free_fun = free_fun;
		else if (!copy_fun)
			st->syment_free_fun = def_free_fun;
		else
			st->syment_free_fun = NULL;
		st->tab = calloc(hash_size[st->hash_num], sizeof(*st->tab));
		if (!st->tab) {
			free(st);
			st = NULL;
		}
	}
	return st;
}

struct grecs_symtab *
grecs_symtab_create_default(size_t elsize)
{
	return grecs_symtab_create(elsize,
				   NULL, NULL, NULL, NULL, NULL);
}

void
grecs_symtab_free(struct grecs_symtab *st)
{
	if (st) {
		grecs_symtab_clear(st);
		grecs_list_free(st->defer_list[defer_add]);
		grecs_list_free(st->defer_list[defer_del]);
		free(st->tab);
		free(st);
	}
}

size_t
grecs_symtab_count(struct grecs_symtab *st)
{
	return st->elcount;
}

int
grecs_symtab_foreach(struct grecs_symtab *st, grecs_symtab_enumerator_t fun,
		     void *data)
{
	unsigned i;
	struct grecs_syment *ep;
	
	if (!st)
		return 0;

	++st->itr_level;

	for (i = 0; i < hash_size[st->hash_num]; i++) {
		ep = st->tab[i];
		if (ep && !grecs_list_locate(st->defer_list[defer_del], ep)) {
			int rc = fun(ep, data);
			if (rc)
				return rc;
		}
	}

	if (--st->itr_level == 0) {
		while ((ep = grecs_list_pop(st->defer_list[defer_del])) != NULL)
			grecs_symtab_remove(st, ep);
		
		while ((ep = grecs_list_pop(st->defer_list[defer_add])) != NULL) {
			int install = 1;
			if (grecs_symtab_get_index(&i, st, ep,
						   &install) == 0) {
				st->tab[i] = ep;
				st->elcount++;
			}
		}
	}
	
	return 0;
}








   
