#include "externals.hh"
#include "control.hh"
#include "globals.hh"
#include "sync.hh"

using namespace cops;


/*= Command execution ========================================================*/

void cops::Execute(
		T_Operations const& operations ,
		T_Context& context )
{
	for ( auto const& op : operations ) {
		op->execute( context );
	}
}

T_Operations& T_Program::function(
		T_String const& name ,
		const uint32_t args )
{
	T_Function* op( functions.get( name ) );
	if ( !op ) {
		functions.add( T_Function{ name , args } );
		op = functions.get( name );
	}
	assert( op->arguments == args );
	return op->operations;
}


/*= X_OpFailure ==============================================================*/

X_OpFailure::X_OpFailure(
		T_String const& source ,
		uint32_t line ,
		T_String&& error ) noexcept
	: std::exception( ) , source_( source ) , line_( line ) ,
		error_( std::move( error ) )
{
	ebcl::T_StringBuilder sb;
	sb << "Program error (" << source << ", l. " << line << "): " << error
		<< '\0';
	fullMessage_ = std::move( sb );
}

char const* X_OpFailure::what( ) const noexcept
{
	return fullMessage_.data( );
}


/*= T_Context ================================================================*/

T_Context& T_Context::store(
		T_String const& name ,
		const float value )
{
	vars.set( name , value );
	return *this;
}


/*= T_Op =====================================================================*/

T_Op::T_Op( const E_Op op )
	: op_( op )
{ }

T_Op::~T_Op( ) { }


X_OpFailure T_Op::error(
		ebcl::T_StringBuilder& message ) const noexcept
{
	return X_OpFailure( source , line , std::move( message ) );
}

X_OpFailure T_Op::error(
		T_String const& message ) const noexcept
{
	return X_OpFailure( source , line , T_String( message ) );
}


/*= OPLoadConstant ===========================================================*/

OPLoadConstant::OPLoadConstant(
		const float constant )
	: T_Op( OP_LOAD_CONSTANT ) , constant( constant )
{ }

void OPLoadConstant::execute(
		T_Context& ctx ) const
{
	ctx.opStack.add( constant );
}


/*= OPLoadVariable ===========================================================*/

OPLoadVariable::OPLoadVariable(
		T_String const& variable )
	: T_Op( OP_LOAD_VARIABLE ) , variable( variable )
{ }

void OPLoadVariable::execute(
		T_Context& ctx ) const
{
	auto const* v( ctx.vars.get( variable ) );
	if ( !v ) {
		ebcl::T_StringBuilder sb;
		sb << "variable '" << variable << "' not found" << '\0';
		throw error( sb );
	}
	ctx.opStack.add( *v );
}


/*= OPLoadInput ==============================================================*/

OPLoadInput::OPLoadInput(
		T_String const& input )
	: T_Op( OP_LOAD_VARIABLE ) , input( input )
{ }

void OPLoadInput::execute(
		T_Context& ctx ) const
{
	const auto i( Globals::Sync( ).inputPos( input ) );
	ctx.opStack.add( Globals::Sync( ).inputs( )[ i ] );
}


/*= OPStoreVariable ==========================================================*/

OPStoreVariable::OPStoreVariable(
		T_String const& variable )
	: T_Op( OP_LOAD_VARIABLE ) , variable( variable )
{ }

void OPStoreVariable::execute(
		T_Context& ctx ) const
{
	if ( ctx.opStack.empty( ) ) {
		throw error( "stack is empty" );
	}

	ctx.store( variable , ctx.opStack.last( ) );
	ctx.opStack.removeLast( );
}


/*= Arithmetic operators =====================================================*/

void OPAdd::execute(
		T_Context& ctx ) const
{
	if ( ctx.opStack.size( ) < 2 ) {
		throw error( "missing operands in stack" );
	}
	const float v( ctx.opStack.last( ) );
	ctx.opStack.removeLast( );
	ctx.opStack.last( ) += v;
}

void OPMul::execute(
		T_Context& ctx ) const
{
	if ( ctx.opStack.size( ) < 2 ) {
		throw error( "missing operands in stack" );
	}
	const float v( ctx.opStack.last( ) );
	ctx.opStack.removeLast( );
	ctx.opStack.last( ) *= v;
}

void OPNeg::execute(
		T_Context& ctx ) const
{
	if ( ctx.opStack.empty( ) ) {
		throw error( "missing operand in stack" );
	}
	ctx.opStack.last( ) = -ctx.opStack.last( );
}

void OPInv::execute(
		T_Context& ctx ) const
{
	if ( ctx.opStack.empty( ) || ctx.opStack.last( ) == 0 ) {
		throw error( "missing operand in stack" );
	}
	ctx.opStack.last( ) = 1.0f / ctx.opStack.last( );
}


/*= Stack operations =========================================================*/

OPDup::OPDup( const uint32_t stackIndex )
	: T_Op( OP_DUP ) , stackIndex( stackIndex )
{ }

void OPDup::execute(
		T_Context& ctx ) const
{
	if ( ctx.opStack.size( ) <= stackIndex ) {
		ebcl::T_StringBuilder sb;
		sb << "stack does not have " << ( stackIndex + 1 )
			<< " items (only " << ctx.opStack.size( ) << ")";
		throw error( sb );
	}
	ctx.opStack.add( *( ctx.opStack.end( ) - stackIndex - 1 ) );
}

/*----------------------------------------------------------------------------*/

OPXchg::OPXchg( const uint32_t stackIndex )
	: T_Op( OP_XCHG ) , stackIndex( stackIndex )
{ }

void OPXchg::execute(
		T_Context& ctx ) const
{
	if ( ctx.opStack.size( ) <= stackIndex ) {
		ebcl::T_StringBuilder sb( "stack does not have " );
		sb << ( stackIndex + 1 ) << " items";
		throw error( sb );
	}
	if ( stackIndex ) {
		std::swap( *( ctx.opStack.end( ) - 1 ) , *( ctx.opStack.end( ) - stackIndex - 1 ) );
	}
}


/*= OPSetUniform =============================================================*/

// GENERAL FIXME
// program identifier should be an entry in a table
// or, you know, a program name

OPSetUniform::OPSetUniform(
		const uint32_t count ,
		const bool integer )
	: T_Op( OP_SET_UNIFORM ) , count( count ) , integer( integer )
{ }

void OPSetUniform::execute(
		T_Context& ctx ) const
{
	if ( count == 0 || ctx.opStack.size( ) < count + 2 ) {
		ebcl::T_StringBuilder sb( "stack does not have " );
		sb << ( count + 2 ) << " items";
		throw error( sb );
	}

	typedef void (*F_SetUniform_)( int , int , int , void* );
	void const* const funcs[] = {
		&glProgramUniform1fv , &glProgramUniform2fv , &glProgramUniform3fv , &glProgramUniform4fv ,
		&glProgramUniform1iv , &glProgramUniform2iv , &glProgramUniform3iv , &glProgramUniform4iv ,
	};
	const F_SetUniform_ func{ *(F_SetUniform_*) funcs[ count + ( integer ? 4 : 0 ) - 1 ] };

	const GLuint program( ctx.opStack.last( ) );
	ctx.opStack.removeLast( );
	const GLuint uniform( ctx.opStack.last( ) );
	ctx.opStack.removeLast( );

	if ( integer ) {
		int values[ count ];
		auto i = count;
		while ( i != 0 ) {
			values[ count - i ] = int( ctx.opStack.last( ) );
			ctx.opStack.removeLast( );
			i --;
		}
		func( program , uniform , 1 , values );
	} else {
		float values[ count ];
		auto i = count;
		while ( i != 0 ) {
			values[ count - i ] = ctx.opStack.last( );
			ctx.opStack.removeLast( );
			i --;
		}
		func( program , uniform , 1 , values );
	}
}


/*= OPUsePipeline ============================================================*/

// GENERAL FIXME
// pipeline identifier should be an entry in a table
// or, you know, a program name

OPUsePipeline::OPUsePipeline(
		const uint32_t index )
	: T_Op( OP_USE_PIPELINE ) , pipeline( index )
{ }

void OPUsePipeline::execute(
		T_Context& ) const
{
	glBindProgramPipeline( pipeline );
}


/*= OPUseTexture =============================================================*/

// GENERAL FIXME
// texture & sampler identifiers should be entries in a table

OPUseTexture::OPUseTexture(
		const uint32_t binding ,
		const uint32_t texture ,
		const uint32_t sampler )
	: T_Op( OP_USE_TEXTURE ) , binding( binding ) ,
		texture( texture ) , sampler( sampler )
{ }

void OPUseTexture::execute(
		T_Context& ) const
{
	glBindTextureUnit( binding , texture );
	glBindSampler( binding , sampler );
}


/*= OPUseFramebuffer =========================================================*/

// GENERAL FIXME
// framebuffer identifier should be an entry in a table

OPUseFramebuffer::OPUseFramebuffer(
		const uint32_t framebuffer )
	: T_Op( OP_USE_FRAMEBUFFER ) , framebuffer( framebuffer )
{ }

void OPUseFramebuffer::execute(
		T_Context& ) const
{
	glBindFramebuffer( GL_FRAMEBUFFER , framebuffer );
}


/*= OPSetViewport ============================================================*/

void OPSetViewport::execute(
		T_Context& ctx ) const
{
	if ( ctx.opStack.size( ) < 4 ) {
		throw error( "stack does not have 4 items" );
	}
	glViewport(
		*( ctx.opStack.end( ) - 1 ) ,
		*( ctx.opStack.end( ) - 2 ) ,
		*( ctx.opStack.end( ) - 3 ) ,
		*( ctx.opStack.end( ) - 4 ) );
	ctx.opStack.resize( ctx.opStack.size( ) - 4 );
}


/*= Draw commands ============================================================*/

void OPClear::execute(
		T_Context& ctx ) const
{
	if ( ctx.opStack.size( ) < 4 ) {
		throw error( "stack does not have 4 items" );
	}
	glClearColor(
		*( ctx.opStack.end( ) - 1 ) ,
		*( ctx.opStack.end( ) - 2 ) ,
		*( ctx.opStack.end( ) - 3 ) ,
		*( ctx.opStack.end( ) - 4 ) );
	ctx.opStack.resize( ctx.opStack.size( ) - 4 );
	glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
}

void OPFullscreen::execute(
		T_Context& ) const
{
	glBindVertexArray( 0 );
	glDrawArrays( GL_TRIANGLE_STRIP , 0 , 4 );
}


/*= Flow control =============================================================*/

OPCall::OPCall(
		T_String const& function )
	: T_Op( OP_CALL ) , function( function )
{ }

void OPCall::execute(
		T_Context& ctx ) const
{
	auto const* fp( ctx.program->functions.get( function ) );
	if ( !fp ) {
		ebcl::T_StringBuilder sb( "function '" );
		sb << function << "' not found" << '\0';
		throw error( sb );
	}

	auto const& func( *fp );
	const auto ssz( ctx.opStack.size( ) );
	if ( ssz < func.arguments ) {
		ebcl::T_StringBuilder sb( "function '" );
		sb << function << "' requires " << func.arguments << " argument"
			<< ( func.arguments > 1 ? "s" : "" ) << '\0';
		throw error( sb );
	}

	Execute( func.operations , ctx );
	if ( ctx.opStack.size( ) < ssz ) {
		ebcl::T_StringBuilder sb( "function '" );
		sb << function << "' ate the stack" << '\0';
		throw error( sb );
	}
	ctx.opStack.resize( ssz - func.arguments );
}