#ifndef _ENVIRONMENT_H_
#define _ENVIRONMENT_H_

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

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

#include <abstraction/ValueHolder.hpp>
#include <command/CommandResult.h>
#include <lexer/CharSequence.h>

namespace cli {

class Command;

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;

	ext::optional_ref < 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 ( ext::optional_ref < 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 );
	}

	std::shared_ptr < abstraction::Value > getResult ( ) const {
		return m_result;
	}

	cli::CommandResult execute ( std::shared_ptr < cli::LineInterface > lineInterface );

	cli::CommandResult execute_line ( cli::CharSequence charSequence );

	Environment & getGlobalScope ( ) {
		if ( ! ( bool ) m_upper )
			return * this;
		return getGlobalScope ( );
	}
};

} /* namespace cli */

#endif /* _ENVIRONMENT_H_ */
