/*
 * Hash map support
 *
 * Copyright 2020 Philip Rebohle for Valve Corporation
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
 */

#ifndef __VKD3D_HASHMAP_H
#define __VKD3D_HASHMAP_H

#include <stddef.h>

#include "vkd3d_memory.h"

enum hash_map_entry_flag
{
    HASH_MAP_ENTRY_OCCUPIED = (1 << 0),
};

struct hash_map_entry
{
    uint32_t hash_value;
    uint32_t flags;
};

typedef uint32_t (*pfn_hash_func)(const void* key);
typedef bool (*pfn_hash_compare_func)(const void *key, const struct hash_map_entry *entry);

/* Open-addressing hash table */
struct hash_map
{
    pfn_hash_func hash_func;
    pfn_hash_compare_func compare_func;
    void *entries;
    size_t entry_size;
    uint32_t entry_count;
    uint32_t used_count;
};

static inline struct hash_map_entry *hash_map_get_entry(const struct hash_map *hash_map, uint32_t entry_idx)
{
    return void_ptr_offset(hash_map->entries, hash_map->entry_size * entry_idx);
}

static inline uint32_t hash_map_get_entry_idx(const struct hash_map *hash_map, uint32_t hash_value)
{
    return hash_value % hash_map->entry_count;
}

static inline uint32_t hash_map_next_entry_idx(const struct hash_map *hash_map, uint32_t entry_idx)
{
    uint32_t next_idx = entry_idx + 1;
    return next_idx < hash_map->entry_count ? next_idx : 0;
}

static inline uint32_t hash_map_next_size(uint32_t old_size)
{
    /* This yields a sequence of primes and numbers with two
     * relatively large prime factors for any reasonable hash
     * table size */
    return old_size ? (old_size * 2 + 5) : 37;
}

static inline bool hash_map_grow(struct hash_map *hash_map)
{
    uint32_t i, old_count, new_count;
    void *new_entries, *old_entries;

    old_count = hash_map->entry_count;
    old_entries = hash_map->entries;

    new_count = hash_map_next_size(hash_map->entry_count);

    if (!(new_entries = vkd3d_calloc(new_count, hash_map->entry_size)))
        return false;

    hash_map->entry_count = new_count;
    hash_map->entries = new_entries;

    for (i = 0; i < old_count; i++)
    {
        /* Relocate existing entries one by one */
        struct hash_map_entry *old_entry = void_ptr_offset(old_entries, i * hash_map->entry_size);

        if (old_entry->flags & HASH_MAP_ENTRY_OCCUPIED)
        {
            uint32_t entry_idx = hash_map_get_entry_idx(hash_map, old_entry->hash_value);
            struct hash_map_entry *new_entry = hash_map_get_entry(hash_map, entry_idx);

            while (new_entry->flags & HASH_MAP_ENTRY_OCCUPIED)
            {
                entry_idx = hash_map_next_entry_idx(hash_map, entry_idx);
                new_entry = hash_map_get_entry(hash_map, entry_idx);
            }

            memcpy(new_entry, old_entry, hash_map->entry_size);
        }
    }

    vkd3d_free(old_entries);
    return true;
}

static inline bool hash_map_should_grow_before_insert(struct hash_map *hash_map)
{
    /* Allow a load factor of 0.7 for performance reasons */
    return 10 * hash_map->used_count >= 7 * hash_map->entry_count;
}

static inline struct hash_map_entry *hash_map_find(const struct hash_map *hash_map, const void *key)
{
    uint32_t hash_value, entry_idx;

    if (!hash_map->entries)
        return NULL;

    hash_value = hash_map->hash_func(key);
    entry_idx = hash_map_get_entry_idx(hash_map, hash_value);

    /* We never allow the hash table to be completely
     * populated, so this is guaranteed to return */
    while (true)
    {
        struct hash_map_entry *entry = hash_map_get_entry(hash_map, entry_idx);

        if (!(entry->flags & HASH_MAP_ENTRY_OCCUPIED))
            return NULL;

        if (entry->hash_value == hash_value && hash_map->compare_func(key, entry))
            return entry;

        entry_idx = hash_map_next_entry_idx(hash_map, entry_idx);
    }
}

static inline struct hash_map_entry *hash_map_insert(struct hash_map *hash_map, const void *key, const struct hash_map_entry *entry)
{
    struct hash_map_entry *target = NULL;
    uint32_t hash_value, entry_idx;

    if (hash_map_should_grow_before_insert(hash_map))
    {
        if (!hash_map_grow(hash_map))
            return NULL;
    }

    hash_value = hash_map->hash_func(key);
    entry_idx = hash_map_get_entry_idx(hash_map, hash_value);

    while (!target)
    {
        struct hash_map_entry *current = hash_map_get_entry(hash_map, entry_idx);

        if (!(current->flags & HASH_MAP_ENTRY_OCCUPIED) ||
                (current->hash_value == hash_value && hash_map->compare_func(key, current)))
            target = current;
        else
            entry_idx = hash_map_next_entry_idx(hash_map, entry_idx);
    }

    if (!(target->flags & HASH_MAP_ENTRY_OCCUPIED))
    {
        hash_map->used_count += 1;
        target->flags = HASH_MAP_ENTRY_OCCUPIED;
        target->hash_value = hash_value;
        memcpy(target + 1, entry + 1, hash_map->entry_size - sizeof(*entry));
    }

    /* If target is occupied, we already have an entry in the hashmap.
     * Return old one, caller is responsible for cleaning up the node we attempted to add. */

    return target;
}

static inline void hash_map_init(struct hash_map *hash_map, pfn_hash_func hash_func, pfn_hash_compare_func compare_func, size_t entry_size)
{
    hash_map->hash_func = hash_func;
    hash_map->compare_func = compare_func;
    hash_map->entries = NULL;
    hash_map->entry_size = entry_size;
    hash_map->entry_count = 0;
    hash_map->used_count = 0;
    assert(entry_size > sizeof(struct hash_map_entry));
}

static inline void hash_map_clear(struct hash_map *hash_map)
{
    vkd3d_free(hash_map->entries);
    hash_map->entries = NULL;
    hash_map->entry_count = 0;
    hash_map->used_count = 0;
}

static inline uint32_t hash_combine(uint32_t old_hash, uint32_t new_hash) {
    return old_hash ^ (new_hash + 0x9e3779b9 + (old_hash << 6) + (old_hash >> 2));
}

static inline uint32_t hash_uint64(uint64_t n)
{
    return hash_combine((uint32_t)n, (uint32_t)(n >> 32));
}

/* A somewhat stronger hash when we're meant to store the hash (pipeline caches, etc). Based on FNV-1a. */
static inline uint64_t hash_fnv1_init()
{
    return 0xcbf29ce484222325ull;
}

static inline uint64_t hash_fnv1_iterate_u8(uint64_t h, uint8_t value)
{
    return (h * 0x100000001b3ull) ^ value;
}

static inline uint64_t hash_fnv1_iterate_u32(uint64_t h, uint32_t value)
{
    return (h * 0x100000001b3ull) ^ value;
}

static inline uint64_t hash_fnv1_iterate_f32(uint64_t h, float value)
{
    union u { float f32; uint32_t u32; } v;
    v.f32 = value;
    return hash_fnv1_iterate_u32(h, v.u32);
}

static inline uint64_t hash_fnv1_iterate_u64(uint64_t h, uint64_t value)
{
    h = hash_fnv1_iterate_u32(h, value & UINT32_MAX);
    h = hash_fnv1_iterate_u32(h, value >> 32);
    return h;
}

static inline uint64_t hash_fnv1_iterate_string(uint64_t h, const char *str)
{
    if (str)
        while (*str)
            h = hash_fnv1_iterate_u8(h, *str++);
    h = hash_fnv1_iterate_u8(h, 0);
    return h;
}

#endif  /* __VKD3D_HASHMAP_H */
