/*-
 * Copyright (c) 2021 Baptiste Daroussin <bapt@FreeBSD.org>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer
 *    in this position and unchanged.
 * 2. 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 THE AUTHOR(S) ``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 THE AUTHOR(S) 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 "pkghash.h"

#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <mum.h>
#include <xmalloc.h>

#ifndef STREQ
#define STREQ(s1, s2) (strcmp(s1, s2) == 0)
#endif

struct pkghash {
	pkghash_entry *entries;
	size_t capacity;
	size_t count;
	size_t index;
};

pkghash *
pkghash_new(void)
{
	pkghash *table = xmalloc(sizeof(pkghash));
	table->count = 0;
	table->capacity = 128;

	table->entries = xcalloc(table->capacity, sizeof(pkghash_entry));
	return (table);
}

void
pkghash_destroy(pkghash *table)
{
	if (table == NULL)
		return;

	for (size_t i = 0; i < table->capacity; i++) {
		if (table->entries[i].key != NULL)
			free(table->entries[i].key);
		if (table->entries[i].free_func != NULL)
			table->entries[i].free_func(table->entries[i].value);
	}
	free(table->entries);
	free(table);
}

pkghash_entry *
pkghash_get(pkghash *table, const char *key)
{
	if (table == NULL)
		return (NULL);
	uint64_t hash = mum_hash(key, strlen(key), 0);
	size_t index = (size_t)(hash & (uint64_t)(table->capacity -1));

	while (table->entries[index].key != NULL) {
		if (STREQ(key, table->entries[index].key))
			return (&table->entries[index]);
		index++;
		if (index >= table->capacity)
			index = 0;
	}
	return (NULL);
}

void *
pkghash_get_value(pkghash *table, const char *key)
{
	pkghash_entry *e;

	e = pkghash_get(table, key);
	return (e != NULL ? e->value : NULL);

}

static bool
pkghash_set_entry(pkghash_entry *entries, size_t capacity,
    const char *key, void *value, size_t *pcount, void (*free_func)(void *)) {
	uint64_t hash = mum_hash(key, strlen(key), 0);
	size_t index = (size_t)(hash & (uint64_t)(capacity - 1));

	while (entries[index].key != NULL) {
		if (STREQ(key, entries[index].key))
			return (false);
		index++;
		if (index >= capacity)
			index = 0;
	}

	if (pcount != NULL) {
		key = xstrdup(key);
		(*pcount)++;
	}
	entries[index].key = (char *)key;
	entries[index].value = value;
	entries[index].free_func = free_func;
	return (true);
}

static bool
pkghash_expand(pkghash *table)
{
	size_t new_capacity = table->capacity * 2;
	if (new_capacity < table->capacity)
		return (false);
	pkghash_entry *new_entries = xcalloc(new_capacity, sizeof(pkghash_entry));

	for (size_t i = 0; i < table->capacity; i++) {
		pkghash_entry entry = table->entries[i];
		if (entry.key != NULL)
			pkghash_set_entry(new_entries, new_capacity, entry.key,
			    entry.value, NULL, entry.free_func);
	}

	free(table->entries);
	table->entries = new_entries;
	table->capacity = new_capacity;
	return (true);
}

bool
pkghash_add(pkghash *table, const char *key, void *value, void (*free_func)(void *))
{
	if (table->count * 2 >= table->capacity && !pkghash_expand(table))
		return (false);

	return (pkghash_set_entry(table->entries, table->capacity, key, value,
	    &table->count, free_func));
}

size_t
pkghash_count(pkghash *table)
{
	if (table == NULL)
		return (0);
	return (table->count);
}

pkghash_it
pkghash_iterator(pkghash *table)
{
	pkghash_it it = { 0 };
	it._table = table;
	return (it);
}

bool
pkghash_next(pkghash_it *it)
{
	pkghash *table = it->_table;
	if (table == NULL)
		return (false);
	if (table->count == 0)
		return (false);
	while (it->_index < table->capacity) {
		size_t i = it->_index;
		it->_index++;
		if (table->entries[i].key != NULL) {
			pkghash_entry entry = table->entries[i];
			it->key = entry.key;
			it->value = entry.value;
			return (true);
		}
	}
	return (false);
}

bool
pkghash_del(pkghash *h, const char *key)
{
	pkghash_entry *e = pkghash_get(h, key);
	if (e == NULL)
		return (false);
	free(e->key);
	e->key = NULL;
	if (e->free_func != NULL)
		e->free_func(e->value);
	h->count--;
	return (true);
}

void *
pkghash_delete(pkghash *h, const char *key)
{
	pkghash_entry *e = pkghash_get(h, key);
	if (e == NULL)
		return (NULL);
	free(e->key);
	e->key = NULL;
	h->count--;
	return (e->value);
}
