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

#ifndef _WRAPPER_ABSTRACTION_HPP_
#define _WRAPPER_ABSTRACTION_HPP_

#include <abstraction/OperationAbstraction.hpp>

namespace abstraction {

template < class ... ParamTypes >
class WrapperAbstraction : public OperationAbstraction {
	std::function < std::shared_ptr < OperationAbstraction > ( ParamTypes ... ) > m_WrapperFinder;

	ext::variant < void, std::shared_ptr < OperationAbstraction > > m_data;

protected:
	ext::tuple < std::shared_ptr < ValueProvider < ParamTypes > > ... > m_params;
	std::array < bool, sizeof ... ( ParamTypes ) > m_moves;

public:
	WrapperAbstraction ( std::function < std::shared_ptr < OperationAbstraction > ( ParamTypes ... ) > wrapperFinder ) : m_WrapperFinder ( wrapperFinder ) {
		for ( unsigned i = 0; i < sizeof ... ( ParamTypes ); ++ i )
			m_moves [ i ] = false;
	}

private:
	virtual bool attachInput ( const std::shared_ptr < OperationAbstraction > & input, unsigned index, bool move ) override {
		if ( index >= m_moves.size ( ) )
			throw std::invalid_argument ( "Parameter index " + ext::to_string ( index ) + " out of bounds.");

		auto attachCallback = [ & ] ( auto & param ) {
			if ( param != nullptr )
				return false;

			typename std::decay < decltype ( param )>::type validData = std::dynamic_pointer_cast < typename std::decay < decltype ( param ) >::type::element_type > ( input->getProxyAbstraction ( ) );
			if ( validData ) {
				m_moves [ index ] = move;
				param = validData;
				return true;
			} else {
				return false;
			}
		};

		return ext::call_on_nth < bool > ( m_params, index, attachCallback );
	}

	virtual bool detachInput ( unsigned index ) override {
		if ( index >= m_moves.size ( ) )
			throw std::invalid_argument ( "Parameter index " + ext::to_string ( index ) + " out of bounds.");

		m_moves [ index ] = false;

		auto detachCallback = [ & ] ( auto & param ) {
			param = nullptr;
			return true;
		};

		return ext::call_on_nth < bool > ( m_params, index, detachCallback );
	}

public:
	virtual bool inputsReady ( ) const override {
		auto readyCallback = [ ] ( const auto & param ) {
			return ( bool ) param && param->isReady ( );
		};

		return ext::all_of ( m_params, readyCallback );
	}

	virtual bool inputsAttached ( ) const override {
		auto attachedCallback = [ ] ( const auto & param ) {
			return ( bool ) param;
		};

		return ext::all_of ( m_params, attachedCallback );
	}

private:
	template < typename ... Ts, size_t ... Indexes >
	inline void finder_helper ( const ext::tuple < Ts ... > & params, const std::array < bool, sizeof ... ( Indexes ) > moves, std::index_sequence < Indexes ... > ) {
		/* make unused parameter warning go away in case of sizeof ... ( Ts ) == 0 */
		( void ) params;
		( void ) moves;

		m_data = m_WrapperFinder ( std::get < Indexes > ( params )->getValue ( std::get < Indexes > ( moves ) ) ... );
	}

public:
	virtual bool run ( ) override {
		if ( m_data.is < void > ( ) )
			this->finder_helper ( this->m_params, this->m_moves, std::make_index_sequence < sizeof ... ( ParamTypes ) > { } );

		auto getParam = [ & ] ( auto & param ) {
			return std::dynamic_pointer_cast < abstraction::OperationAbstraction > ( param );
		};

		std::shared_ptr < OperationAbstraction > abstraction = m_data.template get < std::shared_ptr < OperationAbstraction > > ( );
		for ( unsigned index = 0; index < sizeof ... ( ParamTypes ); ++ index )
			abstraction->attachInput ( ext::call_on_nth < std::shared_ptr < abstraction::OperationAbstraction > > ( m_params, index, getParam ), index, m_moves [ index ] );

		return abstraction->run ( );
	}

	virtual bool eval ( ) override {
		if ( ! inputsAttached ( ) )
			return false;

		if ( this->cached ( ) )
			return true;

		auto evalCallback = [ ] ( const auto & param ) {
			return param->eval ( );
		};

		if ( ! ext::all_of ( m_params, evalCallback ) )
			return false;

		return this->run ( );
	}

	virtual unsigned numberOfParams ( ) const override {
		return sizeof ... ( ParamTypes );
	}

	virtual bool isReady ( ) const override {
		return m_data.template is < std::shared_ptr < OperationAbstraction > > ( ) && m_data.template get < std::shared_ptr < OperationAbstraction > > ( )->isReady ( );
	}

	virtual bool cached ( ) const override {
		return isReady ( );
	}

	virtual ext::type_index getParamTypeIndex ( unsigned index ) const override {
		auto callback = [ & ] ( auto & param ) {
			return ext::type_index ( typeid ( typename std::decay < decltype ( param ) >::type::element_type::return_type ) );
		};
		return ext::call_on_nth < ext::type_index > ( m_params, index, callback );
	}

	virtual ext::type_index getReturnTypeIndex ( ) const override {
		return getRuntimeReturnTypeIndex ( );
	}

	virtual ext::type_index getRuntimeReturnTypeIndex ( ) const override {
		if ( isReady ( ) )
			return m_data.template get < std::shared_ptr < OperationAbstraction > > ( )->getProxyAbstraction ( )->getRuntimeReturnTypeIndex ( );
		else
			throw std::domain_error ( "Runtime type unknown before evaluation." );
	}

	virtual std::shared_ptr < abstraction::OperationAbstraction > getProxyAbstraction ( ) override {
		if ( isReady ( ) )
			return m_data.template get < std::shared_ptr < OperationAbstraction > > ( )->getProxyAbstraction ( );
		else
			throw std::domain_error ( "Runtime type unknown before evaluation." );
	}

};

} /* namespace abstraction */

#endif /* _WRAPPER_ABSTRACTION_HPP_ */