#include "map.h"
#include "crash.h"
#include <stdlib.h>

#define DEFAULT_CAPACITY 16

void map_init(
    map_t* map,
    hash_func_t hash_func,
    eq_func_t eq_func,
    double load_limit
) {
    map_init_capacity(
        map, hash_func, eq_func, load_limit, DEFAULT_CAPACITY);
}

void map_init_capacity(
    map_t* map,
    hash_func_t hash_func,
    eq_func_t eq_func,
    double load_limit,
    size_t capacity
) {
    map->hash_func = hash_func;
    map->eq_func = eq_func;
    map->load_limit = load_limit;
    map->size = 0;
    map->__num_buckets = capacity / load_limit + 1;
    map->__buckets = calloc(map->__num_buckets, sizeof(struct __map_entry*));

    if (map->__buckets == NULL)
        crash(
            "Out of memory allocating %ld hash buckets\n", map->__num_buckets);
}

void map_destroy(map_t* map) {
    for (size_t i = 0; i < map->__num_buckets; i++) {
        struct __map_entry* node = map->__buckets[i];
        while (node != NULL) {
            struct __map_entry* next = node->next;
            free(node);
            node = next;
        }
    }

    free(map->__buckets);
}

static struct __map_entry** fetch_map_slot(const map_t* map, const void* key) {
    struct __map_entry** bucket =
        &map->__buckets[map->hash_func(key) % map->__num_buckets];
    struct __map_entry* cur = *bucket;
    if (cur == NULL || map->eq_func(key, cur->key)) return bucket;

    while (cur->next != NULL && !map->eq_func(key, cur->next->key)) {
        cur = cur->next;
    }
    return &cur->next;
}

bool map_contains(const map_t* map, const void *key) {
    return *fetch_map_slot(map, key) != NULL;
}

void* map_get(const map_t* map, const void* key) {
    return map_get_or_default(map, key, NULL);
}

void* map_get_or_default(const map_t* map, const void* key, void* default_val) {
    struct __map_entry* entry = *fetch_map_slot(map, key);
    return entry == NULL ? default_val : entry->value;
}

static void rehash_map(map_t* map) {
    size_t old_num_buckets = map->__num_buckets;
    struct __map_entry** old_buckets = map->__buckets;
    map->__num_buckets <<= 1;
    map->__buckets = calloc(map->__num_buckets, sizeof(struct __map_entry*));

    for (size_t i = 0; i < old_num_buckets; i++) {
        struct __map_entry* entry = old_buckets[i];
        while (entry != NULL) {
            struct __map_entry* next = entry->next;
            entry->next = NULL;
            struct __map_entry** slot = fetch_map_slot(map, entry->key);
            *slot = entry;
            entry = next;
        }
    }

    free(old_buckets);
}

static void map_insert_allocating(
    map_t* map,
    struct __map_entry** slot,
    void* key,
    void* val
) {
    if ((double) ++map->size / map->__num_buckets > map->load_limit) {
        rehash_map(map);
        slot = fetch_map_slot(map, key);
    }

    struct __map_entry* entry = calloc(1, sizeof(struct __map_entry));
    if (entry == NULL)
        crash("Out of memory allocating hash entry\n");

    entry->key = key;
    entry->value = val;
    entry->next = NULL;
    *slot = entry;
}

void* map_compute_if_absent(map_t* map, void* key, void* default_val) {
    struct __map_entry** slot = fetch_map_slot(map, key);
    struct __map_entry* entry = *slot;
    if (entry != NULL) return entry->value;

    map_insert_allocating(map, slot, key, default_val);
    return default_val;
}

void map_put(map_t* map, void* key, void* val) {
    struct __map_entry** slot = fetch_map_slot(map, key);
    struct __map_entry* entry = *slot;
    if (entry != NULL) {
        entry->value = val;
        return;
    }

    map_insert_allocating(map, slot, key, val);
}

void* map_remove(map_t* map, const void* key) {
    struct __map_entry** slot = fetch_map_slot(map, key);
    struct __map_entry* entry = *slot;
    if (entry == NULL) return NULL;

    map->size--;
    *slot = entry->next;
    void* rv = entry->value;
    free(entry);
    return rv;
}

void map_foreach_readonly(
    map_t* map,
    map_foreach_func_t foreach_func,
    void* data
) {
    for (size_t i = 0; i < map->__num_buckets; i++) {
        struct __map_entry* entry = map->__buckets[i];
        while (entry != NULL) {
            foreach_func(entry->key, entry->value, data);
            entry = entry->next;
        }
    }
}

void map_foreach_readwrite(
    map_t* map,
    map_foreach_func_t foreach_func,
    void* data
) {
    void** keys = malloc(map->size * sizeof(void*));
    void** vals = malloc(map->size * sizeof(void*));
    size_t idx = 0;

    for (size_t i = 0; i < map->__num_buckets; i++) {
        struct __map_entry* entry = map->__buckets[i];
        while (entry != NULL) {
            keys[idx] = entry->key;
            vals[idx++] = entry->value;
            entry = entry->next;
        }
    }

    for (size_t i = 0; i < idx; i++) {
        foreach_func(keys[i], vals[i], data);
    }

    free(keys);
    free(vals);
}
