//
// BALLISTI-K INTERPRETER v 0.5
// Russell Bornschlegel, 4/9/2001
//

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
#include <ctype.h>

#ifndef TRUE
#define TRUE 1
#define FALSE 0
#endif

#define MAX_LINE_LENGTH		256
#define PARANOIA			2

#define OPCODE_NOP			0
#define OPCODE_LOAD			1
#define OPCODE_LOADN		2
#define OPCODE_LOADC		3
#define OPCODE_PRINT		4
#define OPCODE_PRINTN		5
#define OPCODE_PRINTC		6
#define OPCODE_THROW		7
#define OPCODE_THROWA		8
#define OPCODE_PASS			9
#define OPCODE_ADD			10
#define OPCODE_SUB			11
#define OPCODE_JUMP			12
#define OPCODE_JZ			13
#define OPCODE_PRINTL		14
#define OPCODE_END			99

// mode flags
int hail_eris = FALSE;		// do throws sometimes miss?
int busker = FALSE;			// are we rewarded for high throws?
int debug = FALSE;			// dump info?

// source code tracking
int lines = 0;
int max_lines = 0;
typedef struct line_s
{
	int opcode;
	int operand;
	char* print_operand;
} line_t;

line_t* line_data = NULL;

// thrown data tracking
typedef struct throw_s
{
	int value;
	int count;
} throw_t;

int throws = 0;
int max_throws = 0;
throw_t* throw_data = NULL;

int chamber = 0;
int accumulator = 0;
int tick = 0;
long throwtotal = 0;

// expand the throw array to support the given number of lines
int expand_throws( int throws_needed )
{
	int i;
	int old = max_throws;
	// do we need a bigger line array?
	if (max_throws < throws_needed)
	{
		// try to double line count
		max_throws *= 2;
		// this handles the initial case where max_throws is 0
		if (max_throws < throws_needed)
		{
			max_throws = throws_needed;
		}
		// get the new line array
		throw_data = (throw_t*)realloc( throw_data, max_throws*sizeof( throw_t ) );
		if (throw_data == NULL)
		{
			fprintf( stderr, "Out of memory (trying to expand to %d throws)\n", max_throws );
			return FALSE;
		}

		for (i = old; i < max_throws; i++)
		{
			throw_data[i].value = 0;
			throw_data[i].count = 0;
		}
	}

	return TRUE;
}

// expand the line array to support the given number of lines
int expand( int lines_needed )
{
	// do we need a bigger line array?
	if (max_lines < lines_needed)
	{
		// try to double line count
		max_lines *= 2;
		// this handles the initial case where max_lines is 0
		if (max_lines < lines_needed)
		{
			max_lines = lines_needed;
		}
		// get the new line array
		line_data = (line_t*)realloc( line_data, max_lines*sizeof( line_t ) );
		if (line_data == NULL)
		{
			fprintf( stderr, "Out of memory (trying to expand to %d lines)\n", max_lines );
			return FALSE;
		}

	}

	return TRUE;
}

void fall( void )
{						
	int i;
	int land = 0;
	int landed = FALSE;

	// for each throw
	for (i = 0; i < max_throws; i++)
	{
		// if the throw hasn't landed yet
		if (throw_data[i].count > 0)
		{
			// countdown
			throw_data[i].count--;
			// if it landed
			if (!throw_data[i].count)
			{
				if (debug)
				{
					fprintf( stderr, "%d landed\n", throw_data[i].value );
				}

				// XOR it into the landing value
				land ^= throw_data[i].value;
				landed = TRUE;
			}
		}
	}

	// if anything landed
	if (landed)
	{
		// put it into the accumulator
		if (debug)
		{
			fprintf( stderr, "accumulator gets %d\n", land );
		}

		accumulator = land;
	}
}

int findthrow( void )
{
	int i;

	// until we find one
	while (1)
	{
		// look at the existing throws
		for (i = 0; i < max_throws; i++)
		{
			// if one is not in use
			if (throw_data[i].count == 0)
			{
				return i;
			}
		}
		// we need at least one more...
		expand_throws(max_throws+1);
	}
}

void dothrow( int value, int count )
{
	int index;

	// check for Discord

	if (hail_eris)
	{
		// N% chance of losing the throw 
		if (rand()%100 < count)
			return;
	}
	// find an empty throw

	index = findthrow();

	throw_data[ index ].count = count;
	throw_data[ index ].value = value;

	if (debug)
	{
		fprintf( stderr, "Throwing %d to tick %d\n", value, tick+count );
	}

	throwtotal += count;
}

int execute( void )
{
	int line = 0;
	int value;
	char cvalue;

	throw_data = NULL;
	throwtotal = throws = max_throws = tick = chamber = accumulator = 0;

	while (1)
	{
		if (debug)
		{
			fprintf( stderr, "Tick %6d Line %6d\n", tick, line );
		}
		// run off the end?
		if (line >= lines)
		{
			return TRUE;
		}
		if (line < 0)
		{
			fprintf( stderr, "Error: Jumped past beginning of program.\n" );
			return FALSE;
		}

		switch (line_data[line].opcode)
		{
		case OPCODE_NOP		:
			if (debug)	fprintf( stderr, "NOP\n" );
			break;
		case OPCODE_LOAD	:	
			if (debug)	fprintf( stderr, "LOAD %d\n", line_data[line].operand );
			chamber = line_data[line].operand;
			break;
		case OPCODE_LOADN	:
			if (debug)	fprintf( stderr, "LOADN\n" );
			scanf( "%d", &value ); 
			chamber = value;
			break;
		case OPCODE_LOADC	:
			if (debug)	fprintf( stderr, "LOADC\n" );
			scanf( "%c", &cvalue ); 
			chamber = cvalue;
			break;
		case OPCODE_PRINT	:
			if (debug)	fprintf( stderr, "PRINT %s\n", line_data[line].print_operand );
			fprintf( stdout, "%s", line_data[line].print_operand );
			break;
		case OPCODE_PRINTN	:
			if (debug)	fprintf( stderr, "PRINTN\n" );
			fprintf( stdout, "%d", accumulator );
			break;
		case OPCODE_PRINTC	:
			if (debug)	fprintf( stderr, "PRINTC\n" );
			fprintf( stdout, "%c", accumulator );
			break;
		case OPCODE_PRINTL	:
			if (debug)	fprintf( stderr, "PRINTL\n" );
			fprintf( stdout, "\n" );
			break;
		case OPCODE_THROW	:
			if (debug)	fprintf( stderr, "THROW %d\n", line_data[line].operand );
			dothrow( chamber, line_data[line].operand );
			break;
		case OPCODE_THROWA	:
			if (debug)	fprintf( stderr, "THROWA\n" );
			dothrow( chamber, accumulator );
			break;
		case OPCODE_PASS	:	
			if (debug)	fprintf( stderr, "PASS\n" );
			chamber = accumulator;
			break;
		case OPCODE_ADD		:
			if (debug)	fprintf( stderr, "ADD\n" );
			chamber += accumulator;
			break;
		case OPCODE_SUB		:
			if (debug)	fprintf( stderr, "SUB\n" );
			chamber -= accumulator;
			break;
		case OPCODE_JUMP	:	
			if (debug)	fprintf( stderr, "JUMP %d\n", line_data[line].operand );
			line += line_data[line].operand;
			break;
		case OPCODE_JZ		:
			if (debug)	fprintf( stderr, "JZ %d\n", line_data[line].operand );
			if (accumulator == 0)
			{
				line += line_data[line].operand;
			}
			break;
		case OPCODE_END		:
			if (debug)	fprintf( stderr, "END\n", line_data[line].operand );
			return TRUE;
			break;
		default:
			fprintf( stdout, "Unrecognized opcode %d", line_data[line].opcode );
			return FALSE;
			break;
		}
		// advance to next line
		line++;
		// drop the throws
		fall();
		// advance to next tick
		tick++;
	}
}

void cleanup( void )
{
	int i;

	// busker reward:

	if (busker)
	{
		if (tick)
		{
			fprintf( stderr, "You earned $%6.02lf\n", ((double)throwtotal)/((double)(tick+1)) );
		}
	}


	// clean up throws

	free( throw_data );

	// clean up string args

	for (i = 0; i < lines; i++)
	{
		if (line_data[i].print_operand != NULL)
		{
			free( line_data[i].print_operand );
		}
	}

	// clean up tokenized code

	free( line_data );
}

int parse_line( char* line, line_t* op )
{
	char* mnemonic;
	char* parameter;

	op->opcode = OPCODE_NOP;
	op->operand = 0;
	op->print_operand = NULL;

	// flatten it to lowercase
	strlwr( line );

   	mnemonic = strtok( line, " \t\n\r" );
   	if (mnemonic == NULL)
   		return FALSE;

   	// recognized comment characters
   	if (	(*mnemonic == '*')	||
   			(*mnemonic == ';')	||
   			(*mnemonic == '#')	||
   			(*mnemonic == '\"')	||
   			(*mnemonic == '\'')	||
   			((mnemonic[0] == '/') && (mnemonic[1] == '/'))	)
   	{
   		return FALSE;
   	}

   	if (strcmp( mnemonic, "load" ) == 0)
   	{
   		// Load a value into the chamber
   		// LOAD <immediate>
   		op->opcode = OPCODE_LOAD;
   		parameter = strtok( NULL, " \t\n\r" );
   		if (parameter)
   		{
   			op->operand = atoi( parameter );
   		}
   		else
   		{
   			fprintf( stderr, "Warning: LOAD operation with no parameter, ignored.\n" );
   			return FALSE;
   		}
   	}
   	else if (strcmp( mnemonic, "loadn" ) == 0)
   	{
   		// Load a number from stdin into the chamber
   		// LOADN
   		op->opcode = OPCODE_LOADN;

   	}
   	else if (strcmp( mnemonic, "loadc" ) == 0)
   	{
   		// Load a character from stdin into the chamber
   		// LOADC
   		op->opcode = OPCODE_LOADC;
   	}
   	else if (strcmp( mnemonic, "print" ) == 0)
   	{
   		// Print arbitrary text
   		// PRINT Text
   		op->opcode = OPCODE_PRINT;
   		parameter = strtok( NULL, "\n\r" );
		op->print_operand = strdup( parameter );
   	}
   	else if (strcmp( mnemonic, "printn" ) == 0)
   	{
   		// Print A as a number
   		// PRINTN
   		op->opcode = OPCODE_PRINTN;
   	}
   	else if (strcmp( mnemonic, "printc" ) == 0)
   	{
   		// Print A as a character
   		// PRINTC
   		op->opcode = OPCODE_PRINTC;
   	}
   	else if (strcmp( mnemonic, "printl" ) == 0)
   	{
   		// Print newline
   		// PRINTL
   		op->opcode = OPCODE_PRINTL;
   	}
   	else if (strcmp( mnemonic, "throw" ) == 0)
   	{
   		// Throw the contents of the chamber to land in # clocks
   		// THROW <immediate>
   		op->opcode = OPCODE_THROW;
   		parameter = strtok( NULL, " \t\n\r" );
   		if (parameter)
   		{
   			op->operand = atoi( parameter );
   		}
   		else
   		{
   			fprintf( stderr, "Warning: THROW operation with no parameter, ignored.\n" );
   			return FALSE;
   		}
   	}
   	else if (strcmp( mnemonic, "throwa" ) == 0)
   	{
   		// Throw the contents of the chamber to land in A clocks
   		// THROWA
   		op->opcode = OPCODE_THROWA;
   	}
   	else if (strcmp( mnemonic, "pass" ) == 0)
   	{
   		// Pass from A to the chamber
   		// PASS
   		op->opcode = OPCODE_PASS;
   	}
   	else if (strcmp( mnemonic, "add" ) == 0)
   	{
   		// Add A to the chamber
   		// ADD
   		op->opcode = OPCODE_ADD;
   	}
   	else if (strcmp( mnemonic, "sub" ) == 0)
   	{
   		// Subtract A from the chamber
   		// SUB
   		op->opcode = OPCODE_SUB;
   	}
   	else if (strcmp( mnemonic, "jump" ) == 0)
   	{
   		// Unconditional jump
   		// JUMP <offset>
   		op->opcode = OPCODE_JUMP;
   		parameter = strtok( NULL, " \t\n\r" );
   		if (parameter)
   		{
   			op->operand = atoi( parameter );
   		}
   		else
   		{
   			fprintf( stderr, "Warning: JUMP operation with no parameter, ignored.\n"  );
   			return FALSE;
   		}
   	}
   	else if (strcmp( mnemonic, "jz" ) == 0)
   	{
   		// Conditional jump
   		// JZ <offset>
   		op->opcode = OPCODE_JZ;
   		parameter = strtok( NULL, " \t\n\r" );
   		if (parameter)
   		{
   			op->operand = atoi( parameter );
   		}
   		else
   		{
   			fprintf( stderr, "Warning: JZ operation with no parameter, ignored.\n"	  );
   			return FALSE;
   		}
   	}
   	else if (strcmp( mnemonic, "nop" ) == 0)
   	{
   		// No operation
   		// NOP
   		op->opcode = OPCODE_NOP;
   	}
   	else if (strcmp( mnemonic, "end" ) == 0)
   	{
   		// End program
   		// END
   		op->opcode = OPCODE_END;
   	}
   	else
   	{
   	 	fprintf( stderr, "Warning: Unrecognized instruction %s, ignored.\n", mnemonic );
   		return FALSE;
   	}

	return TRUE;
}

// parse an open source code filestream
// returns TRUE if parse stage worked and we can execute
// returns FALSE if there was a parsing error

int parse_open_file( FILE* stream )
{
	char new_line[ MAX_LINE_LENGTH+PARANOIA ];

	// while not eof
	while (!feof( stream ))
	{
		// make sure we have room for another line
		if (expand( lines+1 ) == FALSE)
		{
			// expand failed, quit parsing
			return FALSE;
		}

		// read the line up to length-1 chars only
		fgets( new_line, MAX_LINE_LENGTH, stream ); 
		// tokenize it
		line_data[lines].opcode = OPCODE_END;
		line_data[lines].operand = 0;

		if (parse_line( new_line, &line_data[lines] ))
		{
			// if successful, it's one more line
			lines++;
		}
	}

   	// execute the program
   	execute();
   	// clean up the program (line data and throw data)
   	cleanup();
	return TRUE;
}

// parse a file by name
void parse_file( char* filename )
{
	FILE* f;

	// open the file
	f = fopen( filename, "rt" );
	// did it open?
	if (f)
	{
		// if it opened, parse it
		parse_open_file( f );
		// ...and close it
		fclose( f );
	}
	else
	{
		fprintf( stderr, "Error: Couldn't open file %s\n", filename );
	}
}

// parse an option string
void parse_option( char* option )
{
	switch( option[1] )
	{
	case 0:
		parse_open_file( stdin );
		break;

	case 'k':
		fprintf( stderr, "Hail Eris! Kallisti-B mode enabled.\n"  );
		hail_eris = TRUE;
		break;

	case 'd':
		fprintf( stderr, "Don't trust me? Debug mode enabled.\n"  );
		debug = TRUE;
		break;

	case 'b':
		fprintf( stderr, "Spare change? Busker mode enabled.\n"  );
		busker = TRUE;
		break;

	default:
		fprintf( stderr, "Unrecognized option: %s\n", option );
		break;
	}
}

int main( int argc, char* argv[] )
{
	int i;

	// randomize
	srand( time( NULL ) );

	// if we got no args, print usage
	if (argc == 1)
	{
		fprintf( stderr, "Usage: ballistik [options...] [filename...]\n" );
		fprintf( stderr, "    options:\n" );
		fprintf( stderr, "    -k         Kallisti-B mode\n" );
		fprintf( stderr, "    -b         Busker mode\n" );
		fprintf( stderr, "    -d         Debug mode\n" );
		fprintf( stderr, "    -          Use stdin as source\n" );
	}


	// for each arg
	for (i = 1; i < argc; i++)
	{
		// is it an option?
		if (argv[i][0] == '-')
		{
			// handle the option
			parse_option( argv[i] );
		}
		else
		{
			// handle the filename
			parse_file( argv[i] );
		}
	}
}