// (c)1998, Carl Burch. This may not be redistributed
import java.applet.*;
import java.awt.*;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.datatransfer.*;

import TextAreaActionListener;
import Register;
import DLX;
import DLXProg;

public class Assembler extends Frame
		implements DLXListener, ActionListener, TextAreaActionListener,
			ItemListener, ClipboardOwner, WindowListener {
	protected static String FONT_PARM = "font";
	protected static String NUM_BYTES_PARM = "memsize";
	protected static String INITIAL_LABEL =
		"Welcome to HYMN! (c) 1998, Carl Burch";

	// memory bytes per line in memory dump
	protected static final int BYTES_PER_LINE = 32;
	// registers per line in memory dump
	protected static final int REGS_PER_LINE = 8;

	public static final int MAX_TIME = 2048;
	public static final int NUM_PRINT = 100;

	// parameters for the view choice menu
	protected static final int VIEW_ERRS = 0;
	protected static final int VIEW_TRACE = 1;
	protected static final int VIEW_REGS = 2;
	protected static final int VIEW_MEM_WORD = 3;
	protected static final String[] VIEW_NAMES = {
		"Program errors",
		"Execution trace",
		"Registers",
		"Memory (by word)"
	};

	// the machine
	DLX mach;

	// the graphics components
	Button reset;
	Button load;
	Button step;
	Button run;
	Button copy;
	Button paste;
	Choice view;
	TextArea editor;
	ClickableTextArea console;
	Label result;

	// the current contents of the console
	ParseException parse_errs = null;
	int cur_type = -1; // current selection for register/memory
	int cur_which = -1;
	int cur_num = -1;

    public Assembler() {
		this.addWindowListener(this);

		// initialize the machine
		mach = new DLX(getIntParameter(NUM_BYTES_PARM, DLX.NUM_BYTES_DFLT));
		mach.addListener(this);

		// load the fixed-width font
		Font fixed_width = Font.getFont(FONT_PARM);
		if(fixed_width == null) {
			fixed_width = new Font("Courier", Font.PLAIN, 10);
		}

		// set the interface stuff
        Color bg = getColorParameter("background", null);
        if(bg != null) this.setBackground(bg);

		GridBagLayout overall = new GridBagLayout();
		GridBagConstraints c = new GridBagConstraints();
		this.setLayout(overall);
		c.gridx = 0;
		c.fill = GridBagConstraints.BOTH;

		// add button panel
		Panel buttons = new Panel(); // layout for buttons
		buttons.setLayout(new GridLayout(2, 4));

		reset = new Button("Reset");
		reset.addActionListener(this);
		buttons.add(reset);
		load = new Button("Assemble");
		load.addActionListener(this);
		buttons.add(load);
		step = new Button("Step");
		step.addActionListener(this);
		buttons.add(step);
		run = new Button("Run");
		run.addActionListener(this);
		buttons.add(run);
		copy = new Button("Copy");
		copy.addActionListener(this);
		buttons.add(copy);
		paste = new Button("Paste");
		paste.addActionListener(this);
		buttons.add(paste);
		view = new Choice();
		view.addItemListener(this);
		for(int i = 0; i < VIEW_NAMES.length; i++) {
			view.addItem(VIEW_NAMES[i]);
		}
		buttons.add(view);

		c.gridy = 0;
		c.weighty = 0.0;
		overall.setConstraints(buttons, c);
		this.add(buttons);

        editor = new TextArea();
		editor.setEditable(true);
		if(fixed_width != null) editor.setFont(fixed_width);
		c.gridy = 1;
		c.weighty = 2.0;
		overall.setConstraints(editor, c);
		this.add(editor);

		console = new ClickableTextArea();
		if(fixed_width != null) console.setFont(fixed_width);
		c.gridy = 2;
		c.weighty = 1.0;
		overall.setConstraints(console, c);
		this.add(console);
		console.addActionListener(this);

		result = new Label(INITIAL_LABEL);
		c.gridy = 3;
		c.weighty = 0.0;
		overall.setConstraints(result, c);
		this.add(result);
		
		this.pack();

		mach.addListener(this);
    }

    public Color getColorParameter(String name, Color dflt) {
    	return dflt; /*
        String value = this.getParameter(name);
        if(value == null) return dflt;
        try {
            int code = Integer.parseInt(value, 16);
            return new Color(code);
        } catch(NumberFormatException e) {
            return dflt;
        }
    */ }

	public int getIntParameter(String name, int dflt) {
		return dflt; /*
		String value = this.getParameter(name);
		if(value == null) return dflt;
		try {
			return Integer.parseInt(value);
		} catch(NumberFormatException e) {
			return dflt;
		}
	*/ }

	public void actionHeard(Component what, int line, int col) {
		int x;

		switch(view.getSelectedIndex()) {
		case VIEW_ERRS:
			if(line >= 0 && line < parse_errs.size()) {
				int dest = parse_errs.getLine(line);

				// find the line
				int cur_line = 0; // current line in text
				int last_line_beg = 0; // index of beginning of line
				String text = editor.getText();
				for(int i = 0; i < text.length(); i++) {
					if(text.charAt(i) == '\n') {
						if(cur_line == dest) {
							editor.select(last_line_beg, i);
							return;
						}
						++cur_line;
						last_line_beg = i + 1;
					}
				}
				if(cur_line == dest) {
					editor.select(last_line_beg, text.length());
					return;
				}
			}
			break;
		case VIEW_REGS:
			x = 4;
			for(int i = 0; i < REGS_PER_LINE; i++) {
				x += 1;
				if(col >= x && col < x + 8) {
					cur_type = DLX.ELT_REG;
					cur_which = line * REGS_PER_LINE + i;
					cur_num = 1;
					break;
				}
				x += 8;
			}
			changeMemoryLabel();
			break;
		case VIEW_MEM_WORD:
			x = 4;
			for(int i = 0; i < BYTES_PER_LINE; i += 4) {
				x += 1;
				if(col >= x && col < x + 8) {
					cur_type = DLX.ELT_MEM;
					cur_which = line * BYTES_PER_LINE + i;
					cur_num = 4;
					break;
				}
				x += 8;
			}
			changeMemoryLabel();
		}
	}

	public void changeMemoryLabel() {
		try {
			switch(cur_type) {
			case DLX.ELT_REG:
				int reg_val = mach.getReg(cur_which);
				result.setText("R" + Integer.toString(cur_which)
					+ ": hex " + DLX.intToHex(reg_val, 8)
					+ ", dec " + Long.toString(reg_val));
				break;
			case DLX.ELT_MEM:
				int mem_val = mach.getMemWord(cur_which);
				result.setText("addr " + DLX.intToHex(cur_which, 4)
					+ "(" + Integer.toString(cur_which) + ")"
					+ ": hex " + DLX.intToHex(mem_val, 8)
					+ ", dec " + Integer.toString(mem_val)
					+ ", instr " + DLXProg.decode(mem_val));
				break;
			}
		} catch(RunException e) {
			result.setText("Access problem: " + e.getMessage());
		}
	}

	public void machineChanged(DLX machine, int elt_type, int elt_which, int num_elts) {
		// change label if appropriate
		if(elt_type == cur_type && cur_which + cur_num >= elt_which
				&& cur_which < elt_which + num_elts) {
			changeMemoryLabel();
		}

		// change console if appropriate
		switch(view.getSelectedIndex()) {
		case VIEW_REGS:
			if(elt_type == DLX.ELT_REG) {
				console.setLine(elt_which / REGS_PER_LINE,
					regStateLine(elt_which / REGS_PER_LINE));
			}
			break;
		case VIEW_MEM_WORD:
			if(elt_type == DLX.ELT_MEM) {
				for(int line = elt_which / BYTES_PER_LINE;
						line * BYTES_PER_LINE < elt_which + num_elts;
						line++) {
					console.setLine(line, memStateLine(line));
				}
			}
			break;
		default: ;
		}
	}

	public void showRegisters() {
		view.select(VIEW_REGS);
		console.setText("");
		for(int i = 0; i < DLX.NUM_REGS; i += REGS_PER_LINE) {
			console.appendText(regStateLine(i / REGS_PER_LINE) + "\n");
		}
	}

	public void showMemory() {
		view.select(VIEW_MEM_WORD);
		console.setText("");
		for(int i = 0; i < mach.numBytes(); i += BYTES_PER_LINE) {
			console.appendText(memStateLine(i / BYTES_PER_LINE) + "\n");
		}
	}

	protected String regStateLine(int which) {
		int start = which * REGS_PER_LINE;
		StringBuffer ret = new StringBuffer("r" + intToStr(start, 2) + " ");
		try {
			for(int i = 0; i < REGS_PER_LINE; i++) {
				ret.append(" " + DLX.intToHex(mach.getReg(start + i), 8));
			}
		} catch(RunException e) {
			result.setText("Internal error: " + e.getMessage());
		}
		return new String(ret);
	}

	protected String memStateLine(int which) {
		int start = which * BYTES_PER_LINE;
		StringBuffer ret = new StringBuffer(DLX.intToHex(start, 4));
		try {
			for(int i = 0; i < BYTES_PER_LINE; i += 4) {
				if(i < mach.numBytes()) {
					ret.append(" "
						+ DLX.intToHex(mach.getMemWord(start + i), 8));
				}
			}
		} catch(RunException e) {
			result.setText("Internal error: " + e.getMessage());
		}
		return new String(ret);
	}

	protected String intToStr(int val, int length) {
		String init = Integer.toString(val);
		if(init.length() >= length) return init;

		StringBuffer ret = new StringBuffer();
		for(int i = init.length(); i < length; i++) {
			ret.append(' ');
		}
		ret.append(init);
		return new String(ret);
	}

	protected String machineStateStr() {
		StringBuffer ret = new StringBuffer();
		try {
			for(int i = 0; i < DLX.NUM_REGS; i++) {
				ret.append(DLX.intToHex(mach.getReg(i), 8) + " ");
			}
			ret.append(DLX.intToHex(mach.getPC(), 8));
		} catch(RunException e) {
			result.setText("Error printing state: "
				+ e.getMessage());
		}
		return new String(ret);
	}

	public void actionPerformed(ActionEvent evt) {
		Object source = evt.getSource();
		if(source == reset) {
			mach.reset();
			switch(view.getSelectedIndex()) {
			case VIEW_REGS: showRegisters(); break;
			case VIEW_MEM_WORD: showMemory(); break;
			default: ;
			}
			result.setText("Machine reset");
		} else if(source == load) {
			if(view.getSelectedIndex() == VIEW_ERRS) console.setText("");
			try {
				mach.reset();
				DLXProg.load(mach, editor.getText());
				result.setText("Assemble and load completed successfully");
			} catch(ParseException e) {
				view.select(VIEW_ERRS);
				parse_errs = e;
				result.setText("Errors found in program");
				console.setText("");
				for(int i = 0; i < e.size(); i++) {
					console.appendText(
						Integer.toString(e.getLine(i) + 1) + ": "
						+ e.getDescript(i) + "\n");
				}
			}
		} else if(source == step) {
			if(view.getSelectedIndex() == VIEW_ERRS) {
				view.select(VIEW_TRACE);
				console.setText("");
			}
			try {
				if(mach.isHalted()) {
					result.setText("Machine has reached end of program");
				} else {
					mach.executeStep();
					if(view.getSelectedIndex() == VIEW_TRACE) {
						console.appendText(machineStateStr() + "\n");
					}
				}
			} catch(RunException e) {
				result.setText("Run-time error: " + e.getMessage());
			}
		} else if(source == run) {
			if(view.getSelectedIndex() == VIEW_ERRS) {
				view.select(VIEW_TRACE);
				console.setText("");
			}
			try {
				for(int cycles = 0; !mach.isHalted() && cycles != MAX_TIME; cycles++) {
					if(cycles < NUM_PRINT && view.getSelectedIndex() == VIEW_TRACE) {
						console.appendText(machineStateStr()
							+ "  execution " +
							Integer.toString(cycles) + "\n");
					}
					mach.executeStep();
				}
				if(mach.isHalted()) {
					if(view.getSelectedIndex() == VIEW_TRACE) {
						console.appendText(machineStateStr()
							+ "  end of program\n");
					}
					result.setText("End of program reached");
				} else {
					result.setText("Exceeded time limit of "
						+ Integer.toString(MAX_TIME) + " executes");
				}
			} catch(RunException e) {
				result.setText("Run-time error: " + e.getMessage() + "");
			}
		} else if(source == copy) {
			Toolkit toolk = Toolkit.getDefaultToolkit();
			Clipboard clipb = toolk.getSystemClipboard();
			clipb.setContents(new StringSelection(editor.getSelectedText()), this);
		} else if(source == paste) {
			Toolkit toolk = Toolkit.getDefaultToolkit();
			Clipboard clipb = toolk.getSystemClipboard();
			Transferable trans = clipb.getContents(this);
			if(trans == null) {
				result.setText("The clipboard is empty");
			} else if(!trans.isDataFlavorSupported(DataFlavor.stringFlavor)) {
				DataFlavor[] options = trans.getTransferDataFlavors();
				result.setText("Cannot interpret clipboard data "
					+ (options.length > 0 ? options[0].getHumanPresentableName() : "[unknown type]"));
			} else {
				try {
					String repl = (String) trans.getTransferData(DataFlavor.stringFlavor);
					int start = editor.getSelectionStart();
					editor.replaceRange(repl, start,
						editor.getSelectionEnd());
					editor.setSelectionStart(start + repl.length());
					editor.setSelectionEnd(start + repl.length());
					editor.setCaretPosition(start + repl.length());
				} catch(Exception e) {
					result.setText("Cannot get clipboard data");
				}
			}
		}
	}

	public void lostOwnership(Clipboard clipboard,
										Transferable contents) { }

	public void itemStateChanged(ItemEvent e) {
		switch(view.getSelectedIndex()) {
		case VIEW_REGS: showRegisters(); break;
		case VIEW_MEM_WORD: showMemory(); break;
		default: console.setText("");
		}
	}
	
    //
    // The following methods are for handling WindowEvents that I might
    // receive (since I registered myself as a WindowListener). The
    // only one I really care about is windowClosing, but all of
    // them must be defined. In the case of windowClosing, I want
    // the application to exit altogether.
    //
    public void windowOpened(WindowEvent e) { }
    public void windowClosed(WindowEvent e) { }
    public void windowIconified(WindowEvent e) { }
    public void windowDeiconified(WindowEvent e) { }
    public void windowActivated(WindowEvent e) { }
    public void windowDeactivated(WindowEvent e) { }
    public void windowClosing(WindowEvent e) {
        this.dispose(); // destroy the window
    }
	
	public static void main(String[] args) {
		Assembler assem = new Assembler();
		assem.show();
	}
}
