
#pragma once

#include "Common.hpp"
#include "ContainersCommon.hpp"

namespace tp {

	template <typename Key>
	ualni DefaultHashFunc(SelectValueOrReference<Key> key) {
		return hash(key);
	}

	template <typename tKey, typename tVal, class tAllocator = DefaultAllocator, ualni (*tHashFunc)(SelectValueOrReference<tKey>) = DefaultHashFunc<tKey>, int tTableInitialSize = 4>
	class Map {

		enum {
			MAP_PERTURB_SHIFT = 5,
			MAP_MIN_SIZE = 4,
			MAP_MAX_LOAD_PERCENTAGE = 66,
		};

		typedef SelectValueOrReference<tKey> KeyArg;
		typedef SelectValueOrReference<tVal> ValArg;

	public:
		class Node {
			friend Map;
			Node(KeyArg aKey, ValArg aVal) :
				key(aKey),
				val(aVal) {}

		public:
			tKey key;
			tVal val;
		};

		struct Idx {
			alni idx = -1;
			[[nodiscard]] bool isValid() const { return bool(*this); }
			explicit operator bool() const { return idx != -1; }
		};

	private:
		tAllocator mAlloc;
		Node** mTable = nullptr;
		ualni mNSlots = 0;
		ualni mNEntries = 0;

	private:
		constexpr halnf maxLoadFactor() { return halnf(MAP_MAX_LOAD_PERCENTAGE) / 100.f; }

		inline Node** newTable(const ualni len) { return new (mAlloc.allocate(sizeof(Node*) * len)) Node*[len](); }

		inline Node* newNode(KeyArg key, ValArg val) { return new (mAlloc.allocate(sizeof(Node))) Node(key, val); }

		inline Node* newNodeNotConstructed() { return (Node*) mAlloc.allocate(sizeof(Node)); }

		inline void deleteTable(Node** table) { mAlloc.deallocate(table); }

		inline void deleteNode(Node* p) {
			p->~Node();
			mAlloc.deallocate(p);
		}

		void markDeletedSlot(ualni idx) const { mTable[idx] = (Node*) -1; }

		static bool isDeletedNode(Node* node) { return node == (Node*) -1; }

		void rehash() {
			alni nSlotsOld = mNSlots;
			Node** tableOld = mTable;

			mNSlots = next2pow((uhalni) ((1.f / (maxLoadFactor())) * mNEntries + 1));
			mTable = newTable(mNSlots);
			mNEntries = 0;

			for (alni i = 0; i < nSlotsOld; i++) {
				if (!tableOld[i] || isDeletedNode(tableOld[i])) {
					continue;
				}

				alni idx = findSlotWrite(tableOld[i]->key);
				mTable[idx] = tableOld[i];
				mNEntries++;
			}

			deleteTable(tableOld);
		}

		alni findSlotRead(KeyArg key) const {
			ualni const hashed_key = tHashFunc(key);
			ualni const mask = mNSlots - 1;
			ualni const shift = (hashed_key >> MAP_PERTURB_SHIFT) & ~1;
			alni idx = hashed_key & mask;
		NEXT:
			if (isDeletedNode(mTable[idx])) {
				goto SKIP;
			}
			if (!mTable[idx]) {
				return -1;
			}
			if (mTable[idx]->key == key) {
				return idx;
			}
		SKIP:
			idx = ((5 * idx) + 1 + shift) & mask;
			goto NEXT;
		}

		// compares keys only when collisions occur
		alni findSlotReadExisting(KeyArg key) const {
			ualni const hashed_key = tHashFunc(key);
			ualni const mask = mNSlots - 1;
			ualni const shift = (hashed_key >> MAP_PERTURB_SHIFT) & ~1;
			alni idx = hashed_key & mask;
		NEXT:
			if (isDeletedNode(mTable[idx])) {
				goto SKIP;
			}
			if (!mTable[idx]) {
				return -1;
			}
			if (mTable[((5 * idx) + 1 + shift) & mask] == nullptr) {
				return idx;
			}
			if (mTable[idx]->key == key) {
				return idx;
			}
		SKIP:
			idx = ((5 * idx) + 1 + shift) & mask;
			goto NEXT;
		}

		ualni findSlotWrite(KeyArg key) const {
			ualni const hashed_key = tHashFunc(key);
			ualni const mask = mNSlots - 1;
			ualni const shift = (hashed_key >> MAP_PERTURB_SHIFT) & ~1;
			ualni idx = hashed_key & mask;
		NEXT:
			if (isDeletedNode(mTable[idx]) || !mTable[idx]) {
				return idx;
			}
			if (mTable[idx]->key == key) {
				return idx;
			}
			idx = ((5 * idx) + 1 + shift) & mask;
			goto NEXT;
		}

		void put(Node* node) {
			const ualni idx = findSlotWrite(node->key);

			if (!mTable[idx] || isDeletedNode(mTable[idx])) {
				mNEntries++;
			}

			mTable[idx] = node;

			if ((halnf) mNEntries / mNSlots > maxLoadFactor()) {
				rehash();
			}
		}

	public:
		Map() {
			mNSlots = next2pow(uhalni(tTableInitialSize - 1));
			mTable = newTable(mNSlots);
		}

		Map(const Map& in) { this->operator=(in); }

		Node** buff() const { return mTable; }

		[[nodiscard]] ualni size() const { return mNEntries; }

		[[nodiscard]] ualni slotsSize() const { return mNEntries; }

		[[nodiscard]] const tAllocator& getAllocator() const { return mAlloc; }

		void put(KeyArg key, ValArg val) {
			const ualni idx = findSlotWrite(key);
			if (!mTable[idx] || isDeletedNode(mTable[idx])) {
				mTable[idx] = newNode(key, val);
				mNEntries++;
			} else {
				mTable[idx]->val = val;
			}
			if ((halnf) mNEntries / mNSlots > maxLoadFactor()) {
				rehash();
			}
		}

		// undefined behavior if item is not presents
		tVal& get(KeyArg key) {
			DEBUG_ASSERT(findSlotRead(key) != -1 && "Key Error")
			return mTable[findSlotReadExisting(key)]->val;
		}

		const tVal& get(KeyArg key) const {
			DEBUG_ASSERT(findSlotRead(key) != -1 && "Key Error")
			return mTable[findSlotReadExisting(key)]->val;
		}

		[[nodiscard]] Idx presents(KeyArg key) const { return { findSlotRead(key) }; }

		void remove(KeyArg key) {
			DEBUG_ASSERT(findSlotRead(key) != -1 && "Key Error")
			auto idx = findSlotReadExisting(key);

			deleteNode(mTable[idx]);

			markDeletedSlot(idx);

			mNEntries--;
			if (halnf(mNEntries / mNSlots) < 1.f - maxLoadFactor()) {
				rehash();
			}
		}

		const tVal& getSlotVal(ualni slot) const {
			DEBUG_ASSERT(slot < mNSlots && (mTable[slot] && !isDeletedNode(mTable[slot])) && "Key Error")
			return mTable[slot]->val;
		}

		tVal& getSlotVal(ualni slot) {
			DEBUG_ASSERT(slot < mNSlots && (mTable[slot] && !isDeletedNode(mTable[slot])) && "Key Error")
			return mTable[slot]->val;
		}

		const tVal& getSlotVal(Idx slot) const {
			DEBUG_ASSERT(slot.idx < mNSlots && (mTable[slot.idx] && !isDeletedNode(mTable[slot.idx])) && "Key Error")
			return mTable[slot.idx]->val;
		}

		tVal& getSlotVal(Idx slot) {
			DEBUG_ASSERT(slot.idx < mNSlots && (mTable[slot.idx] && !isDeletedNode(mTable[slot.idx])) && "Key Error")
			return mTable[slot.idx]->val;
		}

		Map& operator=(const Map& in) {
			if (this == &in) {
				return *this;
			}
			
			for (ualni i = 0; i < mNSlots; i++) {
				if (mTable[i] && !isDeletedNode(mTable[i])) {
					deleteNode(mTable[i]);
				}
			}
			mNEntries = 0;
			mNSlots = in.mNSlots;
			deleteTable(mTable);
			mTable = newTable(mNSlots);
			for (alni i = 0; i < mNSlots; i++) {
				if (in.mTable[i] && !isDeletedNode(in.mTable[i])) {
					put(in.mTable[i]->key, in.mTable[i]->val);
				}
			}
			return *this;
		}

		[[nodiscard]] bool operator==(const Map& in) const {
			if (this == &in) {
				return true;
			}
			if (in.mNEntries != mNEntries) {
				return false;
			}
			for (auto i : in) {
				if (!presents(i->key) || get(i->key) != i->val) {
					return false;
				}
			}
			return true;
		}

		void removeAll() {
			for (ualni i = 0; i < mNSlots; i++) {
				if (mTable[i] && !isDeletedNode(mTable[i])) {
					deleteNode(mTable[i]);
				}
			}
			deleteTable(mTable);
			mTable = newTable(tTableInitialSize);
			mNSlots = tTableInitialSize;
			mNEntries = 0;
		}

		[[nodiscard]] alni slotIdx(alni entry_idx_in) const {
			alni entry_idx = -1;
			for (alni slot_idx = 0; slot_idx < mNSlots; slot_idx++) {
				if (mTable[slot_idx]) {
					entry_idx++;
				}
				if (entry_idx == entry_idx_in) {
					return slot_idx;
				}
			}
			return -1;
		}

		Node* GetEntry(ualni idx) {
			auto slot = slotIdx(idx);
			DEBUG_ASSERT(slot != -1 && "Key error")
			return mTable[slot];
		}

		const Node* GetEntry(ualni idx) const {
			auto slot = slotIdx(idx);
			DEBUG_ASSERT(slot != -1 && "Key error")
			return mTable[slot];
		}

	public:
		class Iterator {
			const Map* map;
			Node* mIter;
			alni mSlot;
			alni mEntry;

			friend Map;
			explicit Iterator(const Map* _map) {
				mSlot = -1;
				mEntry = -1;
				map = _map;
				this->operator++();
			}

		public:
			Node* operator->() { return mIter; }
			const Node* operator->() const { return mIter; }
			const Iterator& operator*() const { return *this; }

			bool operator!=(ualni idx) const { return mSlot != idx; }

			void operator++() {
				mSlot++;
				while ((map->isDeletedNode(map->mTable[mSlot]) || !map->mTable[mSlot]) && (mSlot != (map->mNSlots - 1))) {
					mSlot++;
				}
				if (mSlot != map->mNSlots) {
					mIter = map->mTable[mSlot];
					mEntry++;
				}
			}
		};

		[[nodiscard]] Iterator begin() const { return Iterator(this); }

		[[nodiscard]] ualni end() const { return mNSlots - 1; }

		template <class Archiver>
		void archiveWrite(Archiver& ar) const {
			ar << mNEntries;
			for (auto item : *this) {
				ar << item->val;
				ar << item->key;
			}
		}

		template <class Archiver>
		void archiveRead(Archiver& ar) {
			removeAll();
			decltype(mNSlots) len;
			ar >> len;
			for (auto i = len; i; i--) {
				auto node = newNodeNotConstructed();
				ar >> node->val;
				ar >> node->key;
				put(node);
			}
		}

		~Map() {
			for (ualni i = 0; i < mNSlots; i++) {
				if (mTable[i] && !isDeletedNode(mTable[i])) {
					deleteNode(mTable[i]);
				}
			}
			deleteTable(mTable);
		}
	};
}