#ifndef _ENVIRONMENT_H_
#define _ENVIRONMENT_H_

#include <alib/string>
#include <alib/map>
#include <alib/memory>
#include <alib/typeinfo>

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

#include <abstraction/ValueHolder.hpp>

namespace cli {

class Environment {
	ext::map < std::string, std::string > m_bindings;
	ext::map < std::string, std::shared_ptr < abstraction::Value > > m_variables;
	std::shared_ptr < abstraction::Value > m_result;

	std::unique_ptr < Environment > m_upper;

	std::shared_ptr < abstraction::Value > getVariableInt ( const std::string & name ) const {
		auto res = m_variables.find ( name );

		if ( res != m_variables.end ( ) )
			return res->second;

		if ( ( bool ) m_upper )
			return m_upper->getVariable ( name );
		else
			throw exception::CommonException ( "Variable of name " + name + " not found." );
	}

	void setVariableInt ( std::string name, std::shared_ptr < abstraction::Value > value ) {
		m_variables [ std::move ( name ) ] = std::move ( value );
	}

public:
	Environment ( ) = default;

	Environment ( std::unique_ptr < Environment > upper ) : m_upper ( std::move ( upper ) ) {
	}

	std::string getBinding ( const std::string & name ) const {
		auto res = m_bindings.find ( name );

		if ( res != m_bindings.end ( ) )
			return res->second;

		if ( ( bool ) m_upper )
			return m_upper->getBinding ( name );
		else
			throw exception::CommonException ( "Binded value of name " + name + " not found." );
	}

	std::set < std::string > getBindingNames ( ) const {
		std::set < std::string > res;

		for ( const std::pair < const std::string, std::string > & kv : m_bindings ) {
			res.insert ( kv.first );
		}

		return res;
	}

	void setBinding ( std::string name, std::string value ) {
		m_bindings [ std::move ( name ) ] = std::move ( value );
	}

	bool clearBinding ( const std::string & name ) {
		return m_bindings.erase ( name );
	}

	std::shared_ptr < abstraction::Value > getVariable ( const std::string & name ) const {
		return getVariableInt ( name );
	}

	template < class T >
	const T & getVariable ( const std::string & name ) const {
		return abstraction::retrieveValue < const T & > ( getVariableInt ( name ), false );
	}

	std::set < std::string > getVariableNames ( ) const {
		std::set < std::string > res;

		for ( const std::pair < const std::string, std::shared_ptr < abstraction::Value > > & kv : m_variables ) {
			res.insert ( kv.first );
		}

		return res;
	}

	void setVariable ( std::string name, std::shared_ptr < abstraction::Value > value ) {
		setVariableInt ( std::move ( name ), std::move ( value ) );
	}

	template < class T >
	void setVariable ( std::string name, T value ) {
		auto variable = std::make_shared < abstraction::ValueHolder < T > > ( nullptr, std::move ( value ) );
		setVariableInt ( std::move ( name ), variable );
	}

	bool clearVariable ( const std::string & name ) {
		return m_variables.erase ( name );
	}

	void setResult ( std::shared_ptr < abstraction::Value > value ) {
		m_result = std::move ( value );
	}

	int getResult ( ) const {
		if ( m_result ) {
			std::shared_ptr < abstraction::ValueHolderInterface < int > > ptr1 = std::dynamic_pointer_cast < abstraction::ValueHolderInterface < int > > ( m_result );
			if ( ptr1 )
				return ptr1->getValue ( );
			std::shared_ptr < abstraction::ValueHolderInterface < unsigned > > ptr2 = std::dynamic_pointer_cast < abstraction::ValueHolderInterface < unsigned > > ( m_result );
			if ( ptr2 )
				return ptr2->getValue ( );
			std::shared_ptr < abstraction::ValueHolderInterface < bool > > ptr3 = std::dynamic_pointer_cast < abstraction::ValueHolderInterface < bool > > ( m_result );
			if ( ptr3 )
				return static_cast < int > ( ! ptr3->getValue ( ) );

			throw exception::CommonException ( "Invalid result type. Provided: " + m_result->getType ( ) );
		} else {
			return 0;
		}
	}
};

} /* namespace cli */

#endif /* _ENVIRONMENT_H_ */
