#pragma once

#include <boost/preprocessor/seq/for_each.hpp>
#include <boost/preprocessor/stringize.hpp>

#include <psemek/util/to_string.hpp>
#include <psemek/util/type_name.hpp>
#include <psemek/util/hash_table.hpp>
#include <psemek/util/exception.hpp>

#include <type_traits>
#include <optional>
#include <string_view>
#include <iostream>

namespace psemek::util
{

	struct unknown_enum_value_exception_base
		: exception
	{
		using exception::exception;
	};

	template <typename Enum>
	struct unknown_enum_value_exception
		: unknown_enum_value_exception_base
	{
		unknown_enum_value_exception(Enum value, util::stacktrace stacktrace = {})
			: unknown_enum_value_exception_base(to_string("unknown ", type_name<Enum>(), " value: ", static_cast<std::underlying_type_t<Enum>>(value)), std::move(stacktrace))
			, value_(value)
		{}

		Enum value() const noexcept { return value_; }

	private:
		Enum value_;
	};

	template <typename Enum>
	struct enum_iterator
	{
		using underlying_type = std::underlying_type_t<Enum>;
		using difference_type = underlying_type;
		using value_type = Enum;
		using pointer = Enum const *;
		using reference = Enum;
		using iterator_category = std::bidirectional_iterator_tag;

		underlying_type value;

		constexpr Enum operator *() const { return static_cast<Enum>(value); }

		enum_iterator operator ++() { ++value; return *this; }
		enum_iterator operator ++ (int)
		{
			enum_iterator copy = *this;
			++(*this);
			return copy;
		}

		enum_iterator operator --() { --value; return *this; }
		enum_iterator operator -- (int)
		{
			enum_iterator copy = *this;
			--(*this);
			return copy;
		}

		friend bool operator == (enum_iterator const & it1, enum_iterator const & it2) = default;
		friend bool operator != (enum_iterator const & it1, enum_iterator const & it2) = default;
	};

	template <typename Enum>
	struct enum_range
	{
		enum_iterator<Enum> first, last;

		auto begin() const { return first; }
		auto end() const { return last; }

		auto rbegin() const { return std::make_reverse_iterator(end()); }
		auto rend() const { return std::make_reverse_iterator(begin()); }

		constexpr auto size() const { return last.value - first.value; }
	};

}

#define psemek_declare_enum_detail_enumerator(r, data, value) value,

#define psemek_declare_enum_detail_to_string(r, data, value) case data :: value : return BOOST_PP_STRINGIZE(value);

#define psemek_declare_enum_detail_from_string(r, data, value) { BOOST_PP_STRINGIZE(value), data::value },

#define psemek_declare_enum(name, type, values) \
	enum class name : type { \
		BOOST_PP_SEQ_FOR_EACH(psemek_declare_enum_detail_enumerator, _, values) \
	}; \
	inline ::std::string_view to_string(name value) { \
		switch (value) { \
			BOOST_PP_SEQ_FOR_EACH(psemek_declare_enum_detail_to_string, name, values) \
			default: throw ::psemek::util::unknown_enum_value_exception<name>(value); \
		} \
	} \
	inline std::optional<name> BOOST_PP_CAT(name, _from_string)(::std::string_view str) { \
		static const ::psemek::util::hash_map<::std::string_view, name> map = { \
			BOOST_PP_SEQ_FOR_EACH(psemek_declare_enum_detail_from_string, name, values) \
		}; \
		auto it = map.find(str); if (it == map.end()) return std::nullopt; return it->second; \
	} \
	template <typename Char, typename Traits> \
	inline ::std::basic_ostream<Char, Traits> & operator << (::std::basic_ostream<Char, Traits> & os, name value) { return os << to_string(value); } \
	constexpr inline ::psemek::util::enum_range<name> BOOST_PP_CAT(name, _values) () { return {{0}, {BOOST_PP_SEQ_SIZE(values)}}; }


