/*
 * Copyright (c) 2016 MariaDB Corporation Ab
 *
 * Use of this software is governed by the Business Source License included
 * in the LICENSE.TXT file and at www.mariadb.com/bsl11.
 *
 * Change Date: 2020-01-01
 *
 * On the date above, in accordance with the Business Source License, use
 * of this software will be governed by version 2 or later of the General
 * Public License.
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <maxscale/alloc.h>
#include <maxscale/atomic.h>
#include <maxscale/hashtable.h>

/**
 * @file hashtable.c General purpose hashtable routines
 *
 * The hashtable can be create with a custom number of hash buckets,
 * a hash function and optional functions to call make copies of the key
 * and value and to free them.
 *
 * The hashtable is arrange as a set of linked lists, the number of linked
 * lists being the hashsize as requested by the user. Entries are hashed by
 * calling the hash function that is passed in by the user, this is used as
 * an index into the array of linked lists, usign modulo hashsize.
 *
 * The linked lists are searched using the key comparison function that is
 * passed into the hash table creation routine.
 *
 * By default the hash table keeps the original pointers that are passed in
 * for the keys and values, however two functions can be supplied to copy these
 * a copy function and a free function. Please note the same function is used for
 * the key and the value, if the actions required are different the called functions
 * must understand how to differenate the key and value.
 *
 * The hash table implements a single write, multiple reader locking policy by
 * using a pair of counters and a spinlock. The spinlock is used to protect the
 * number of readers and writers counters when taking out locks. Releasing of
 * locks uses pure atomic actions and thus does not require spinlock protection.
 *
 * @verbatim
 * Revision History
 *
 * Date         Who                     Description
 * 23/06/2013   Mark Riddoch            Initial implementation
 * 23/07/2013   Mark Riddoch            Addition of hashtable iterator
 * 08/01/2014   Massimiliano Pinto      Added copy and free funtion pointers for keys and values:
 *                                      it's possible to copy and free different data types via
 *                                      kcopyfn/kfreefn, vcopyfn/vfreefn
 * 06/02/2015   Mark Riddoch            Addition of hashtable_save and hashtable_load
 *
 * @endverbatim
 */

static  void hashtable_read_lock(HASHTABLE *table);
static  void hashtable_read_unlock(HASHTABLE *table);
static  void hashtable_write_lock(HASHTABLE *table);
static  void hashtable_write_unlock(HASHTABLE *table);
static HASHTABLE *hashtable_alloc_real(HASHTABLE* target,
                                       int size,
                                       HASHHASHFN hashfn,
                                       HASHCMPFN cmpfn);

/**
 * Special identity function used as default key/value copy function in the hashtable
 * implementation. This avoids having to special case the code that manipulates
 * the keys and values
 *
 * @param       data    The data pointer
 * @return      Return the value we were called with
 */
static void *
identityfn(const void *data)
{
    return (void*)data;
}

/**
 * Special null function used as default free function in the hashtable
 * implementation. This avoids having to special case the code that manipulates
 * the keys and values
 *
 * @param       data    The data pointer
 */
static void
nullfn(void *data)
{
}

/**
 * Allocate a new hash table.
 *
 * The hashtable must have a size of at least one, however to be of any
 * practical use a larger size sould be chosen as the size relates to the number
 * of has buckets in the table.
 *
 * @param size          The size of the hash table, msut be > 0
 * @param hashfn        The user supplied hash function
 * @param cmpfn         The user supplied key comparison function
 * @return The hashtable table
 */
HASHTABLE *
hashtable_alloc(int size, HASHHASHFN hashfn, HASHCMPFN cmpfn)
{
    return hashtable_alloc_real(NULL, size, hashfn, cmpfn);
}

HASHTABLE* hashtable_alloc_flat(HASHTABLE* target,
                                int size,
                                HASHHASHFN hashfn,
                                HASHCMPFN cmpfn)
{
    return hashtable_alloc_real(target, size, hashfn, cmpfn);
}

static HASHTABLE *
hashtable_alloc_real(HASHTABLE* target,
                     int        size,
                     HASHHASHFN hashfn,
                     HASHCMPFN  cmpfn)
{
    HASHTABLE *rval;

    if (target == NULL)
    {
        if ((rval = (HASHTABLE*)MXS_MALLOC(sizeof(HASHTABLE))) == NULL)
        {
            return NULL;
        }
        rval->ht_isflat = false;
    }
    else
    {
        rval = target;
        rval->ht_isflat = true;
    }

#if defined(SS_DEBUG)
    rval->ht_chk_top = CHK_NUM_HASHTABLE;
    rval->ht_chk_tail = CHK_NUM_HASHTABLE;
#endif
    rval->hashsize = size > 0 ? size : 1;
    rval->hashfn = hashfn;
    rval->cmpfn = cmpfn;
    rval->kcopyfn = identityfn;
    rval->vcopyfn = identityfn;
    rval->kfreefn = nullfn;
    rval->vfreefn = nullfn;
    rval->n_readers = 0;
    rval->writelock = 0;
    rval->n_elements = 0;
    spinlock_init(&rval->spin);
    if ((rval->entries = (HASHENTRIES **)MXS_CALLOC(rval->hashsize, sizeof(HASHENTRIES *))) == NULL)
    {
        MXS_FREE(rval);
        return NULL;
    }
    memset(rval->entries, 0, rval->hashsize * sizeof(HASHENTRIES *));

    return rval;
}

/**
 * Delete an entire hash table
 *
 * @param       table   The hash table to delete
 */
void
hashtable_free(HASHTABLE *table)
{
    int i;
    HASHENTRIES *entry, *ptr;

    if (table == NULL)
    {
        return;
    }

    hashtable_write_lock(table);
    for (i = 0; i < table->hashsize; i++)
    {
        entry = table->entries[i];
        while (entry)
        {
            ptr = entry->next;
            table->kfreefn(entry->key);
            table->vfreefn(entry->value);
            MXS_FREE(entry);
            entry = ptr;
        }
    }
    MXS_FREE(table->entries);

    hashtable_write_unlock(table);
    if (!table->ht_isflat)
    {
        MXS_FREE(table);
    }
}

/**
 * Provide memory management functions to the hash table. This allows
 * function pointers to be registered that can make copies of the
 * key and value and free them as well.
 *
 * @param table         The hash table
 * @param kcopyfn       The copy function for the key
 * @param vcopyfn       The copy function for the value
 * @param kfreefn       The free function for the key
 * @param vfreefn       The free function for the value
 */
void
hashtable_memory_fns(HASHTABLE  *table,
                     HASHCOPYFN kcopyfn,
                     HASHCOPYFN vcopyfn,
                     HASHFREEFN kfreefn,
                     HASHFREEFN vfreefn)
{
    if (kcopyfn != NULL)
    {
        table->kcopyfn = kcopyfn;
    }
    if (vcopyfn != NULL)
    {
        table->vcopyfn = vcopyfn;
    }
    if (kfreefn != NULL)
    {
        table->kfreefn = kfreefn;
    }
    if (vfreefn != NULL)
    {
        table->vfreefn = vfreefn;
    }
}

/**
 * Add an item to the hash table.
 *
 * @param table         The hash table to which to add the item
 * @param key           The key of the item
 * @param value         The value for the item
 * @return      Return the number of items added
 */
int
hashtable_add(HASHTABLE *table, void *key, void *value)
{
    unsigned int            hashkey;
    HASHENTRIES     *entry;

    if (table == NULL || key == NULL || value == NULL)
    {
        return 0;
    }

    if (table->hashsize <= 0)
    {
        return 0;
    }
    else
    {
        hashkey = table->hashfn(key) % table->hashsize;
    }
    hashtable_write_lock(table);
    entry = table->entries[hashkey % table->hashsize];
    while (entry && table->cmpfn(key, entry->key) != 0)
    {
        entry = entry->next;
    }
    if (entry && table->cmpfn(key, entry->key) == 0)
    {
        /* Duplicate key value */
        hashtable_write_unlock(table);
        return 0;
    }
    else
    {
        HASHENTRIES *ptr = (HASHENTRIES *)MXS_MALLOC(sizeof(HASHENTRIES));
        if (ptr == NULL)
        {
            hashtable_write_unlock(table);
            return 0;
        }

        /* copy the key */
        ptr->key = table->kcopyfn(key);

        /* check succesfull key copy */
        if (ptr->key  == NULL)
        {
            MXS_FREE(ptr);
            hashtable_write_unlock(table);

            return 0;
        }

        /* copy the value */
        ptr->value = table->vcopyfn(value);

        /* check succesfull value copy */
        if  (ptr->value == NULL)
        {
            /* remove the key ! */
            table->kfreefn(ptr->key);
            MXS_FREE(ptr);

            /* value not copied, return */
            hashtable_write_unlock(table);

            return 0;
        }

        ptr->next = table->entries[hashkey % table->hashsize];
        table->entries[hashkey % table->hashsize] = ptr;
    }
    table->n_elements++;
    hashtable_write_unlock(table);

    return 1;
}

/**
 * Delete an item from the hash table that has a given key
 *
 * @param table         The hash table to delete from
 * @param key           The key value of the item to remove
 * @return Return the number of items deleted
 */
int
hashtable_delete(HASHTABLE *table, void *key)
{
    unsigned int hashkey;
    HASHENTRIES *entry, *ptr;

    if (table == NULL || key == NULL)
    {
        return 0;
    }

    hashkey = table->hashfn(key) % table->hashsize;
    hashtable_write_lock(table);
    entry = table->entries[hashkey % table->hashsize];
    while (entry && entry->key && table->cmpfn(key, entry->key) != 0)
    {
        entry = entry->next;
    }
    if (entry == NULL)
    {
        /* Not found */
        hashtable_write_unlock(table);
        return 0;
    }

    if (entry == table->entries[hashkey % table->hashsize])
    {
        /* We are removing from the first entry */
        table->entries[hashkey % table->hashsize] = entry->next;
        table->kfreefn(entry->key);
        table->vfreefn(entry->value);

        if (entry->next != NULL)
        {
            entry->key = entry->next->key;
            entry->value = entry->next->value;
        }
        else
        {
            entry->key = NULL;
            entry->value = NULL;
        }
        MXS_FREE(entry);
    }
    else
    {
        ptr = table->entries[hashkey % table->hashsize];
        while (ptr && ptr->next != entry)
        {
            ptr = ptr->next;
        }
        if (ptr == NULL)
        {
            hashtable_write_unlock(table);
            return 0;       /* This should never happen */
        }
        ptr->next = entry->next;
        table->kfreefn(entry->key);
        table->vfreefn(entry->value);
        MXS_FREE(entry);
    }
    table->n_elements--;
    assert(table->n_elements >= 0);
    hashtable_write_unlock(table);
    return 1;
}

/**
 * Fetch an item with a given key value from the hash table
 *
 * @param table         The hash table
 * @param key           The key value
 * @return The item or NULL if the item was not found
 */
void *
hashtable_fetch(HASHTABLE *table, void *key)
{
    unsigned int hashkey;
    HASHENTRIES *entry;

    if (table == NULL || key == NULL || 0 == table->hashsize)
    {
        return NULL;
    }

    hashkey = table->hashfn(key) % table->hashsize;
    hashtable_read_lock(table);
    entry = table->entries[hashkey % table->hashsize];
    while (entry && entry->key && table->cmpfn(key, entry->key) != 0)
    {
        entry = entry->next;
    }
    if (entry == NULL)
    {
        hashtable_read_unlock(table);
        return NULL;
    }
    else
    {
        hashtable_read_unlock(table);
        return entry->value;
    }
}

/**
 * Print hash table statistics to the standard output
 *
 * @param table         The hash table
 */
void
hashtable_stats(HASHTABLE *table)
{
    int total, longest, i, j;
    HASHENTRIES *entries;

    if (table == NULL)
    {
        return;
    }

    printf("Hashtable: %p, size %d\n", table, table->hashsize);
    total = 0;
    longest = 0;
    hashtable_read_lock(table);
    for (i = 0; i < table->hashsize; i++)
    {
        j = 0;
        entries = table->entries[i];
        while (entries)
        {
            j++;
            entries = entries->next;
        }
        total += j;
        if (j > longest)
        {
            longest = j;
        }
    }
    hashtable_read_unlock(table);
    printf("\tNo. of entries:       %d\n", total);
    printf("\tAverage chain length: %.1f\n", (float)total / table->hashsize);
    printf("\tLongest chain length: %d\n", longest);
}

/**
 * Produces stat output about hashtable
 *
 * Parameters:
 * @param table - <usage>
 *          <description>
 *
 * @param hashsize - <usage>
 *          <description>
 *
 * @param nelems - <usage>
 *          <description>
 *
 * @param longest - <usage>
 *          <description>
 *
 * @return void
 *
 *
 */
void hashtable_get_stats(void* table,
                         int*  hashsize,
                         int*  nelems,
                         int*  longest)
{
    HASHTABLE* ht;
    HASHENTRIES* entries;
    int i;
    int j;

    *nelems = 0;
    *longest = 0;
    *hashsize = 0;

    if (table != NULL)
    {
        ht = (HASHTABLE *)table;
        CHK_HASHTABLE(ht);
        hashtable_read_lock(ht);

        for (i = 0; i < ht->hashsize; i++)
        {
            j = 0;
            entries = ht->entries[i];
            while (entries)
            {
                j++;
                entries = entries->next;
            }
            *nelems += j;
            if (j > *longest)
            {
                *longest = j;
            }
        }
        *hashsize = ht->hashsize;
        hashtable_read_unlock(ht);
    }
}


/**
 * Take a read lock on the hashtable.
 *
 * The hashtable support multiple readers and a single writer,
 * we have a spinlock to protect the two counts, n_readers and
 * writelock.
 *
 * We take the hashtable spinlock and then check that writelock
 * is set to zero. If not we release the spinlock and do dirty
 * reads of writelock until it goes to 0. Once it is zero we
 * acquire the spinlock again and test that writelock is still
 * 0.
 *
 * With writelock set to zero we increment n_readers with the
 * spinlock still held.
 *
 * @param table         The hashtable to lock.
 */
static void
hashtable_read_lock(HASHTABLE *table)
{
    spinlock_acquire(&table->spin);
    while (table->writelock)
    {
        spinlock_release(&table->spin);
        while (atomic_add(&table->writelock, 1) != 0)
        {
            atomic_add(&table->writelock, -1);
        }
        atomic_add(&table->writelock, -1);
        spinlock_acquire(&table->spin);
    }
    atomic_add(&table->n_readers, 1);
    spinlock_release(&table->spin);
}

/**
 * Release a previously obtained readlock.
 *
 * Simply decrement the n_readers value for the hash table
 *
 * @param table         The hash table to unlock
 */
static void
hashtable_read_unlock(HASHTABLE *table)
{
    atomic_add(&table->n_readers, -1);
}

/**
 * Obtain an exclusive write lock for the hash table.
 *
 * We acquire the hashtable spinlock, check for the number of
 * readers beign zero. If it is not we hold the spinlock and
 * loop waiting for the n_readers to reach zero. This will prevent
 * any new readers beign granted access but will not prevent current
 * readers releasing the read lock.
 *
 * Once we have no readers we increment writelock and test if we are
 * the only writelock holder, if not we repeat the process. We hold
 * the spinlock throughout the process since both read and write
 * locks do not require the spinlock to be acquired.
 *
 * @param table The table to lock for updates
 */
static void
hashtable_write_lock(HASHTABLE *table)
{
    int available;

    spinlock_acquire(&table->spin);
    do
    {
        while (atomic_add(&table->n_readers, 1) != 0)
        {
            atomic_add(&table->n_readers, -1);
        }
        atomic_add(&table->n_readers, -1);
        available = atomic_add(&table->writelock, 1);
        if (available != 0)
        {
            atomic_add(&table->writelock, -1);
        }
    }
    while (available != 0);
    spinlock_release(&table->spin);
}

/**
 * Release the write lock on the hash table.
 *
 * @param table The hash table to unlock
 */
static void
hashtable_write_unlock(HASHTABLE *table)
{
    atomic_add(&table->writelock, -1);
}

/**
 * Create an iterator on a hash table
 *
 * @param table         The table to ceate an iterator on
 * @return      An iterator to use in future calls
 */
HASHITERATOR *
hashtable_iterator(HASHTABLE *table)
{
    HASHITERATOR *rval = (HASHITERATOR *)MXS_MALLOC(sizeof(HASHITERATOR));

    if (rval)
    {
        rval->table = table;
        rval->chain = 0;
        rval->depth = -1;
    }
    return rval;
}

/**
 * Return the next key for a hashtable iterator
 *
 * @param iter  The hashtable iterator
 * @return      The next key value or NULL
 */
void *
hashtable_next(HASHITERATOR *iter)
{
    int i;
    HASHENTRIES *entries;

    if (iter == NULL)
    {
        return NULL;
    }

    iter->depth++;
    while (iter->chain < iter->table->hashsize)
    {
        hashtable_read_lock(iter->table);
        if ((entries = iter->table->entries[iter->chain]) != NULL)
        {
            i = 0;
            while (entries && i < iter->depth)
            {
                entries = entries->next;
                i++;
            }
            hashtable_read_unlock(iter->table);
            if (entries)
            {
                return entries->key;
            }
        }
        else
        {
            hashtable_read_unlock(iter->table);
        }
        iter->depth = 0;
        iter->chain++;
    }
    return NULL;
}

/**
 * Free a hashtable iterator
 *
 * @param iter  The iterator to free
 */
void
hashtable_iterator_free(HASHITERATOR *iter)
{
    MXS_FREE(iter);
}

/**
 * Save a hashtable to disk
 *
 * @param table         Hashtable to save
 * @param filename      Filename to write hashtable into
 * @param keywrite      Pointer to function that writes a single key
 * @param valuewrite    Pointer to function that writes a single value
 * @return              Number of entries written or -1 on error
 */
int
hashtable_save(HASHTABLE *table, const char *filename,
               int (*keywrite)(int, void*),
               int (*valuewrite)(int, void*))
{
    int fd, rval = 0;
    HASHITERATOR *iter;
    void *key, *value;

    if ((fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666)) == -1)
    {
        return -1;
    }
    if (write(fd, "HASHTABLE", 7) != 7)     // Magic number
    {
        close(fd);
        return -1;
    }
    if (write(fd, &rval, sizeof(rval)) == -1) // Write zero counter, will be overrwriten at end
    {
        MXS_ERROR("Failed to write hashtable item count: %d, %s", errno,
                  mxs_strerror(errno));
    }
    if ((iter = hashtable_iterator(table)) != NULL)
    {
        while ((key = hashtable_next(iter)) != NULL)
        {
            if (!(*keywrite)(fd, key))
            {
                close(fd);
                hashtable_iterator_free(iter);
                return -1;
            }
            if ((value = hashtable_fetch(table, key)) == NULL ||
                (*valuewrite)(fd, value) == 0)
            {
                close(fd);
                hashtable_iterator_free(iter);
                return -1;
            }
            rval++;
        }
    }

    /* Now go back and write the count of entries */
    if (lseek(fd, 7L, SEEK_SET) != -1)
    {
        if (write(fd, &rval, sizeof(rval)) == -1)
        {
            MXS_ERROR("Failed to write hashtable item count: %d, %s", errno,
                      mxs_strerror(errno));
        }
    }

    close(fd);
    hashtable_iterator_free(iter);
    return rval;
}

/**
 * Load a hashtable from disk
 *
 * @param table         Hashtable to load
 * @param filename      Filename to read hashtable from
 * @param keyread       Pointer to function that reads a single key
 * @param valueread     Pointer to function that reads a single value
 * @return              Number of entries read or -1 on error
 */
int
hashtable_load(HASHTABLE *table, const char *filename,
               void *(*keyread)(int),
               void *(*valueread)(int))
{
    int fd, count, rval = 0;
    void *key, *value;
    char buf[40];

    if ((fd = open(filename, O_RDONLY)) == -1)
    {
        return -1;
    }
    if (read(fd, buf, 7) != 7)
    {
        close(fd);
        return -1;
    }
    if (strncmp(buf, "HASHTABLE", 7) != 0)
    {
        close(fd);
        return -1;
    }
    if (read(fd, &count, sizeof(count)) != sizeof(count))
    {
        close(fd);
        return -1;
    }
    while (count--)
    {
        key = keyread(fd);
        value = valueread(fd);
        if (key == NULL || value == NULL)
        {
            break;
        }
        hashtable_add(table, key, value);
        rval++;
    }

    close(fd);
    return rval;
}

/**
 * Return the number of elements added to the hashtable
 * @param table Hashtable to measure
 * @return Number of inserted elements or 0 if table is NULL
 */
int hashtable_size(HASHTABLE *table)
{
    assert(table);
    spinlock_acquire(&table->spin);
    int rval = table->n_elements;
    spinlock_release(&table->spin);
    return rval;
}

/**
 * Frees memory assumed to have been allocated using one of the MaxScale
 * allocation functions. Intended to be used together with a hashtable
 * copy function that merely makes a straight memory copy of the key/value,
 * e.g. hashtable_item_strdup.
 * @param data The memory to be freed.
 */
void hashtable_item_free(void *data)
{
    MXS_FREE(data);
}

/**
 * Convenience function intended for use as the comparison function of a hashtable,
 * when the key is a NULL terminated string. Behaves as strcasecmp.
 * @param str1 Pointer to string.
 * @param str2 Pointer to string.
 * @return Same as strcasecmp.
 */
int hashtable_item_strcasecmp(const void *str1, const void *str2)
{
    return strcasecmp((const char*)str1, (const char*)str2);
}

/**
 * Convenience function intended for use as the comparison function of a hashtable,
 * when the key is a NULL terminated string. Behaves as strcmp.
 * @param str1 Pointer to string.
 * @param str2 Pointer to string.
 * @return Same as strcmp.
 */
int hashtable_item_strcmp(const void *str1, const void *str2)
{
    return strcmp((const char*)str1, (const char*)str2);
}

/**
 * Convenience function intended for use as the copy function of a hashtable,
 * when the key/value is a NULL terminated string.
 * @param data A pointer to a NULL terminated string.
 * @return A copy of the provided string or NULL if memory
 *         allocation fails.
 */
void* hashtable_item_strdup(const void* data)
{
    return MXS_STRDUP((const char*)data);
}

/**
 * Convenience function intended for use as the hash function of a hashtable,
 * when the key is a NULL terminated string.
 * @param data A pointer to a NULL terminated string.
 * @return A hash of the string.
 */
int hashtable_item_strhash(const void* data)
{
    int hash = 0;

    if (data)
    {
        const char* key = (const char*)data;

        int c;

        while ((c = *key++))
        {
            hash = c + (hash << 6) + (hash << 16) - hash;
        }
    }

    return hash;
}
