#include <registry/AlgorithmRegistry.hpp>

#include <ext/algorithm>

#include <exception>

#include <common/OverloadResolution.hpp>

#include <abstraction/RawAbstraction.hpp>

namespace abstraction {

std::unique_ptr < abstraction::OperationAbstraction > AlgorithmRegistry::RawImpl::getAbstraction ( ) const {
	return std::make_unique < abstraction::RawAbstraction > ( getEntryInfo ( ).getParams ( ), m_rawCallback );
}

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

bool AlgorithmRegistry::isRegistered ( const std::string & algorithm, const ext::vector < std::string > & templateParams, const AlgorithmBaseInfo & entryInfo ) {
	const auto & group = getEntries ( ) [ ext::tie ( algorithm, templateParams ) ].second;

	return std::any_of ( group.begin ( ), group.end ( ), [ & ] ( const std::unique_ptr < Entry > & entry ) { return entry->getEntryInfo ( ).getCategory ( ) == entryInfo.getCategory ( ) && entry->getEntryInfo ( ).getParams ( ) == entryInfo.getParams ( ); } );
}

void AlgorithmRegistry::registerInternal ( std::string algorithm, ext::vector < std::string > templateParams, std::unique_ptr < Entry > value ) {
	if ( isRegistered ( algorithm, templateParams, value->getEntryInfo ( ) ) )
		throw std::invalid_argument ( ext::concat ( "Callback for ", algorithm, " with params ", value->getEntryInfo ( ).getParams ( ), " already registered." ) );

	auto & group = getEntries ( ) [ ext::make_pair ( std::move ( algorithm ), std::move ( templateParams ) ) ].second;

	group.insert ( group.end ( ), std::move ( value ) );
}

void AlgorithmRegistry::unregisterInternal ( const std::string & algorithm, const ext::vector < std::string > & templateParams, const AlgorithmBaseInfo & entryInfo ) {
	auto & group = getEntries ( ) [ ext::tie ( algorithm, templateParams ) ].second;
	auto iter = find_if ( group.begin ( ), group.end ( ), [ & ] ( const std::unique_ptr < Entry > & entry ) {
				return entry->getEntryInfo ( ).getCategory ( ) == entryInfo.getCategory ( ) && entry->getEntryInfo ( ).getParams ( ) == entryInfo.getParams ( );
			} );
	if ( iter == group.end ( ) ) {
		if ( templateParams.empty ( ) )
			throw std::invalid_argument ( ext::concat ( "Entry ", algorithm, " with parameters ", entryInfo.getParams ( ), " not registered." ) );
		else
			throw std::invalid_argument ( ext::concat ( "Templated entry ", algorithm, " < ", templateParams, " > with parameters ", entryInfo.getParams ( ), " not registered." ) );
	}
	group.erase ( iter );
	if ( group.empty ( ) )
		getEntries ( ).erase ( ext::tie ( algorithm, templateParams ) );
}

void AlgorithmRegistry::setDocumentation ( const std::string & algorithm, const ext::vector < std::string > & templateParams, const AlgorithmBaseInfo & entryInfo, std::string documentation ) {
	auto & group = getEntries ( ) [ ext::tie ( algorithm, templateParams ) ].second;
	auto iter = find_if ( group.begin ( ), group.end ( ), [ & ] ( const std::unique_ptr < Entry > & entry ) {
				return entry->getEntryInfo ( ).getCategory ( ) == entryInfo.getCategory ( ) && entry->getEntryInfo ( ).getParams ( ) == entryInfo.getParams ( );
			} );
	if ( iter == group.end ( ) ) {
		if ( templateParams.empty ( ) )
			throw std::invalid_argument ( ext::concat ( "Entry ", algorithm, " with parameters ", entryInfo.getParams ( ), " not registered." ) );
		else
			throw std::invalid_argument ( ext::concat ( "Templated entry ", algorithm, " < ", templateParams, " > with parameters ", entryInfo.getParams ( ), " not registered." ) );
	}
	(*iter)->setDocumentation ( std::move ( documentation ) );
}

void AlgorithmRegistry::setCommonDocumentation ( const std::string & algorithm, const ext::vector < std::string > & templateParams, std::string documentation ) {
	auto & documentationEntry = getEntries ( ) [ ext::tie ( algorithm, templateParams ) ].first;
	if ( ! documentationEntry.empty ( ) ) {
		if ( templateParams.empty ( ) )
			throw std::invalid_argument ( ext::concat ( "Documentation of entry ", algorithm, " is already set." ) );
		else
			throw std::invalid_argument ( ext::concat ( "Documentation of templated entry ", algorithm, " < ", templateParams, " > is already set." ) );
	}
	documentationEntry = std::move ( documentation );
}

ext::list < std::unique_ptr < AlgorithmRegistry::Entry > > & AlgorithmRegistry::findAbstractionGroup ( const std::string & name, const ext::vector < std::string > & templateParams ) {
	auto group = getEntries ( ).find ( ext::tie ( name, templateParams ) );
	if ( group == getEntries ( ).end ( ) ) {
		for ( auto iter = getEntries ( ).begin ( ); iter != getEntries ( ).end ( ); ++ iter ) {
			if ( ext::is_same_type ( name, iter->first.first ) && ext::are_same_types ( templateParams, iter->first.second ) ) {
				if ( group == getEntries ( ).end ( ) )
					group = iter;
				else if ( templateParams.empty ( ) )
					throw std::invalid_argument ( ext::concat ( "Name ", name, " is ambigous " ) );
				else
					throw std::invalid_argument ( ext::concat ( "Templated name ", name, " < ", templateParams, " > is ambigous " ) );
			}
		}
	}
	if ( group == getEntries ( ).end ( ) ) {
		if ( templateParams.empty ( ) )
			throw std::invalid_argument ( ext::concat ( "Entry ", name, " not available" ) );
		else
			throw std::invalid_argument ( ext::concat ( "Templated entry ", name, " < ", templateParams, " > not available" ) );
	}
	return group->second.second;
}

std::unique_ptr < abstraction::OperationAbstraction > AlgorithmRegistry::getAbstraction ( const std::string & name, const ext::vector < std::string > & templateParams, const ext::vector < core::type_details > & paramTypes, const ext::vector < abstraction::TypeQualifiers::TypeQualifierSet > & typeQualifiers, AlgorithmCategories::AlgorithmCategory category ) {
	const auto & group = findAbstractionGroup ( name, templateParams );

	try {
		return getOverload ( group, paramTypes, typeQualifiers, category );
	} catch ( ... ) {
		if ( templateParams.empty ( ) )
			std::throw_with_nested ( std::invalid_argument ( ext::concat ( "Entry overload ", name, " ", paramTypes, " not available" ) ) );
		else
			std::throw_with_nested ( std::invalid_argument ( ext::concat ( "Templated entry overload ", name, " < ", templateParams, " > ", paramTypes, " not available" ) ) );
	}
}

ext::set < ext::pair < std::string, ext::vector < std::string > > > AlgorithmRegistry::listGroup ( const std::string & group ) {
	ext::set < ext::pair < std::string, ext::vector < std::string > > > res;

	for ( const std::pair < const ext::pair < std::string, ext::vector < std::string > >, ext::pair < std::string, ext::list < std::unique_ptr < Entry > > > > & entry : getEntries ( ) )
		if ( entry.first.first.find ( group ) == 0 ) //found at the begining
			res.insert ( entry.first );

	return res;
}

ext::list < ext::tuple < AlgorithmFullInfo, std::optional < std::string > > > AlgorithmRegistry::listOverloads ( const std::string & algorithm, const ext::vector < std::string > & templateParams ) {
	auto & group = findAbstractionGroup ( algorithm, templateParams );

	ext::list < ext::tuple < AlgorithmFullInfo, std::optional < std::string > > > res;

	std::transform ( group.begin ( ), group.end ( ), std::back_inserter ( res ), [ ] ( const std::unique_ptr < Entry > & overloads ) {
		return ext::make_tuple ( overloads->getEntryInfo ( ), overloads->getDocumentation ( ) );
	} );

	return res;
}

std::optional < std::string > AlgorithmRegistry::getCommonAlgoDocumentation ( const std::string & algorithm, const ext::vector < std::string > & templateParams ) {
	if ( auto group = getEntries ( ).find ( ext::tie ( algorithm, templateParams ) ); group != getEntries ( ).end ( ) )
		return group->second.first;
	else
		return std::nullopt;
}

ext::set < ext::pair < std::string, ext::vector < std::string > > > AlgorithmRegistry::list ( ) {
	ext::set < ext::pair < std::string, ext::vector < std::string > > > res;

	std::transform ( getEntries ( ).begin ( ), getEntries ( ).end ( ), std::inserter ( res, res.begin ( ) ), [ ] ( const std::pair < const ext::pair < std::string, ext::vector < std::string > >, ext::pair < std::string, ext::list < std::unique_ptr < Entry > > > > & entry ) {
		return entry.first;
	} );

	return res;
}

} /* namespace abstraction */