/*
 * 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 "stringview.hpp"
#include "strops.hpp"
#include "vector.hpp"

namespace ox {

template<typename K, typename T>
class HashMap {

	public:
		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(HashMap const&other);

		constexpr HashMap(HashMap &&other) noexcept;

		constexpr ~HashMap();

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

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

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

		constexpr T &operator[](MaybeView_t<K> const&key);

		constexpr Result<T*> at(MaybeView_t<K> const&key) noexcept;

		constexpr Result<const T*> at(MaybeView_t<K> const&key) const noexcept;

		constexpr void erase(MaybeView_t<K> const&key);

		[[nodiscard]]
		constexpr bool contains(MaybeView_t<K> const&key) const noexcept;

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

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

		constexpr void clear();

	private:
		constexpr void expand();

		constexpr static uint64_t hash(Integral_c auto) noexcept;

		constexpr static uint64_t hash(StringView const&) noexcept;

		template<typename KK>
		constexpr Pair *const&access(Vector<Pair*> const&pairs, KK const&key) const;

		template<typename KK>
		constexpr Pair *&access(Vector<Pair*> &pairs, KK const&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(HashMap<K, T> const&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==(HashMap const&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=(HashMap<K, T> const&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) noexcept {
	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[](MaybeView_t<K> const&k) {
	auto &p = access(m_pairs, k);
	if (p == nullptr) {
		if (static_cast<double>(m_pairs.size()) * 0.7 <
			 static_cast<double>(m_keys.size())) {
			expand();
		}
		p = new Pair;
		p->key = k;
		m_keys.emplace_back(k);
	}
	return p->value;
}

template<typename K, typename T>
constexpr Result<T*> HashMap<K, T>::at(MaybeView_t<K> const&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(MaybeView_t<K> const&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(MaybeView_t<K> const&k) {
	if (!contains(k)) {
		return;
	}
	auto h = hash(k) % m_pairs.size();
	while (true) {
		const auto &p = m_pairs[h];
		if (p == nullptr || p->key == k) {
			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(MaybeView_t<K> const&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 Vector<K> const&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 const&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(Integral_c auto k) noexcept {
	uint64_t sum = 1;
	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;
	}
	return sum;
}

template<typename K, typename T>
constexpr uint64_t HashMap<K, T>::hash(StringView const&k) noexcept {
	uint64_t sum = 1;
	for (auto i = 0u; i < k.len(); ++i) {
		sum += ((sum + static_cast<uint64_t>(k[i])) * 7) * sum;
	}
	return sum;
}

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

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

}
