/*
	Example of usage:

	#define MAP_TYPE String
	#define MAP_PROC map_string_
	#define MAP_NAME MapString
	#include "map.c"
*/

#ifndef MAP_UTIL_STUFF
#define MAP_UTIL_STUFF
// NOTE(bill): This util stuff is the same for every `Map`
typedef struct MapFindResult {
	isize hash_index;
	isize entry_prev;
	isize entry_index;
} MapFindResult;

typedef enum HashKeyKind {
	HashKey_Default,
	HashKey_String,
	HashKey_Pointer,
} HashKeyKind;

typedef struct HashKey {
	HashKeyKind kind;
	u64         key;
	union {
		String string; // if String, s.len > 0
		void * ptr;
	};
} HashKey;

gb_inline HashKey hashing_proc(void const *data, isize len) {
	HashKey h = {HashKey_Default};
	h.kind = HashKey_Default;
	// h.key = gb_murmur64(data, len);
	h.key = gb_fnv64a(data, len);
	return h;
}

gb_inline HashKey hash_string(String s) {
	HashKey h = hashing_proc(s.text, s.len);
	h.kind = HashKey_String;
	h.string = s;
	return h;
}

gb_inline HashKey hash_pointer(void *ptr) {
	HashKey h = {HashKey_Default};
	h.key = cast(u64)cast(uintptr)ptr;
	h.ptr = ptr;
	h.kind = HashKey_Default;
	return h;
}

bool hash_key_equal(HashKey a, HashKey b) {
	if (a.key == b.key) {
		// NOTE(bill): If two string's hashes collide, compare the strings themselves
		if (a.kind == HashKey_String) {
			if (b.kind == HashKey_String) {
				return str_eq(a.string, b.string);
			}
			return false;
		}
		return true;
	}
	return false;
}
#endif

#define _J2_IND(a, b) a##b
#define _J2(a, b) _J2_IND(a, b)

/*
MAP_TYPE - Entry type
MAP_PROC - Function prefix (e.g. entity_map_)
MAP_NAME - Name of Map (e.g. EntityMap)
*/
#define MAP_ENTRY _J2(MAP_NAME,Entry)

typedef struct MAP_ENTRY {
	HashKey  key;
	isize    next;
	MAP_TYPE value;
} MAP_ENTRY;

typedef struct MAP_NAME {
	Array(isize)     hashes;
	Array(MAP_ENTRY) entries;
} MAP_NAME;

void      _J2(MAP_PROC,init)             (MAP_NAME *h, gbAllocator a);
void      _J2(MAP_PROC,init_with_reserve)(MAP_NAME *h, gbAllocator a, isize capacity);
void      _J2(MAP_PROC,destroy)          (MAP_NAME *h);
MAP_TYPE *_J2(MAP_PROC,get)              (MAP_NAME *h, HashKey key);
void      _J2(MAP_PROC,set)              (MAP_NAME *h, HashKey key, MAP_TYPE value);
void      _J2(MAP_PROC,remove)           (MAP_NAME *h, HashKey key);
void      _J2(MAP_PROC,clear)            (MAP_NAME *h);
void      _J2(MAP_PROC,grow)             (MAP_NAME *h);
void      _J2(MAP_PROC,rehash)           (MAP_NAME *h, isize new_count);

// Mutlivalued map procedure
MAP_ENTRY *_J2(MAP_PROC,multi_find_first)(MAP_NAME *h, HashKey key);
MAP_ENTRY *_J2(MAP_PROC,multi_find_next) (MAP_NAME *h, MAP_ENTRY *e);

isize _J2(MAP_PROC,multi_count)     (MAP_NAME *h, HashKey key);
void  _J2(MAP_PROC,multi_get_all)   (MAP_NAME *h, HashKey key, MAP_TYPE *items);
void  _J2(MAP_PROC,multi_insert)    (MAP_NAME *h, HashKey key, MAP_TYPE value);
void  _J2(MAP_PROC,multi_remove)    (MAP_NAME *h, HashKey key, MAP_ENTRY *e);
void  _J2(MAP_PROC,multi_remove_all)(MAP_NAME *h, HashKey key);



gb_inline void _J2(MAP_PROC,init)(MAP_NAME *h, gbAllocator a) {
	array_init(&h->hashes,  a);
	array_init(&h->entries, a);
}

gb_inline void _J2(MAP_PROC,init_with_reserve)(MAP_NAME *h, gbAllocator a, isize capacity) {
	array_init_reserve(&h->hashes,  a, capacity);
	array_init_reserve(&h->entries, a, capacity);
}

gb_inline void _J2(MAP_PROC,destroy)(MAP_NAME *h) {
	array_free(&h->entries);
	array_free(&h->hashes);
}

gb_internal isize _J2(MAP_PROC,_add_entry)(MAP_NAME *h, HashKey key) {
	MAP_ENTRY e = {0};
	e.key = key;
	e.next = -1;
	array_add(&h->entries, e);
	return h->entries.count-1;
}

gb_internal MapFindResult _J2(MAP_PROC,_find)(MAP_NAME *h, HashKey key) {
	MapFindResult fr = {-1, -1, -1};
	if (h->hashes.count > 0) {
		fr.hash_index  = key.key % h->hashes.count;
		fr.entry_index = h->hashes.e[fr.hash_index];
		while (fr.entry_index >= 0) {
			if (hash_key_equal(h->entries.e[fr.entry_index].key, key)) {
				return fr;
			}
			fr.entry_prev = fr.entry_index;
			fr.entry_index = h->entries.e[fr.entry_index].next;
		}
	}
	return fr;
}

gb_internal MapFindResult _J2(MAP_PROC,_find_from_entry)(MAP_NAME *h, MAP_ENTRY *e) {
	MapFindResult fr = {-1, -1, -1};
	if (h->hashes.count > 0) {
		fr.hash_index  = e->key.key % h->hashes.count;
		fr.entry_index = h->hashes.e[fr.hash_index];
		while (fr.entry_index >= 0) {
			if (&h->entries.e[fr.entry_index] == e) {
				return fr;
			}
			fr.entry_prev = fr.entry_index;
			fr.entry_index = h->entries.e[fr.entry_index].next;
		}
	}
	return fr;
}


gb_internal b32 _J2(MAP_PROC,_full)(MAP_NAME *h) {
	return 0.75f * h->hashes.count <= h->entries.count;
}

gb_inline void _J2(MAP_PROC,grow)(MAP_NAME *h) {
	isize new_count = ARRAY_GROW_FORMULA(h->entries.count);
	_J2(MAP_PROC,rehash)(h, new_count);
}

void _J2(MAP_PROC,rehash)(MAP_NAME *h, isize new_count) {
	isize i, j;
	MAP_NAME nh = {0};
	_J2(MAP_PROC,init)(&nh, h->hashes.allocator);
	array_resize(&nh.hashes, new_count);
	array_reserve(&nh.entries, h->entries.count);
	for (i = 0; i < new_count; i++) {
		nh.hashes.e[i] = -1;
	}
	for (i = 0; i < h->entries.count; i++) {
		MAP_ENTRY *e = &h->entries.e[i];
		MapFindResult fr;
		if (nh.hashes.count == 0) {
			_J2(MAP_PROC,grow)(&nh);
		}
		fr = _J2(MAP_PROC,_find)(&nh, e->key);
		j = _J2(MAP_PROC,_add_entry)(&nh, e->key);
		if (fr.entry_prev < 0) {
			nh.hashes.e[fr.hash_index] = j;
		} else {
			nh.entries.e[fr.entry_prev].next = j;
		}
		nh.entries.e[j].next = fr.entry_index;
		nh.entries.e[j].value = e->value;
		if (_J2(MAP_PROC,_full)(&nh)) {
			_J2(MAP_PROC,grow)(&nh);
		}
	}
	_J2(MAP_PROC,destroy)(h);
	*h = nh;
}

gb_inline MAP_TYPE *_J2(MAP_PROC,get)(MAP_NAME *h, HashKey key) {
	isize index = _J2(MAP_PROC,_find)(h, key).entry_index;
	if (index >= 0) {
		return &h->entries.e[index].value;
	}
	return NULL;
}

void _J2(MAP_PROC,set)(MAP_NAME *h, HashKey key, MAP_TYPE value) {
	isize index;
	MapFindResult fr;
	if (h->hashes.count == 0)
		_J2(MAP_PROC,grow)(h);
	fr = _J2(MAP_PROC,_find)(h, key);
	if (fr.entry_index >= 0) {
		index = fr.entry_index;
	} else {
		index = _J2(MAP_PROC,_add_entry)(h, key);
		if (fr.entry_prev >= 0) {
			h->entries.e[fr.entry_prev].next = index;
		} else {
			h->hashes.e[fr.hash_index] = index;
		}
	}
	h->entries.e[index].value = value;

	if (_J2(MAP_PROC,_full)(h)) {
		_J2(MAP_PROC,grow)(h);
	}
}



void _J2(MAP_PROC,_erase)(MAP_NAME *h, MapFindResult fr) {
	MapFindResult last;
	if (fr.entry_prev < 0) {
		h->hashes.e[fr.hash_index] = h->entries.e[fr.entry_index].next;
	} else {
		h->entries.e[fr.entry_prev].next = h->entries.e[fr.entry_index].next;
	}
	if (fr.entry_index == h->entries.count-1) {
		array_pop(&h->entries);
		return;
	}
	h->entries.e[fr.entry_index] = h->entries.e[h->entries.count-1];
	last = _J2(MAP_PROC,_find)(h, h->entries.e[fr.entry_index].key);
	if (last.entry_prev >= 0) {
		h->entries.e[last.entry_prev].next = fr.entry_index;
	} else {
		h->hashes.e[last.hash_index] = fr.entry_index;
	}
}

void _J2(MAP_PROC,remove)(MAP_NAME *h, HashKey key) {
	MapFindResult fr = _J2(MAP_PROC,_find)(h, key);
	if (fr.entry_index >= 0) {
		_J2(MAP_PROC,_erase)(h, fr);
	}
}

gb_inline void _J2(MAP_PROC,clear)(MAP_NAME *h) {
	array_clear(&h->hashes);
	array_clear(&h->entries);
}


#if 1
MAP_ENTRY *_J2(MAP_PROC,multi_find_first)(MAP_NAME *h, HashKey key) {
	isize i = _J2(MAP_PROC,_find)(h, key).entry_index;
	if (i < 0) {
		return NULL;
	}
	return &h->entries.e[i];
}

MAP_ENTRY *_J2(MAP_PROC,multi_find_next)(MAP_NAME *h, MAP_ENTRY *e) {
	isize i = e->next;
	while (i >= 0) {
		if (hash_key_equal(h->entries.e[i].key, e->key)) {
			return &h->entries.e[i];
		}
		i = h->entries.e[i].next;
	}
	return NULL;
}

isize _J2(MAP_PROC,multi_count)(MAP_NAME *h, HashKey key) {
	isize count = 0;
	MAP_ENTRY *e = _J2(MAP_PROC,multi_find_first)(h, key);
	while (e != NULL) {
		count++;
		e = _J2(MAP_PROC,multi_find_next)(h, e);
	}
	return count;
}

void _J2(MAP_PROC,multi_get_all)(MAP_NAME *h, HashKey key, MAP_TYPE *items) {
	isize i = 0;
	MAP_ENTRY *e = _J2(MAP_PROC,multi_find_first)(h, key);
	while (e != NULL) {
		items[i++] = e->value;
		e = _J2(MAP_PROC,multi_find_next)(h, e);
	}
}

void _J2(MAP_PROC,multi_insert)(MAP_NAME *h, HashKey key, MAP_TYPE value) {
	MapFindResult fr;
	isize i;
	if (h->hashes.count == 0) {
		_J2(MAP_PROC,grow)(h);
	}
	fr = _J2(MAP_PROC,_find)(h, key);
	i = _J2(MAP_PROC,_add_entry)(h, key);
	if (fr.entry_prev < 0) {
		h->hashes.e[fr.hash_index] = i;
	} else {
		h->entries.e[fr.entry_prev].next = i;
	}
	h->entries.e[i].next = fr.entry_index;
	h->entries.e[i].value = value;
	if (_J2(MAP_PROC,_full)(h)) {
		_J2(MAP_PROC,grow)(h);
	}
}

void _J2(MAP_PROC,multi_remove)(MAP_NAME *h, HashKey key, MAP_ENTRY *e) {
	MapFindResult fr = _J2(MAP_PROC,_find_from_entry)(h, e);
	if (fr.entry_index >= 0) {
		_J2(MAP_PROC,_erase)(h, fr);
	}
}

void _J2(MAP_PROC,multi_remove_all)(MAP_NAME *h, HashKey key) {
	while (_J2(MAP_PROC,get)(h, key) != NULL) {
		_J2(MAP_PROC,remove)(h, key);
	}
}
#endif


#undef _J2
#undef MAP_TYPE
#undef MAP_PROC
#undef MAP_NAME
#undef MAP_ENTRY
