/*
 * AlgorithmRegistry.hpp
 *
 *  Created on: 11. 7. 2017
 *	  Author: Jan Travnicek
 */

#ifndef _ALGORITHM_REGISTRY_HPP_
#define _ALGORITHM_REGISTRY_HPP_

#include <functional>
#include <memory>
#include <vector>
#include <string>

#include <exception/CommonException.h>
#include <abstraction/OperationAbstraction.hpp>

namespace abstraction {

class AlgorithmRegistry {
	class Entry {
		bool m_downcast;
		bool m_normalize;

	public:
		Entry ( bool downcast, bool normalize ) : m_downcast ( downcast ), m_normalize ( normalize ) {

		}

		virtual std::shared_ptr < abstraction::OperationAbstraction > getAbstraction ( ) const = 0;

		bool getDowncast ( ) const {
			return m_downcast;
		}

		bool getNormalize ( ) const {
			return m_normalize;
		}
	};

	template < class Return, class ... Params >
	class EntryImpl : public Entry {
		std::function < Return ( Params ... ) > m_callback;

	public:
		EntryImpl ( std::function < Return ( Params ... ) > callback, bool downcast, bool normalize ) : Entry ( downcast, normalize ), m_callback ( callback ) {
		}

		virtual std::shared_ptr < abstraction::OperationAbstraction > getAbstraction ( ) const override;
	};

	template < class Return, class ... Params >
	class EntryImpl < Return *, Params ... > : public Entry {
		std::function < Return * ( Params ... ) > m_callback;

	public:
		EntryImpl ( std::function < Return * ( Params ... ) > callback, bool downcast, bool normalize ) : Entry ( downcast, normalize ), m_callback ( callback ) {
		}

		virtual std::shared_ptr < abstraction::OperationAbstraction > getAbstraction ( ) const override;
	};

	static std::map < std::string, std::map < std::vector < std::type_index >, std::unique_ptr < Entry > > > & getEntries ( ) {
		static std::map < std::string, std::map < std::vector < std::type_index >, std::unique_ptr < Entry > > > algorithmGroups;
		return algorithmGroups;
	};

public:
	template < class Algo, class ReturnType, class ... ParamTypes >
	static void registerAlgorithm ( ReturnType ( * callback ) ( ParamTypes ... ), bool downcast, bool normalize ) {
		std::string algorithm = std::type_name < Algo > ( );

		std::vector < std::type_index > params;
		( void ) std::initializer_list < int > { ( params.push_back ( std::type_index ( typeid ( typename std::decay < ParamTypes >::type ) ) ), 0 ) ... };

		if ( ! getEntries ( ) [ algorithm ].insert ( std::make_pair ( params, std::unique_ptr < Entry > ( new EntryImpl < ReturnType, ParamTypes ... > ( callback, downcast, normalize ) ) ) ).second )
			throw ::exception::CommonException ( "Callback for " + algorithm + " already registered." );
	}

	static std::shared_ptr < abstraction::OperationAbstraction > getAbstraction ( std::string name, const std::vector < std::type_index > & paramTypes, bool & downcast, bool & normalize ) {
		auto group = getEntries ( ).find ( name );
		if ( group == getEntries ( ).end ( ) )
			throw exception::CommonException ( "Entry " + name + " not available" );

		auto overload = group->second.find ( paramTypes );
		if ( overload == group->second.end ( ) ) {
			std::stringstream ss;
			ss << paramTypes;

			throw exception::CommonException ( "Entry overload " + ss.str ( ) + " not available" );
		}

		downcast = overload->second->getDowncast ( );
		normalize = overload->second->getNormalize ( );

		return overload->second->getAbstraction ( );
	}

	static std::shared_ptr < abstraction::OperationAbstraction > getAbstraction ( std::string name, const std::vector < std::type_index > & paramTypes ) {
		bool downcast;
		bool normalize;
		return getAbstraction ( std::move ( name ), paramTypes, downcast, normalize );
	}

	static void listOverloads ( const std::string & algorithm ) {
		auto group = getEntries ( ).find ( algorithm );
		if ( group == getEntries ( ).end ( ) )
			throw exception::CommonException ( "Entry " + algorithm + " not available" );

		for ( const std::pair < const std::vector < std::type_index >, std::unique_ptr < Entry > > & overloads : group->second ) {
			for ( const std::type_index & param : overloads.first ) {
				std::cout << param << " " << std::endl;
			}
		}
	}

	static void list ( ) {
		for ( const std::pair < const std::string, std::map < std::vector < std::type_index >, std::unique_ptr < Entry > > > & entry : getEntries ( ) ) {
			std::cout << entry.first << std::endl;
		}
	}
};

} /* namespace abstraction */

#include <abstraction/AlgorithmAbstraction.hpp>

namespace abstraction {

template < class Return, class ... Params >
std::shared_ptr < abstraction::OperationAbstraction > AlgorithmRegistry::EntryImpl < Return, Params ... >::getAbstraction ( ) const {
	return std::make_shared < abstraction::AlgorithmAbstraction < Return, Params ... > > ( m_callback );
}

template < class Return, class ... Params >
std::shared_ptr < abstraction::OperationAbstraction > AlgorithmRegistry::EntryImpl < Return *, Params ... >::getAbstraction ( ) const {
	return std::make_shared < abstraction::AlgorithmAbstraction < Return *, Params ... > > ( m_callback );
}

} /* namespace abstraction */

#endif /* _ALGORITHM_REGISTRY_HPP_ */