/*

	CommandParser.java
	by Richard Unger, March 1998
	
	The idea is to provide a flexible way for the program to process events and 
	generate commands. If all events are collected at a central location before
	being dispatched to the appropriate code to execute their corresponding
	commands it means we can a) collect them and save them to create demos.
	b) easily tie in commands from other sources than the interface eg. commands
	from a command line or commands from a network socket.
	
	At the end of this file is a description of how each type of event is handled.
	
*/


import java.awt.*;
import java.awt.event.*;
import java.util.Enumeration;
import java.util.Vector;
import java.util.Date;
import java.io.*;
import java.net.URL;
import java.net.MalformedURLException;

public class CommandParser
implements ActionListener,ItemListener,CommandExecutor
{
	Vector allCommands;		// Vector of commands other objects have registered
	State states;
	CommandTicker ticker;
	boolean inApplet;		// true if we are running as an applet
	URL appletBase;
	boolean recording;		// true if we are recording a file
	String rFileN;			// name of file being recorded into
	BufferedWriter rBW;		// output stream for recording
	long rNumCommands;		// number of commands recorded
	
// consts	
	
	public static final String COMMENTCHARACTER = "#";
	

	public CommandParser(){
		
		allCommands = new Vector(25);	// 25 initial commands
		
		states = null;
		ticker = null;
		
		appletBase = null;
		inApplet = false;
		
		recording = false;
		rFileN = null;
		rBW = null;
		rNumCommands = 0;
		}


	public CommandParser(URL homeBase){
		this();
		inApplet = true;
		appletBase = homeBase;
		}


	public void CommandParserInit(CommandTicker intick){
		ticker = intick;
		states = new State(this);
		
		AddCommand("run <str>",this);		// support reading commands from file...
		if (!inApplet){
			AddCommand("record <str>",this);// support writing commands to file...
			AddCommand("stop record",this);
			}
		}

/*********************************************************
Methods for the adding of new commands...
and interacting with the commandparser
*********************************************************/


	public void AddCommand(String text, CommandExecutor slave){
		Command it  = new Command(text,slave);
		allCommands.addElement(it);
		}

	public void AddCommand(Command it){
		allCommands.addElement(it);
		}

	public void RemoveCommand(Command it){
		allCommands.removeElement(it);
		}


	public void SetCommandTicker(CommandTicker intick){
		ticker = intick;
		}

	public State GetStates(){
		return states;
		}

/*********************************************************
The CommandDispatcher method and its helpers are responsible
for identifying valid commands and calling the corresponding
code.
**********************************************************/


	public void SendCommand(String commandInstance){
		// this method intended for directly sending a command that was read in
		// or that comes from somewhere in the code.
		// at the moment we don't need to do any more here than call commandDispatch		
		CommandDispatcher(commandInstance,true);
		}


	public void SendQuietCommand(String commandInstance){
		// this method will send a command as above, but will not print it to the
		// screen or record it...	
		CommandDispatcher(commandInstance,false);
		}


	public void DisplayCommand(String commandInstance){
		DisplayCommand(commandInstance,true);
		}
	
	public void DisplayCommandError(String commandInstance){
		DisplayCommand(commandInstance,false);
		}

	
	public void DisplayCommand(String commandInstance,boolean wAngle){

		if (ticker == null)
			System.out.println( ((wAngle)?"> ":"")+commandInstance);
		else
			ticker.printCommand( ((wAngle)?"> ":"")+commandInstance);
	
		}


	private void CommandDispatcher(String commandInstance,boolean recordIt){
		// perhaps here is the good place to implement demo-mode
		
		// print the command
		if (recordIt)
			DisplayCommand(commandInstance);
		
		// execute the command
		if (MatchCommands(commandInstance)<1){
			DisplayCommandError("No matching command: "+commandInstance);
			if (recording && recordIt)
				DisplayCommandError("Unrecognized command not recorded.");
			}
		else
			if (recordIt && recording && !commandInstance.startsWith("record"))
				RecordCommand(commandInstance);
		}


// clearly this needs a lot of improving - right now we check all the commands on each
// input we receive - must improve this.

	public int MatchCommands(String commandInstance){
		Command com;
		Enumeration e = allCommands.elements();
		int numMatches = 0;
		String s;
		
		while (e.hasMoreElements()){
			com = (Command)e.nextElement();
			if (com.matchesCommand(commandInstance)){
				s = com.invoke(commandInstance);
				numMatches++;
				if (s != null)
					DisplayCommandError(s);
				}
			}
		return numMatches;
		}



/*********************************************************
All the stuff to handle files/streams - reading commands
from them or logging commands to them.
**********************************************************/



	public void ReadCommandFile(String fileName){
		// given a file name, this method opens the file, attaches the right streams
		// and then executes the commands found in the file one by one...
		BufferedReader br=null;
		URL u=null;
		File f=null;
		
		if (inApplet){		// get file off the net
			try{		
				u = new URL(appletBase,fileName);
				br = new BufferedReader(new InputStreamReader(u.openStream()));
				}
			catch (MalformedURLException e){
				DisplayCommandError("unable to make URL for file");
				System.err.println("CommandParser.ReadCommandFile(): URL creation error: "+e);
				return;
				}
			catch (IOException e){
				DisplayCommandError("can't open URL: "+u);
				System.err.println("CommandParser.ReadCommandFile(): error opening URL: "+e);
				return;
				}
			}
		else{				// get file off local drive if not running as applet
			f = new File(fileName);
			try{
				if (!f.exists()){
					DisplayCommandError("file does not exist: "+f.getName());
					return;
					}
				if (!f.isFile()){
					DisplayCommandError("not a file: "+f.getName());
					return;
					}
				if (!f.canRead()){
					DisplayCommandError("don't have read permissions on: "+f.getName());
					return;
					}
				}
			catch (SecurityException e){
				DisplayCommandError("Security Exception while trying to access file.");
				System.err.println("CommandParser.ReadCommandFile(): error opening file: "+e);
				return;
				}
			try{
				br = new BufferedReader(new FileReader(f));
				}
			catch (IOException e){
				DisplayCommandError("unable to allocate streams...");
				System.err.println("CommandParser.ReadCommandFile(): error allocating streams: "+e);
				return;
				}
			}
		
		// once stream is set up, read from stream...
		int line = 1;
		String theCommand;
		try{
			do{
				theCommand = br.readLine();
				if (theCommand!=null){
					theCommand = theCommand.trim();
					line++;
					if (theCommand.startsWith(COMMENTCHARACTER))
						continue;
					if (theCommand.indexOf(COMMENTCHARACTER)!=-1)
						theCommand = theCommand.substring(0,theCommand.indexOf(COMMENTCHARACTER)).trim();
					if (theCommand.length()<1)
						continue;
					SendCommand(theCommand);
					}
				}while(theCommand!=null);
				
			br.close();		// close the file or URL
				
			}
		catch (IOException e){
			DisplayCommandError("error reading from file line: "+line);
			System.err.println("CommandParser.ReadCommandFile(): error reading file: "+e);
			}
		
		}


	synchronized public void RecordCommandFile(String fName){
		if (recording){
			DisplayCommandError("Already recording to file: "+rFileN);
			return;
			}
		if (inApplet){
			DisplayCommandError("Can't write files in applet");
			return;
			}
		
		try{
			rBW = new BufferedWriter(new FileWriter(fName));
			}
		catch (IOException e){
			DisplayCommandError("Error opening file for writing: "+e);
			rBW = null;
			recording = false;
			return;
			}
		DisplayCommandError("recording...");
		rNumCommands = 0;
		rFileN = fName;	
		recording = true;
		RecordCommand(COMMENTCHARACTER + " SpheresApplet commands file recorded on "+new Date());
		}


	synchronized public void RecordCommand(String commandInstance){
		if (!recording){
			System.err.println("CommandParser.RecordCommand(): Attempt to record command even though we're not recording!");
			return;
			}
		try{
			rBW.write(commandInstance,0,commandInstance.length());
			rBW.newLine();
			}
		catch (IOException e){
			DisplayCommandError("Error trying to write command to file: "+e);
			DisplayCommandError("closing file due to error...");
			StopRecording();
			}
		if (!commandInstance.trim().startsWith(COMMENTCHARACTER))
			rNumCommands++;
		}


	synchronized public void StopRecording(){
		if (!recording)
			return;

		RecordCommand(COMMENTCHARACTER + " finished recording, "+rNumCommands+" commands logged.");

		try{
			rBW.flush();
			rBW.close();
			}
		catch (IOException e){
			System.err.println("CommandParser.StopRecording(): Couldn't close file!");
			}
		
		DisplayCommandError("Finished recording "+rNumCommands+" commands.");
		
		rBW = null;
		rFileN = null;
		recording = false;
		}

	public void finalize(){
		if (recording)
			StopRecording();
		}

/*********************************************************
Here we implement our own commands (ie the commands
supported by the commandparser itself)
*********************************************************/


public String performCommand(String commandInstance){
	
	if (commandInstance.startsWith("run ")){
		ReadCommandFile(commandInstance.substring(4,commandInstance.length()));
		return null;
		}
	if (commandInstance.equals("stop record")){
		StopRecording();
		return null;
		}
	if (commandInstance.startsWith("record ")){
		RecordCommandFile(commandInstance.substring(7,commandInstance.length()));
		}
		
	return null;

	}




/*********************************************************
In this section we catch all events and try to extract the
meaningful command from them.
**********************************************************/


// action events
	public void actionPerformed(ActionEvent e){
		Object target = e.getSource();
		String commandInstance;
		
		if (target instanceof CommandTalker){
			commandInstance = ((CommandTalker)target).talkCommand();
			CommandDispatcher(commandInstance,true);
			return;
			}
		
		if (target instanceof Button){
			// get the button's command, which
			commandInstance = ((Button)target).getActionCommand();
			CommandDispatcher(commandInstance,true);
			return;
			}
		// wasn't a button...
		}

// item events
	public void itemStateChanged(ItemEvent e){
		Object target = e.getItemSelectable();
		String commandInstance;
	
		if (target instanceof CommandTalker){
			commandInstance = ((CommandTalker)target).talkCommand();
			CommandDispatcher(commandInstance,true);
			return;
			}
	
		}

}





/*

Buttons - ActionEvents
The CommandParser implements the actionListener interface. To implement a command from
a button, simply create the button, set the button's action command with the
setActionCommand() method to be the string you want. If you are creating new functionality
remember to add the strings as commands to the command dispatcher...
If the button name is equal to the command it executes, you don't need to set the command
text. If the button's command text changes, perhaps each time it is pressed, you will need
to extend the Button and override the processEvent method or implement the CommandTalker
interface.

Checkboxes - ItemEvents
The Commandparser implements the itemListener interface. The items have to implement
the CommandTalker interface, by creating themselves as TalkerCheckboxes and then
providing an anonymous class to override the useless stub in TalkerCheckboxes so that
talkCommand() returns something meaningful...

*/