module bubel.ecs.hash_map;

import std.traits;

import bubel.ecs.vector;
import bubel.ecs.traits;

enum doNotInline = "version(DigitalMars)pragma(inline,false);version(LDC)pragma(LDC_never_inline);";

private enum HASH_EMPTY = 0;
private enum HASH_DELETED = 0x1;
private enum HASH_FILLED_MARK = ulong(1) << 8 * ulong.sizeof - 1;

export ulong defaultHashFunc(T)(auto ref T t) nothrow @nogc {
	static if (isIntegral!(T)) {
		return hashInt(t);
	} else {
		return 0;//hashInt(t.hashOf); // hashOf is not giving proper distribution between H1 and H2 hash parts
	}
}

// Can turn bad hash function to good one
export ulong hashInt(ulong x) nothrow @nogc @safe {
	x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9;
	x = (x ^ (x >> 27)) * 0x94d049bb133111eb;
	x = x ^ (x >> 31);
	return x;
}

struct HashMap(KeyPar, ValuePar, alias hashFunc = defaultHashFunc) {
	alias Key = KeyPar;
	alias Value = ValuePar;
	nothrow:

	enum rehashFactor = 0.75;
	enum size_t getIndexEmptyValue = size_t.max;

	static struct KeyVal {
		Key key;
		Value value;
	}

	static struct Bucket {
		ulong hash;
		KeyVal keyValue;
	}

	Vector!Bucket elements; // Length should be always power of 2
	size_t length; // Used to compute loadFactor
	size_t markerdDeleted;

	export void clear() {
		elements.clear();
		length = 0;
		markerdDeleted = 0;
	}

	export void reset() {
		elements.reset();
		length = 0;
		markerdDeleted = 0;
	}

	export bool isIn(ref Key el) {
		return getIndex(el) != getIndexEmptyValue;
	}

	export bool isIn(Key el) {
		return getIndex(el) != getIndexEmptyValue;
	}

	export Value* getPtr()(auto ref Key k) {
		size_t index = getIndex(k);
		if (index == getIndexEmptyValue) {
			return null;
		} else {
			return &elements[index].keyValue.value;
		}
	}

	export ref Value get()(auto ref Key k) {
		size_t index = getIndex(k);
		assert(index != getIndexEmptyValue);
		return elements[index].keyValue.value;
	}

	deprecated("Use get with second parameter.") export auto ref Value getDefault()(
			auto ref Key k, auto ref Value defaultValue) {
		return get(k, defaultValue);
	}

	export auto ref Value get()(auto ref Key k, auto ref Value defaultValue) {
		size_t index = getIndex(k);
		if (index == getIndexEmptyValue) {
			return defaultValue;
		} else {
			return elements[index].keyValue.value;
		}
	}

	export ref Value getInsertDefault()(auto ref Key k, auto ref Value defaultValue) {
		size_t index = getIndex(k);
		if (index == getIndexEmptyValue) {
			add(k, defaultValue);
		}
		index = getIndex(k);
		assert(index != getIndexEmptyValue);
		return elements[index].keyValue.value;

	}

	export bool tryRemove(Key el) {
		size_t index = getIndex(el);
		if (index == getIndexEmptyValue) {
			return false;
		}
		length--;
		elements[index].hash = HASH_DELETED;
		markerdDeleted++;
		return true;
	}

	export void remove(Key el) {
		bool ok = tryRemove(el);
		assert(ok);
	}

	export ref Value opIndex()(auto ref Key key) {
		return get(key);
	}

	export void opIndexAssign()(auto ref Value value, auto ref Key key) {
		add(key, value);
	}

	export void add()(auto ref Key key, auto ref Value value) {
		size_t index = getIndex(key);
		if (index != getIndexEmptyValue) {
			elements[index].keyValue.value = value;
			return;
		}

		if (getLoadFactor(length + 1) > rehashFactor
				|| getLoadFactor(length + markerdDeleted) > rehashFactor) {
			rehash();
		}
		length++;

		immutable ulong hash = hashFunc(key) | HASH_FILLED_MARK;
		immutable size_t rotateMask = elements.length - 1;
		index = hash & rotateMask; // Starting point

		while (true) {
			Bucket* gr = &elements[index];
			if ((gr.hash & HASH_FILLED_MARK) == 0) {
				if (gr.hash == HASH_DELETED) {
					markerdDeleted--;
				}
				gr.hash = hash;
				gr.keyValue.key = key;
				gr.keyValue.value = value;
				return;
			}

			index++;
			index = index & rotateMask;
		}
	}

	// For debug
	//int numA;
	//int numB;

	export size_t getIndex(Key el) {
		return getIndex(el);
	}

	export size_t getIndex(ref Key el) {
		mixin(doNotInline);

		immutable size_t groupsLength = elements.length;
		if (groupsLength == 0) {
			return getIndexEmptyValue;
		}

		immutable ulong hash = hashFunc(el) | HASH_FILLED_MARK;
		immutable size_t rotateMask = groupsLength - 1;
		size_t index = hash & rotateMask; // Starting point

		//numA++;
		while (true) {
			//numB++;
			Bucket* gr = &elements[index];
			if (gr.hash == hash && gr.keyValue.key == el) {
				return index;
			}
			if (gr.hash == HASH_EMPTY) {
				return getIndexEmptyValue;
			}

			index++;
			index = index & rotateMask;
		}

	}

	export float getLoadFactor(size_t forElementsNum) {
		if (elements.length == 0) {
			return 1;
		}
		return cast(float) forElementsNum / (elements.length);
	}

	export void rehash()() {
		mixin(doNotInline);
		// Get all elements
		Vector!KeyVal allElements;
		allElements.reserve(elements.length);

		foreach (ref Bucket el; elements) {
			if ((el.hash & HASH_FILLED_MARK) == 0) {
				el.hash = HASH_EMPTY;
				continue;
			}
			el.hash = HASH_EMPTY;
			allElements ~= el.keyValue;

		}

		if (getLoadFactor(length + 1) > rehashFactor) { // Reallocate
			elements.length = (elements.length ? elements.length : 4) << 1; // Power of two, initially 8 elements
		}

		// Insert elements
		foreach (i, ref el; allElements) {
			add(el.key, el.value);
		}
		length = allElements.length;
		markerdDeleted = 0;
	}

	// foreach support
	export int opApply(DG)(scope DG dg) {
		int result;
		foreach (ref Bucket gr; elements) {
			if ((gr.hash & HASH_FILLED_MARK) == 0) {
				continue;
			}
			static if (isForeachDelegateWithTypes!(DG, Key)) {
				result = dg(gr.keyValue.key);
			} else static if (isForeachDelegateWithTypes!(DG, Value)) {
				result = dg(gr.keyValue.value);
			} else static if (isForeachDelegateWithTypes!(DG, Key, Value)) {
				result = dg(gr.keyValue.key, gr.keyValue.value);
			} else {
				static assert(0);
			}
			if (result)
				break;

		}

		return result;
	}

	export int byKey(scope int delegate(Key k) nothrow dg) {
		int result;
		foreach (ref Key k; this) {
			result = dg(k);
			if (result)
				break;
		}
		return result;
	}

	export int byValue(scope int delegate(ref Value k) nothrow dg) {
		int result;
		foreach (ref Value v; this) {
			result = dg(v);
			if (result)
				break;
		}
		return result;
	}

	export int byKeyValue(scope int delegate(ref Key k, ref Value v) nothrow dg) {
		int result;
		foreach (ref Key k, ref Value v; this) {
			result = dg(k, v);
			if (result)
				break;
		}
		return result;
	}
}