/*
 * Copyright 2015 - 2022 gary@drinkingtea.net
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
 */

#pragma once

#include "algorithm.hpp"
#include "strops.hpp"
#include "vector.hpp"

namespace ox {

template<typename K, typename T>
class HashMap {

	using key_t = K;
	using value_t = T;

	private:
		struct Pair {
			K key = {};
			T value{};
		};
		Vector<K> m_keys;
		Vector<Pair*> m_pairs;

	public:
		explicit constexpr HashMap(std::size_t size = 100);

		constexpr HashMap(const HashMap &other);

		constexpr HashMap(HashMap &&other) noexcept;

		constexpr ~HashMap();

		constexpr bool operator==(const HashMap &other) const;

		constexpr HashMap &operator=(const HashMap &other);

		constexpr HashMap &operator=(HashMap &&other);

		/**
		 * K is assumed to be a null terminated string.
		 */
		constexpr T &operator[](const K &key);

		/**
		 * K is assumed to be a null terminated string.
		 */
		constexpr Result<T*> at(const K &key) noexcept;

		/**
		 * K is assumed to be a null terminated string.
		 */
		constexpr Result<const T*> at(const K &key) const noexcept;

		constexpr void erase(const K &key);

		constexpr bool contains(const K &key) const noexcept;

		[[nodiscard]]
		constexpr std::size_t size() const noexcept;

		[[nodiscard]]
		constexpr const Vector<K> &keys() const noexcept;

		constexpr void clear();

	private:
		constexpr void expand();

		/**
		 * K is assumed to be a null terminated string.
		 */
		constexpr static uint64_t hash(auto) noexcept;

		/**
		 * K is assumed to be a null terminated string.
		 */
		constexpr Pair *const&access(const Vector<Pair*> &pairs, const K &key) const;

		/**
		 * K is assumed to be a null terminated string.
		 */
		constexpr Pair *&access(Vector<Pair*> &pairs, const K &key);

};

template<typename K, typename T>
constexpr HashMap<K, T>::HashMap(std::size_t size): m_pairs(size) {
}

template<typename K, typename T>
constexpr HashMap<K, T>::HashMap(const HashMap<K, T> &other) {
	m_pairs = other.m_pairs;
}

template<typename K, typename T>
constexpr HashMap<K, T>::HashMap(HashMap<K, T> &&other) noexcept {
	m_keys = std::move(other.m_keys);
	m_pairs = std::move(other.m_pairs);
}

template<typename K, typename T>
constexpr HashMap<K, T>::~HashMap() {
	clear();
}

template<typename K, typename T>
constexpr bool HashMap<K, T>::operator==(const HashMap &other) const {
	if (m_keys != other.m_keys) {
		return false;
	}
	for (int i = 0; i < m_keys.size(); i++) {
		auto &k = m_keys[i];
		if (at(k) != other.at(k)) {
			return false;
		}
	}
	return true;
}

template<typename K, typename T>
constexpr HashMap<K, T> &HashMap<K, T>::operator=(const HashMap<K, T> &other) {
	if (this != &other) {
		clear();
		m_keys = other.m_keys;
		m_pairs = other.m_pairs;
	}
	return *this;
}

template<typename K, typename T>
constexpr HashMap<K, T> &HashMap<K, T>::operator=(HashMap<K, T> &&other) {
	if (this != &other) {
		clear();
		m_keys = std::move(other.m_keys);
		m_pairs = std::move(other.m_pairs);
	}
	return *this;
}

template<typename K, typename T>
constexpr T &HashMap<K, T>::operator[](const K &k) {
	auto &p = access(m_pairs, k);
	if (p == nullptr) {
		if (m_pairs.size() * 0.7 < m_keys.size()) {
			expand();
		}
		p = new Pair;
		p->key = k;
		m_keys.push_back(k);
	}
	return p->value;
}

template<typename K, typename T>
constexpr Result<T*> HashMap<K, T>::at(const K &k) noexcept {
	auto p = access(m_pairs, k);
	if (!p) {
		return {nullptr, OxError(1, "value not found for given key")};
	}
	return &p->value;
}

template<typename K, typename T>
constexpr Result<const T*> HashMap<K, T>::at(const K &k) const noexcept {
	auto p = access(m_pairs, k);
	if (!p) {
		return {nullptr, OxError(1, "value not found for given key")};
	}
	return &p->value;
}

template<typename K, typename T>
constexpr void HashMap<K, T>::erase(const K &k) {
	if (!contains(k)) {
		return;
	}
	auto h = hash(k) % m_pairs.size();
	while (true) {
		const auto &p = m_pairs[h];
		if (p == nullptr || ox_strcmp(p->key, k) == 0) {
			oxIgnoreError(m_pairs.erase(h));
			break;
		} else {
			h = hash(h) % m_pairs.size();
		}
	}
	oxIgnoreError(m_keys.erase(ox::find(m_keys.begin(), m_keys.end(), k)));
}

template<typename K, typename T>
constexpr bool HashMap<K, T>::contains(const K &k) const noexcept {
	return access(m_pairs, k) != nullptr;
}

template<typename K, typename T>
constexpr std::size_t HashMap<K, T>::size() const noexcept {
	return m_keys.size();
}

template<typename K, typename T>
constexpr const Vector<K> &HashMap<K, T>::keys() const noexcept {
	return m_keys;
}

template<typename K, typename T>
constexpr void HashMap<K, T>::clear() {
	for (std::size_t i = 0; i < m_pairs.size(); i++) {
		delete m_pairs[i];
	}
	m_pairs.clear();
	m_pairs.resize(100);
}

template<typename K, typename T>
constexpr void HashMap<K, T>::expand() {
	Vector<Pair*> r;
	for (std::size_t i = 0; i < m_keys.size(); ++i) {
		auto k = m_keys[i];
		access(r, k) = std::move(access(m_pairs, k));
	}
	m_pairs = std::move(r);
}

template<typename K, typename T>
constexpr uint64_t HashMap<K, T>::hash(auto k) noexcept {
	uint64_t sum = 1;
	if constexpr(is_integral_v<decltype(k)>) {
		for (auto i = 0u; i < sizeof(K); ++i) {
			const auto shift = i * 8;
			const auto v = static_cast<uint64_t>(k >> shift & 0xff);
			sum += (sum + v) * 7 * sum;
		}
	} else {
		for (auto i = 0u; k[i]; ++i) {
			sum += ((sum + static_cast<uint64_t>(k[i])) * 7) * sum;
		}
	}
	return sum;
}

template<typename K, typename T>
constexpr typename HashMap<K, T>::Pair *const&HashMap<K, T>::access(const Vector<Pair*> &pairs, const K &k) const {
	auto h = hash(k) % pairs.size();
	while (true) {
		const auto &p = pairs[h];
		if (p == nullptr || ox_strcmp(p->key, k) == 0) {
			return p;
		} else {
			h = (h + 1) % pairs.size();
		}
	}
}

template<typename K, typename T>
constexpr typename HashMap<K, T>::Pair *&HashMap<K, T>::access(Vector<Pair*> &pairs, const K &k) {
	auto h = hash(k) % pairs.size();
	while (true) {
		auto &p = pairs[h];
		bool matches = [&] {
			if constexpr (is_integral_v<K>) {
				return p == nullptr || k == p->key;
			} else {
				return p == nullptr || ox_strcmp(p->key, k) == 0;
			}
		}();
		if (matches) {
			return p;
		} else {
			h = (h + 1) % pairs.size();
		}
	}
}

}
