/*

THIS SOFTWARE IS DISTRIBUTED UNDER GPL LICENSE

Copyright (C) 2006  Lukasz Grzegorz Maciak
http://terminally-incoherent.com

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
02110-1301, USA or visit http://www.gnu.org/licenses/gpl.txt.

*/


/*
 * Created on Apr 16, 2005
 */

/**
 * @author Lukasz Grzegorz Maciak
 *
 * This code contains Pass 1 of the assembler.
 *
 */

import java.io.*;
import java.util.*;


public class Assembler
{
	// compiler command line static paramerers - these are used
	// only when the class is called from the command line via
	// the main function
	private static boolean verbose = false; // if set more output
	private static boolean debug = false; // switches on printing stack traces on exceptions
	private static boolean show_sym = false; // if set display symtab
	private static boolean show_op = false; // if set display optab
	private static boolean p1_only = false; // if set do pass 1 only

	private static String lang_def = "sic.op"; // the default loc of language def file
	private static String target = ""; // usually target name is defined at runtime

	private static int input_at = 0; // iternal use - where is input file in cmd param list
	private static int target_at = 0; // internal use - which agument is the target name
	private static boolean t = false; // use target as output name


	// ### Object Vars ###

	//	language definition - note hat java.util.Properties is derived
	//	from the Hashtable class so this is essentially a modified hashtable
	private Properties OPTAB;

	//	SYMTAB is implemented using java.util.Hashtable class
	private Hashtable SYMTAB;

	// IN is the name of the program file to be assembled
	// LANG is the name of the file where the opcode values are defined
	private final String IN, LANG;

	// program length after 1st pass
	private int length;

	/**
	 * Main Class with which you initialize the program
	 *
	 * @param args - command line arguments
	 */
	public static void main(String[] args)
	{
		System.out.println("\nSIC Assembler: ");

		// display usage if the class is called with no arguments
		if(args.length <= 0 )
		{
			System.out.println("\nUsage: AssemblerPass1 [-vdlos1t] [lang_def.op] program.sic\n");
			System.out.println("\t[v]\t verbose (display all debuging info)");
			System.out.println("\t[d]\t debug mode - very verbose (verbose mode + print stack traces on exceptions)");
			System.out.println("\t[l]\t load different language file specified as first argument (program file goes second)");
			System.out.println("\t[o]\t display OPTAB");
			System.out.println("\t[s]\t display SYMTAB");
			System.out.println("\t[1]\t pass 1 only");
			System.out.println("\t[t]\t target - use the argument given as the name of target file");

			System.exit(0);
		}

		// if the argument starts with - then parse the argument list for
		// special flags and indicators
		if(args[0].charAt(0) == '-')
				params(args);

		long st = System.currentTimeMillis(); // measure time
		Assembler p1 = new Assembler(args[input_at], lang_def);

		// generating the hashtables
		p1.genOPTAB();
		p1.genSYMTAB();

		System.out.println("\nPass 1 finished in " + (System.currentTimeMillis() - st) + " milliseconds");

		if(!p1_only)
		{
			st = System.currentTimeMillis();
			p1.pass_2();
			System.out.println("\nPass 2 finished in " + (System.currentTimeMillis() - st) + " milliseconds");
		}

		// If flags are set print tables to stdout

		if(show_op)
		{
			System.out.println("\nDisplaying the OPTAB:\n");
			display(p1.getOp());
		}

		if(show_sym)
		{
			System.out.println("\nDisplaying the SYMTAB:\n");
			display(p1.getSym());
		}

	}

	/**
	 * Parses the command line arguments and sets the static flags
	 * which enable certain modes such as debug, verbose etc...
	 *
	 * @param params - array of all the command line arguments
	 */
	private static void params(String[] params)
	{
		boolean l, t; l=t=false;
		for(int i=0; i <= params[0].length()-1; i++)
		{
			switch(params[0].charAt(i))
			{
				case '-':
					input_at = 1;
					break;
				case 'v':
					System.out.println("Verbose flag set...");
					verbose = true;
					break;
				case 'l': // setting nonstandard language def file
					System.out.println("Using nonstandard language definition...");

					if(!t) // t not set yer
					{
						lang_def = params[1]; // set the lang def file to user specified
						print("lang_def set to " + params[1]);
						input_at = 2; // tell the compiler to get the second filename as input
						l=true;
					}
					else // t was already set
					{
						lang_def = params[2]; // set the lang def file to user specified
						print("lang_def set to " + params[2]);
						input_at = 3; // tell the compiler to get the second filename as input
						l=true;
					}
					break;
				case 'o':
					System.out.println("OPTAB display flag set...");
					show_op = true;
					break;
				case 's':
					System.out.println("SYMTAB display flag set...");
					show_sym = true;
					break;
				case 'd':
					System.out.println("Debug flag set...");
					debug = true;
					verbose = true;
					break;
				case '1':
					System.out.println("Pass 1 only flag set...");
					p1_only = true;
					break;
				case 't': // setting nonstandard language def file
					System.out.println("Using nonstandard output name...");

					if(!l)
					{
						target = params[1];
						input_at = 2;
					}
					else
					{
						target = params[2];
						input_at = 3;
					}

					print("Target Output file set to: " + target);
					Assembler.t = true;

					break;

			}
		}
	}

	/**
	 * This is an alternative print function used for debuging
	 * All non essential debug output is chaneled through this
	 * method and printed only when verbose flag is set
	 *
	 * @param print text to be printed to stdout
	 */
	private static void print(String print)
	{
		if(verbose) System.out.println("\tv:\t" + print);
	}


	/**
	 *
	 * For printing out debuging information
	 *
	 * @param print text to be printed to stdout
	 */
	private static void debug(String print)
	{
		if(debug) System.out.println("\t\t>>[d]::\t\t" + print);
	}

	/**
	 * Dumps the hash table in a nice tabber format to the stdout
	 *
	 * @param table hash table to be displayed
	 */
	public static void display(Hashtable table)
	{
		Enumeration keys = table.keys();
		Enumeration values = table.elements();

		while(keys.hasMoreElements())
		{
			System.out.println("\t" + keys.nextElement() + "\t\t" + values.nextElement());
		}

	}

	// ################## NONSTATIC STUFF STARTS HERE ######################


	/**
	 * Constructor...
	 */
	public Assembler(String input, String langdef)
	{
		IN = input;
		LANG = langdef;

		//genOPTAB(); 	// generate OPTAB
		//genSYMTAB();	// generate SYMTAB
	}

	public Assembler(String input, String langdef, boolean p1_only)
	{
		this(input, langdef);

		if(p1_only)
			pass_2();
	}

	/**
	 * Generates the OPTAB by using the language definition file
	 * specified in LANG
	 *
	 * Please note that OPTAB is a java Properties class which is in
	 * a sense a Hashtable with additional functionality (such as automatic
	 * parsing of the input file on loading).
	 *
	 */
	private void genOPTAB()
	{
		print("Reading the language definition file...");
		print("LANG (language definition file) is set to: " + LANG);

		print("Creating hashtable for OPTAB..");
		OPTAB = new Properties();

		try
		{
			print("Loading the values from LANG file into hashtable");

			// load properties from the LANG file
			OPTAB.load(new FileInputStream(new File(LANG)));

			print("OPTAB generated sucessfuly.");

			// if debug flag is set dump the OPTAB into stdout
			if(debug)
				OPTAB.list(System.out);

		}
		catch(IOException ioe)
		{
			if(!debug)
				System.out.println("An error has occured while reading the language definition file:" + ioe.getMessage());
			else
				ioe.printStackTrace();

		}

	}

	/**
	 * Generating the SYMTAB using the algorithm from the book
	 *
	 */
	private void genSYMTAB()
	{
		print("Preparing to read the source file");
		print("Source file is: " + IN);

		print("Declaring the LOCCTR, and temporary variables");
		int LOCCTR, start = 0; 	// LOCCTR initialized to 0 - this will be adjusted later
		String label, op, arg; 	// temp vars used to hold current values in the loop

		print("Generating empty SYMTAB hashtable");
		SYMTAB = new Hashtable(); // need to initialize this before we go into the loop

		try
		{
			print("Opening the source file...");
			BufferedReader src = new BufferedReader(new FileReader(IN));

			print("Initializing tokenizer object to parse out text.");
			StringTokenizer line;

			print("Reading the 1st line of the source file...");
			String inline = src.readLine();
			line = new StringTokenizer(inline);	// tokenized uses whitespace as delimiters

			// this variable is used to temporarily hold the first token read on the
			// line. Since 1st token can be either a '.' (dot - comment), a label or an opcode
			// we need to evaluate it first before assigning it to either op or label
			String tmp;

			int next; // next line to read is ...

			if(line.countTokens()<3)
			{
				op = line.nextToken();
				arg = line.nextToken();
			}
			else
			{
				label = line.nextToken();
				op = line.nextToken();
				arg = line.nextToken();
			}

			if(op.toUpperCase().equals("START"))
			{
				print("Found START directive. Setting LOCCTR.");
				start = Integer.parseInt(arg, 16); // assigning the start address to start (must be in hex)
				LOCCTR = start;

				//write?

				print("Reading a line of the source file");
				line = new StringTokenizer(src.readLine());  // read new line
				next = 2;

			}
			else
			{
				print("No START directive on line 1.");
				LOCCTR = 0;
				line = new StringTokenizer(inline); // reread the 1st line into the tokenizer
				next = 1;
			}

			int l = next; // source line counter (start at 2 - line 1 was read above)
			int e = 0; // error counter

			// this loop will be broken when current op value is END
			// if there is no op value the exception will be thrown
			while(true)
			{
				print("-- Parsing line " + l + "\t\t\tLOCCTR: " + Integer.toHexString(LOCCTR));

				tmp = line.nextToken();

				if(!tmp.equals("."))
				{
					print("Line " + l + " is not a comment");

					if(line.countTokens()<2)
					{
						label = null;
						op = tmp;

						if(line.hasMoreTokens())
							arg = line.nextToken();
						else
							arg = null;
					}
					else
					{
						label = tmp;
						op = line.nextToken();

						if(line.hasMoreTokens())
							arg = line.nextToken();
						else
							arg = null;
					}

					print(" * Parsing results: \t\t\t\tLABEL=" + label+ "\tOP=" + op + "\tARG=" + arg);

					if(op.toUpperCase().equals("END"))
					{
						print("END directive found on line " + l + " - exiting");
						break;
					}

					if(label != null)
					{
						print("LABEL found on line " + l + "  - searching SYMTAB..");

						if(SYMTAB.containsKey(label.toUpperCase()))
						{
							System.out.println("ERROR: duplicate LABEL on line " + l);
							e++;
						}
						else
						{
							print("LABEL not in SYMTAB - inserting..");
							SYMTAB.put(label, Integer.toHexString(LOCCTR));
						}
					}

					print("Analyzing OPCODE on line " + l);

					if(OPTAB.containsKey(op))
					{

						print("OPCODE found in OPTAB - incrementing LOCCTR");
						LOCCTR += 3;
					}
					else if(op.toUpperCase().equals("WORD"))
					{
						print("Special OPCODE Reserving a word");
						LOCCTR += 3;
					}
					else if(op.toUpperCase().equals("RESW"))
					{
						print("OPCODE is specoal - reserving " + arg + " words");
						LOCCTR += 3 * Integer.parseInt(arg);
					}
					else if(op.toUpperCase().equals("RESB"))
					{
						print("OPCODE is specoal - reserving " + arg + " bytes");
						LOCCTR += Integer.parseInt(arg);
					}
					else if(op.toUpperCase().equals("BYTE"))
					{
						// the byte is usually given in form C'SOMETHING' or X'11' etc..
						// or in other words letter + quote + value + quote - so I
						// subtract 3 chars from length to just ge the value of C

						// For X I'm stil counting 3 waste chars and then each char
						// inside is a nybble (4 bit hex value). Since you can usually
						// only reserve a byte, then the length can be calculated as
						// ceiling((total_length -3)/2)

						int b; // length of the constant in bytes

						if(arg.toUpperCase().charAt(0) == 'C')
							b = arg.length()-3;
						else
							b = (int) Math.ceil((arg.length()-3)/2);

						print("OPCODE is special - reserving " + b + " bytes");
						LOCCTR += b;
					}
					else
					{
						System.out.println("ERROR: invalid operation code on line " + l + " :: " + op + " is undefined");
						e++;
					}
				}
				//write?

				print("Reading a line of the source file");
				line = new StringTokenizer(src.readLine());
				l++; // increment the line count

			}

			length = LOCCTR - start;
			print("Calculating the program length for " + l + " lines: " + length);

			print("Closing the source file...");
			src.close();
			System.out.println("\nDone. [" + l + " lines] [" + length + " bytes] [" + e + " errors]");



		}
		catch(IOException ioe)
		{
			if(!debug)
				System.out.println("An error has occured while reading the program source file:" + ioe.getMessage());
			else
				ioe.printStackTrace();
		}
	}


	/**
	 * Returns OPTAB as a Hashtable object. Please note that it still
	 * retains all the features of Properties object, so you can re-cast
	 * it into Properties type.
	 *
	 * I'm returning it as a Hashtable to conform to project requirements
	 * which state that both OPTAB and SYMTAB need to be hashtables.
	 *
	 * @return Hashtable OPTAB
	 */
	public Hashtable getOp()
	{
		return OPTAB;
	}

	public Hashtable getSym()
	{
		return SYMTAB;
	}



	/**
	 * Pass 2 of the assembler
	 *
	 */
	private void pass_2()
	{

		System.out.println();
		print("Starting Pass 2");

		print("Preparing to read the source file");
		print("Source file is: " + IN);

		String label, op, arg; 	// temp vars used to hold current values in the loop


		// create local reference to the symtab
		Hashtable SYMTAB = getSym();

		try
		{
			print("Opening the source file...");
			BufferedReader src = new BufferedReader(new FileReader(IN));

			print("Opening the destination file...");

			String out;

			if(!t)
			{
				// generate the name from IN
				out = IN.substring(0, IN.lastIndexOf('.')) + ".obj";

				print("Destination file is: " + out);
			}
			else
				out = target;

			RandomAccessFile o = new RandomAccessFile(out, "rw");

			print("Initializing tokenizer object to parse out text.");
			StringTokenizer line;

			// initializing internal counters

			int rl = 0; // lines read
			int wl = 0; // lines written
			int blen = 0; // byte length of the program
			int e =0; // errors

			// number of records on the current line
			// each T record can have at most 10 instruction records
			int crec = 0;

			print("Reading the 1st line of the source file...");
			String inline = src.readLine();
			line = new StringTokenizer(inline);	// tokenized uses whitespace as delimiters

			rl++; // increment readline

			// this variable is used to temporarily hold the first token read on the
			// line. Since 1st token can be either a '.' (dot - comment), a label or an opcode
			// we need to evaluate it first before assigning it to either op or label
			String tmp;

			int next; // next line to read is ...

			String tt = line.nextToken();

			if(line.countTokens()<2)
			{
				label = "";
				op = tt;

				if(line.hasMoreTokens())
					arg = line.nextToken();
				else
					arg = null;
			}
			else
			{
				label = tt;
				op = line.nextToken();

				if(line.hasMoreTokens())
					arg = line.nextToken();
				else
					arg = null;
			}



			// check if the first line has start
			if(op.toUpperCase().equals("START"))
			{
				print("Found START directive on line " + rl);
				inline = src.readLine();
				line = new StringTokenizer(inline);
				rl++;
			}
			else
			{
				print("No START directive found. Using 0 as start.");
				op = "0";
				label ="";
				arg ="0";

				System.out.println("Error on line 1: no START directive found!");
				e++;
			}

			// declare temp variables used for the header
			String name_fill = "", len_fill = "", arg_fill ="";

			for(int i=0; i<=6-label.length(); i++)
				name_fill += " ";

			// length of the program
			String plen = Integer.toHexString(length);

			debug("program length in hex = " + plen);
			debug("program length string len= " + plen.length());

			if(plen.length() < 6)
			{
				for(int i=0; i<=6-plen.length()-1; i++)
					len_fill += "0";

				debug("6-plen.length() = " + (6-plen.length()));
				debug("len_fill = " + len_fill);

				len_fill = len_fill + plen;
			}

			debug("program start = " + arg);
			debug("program start string len= " + arg.length());

			// switch argument to hex
			//arg = Integer.toHexString((Integer.parseInt(arg)));
			if(arg.length()<6)
			{
				for(int i=0; i<=6-arg.length()-1; i++)
					arg_fill += "0";

				debug("6-arg.length() = " + (6-arg.length()));
				debug("arg_fill = " + arg_fill);

				arg_fill = arg_fill + arg;
			}

			print("Writing the HEADER record..");

			debug("Header Text: H" + label + name_fill + arg_fill + len_fill);

			// header ends at location 7 -- so we can seek to 7
			// to fill in the length (don't count the newline)
			o.writeBytes("H" + label + name_fill + arg_fill + len_fill + "\n");

			print("Initializing the first T record..");
			o.writeBytes("T");

			wl++;

			// header is 19 bytes long
			blen += 19;

			String operand, code =""; // temp variables used in main loop

			line = new StringTokenizer(inline);
			tt = line.nextToken();

			// the MAIN LOOP
			while(true)
			{
				print("-- Parsing line " + rl);

				if(line.countTokens()<2)
				{
					label = "";
					op = tt;

					if(line.hasMoreTokens())
						arg = line.nextToken();
					else
						arg = null;
				}
				else
				{
					label = tt;
					op = line.nextToken();

					if(line.hasMoreTokens())
						arg = line.nextToken();
					else
						arg = null;
				}

				if(op.toUpperCase().equals("END"))
					break;

				if(!label.equals("."))
				{
					print("Line " + rl + " is not a comment..");

					if(OPTAB.containsKey(op))
					{
						print("Opcode " + op + " found in OPTAB");
						print("\t\t\t\t\tOP\t" + op + " = " + OPTAB.get(op));

						if(arg != null && SYMTAB.containsKey(arg))
						{
							print("Symbol " + arg + " found in SYMTAB");
							print("\t\t\t\t\tSYM\t" + arg + " = " + SYMTAB.get(arg));
							operand = (String) SYMTAB.get(arg);
						}
						else
						{
							print("Error in line " + rl + " - Undefined Symbol");
							operand = "0000";
							e++;
						}
					}
					else
					{
						print("Opcode " + op + " not found");
						operand = "0000";
					}

					if(crec <=10)
					{
						// add the object code to the first record
						code += ((String) OPTAB.get(op)) + operand;
						crec++;

						debug("code: " + code);
					}
					else
					{
						o.writeBytes(code + "\nT");
						crec=0;
						code="";
					}

					print("Reading a line of the source file");
					String t = src.readLine();

					debug("**LINE:\t " + t);

					line = new StringTokenizer(t);
					rl++;

					tt = line.nextToken();



				}

			}

			print("Closing the source file..");
			src.close();
			print("Closing the output file..");
			o.close();

		}
		catch(IOException ioe)
		{
			if(!debug)
				System.out.println("An error has occured while reading the program source file:" + ioe.getMessage());
			else
				ioe.printStackTrace();
		}



	}



}
