#pragma once

#include <exception>
#include <type_traits>

#include "aligned_union.h"

namespace vlc
{
	class TryException : public std::logic_error
	{
	public:
		using std::logic_error::logic_error;
	};

	class TryUninitialized : public TryException
	{
	public:
		TryUninitialized() : TryException("Try is not initialized")
		{

		}
	};

	template<typename T> class Try
	{
		enum class State : int32_t
		{
			None,
			Success,
			Failure
		};

	public:
		using value_type = T;

		Try() : m_state(State::None)
		{
		}

		~Try()
		{
			switch (m_state)
			{
			case State::Success:
				reinterpret_cast<T*>(&m_storage)->~T();
				break;
			case State::Failure:
				reinterpret_cast<std::exception_ptr*>(&m_storage)->~exception_ptr();
				break;
			case State::None:
				break;
			}
		}

		explicit Try(const T& v) : m_state(State::Success)
		{
			static_assert(std::is_copy_constructible<T>::value, "T must be copy constructible");

			new(&m_storage) T(v);
		}

		explicit Try(T&& v) : m_state(State::Success)
		{
			new(&m_storage) T(std::forward<T>(v));
		}

		explicit Try(std::exception_ptr exc) : m_state(State::Failure)
		{
			new(&m_storage) std::exception_ptr(exc);
		}

		Try(const Try& src) : m_state(src.m_state)
		{
			static_assert(std::is_copy_constructible<T>::value, "T must be copy constructible");

			switch (m_state)
			{
			case State::Success:
				new(&m_storage) T(*reinterpret_cast<T*>(&src.m_storage));
				break;
			case State::Failure:
				new(&m_storage) std::exception_ptr(*reinterpret_cast<std::exception_ptr*>(&src.m_storage));
				break;
			case State::None:
				break;
			}
		}

		Try& operator = (const Try& src)
		{
			static_assert(std::is_copy_constructible<T>::value, "T must be copy constructible");

			m_state = src.m_state;

			switch (m_state)
			{
			case State::Success:
				new(&m_storage) T(*reinterpret_cast<T*>(&src.m_storage));
				break;
			case State::Failure:
				new(&m_storage) std::exception_ptr(*reinterpret_cast<std::exception_ptr*>(&src.m_storage));
				break;
			case State::None:
				break;
			}

			return *this;
		}

		Try(Try&& src) : m_state(src.m_state)
		{
			switch (m_state)
			{
			case State::Success:
				new(&m_storage) T(std::move(*reinterpret_cast<T*>(&src.m_storage)));
				break;
			case State::Failure:
				new(&m_storage) std::exception_ptr(std::move(*reinterpret_cast<std::exception_ptr*>(&src.m_storage)));
				break;
			case State::None:
				break;
			}
		}

		Try& operator = (Try&& src)
		{
			m_state = src.m_state;

			switch (m_state)
			{
			case State::Success:
				new(&m_storage) T(std::move(*reinterpret_cast<T*>(&src.m_storage)));
				break;
			case State::Failure:
				new(&m_storage) std::exception_ptr(std::move(*reinterpret_cast<std::exception_ptr*>(&src.m_storage)));
				break;
			case State::None:
				break;
			}

			return *this;
		}

		T& value() &
		{
			throwIfFailure();
			return *reinterpret_cast<T*>(&m_storage);
		}

		const T& value() const &
		{
			throwIfFailure();
			return *reinterpret_cast<const T*>(&m_storage);
		}

		T&& value() &&
		{
			throwIfFailure();
			return std::move(*reinterpret_cast<T*>(&m_storage));
		}

		const T& operator * () const
		{
			throwIfFailure();
			return value();
		}

		T& operator * ()
		{
			throwIfFailure();
			return value();
		}

		const T* operator -> () const
		{
			throwIfFailure();
			return &value();
		}

		T* operator -> ()
		{
			throwIfFailure();
			return &value();
		}

		std::exception_ptr& exception()
		{
			return *reinterpret_cast<std::exception_ptr*>(&m_storage);
		}

		const std::exception_ptr& exception() const
		{
			return *reinterpret_cast<const std::exception_ptr*>(&m_storage);
		}

		bool isSuccess() const
		{
			return m_state == State::Success;
		}

		bool isFailure() const
		{
			return m_state == State::Failure;
		}

		template<bool isTry, typename R>
		typename std::enable_if<isTry, R>::type
			get()
		{
			return std::forward<R>(*this);
		}

		template<bool isTry, typename R>
		typename std::enable_if<!isTry, R>::type
			get()
		{
			return std::forward<R>(value());
		}

		template<typename Exc, typename F> bool recoverWith(F&& func) const
		{
			if (!isFailure())
			{
				return false;
			}

			try
			{
				std::rethrow_exception(exception());
			}
			catch (Exc& e)
			{
				func(e);
				return true;
			}
			catch (...)
			{

			}

			return false;
		}

	private:
		State m_state;
		typename vlc::aligned_union<4, T, std::exception_ptr>::type m_storage;

		void throwIfFailure() const
		{
			switch (m_state)
			{
			case State::Failure:
				std::rethrow_exception(exception());
				break;
			case State::None:
				throw TryUninitialized();
				break;
			case State::Success:
				break;
			}
		}
	};

	template<> class Try<void>
	{
	public:
		using value_type = void;

		Try() : m_has_value(true)
		{

		}

		explicit Try(std::exception_ptr exc) : m_has_value(false), m_exception(exc)
		{

		}

		Try(const Try& src) : m_has_value(src.m_has_value), m_exception(src.m_exception)
		{
		}

		Try& operator = (const Try& src)
		{
			m_has_value = src.m_has_value;
			m_exception = src.m_exception;

			return *this;
		}

		void value() const
		{
			throwIfFailure();
		}

		void operator * () const
		{
			return value();
		}

		bool isSuccess() const
		{
			return m_has_value;
		}

		bool isFailure() const
		{
			return !isSuccess();
		}

		std::exception_ptr& exception()
		{
			return m_exception;
		}

		const std::exception_ptr& exception() const
		{
			return m_exception;
		}

		template<typename Exc, typename F> bool recoverWith(F&& func) const
		{
			if (!isFailure())
			{
				return false;
			}

			try
			{
				std::rethrow_exception(exception());
			}
			catch (Exc& e)
			{
				func(e);
				return true;
			}
			catch (...)
			{

			}

			return false;
		}

		template <bool, typename R>
		R get() 
		{
			return std::forward<R>(*this);
		}

	private:
		bool m_has_value;
		std::exception_ptr m_exception;

		void throwIfFailure() const
		{
			if (!m_has_value)
			{
				if (m_exception)
				{
					std::rethrow_exception(m_exception);
				}
				else
				{
					throw std::logic_error("Exception is not set");
				}
			}
		}
	};

	template<typename F>
	typename std::enable_if<
		!std::is_same<typename std::result_of<F()>::type, void>::value,
		Try<typename std::result_of<F()>::type>>::type
	make_try_with(F&& f)
	{
		using Result = typename std::result_of<F()>::type;

		try
		{
			return Try<Result>(f());
		}
		catch (...)
		{
			return Try<Result>(std::current_exception());
		}
	}

	template<typename F>
	typename std::enable_if<
		std::is_same<typename std::result_of<F()>::type, void>::value,
		Try<void>>::type
		make_try_with(F&& f)
	{
		try
		{
			f();
			return Try<void>();
		}
		catch (...)
		{
			return Try<void>(std::current_exception());
		}
	}

	namespace detail
	{
		template<typename... T> struct UnwrapHelper
		{
			// Build list of tuple arguments by recursive concatening them and unwrapping from Try at same time
			template<typename... TailList>
			static std::tuple<T...> unwrap(std::tuple<Try<T>...>&& t, TailList&&... tl)
			{
				return unwrap(std::move(t), std::forward<TailList>(tl)..., std::move(*std::get<sizeof...(TailList)>(t)));
			}

			// If that list is compatible with original T... , then we can finally make tuple from this list
			static std::tuple<T...> unwrap(std::tuple<Try<T>...>&&, T&&... args)
			{
				return std::make_tuple(std::forward<T>(args)...);
			}
		};
	}

	template<typename... T> std::tuple<T...> unwrap_tuple(std::tuple<Try<T>...>&& ts)
	{
		return detail::UnwrapHelper<T...>::unwrap(std::forward<std::tuple<Try<T>...>>(ts));
	}
}