/**
 * variant.hpp
 *
 * This file is part of Algorithms library toolkit.
 * Copyright (C) 2017 Jan Travnicek (jan.travnicek@fit.cvut.cz)

 * Algorithms library toolkit is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.

 * Algorithms library toolkit is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.

 * You should have received a copy of the GNU General Public License
 * along with Algorithms library toolkit.  If not, see <http://www.gnu.org/licenses/>.
 *
 * https://gist.github.com/tibordp/6909880
 *
 * Created on: Feb 28, 2014
 * Author: Tibor Djurica Potpara
 * Modified: Jan Travnicek
 */

#pragma once

#include <iostream>
#include <variant>
#include <compare>

#include <extensions/typeindex.h>
#include <extensions/type_traits.hpp>
#include <extensions/concepts.hpp>

#include "string.hpp"

namespace ext {

template < class Visitor, class... Variants >
constexpr auto visit ( Visitor && vis, Variants && ... vars ) {
	return std::visit ( std::forward < Visitor > ( vis ), std::forward < Variants > ( vars ).asStdVariant ( ) ... );
}

/**
 * \brief
 * Class to help building of the variant type or, in case variant is requested to be constructed from single type or more types but all the same, that concrete type.
 *
 * \tparam Res the resulting contstructed variant
 * \tparam Ts ... pack of types to construct the variant from
 */
template < class Res, class ... Ts >
struct variant_builder_impl {
};

/**
 * \brief
 * Recursion ending specialisation of the helper class to build the variant type. The last type T is added as last type to the variant parameters.
 *
 * \tparam ResTs ... pack of types already accepted as types of the variant
 * \tparam T last type to construct the variant from
 */
template < class ... ResTs >
struct variant_builder_impl < std::variant < ResTs ... > > {
	typedef std::variant < ResTs ... > std_variant;

	template < class T >
	using index_in = ext::index_in < T, ResTs ... >;
};

/**
 * \brief
 * Recursive step of class to build the variant type.
 *
 * \tparam ResTs ... pack of types already accepted as types of the variant
 * \tparam T next type to construct the variant from
 * \tparam Ts ... remaining types to add to variant template parameters
 */
template < class ... ResTs, class T, class ... Ts >
struct variant_builder_impl < std::variant < ResTs ... >, T, Ts ... > : public variant_builder_impl < typename std::conditional < is_in < T, ResTs ... >::value, std::variant < ResTs ... >, std::variant < ResTs ..., T > >::type, Ts ... > {
};

template < class T, class ... Ts >
struct variant_builder_start : public variant_builder_impl < std::variant < T >, Ts ... > {
};

template < class ... Ts >
using std_variant = typename variant_builder_start < Ts ... >::std_variant;

/**
 * \brief
 * Implementation of the variant class allowing to store any type of those listed in the template parameters. Void type is allowed, multiply specified same type is irrelevant.
 *
 * \tparam Ts ... pack of allowed types.
 */
template < typename ... Ts >
class variant : public std_variant < Ts ... > {

	template < class Visitor, class... Variants >
	friend constexpr auto visit ( Visitor && vis, Variants && ... vars );

	/**
	 * Internal variant to string callback implementation.
	 */
	class VariantToString {
	public:
		/**
		 * The callback for types different from void
		 *
		 * \tparam T the accepted tye
		 *
		 * \param value the accepted value
		 *
		 * \return string representation of the value
		 */
		template < class T >
		std::string operator ( ) ( const T & value ) const {
			return ext::to_string ( value );
		}
	};

	/**
	 * Internal variant to ostream callback implementation.
	 */
	class VariantToStream {
		/**
		 * The stream to print to.
		 */
		std::ostream & m_out;
	public:
		/**
		 * Constructor of the variant to stream callback.
		 *
		 * \param out the output stream to use in function call operators.
		 */
		explicit VariantToStream ( std::ostream & out ) : m_out ( out ) {
		}

		/**
		 * The callback for types different from void
		 *
		 * \tparam T the accepted tye
		 *
		 * \param value the accepted value
		 */
		template < class T >
		void operator ( ) ( const T & value ) {
			m_out << value;
		}
	};

	/**
	 * Remember the base class, i.e. std::variant
	 */
	typedef std_variant < Ts ... > base;

	const base & asStdVariant ( ) const & {
		return * this;
	}

	base & asStdVariant ( ) & {
		return * this;
	}

	base && asStdVariant ( ) && {
		return std::move ( * this );
	}

public:
	/**
	 * Inherit constructors of the standard variant
	 */
	using std_variant < Ts ... >::std_variant; // NOLINT(modernize-use-equals-default)

	/**
	 * Inherit operator = of the standard variant
	 */
	using std_variant < Ts ... >::operator=;
#ifndef __clang__

	/**
	 * Default constructor needed by g++ since it is not inherited
	 */
	variant ( ) = default;

	/**
	 * Copy constructor needed by g++ since it is not inherited
	 */
	variant ( const variant & other ) = default;

	/**
	 * Move constructor needed by g++ since it is not inherited
	 */
	variant ( variant && other ) = default;

	/**
	 * Copy operator = needed by g++ since it is not inherited
	 */
	variant & operator = ( variant && other ) = default;

	/**
	 * Move operator = needed by g++ since it is not inherited
	 */
	variant & operator = ( const variant & other ) = default;
#endif

	/**
	 * \brief
	 * Function to test whether the variant currently contains type T.
	 *
	 * \tparam T the type to test
	 *
	 * \return true it the variant contains instance of type T
	 */
	template < typename T >
	bool is() const {
		return std::holds_alternative < T > ( * this );
	}

	/**
	 * \brief
	 * Allows to assign a value to the variant.
	 *
	 * \tparam T the type of the value to assign
	 *
	 * \param value the new value to assign to the variant
	 */
	template < typename T >
	requires ( ext::Included < std::decay_t < T >, Ts ... > )
	void set ( T && value ) {
		* this = std::forward < T > ( value );
	}

	/**
	 * \brief
	 * Allows to retrieve a value from the variant.
	 *
	 * \tparam T the type of the value to retrieve
	 *
	 * \return value retrieved from the variant
	 *
	 * \throws bad_cast exception if the variant stores different type than specified
	 */
	template < typename T >
	T & get ( ) & {
		return std::get < T > ( * this );
	}

	/**
	 * \brief
	 * Allows to retrieve a value from the variant.
	 *
	 * \tparam T the type of the value to retrieve
	 *
	 * \return value retrieved from the variant
	 *
	 * \throws bad_cast exception if the variant stores different type than specified
	 */
	template<typename T >
	const T & get ( ) const & {
		return std::get < T > ( * this );
	}

	/**
	 * \brief
	 * Allows to retrieve a value from the variant.
	 *
	 * \tparam T the type of the value to retrieve
	 *
	 * \return value retrieved from the variant
	 *
	 * \throws bad_cast exception if the variant stores different type than specified
	 */
	template < typename T >
	T && get ( ) && {
		return std::move ( std::get < T > ( * this ) );
	}

	/**
	 * \brief
	 * Callback executor on current variant value.
	 *
	 * \tparam Result the uniform result type
	 * \tparam Callable type of callable able to accept any variant type.
	 *
	 * \param callable object of callable type able to accept any variant type via function call operator. The possible void type is represented by call with no parameters
	 *
	 * \return the result of the callable on the current variable type
	 */
	template < class Result, class Callable >
	Result call ( Callable callable ) const {
		return ext::visit ( callable, * this );
	}

	/**
	 * \brief
	 * Operator to print the variant to the output stream.
	 *
	 * \param out the output stream
	 * \param obj the variant to print
	 *
	 * \return the output stream from the \p out
	 */
	friend std::ostream & operator << ( std::ostream & out, const variant < Ts ... > & obj ) {
		obj.call < void > ( VariantToStream ( out ) );
		return out;
	}

	/**
	 * \brief
	 * To string cast operator.
	 *
	 * \return string representation of the variant
	 */
	explicit operator std::string() const {
		return this->call < std::string > ( VariantToString ( ) );
	}

	template < ext::Included < Ts ... > T >
	auto operator <=> ( const T & other ) const {
		auto indexRes = this->index ( ) <=> variant_builder_start < Ts ... >::template index_in < T >::value;
		if ( indexRes != 0 )
			return indexRes;

		return this->template get < T > ( ) <=> other;
	}

	template < ext::Included < Ts ... > T >
	bool operator == ( const T & other ) const {
		auto indexRes = this->index ( ) == variant_builder_start < Ts ... >::template index_in < T >::value;
		if ( ! indexRes )
			return indexRes;

		return this->template get < T > ( ) == other;
	}
};

/**
 * \brief
 * Overload of to_string function.
 *
 * \param value the vector to be converted to string
 *
 * \tparam Ts ... types in the variant
 *
 * \return string representation
 */
template < class ... Ts >
std::string to_string ( const ext::variant < Ts ... > & value ) {
	return ( std::string ) value;
}

} /* namespace ext */

namespace std {

template < class ... Types >
struct variant_size < ext::variant < Types ... > > : public variant_size < typename ext::variant < Types ... >::std_variant > { };

template < size_t Np, typename ... Types >
struct variant_alternative < Np, ext::variant < Types ... > > {
	using type = variant_alternative_t < Np, typename ext::variant < Types ... >::std_variant >;
};

}

