#pragma once

#include <functional>
#include <array>
#include <tuple>
#include <cstdint>

namespace psemek::util
{

	constexpr void hash_combine(std::uint64_t & seed, std::uint64_t value)
	{
		constexpr std::uint64_t k = 0x9ddfea08eb382d69ULL;
		std::uint64_t a = (value ^ seed) * k;
		a ^= (a >> 47);
		std::uint64_t b = (seed ^ a) * k;
		b ^= (b >> 47);
		seed = b * k;
	}

	template <typename Iterator, typename Hash = std::hash<std::decay_t<decltype(*std::declval<Iterator>())>>>
	constexpr void hash_sequence(std::uint64_t & seed, Iterator begin, Iterator end, Hash hash = Hash{})
	{
		for (; begin != end; ++begin)
		{
			hash_combine(seed, hash(*begin));
		}
	}

	struct any_hash
	{
		struct is_transparent{};

		template <typename T>
		std::uint64_t operator() (T const & x) const
		{
			return std::hash<T>{}(x);
		}
	};

	template <typename ... T>
	constexpr std::uint64_t hash_all(T const & ... x)
	{
		any_hash hash;
		std::uint64_t seed = 0;
		(hash_combine(seed, hash(x)), ...);
		return seed;
	}

}

namespace std
{

	template <typename T, typename H>
	struct hash<std::pair<T, H>>
		: std::pair<std::hash<T>, std::hash<H>>
	{
		std::uint64_t operator()(std::pair<T, H> const & x) const
		{
			std::uint64_t seed = 0;
			::psemek::util::hash_combine(seed, this->first(x.first));
			::psemek::util::hash_combine(seed, this->second(x.second));
			return seed;
		}
	};

	template <typename T, std::size_t N>
	struct hash<std::array<T, N>>
		: std::hash<T>
	{
		using std::hash<T>::operator();

		std::uint64_t operator()(std::array<T, N> const & x) const
		{
			std::uint64_t seed = 0;
			::psemek::util::hash_sequence(seed, x.begin(), x.end(), *this);
			return seed;
		}
	};

	template <typename ... Ts>
	struct hash<std::tuple<Ts...>>
		: std::tuple<std::hash<Ts>...>
	{
		std::uint64_t operator()(std::tuple<Ts...> const & t) const
		{
			return hash_impl(t, std::make_index_sequence<sizeof...(Ts)>{});
		}

	private:
		template <std::size_t ... Is>
		std::uint64_t hash_impl(std::tuple<Ts...> const & t, std::index_sequence<Is...>) const
		{
			std::uint64_t result = 0;
			(::psemek::util::hash_combine(result, std::get<Is>(*this)(std::get<Is>(t))), ...);
			return result;
		}
	};

}
