/*****************************************************************************
 * $Id: gfx_machine.cc,v 1.6 2006/08/01 23:14:02 devel Exp $
 * @file
 * @author	Jim E. Brooks  http://www.jimbrooks.org
 * @brief	State-machine.
 *//*
 * LEGAL:	COPYRIGHT (C) 2005 JIM E. BROOKS
 *          THIS SOURCE CODE IS RELEASED UNDER THE TERMS
 *          OF THE GNU GENERAL PUBLIC LICENSE (GPL).
 *****************************************************************************/

#define GFX_MACHINE_CC 1
#include "gfx.hh"
#include "gfx_stream.hh"
#include "gfx_machine.hh"
namespace gfx {

/*****************************************************************************
 * Constructor.
 *****************************************************************************/
Machine::Machine( Opcode* ip, void* userData )
{
	Init( ip, userData );
}
// For delaying setting IP to Tick(Opcode*).
Machine::Machine( void* userData )
{
	Init( NULL, userData );
}
void
Machine::Init( Opcode* ip, void* userData )
{
	SetIP( ip );						// also sets PROC
	mFLAG		= false;
	mCLOCK		= -1;					// to be incremented to 0
	mUserData	= userData;
}

/*****************************************************************************
 * Switch the machine to another procedure.
 *****************************************************************************/
void
Machine::Switch( Opcode* ip )
{
	SetIP( ip );
}

/*****************************************************************************
 * Set IP to the start of a procedure.
 * NULL is allowed.
 *****************************************************************************/
void
Machine::SetIP( Opcode* ip )
{
	// IP should point to start of a procedure (but NULL is allowed).
#if DEBUG
	if ( EX(ip != NULL) ) ASSERT( *ip == OPC_PROC_BEGIN );
#endif
	mIP			= ip;
#if DEBUG
	mPROC		= ip;
#endif
}

/*****************************************************************************
 * Clock ticks drive the machine which is a virtual processor.
 * @return True if machine has suspended, or false if machine quit or error.
 *****************************************************************************/
bool
Machine::Tick( void )
{
	// Count this tick.
	++mCLOCK;

	// The machine continually decodes/executes instructions
	// until it quits, suspends, or an error occurs.
	eMachineState state;
	do { state = Execute(); } while ( state == eMachineState_RUNNING );

	// Return true if machine was suspended (implies continuing the machine on next clock).
	return state == eMachineState_SUSPEND;
}

/*****************************************************************************
 * Decode/execute one instruction.
 * @return eMachineState (suspend/quit/error).
 *****************************************************************************/
Machine::eMachineState
Machine::Execute( void )
{
// Jump if condition is true, else point IP over JUMP instruction.
#define EXEC_JUMP( cond )											\
	if ( cond )														\
		mIP = reinterpret_cast<Opcode*>( mIP[1] );					\
	else															\
		mIP += OPC_JUMP_LEN;

// Call if condition is true, else point IP over CALL instruction.
// To do CALL, push address after CALL instruction and update mPROC.
#define EXEC_CALL( cond )											\
	if ( cond )														\
	{																\
		mSTACK.push( mIP + OPC_CALL_LEN );							\
		mIP = reinterpret_cast<Opcode*>( mIP[1] );					\
		ASSERT( *mIP == OPC_PROC_BEGIN );							\
		DEBUG_CODE( mSTACK_PROC.push( mPROC ); )					\
		DEBUG_CODE( mPROC = mIP; )									\
	}																\
	else															\
		mIP += OPC_CALL_LEN;										\

#if DEBUG == 2
	// Print instruction.
	*gStreamDebug << "Machine:" << GetProcName() << ':' << GetOpcodeName( *mIP ) << endl;
#endif

	// -- Dave asked, "Hal, what are you doing?". --
	// -- "You've crossed over into the Twilight Zone." --
	// Has IP gone past the current procedure into nowhere?
#if DEBUG
	ASSERT( long(mIP) >= long(mPROC) );
	ASSERT( long(mIP)  < long(mPROC + PROC_MAX_LEN) );
#endif

	// -- "Mmmm...spaghetti code...aahhhgg...[drool]", Homer Simpson. --

	// Decode/execute one instruction.
	// mIP is to be advanced to the next instruction.
	// mFLAG holds the result of the last OPC_FUNC.
	eMachineState state = eMachineState_RUNNING;
	switch ( *mIP )
	{
		case OPC_FUNC:
		{
Func:
			// Opcode for calling a C function.
			// The C function returns a bool which is saved
			// in mFLAG for possible use by conditional jumps.
			// mIP[0] = OPC_FUNC
			// mIP[1] = address of C function
			MachineFunc func = reinterpret_cast<MachineFunc>( mIP[1] );
			mFLAG = func( this );
			mIP += OPC_FUNC_LEN;	// skip over this instruction
#if DEBUG == 2
			*gStreamDebug << "Machine:" << GetProcName() << ":FLAG=" << mFLAG << endl;
#endif
		}
		break;

		case OPC_FUNC_T:
		{
			// Call C function if FLAG is true.
			if ( mFLAG )
				goto Func;
			else
				mIP += OPC_FUNC_LEN;
		}
		break;

		case OPC_FUNC_F:
		{
			// Call C function if FLAG is false.
			if ( ! mFLAG )
				goto Func;
			else
				mIP += OPC_FUNC_LEN;
		}
		break;

		case OPC_JUMP:
		{
			// Unconditional jump.
			EXEC_JUMP( true );
		}
		break;

		case OPC_JUMP_T:
		{
			// Jump if flag is true.
			EXEC_JUMP( mFLAG );
		}
		break;

		case OPC_JUMP_F:
		{
			// Jump if flag is false.
			EXEC_JUMP( ! mFLAG );
		}
		break;

		case OPC_CALL:
		{
			// Unconditionally call a procedure.
			EXEC_CALL( true );
		}
		break;

		case OPC_CALL_T:
		{
			// Call a procedure if flag is true.
			EXEC_CALL( mFLAG );
		}
		break;

		case OPC_CALL_F:
		{
			// Call a procedure if flag is false.
			EXEC_CALL( ! mFLAG );
		}
		break;

		case OPC_RETURN: case OPC_PROC_END:
		{
Return:
			if ( EX( ! mSTACK.empty() ) )
			{
				// Return.  Pop IP from procedure stack.
				mIP = mSTACK.top();
				mSTACK.pop();			// STL idiom: pop() removes but returns nothing
#if DEBUG
				mPROC = mSTACK_PROC.top();
				mSTACK_PROC.pop();
#endif
			}
			else
			{
				// Procedure stack is empty which should mean
				// the first procedure ended.
				goto Quit;
			}
		}
		break;

		case OPC_RETURN_T:
		{
			// Return from procedure if flag is true.
			if ( mFLAG )
				goto Return;
			else
				mIP += OPC_RETURN_LEN;
		}
		break;

		case OPC_RETURN_F:
		{
			// Return from procedure if flag is false.
			if ( ! mFLAG )
				goto Return;
			else
				mIP += OPC_RETURN_LEN;
		}
		break;

		case OPC_NOP:
		{
			mIP += OPC_NOP_LEN;
		}
		break;

		case OPC_NOP2:
		{
			// NOP2 skips two elements.
			mIP += OPC_NOP2_LEN;
		}
		break;

		case OPC_PROC_BEGIN:
		{
			// Beginning of a procedure.
			// OPC_PROC_BEGIN is used to hold a compile flag,
			// and the name of the procedure for debugging.

			// Remember address of procedure.
			DEBUG_CODE( mPROC = mIP; )
	
			// Has the procedure been compiled?
			if ( UX( ! mIP[OPC_PROC_BEGIN_COMPILE_FLAG_IDX] ) ) // check compiledFlag
			{
				Compile();
				mIP[OPC_PROC_BEGIN_COMPILE_FLAG_IDX] = true;
			}

			mIP += OPC_PROC_BEGIN_LEN;
		}
		break;

		case OPC_SUSPEND:
		{
			// Suspend execution of machine.
			state = eMachineState_SUSPEND;
			mIP += OPC_SUSPEND_LEN;
		}
		break;

		case OPC_QUIT:
		{
Quit:
			// Quit execution of machine.
			state = eMachineState_QUIT;
			mIP = NULL;
			mUserData = NULL;
		}
		break;

		default:
		{
#if DEBUG
			*gStreamDebug << "Machine: invalid opcode (" << GetOpcodeName( *mIP ) << ") in procedure " << GetProcName() << endl;
			ASSERT(0);
#endif
			goto Quit;
		}
		break;
	}

	return state;
}

/*****************************************************************************
 * Compile a state-machine procedure.
 * @pre mIP points to the start of a procedure.
 * @verbatim
 * Compiling resolves label addresses which are the targets of jump instructions.
 * Compiling does these steps:
 * - Scans for MNEM_LABEL, stores the labeled addresses, overwrites with OPC_NOP.
 *   MNEM_LABEL, "again",
 *   OPC_NOP, OPC_NOP,    // post-compile
 * - Scans for MNEM_JZ/JNZ/JMP, overwrites the string pointers with labeled addresses.
 *   MNEM_JMP, "again"
 *   OPC_JMP, (an address cast as a long),		// post-compile
 * @endverbatim
 * Error-checking:
 * Missing OPC_PROC_END cannot be caught in all cases since the bytes that follow
 * could fool any self-checking.
 *****************************************************************************/
void
Machine::Compile( void )
{
	// -----------------------------------------------------
	// ASSUMPTION:
	// The loops will encounter two kinds of elements that aren't opcodes:
	// the compiledFlag (is either 0 or 1) or an address (to string or a C function).
	// The former is handled by reserving OPC_0 and OPC_1.
	// The latter is handled by relying on addresses to be >= 4096
	// which is far outside the range of the OPC/MNEM enums,
	// so an address won't be misinterpreted as a OPC/MNEM.
	// -----------------------------------------------------

#if DEBUG
	// Procedures must begin with OPC_PROC_BEGIN.
	if ( UX( *mIP != OPC_PROC_BEGIN ) )
	{
		*gStreamDebug << "SM compiler: procedures must begin with OPC_PROC_BEGIN \n";
		exit( 1 );
	}
	const char* procName = reinterpret_cast<const char*>( mIP[OPC_PROC_BEGIN_PROC_NAME_IDX] );
#endif

	// Skip OPC_PROC_BEGIN_LEN (avoid misinterpreting compiledFlag as an opcode).
	Opcode* const ipStart = mIP + OPC_PROC_BEGIN_LEN;

	// Maps labels with addresses.
	typedef map<string,Opcode*> LabelMap;
	LabelMap labelMap;

	// Scan for labels.  Map labels with addresses.
	for ( Opcode* ip = ipStart; *ip != OPC_PROC_END; )
	{
		ASSERT( ip - ipStart < PROC_MAX_LEN );

		if ( *ip == MNEM_LABEL )
		{
			// Insert label/address into a map.
			string label = reinterpret_cast<char*>( ip[1] );
			Opcode* address = ip + MNEM_LABEL_LEN;	// skip NOPs for speed
#if DEBUG
			if ( UX( labelMap.find( label ) != labelMap.end() ) ) // label shouldn't be mapped yet
			{
				*gStreamDebug << "SM[" << procName << "]: label '" << label << "' redefined \n";
				exit( 1 );
			}
#endif
			labelMap.insert( make_pair( label, address ) );

			// Overwrite with NOPs.
			//ip[0] = ip[1] = OPC_NOP;
			ip[0] = OPC_NOP2;		// NOP2 optimization

			ip += MNEM_LABEL_LEN;
		}
		else
		{
			++ip;
		}
	}

	// Now that labels are mapped to addresses, resolve MNEM_JUMP* mnemonics.
	// MNEM_JUMP* are phony instructions for jumping to labels.
	// They must be compiled into OPC_JUMP* instructions with
	// actual addresses rather than labels.
	for ( Opcode* ip = ipStart; *ip != OPC_PROC_END; )
	{
		ASSERT( ip - ipStart < PROC_MAX_LEN );

		// If any JUMP mnemonic.
		if ( IF_A_MNEM_JUMP( ip[0] ) )
		{
			// ip[0]: Change MNEM_JUMP* to its OPC_JUMP* counterpart.
			ip[0] = ip[0] - MNEM_JUMP + OPC_JUMP;

			// ip[1]: Change label to address.
			string label = reinterpret_cast<char*>( ip[1] );
			LabelMap::iterator itr = labelMap.find( label );
#if DEBUG
			if ( UX( itr == labelMap.end() ) )
			{
				*gStreamDebug << "SM[" << procName << "]: jump target '" << label << "' not defined. \n";
				exit( 1 );
			}
#endif
			Opcode* address = itr->second;
			ip[1] = reinterpret_cast<Opcode>( address );
			ip += MNEM_JUMP_LEN;
		}
		else if ( *ip == MNEM_RERUN )
		{
			// MNEM_RERUN means to re-run the current procedure.
			// DEF_RERUN() expands to { MNEM_RERUN, 0 }
			ip[0] = OPC_JUMP;
			ip[1] = reinterpret_cast<Opcode>( ipStart );
			ip += MNEM_RERUN_LEN;
		}
		else
		{
			++ip;
		}
	}
}

/*****************************************************************************
 * Return name of current procedure.
 *****************************************************************************/
#if DEBUG
const char*
Machine::GetProcName( void )
{
	// mPROC points at start of procedure, ie, at OPC_PROC_BEGIN.
	// { OPC_PROC_BEGIN, "procedureName", compileFlag }
	return reinterpret_cast<const char*>( mPROC[OPC_PROC_BEGIN_PROC_NAME_IDX] );
}
#endif // DEBUG

/*****************************************************************************
 * Return name of opcode.
 *****************************************************************************/
#if DEBUG
const char*
Machine::GetOpcodeName( Machine::Opcode opc )
{
	switch ( opc )
	{
		case OPC_FUNC:		 return "OPC_FUNC";		  break;
		case OPC_FUNC_T:	 return "OPC_FUNC_T";	  break;
		case OPC_FUNC_F:	 return "OPC_FUNC_F";	  break;
		case OPC_JUMP:		 return "OPC_JUMP";		  break;
		case OPC_JUMP_T:	 return "OPC_JUMP_T";	  break;
		case OPC_JUMP_F:	 return "OPC_JUMP_F";	  break;
		case OPC_CALL:		 return "OPC_CALL";		  break;
		case OPC_CALL_T:	 return "OPC_CALL_T";	  break;
		case OPC_CALL_F:	 return "OPC_CALL_F";	  break;
		case OPC_RETURN:	 return "OPC_RETURN";	  break;
		case OPC_RETURN_T:	 return "OPC_RETURN_T";	  break;
		case OPC_RETURN_F:	 return "OPC_RETURN_F";	  break;
		case OPC_SUSPEND:	 return "OPC_SUSPEND";	  break;
		case OPC_QUIT:		 return "OPC_QUIT";		  break;
		case OPC_NOP:		 return "OPC_NOP";		  break;
		case OPC_NOP2:		 return "OPC_NOP2";		  break;
		case OPC_PROC_BEGIN: return "OPC_PROC_BEGIN"; break;
		case OPC_PROC_END:	 return "OPC_PROC_END";	  break;
	}
	return "OPC (UNKNOWN)";
}
#endif // DEBUG

} // namespace gfx
