// ProtocolSimulator.java

package simulator;				// protocol simulator package

import java.awt.*;
import java.awt.event.*;
import java.applet.*;
import java.io.*;
import java.lang.reflect.*;
import java.util.*;
import javax.swing.*;
import support.Protocol;
import protocol.*;

/**
  This is the main class for protocol simulator.

  @author	Iain A. Robin, Kenneth J. Turner
  @version	1.0 (1st September 1999, IAR): initial version
  <br/>		1.1 (22nd December 2000, KJT): revised to support printing
  <br/>		1.2 (5th March 2001, KJT): minor revisions
  <br/>		1.3 (6th June 2001, KJT): minor revisions
  <br/>		1.4 (9th March 2006, KJT): updated for JDK 1.5;, use of Swing
		  graphics; blank lines ignored in scenario files
  <br/>		1.5 (20th July 2010, KJT): unchecked warning suppressed;
		  addition of capability to store/load protocol random numbers
		  in a scenario file
*/
public class ProtocolSimulator extends Applet {

  // Constants

  /** Scenario file header */
  private final static String scenPrefix = "Jasper";

  /** Scenario file suffix */
  private final static String scenSuffix = ".scn";

  /** Window width */
  private final static int winWidth = 570;

  /** Window height */
  private final static int winHeight = 900;

  /** Auto-run wait (ms) */
  private final static long runWait = 1000;

  /** Auto-run loss probability */
  private final static double lossProb = 0.2;

  // Control variables

  /** Protocol */
  private static Protocol protocol;

  /** Protocol type */
  protected static String protocolType;

  /** Auto-start (if true) */
  private static boolean autoStart = true;

  /** Frame */
  private static JFrame frame;

  /** Time sequence diagram */
  private TimeSequenceDiagram seqDiagram;

  /** Action list */
  private Vector<String> actions;

  /** Saved action list */
  private Vector<String> savedActions;

  /** List being updated (if true) */
  private boolean listUpdate = false;

  /** Stand-alone application (if true) */
  private boolean isStandalone = false;

  /** Simulation running automatically (if true) */
  private boolean running = false;

  // User interface components

  /** Control panel */
  private JPanel panelControl = new JPanel();

  /** Main panel */
  private JPanel panelMain = new JPanel();

  /** Button panel */
  private JPanel panelButtons = new JPanel();

  /** Options panel */
  private JPanel panelOptions = new JPanel();

  /** Options list */
  private java.awt.List options = new java.awt.List();

  /** Sequence diagram scrollable pane */
  private ScrollPane sequenceDiagram = new ScrollPane();

  /** Undo button */
  private JButton undo = new JButton("Undo");

  /** Redo button */
  private JButton redo = new JButton("Redo");

  /** Run button */
  private JButton run = new JButton("Run");

  /** Clear button */
  private JButton clear = new JButton("Clear");

  /** Print button */
  private JButton print = new JButton("Print");

  /** Load button */
  private JButton load = new JButton("Load");

  /** Save button */
  private JButton save = new JButton("Save");

  /** Overall border layout */
  private BorderLayout borderLayout1 = new BorderLayout();

  /** Control panel border layout */
  private BorderLayout borderLayout2 = new BorderLayout();

  /** Main panel border layout */
  private BorderLayout borderLayout3 = new BorderLayout();

  /** Options panel border layout */
  private BorderLayout borderLayout4 = new BorderLayout();

  /** Overall grid layout */
  private GridLayout gridLayout1 = new GridLayout();

  /**
    Clear all actions.

    @param actionEvent	action event
  */
  void actionClear(ActionEvent actionEvent) {
    restart();
    undo.setEnabled(false);
    redo.setEnabled(false);
    clear.setEnabled(false);
  }

  // load current actions from scenario file

  /**
    Description of the Method

    @param actionEvent	action event
  */
  void actionLoad(ActionEvent actionEvent) {
    String action;				// user-selected action
    Enumeration enumeration;			// enumerated items
    String dirName;				// enumerated items
    String fileName;				// chosen directory/file
    int linePos = -1;				// position in line
    String scenProtocol = "";			// scenario protocol
    String line;				// first file line
    BufferedReader file;			// output file
    FileDialog dialogue =			// file dialogue
      new FileDialog(frame, "Load Scenario + (name" + scenSuffix + ")",
      FileDialog.LOAD);

    dialogue.setVisible(true);			// display file dialogue
    dirName = dialogue.getDirectory();		// get directory name
    fileName = dialogue.getFile();		// get file name
    if (dirName != null && fileName != null)	// directory/file selected?
      try {					// try to load file
	file = new BufferedReader(		// open file
	  new FileReader(dirName + fileName));
	line = file.readLine();			// read header line
	if (line != null) {			// header line found?
	  linePos = line.indexOf(scenPrefix);	// get protocol position
	  if (linePos != -1) {			// protocol position found?
	    int startPos =			// get protocol name start
	      linePos + scenPrefix.length() + 1;
	    int finishPos =			// get following space (if any)
	      line.indexOf(' ', startPos);
	    if (finishPos != -1) {		// randoms follow protocol?
	      scenProtocol =			// get scenario protocol
		line.substring(startPos, finishPos);
	      String random =			// get randoms
		line.substring(finishPos + 1);
	      String[] randomList =		// split up random numbers
		random.split(",");
	      Vector<Float> randoms =		// initialise random numbers
		new Vector<Float>();
	      for (int i = 0; i < randomList.length; i++) { // go through list
		String number = randomList[i];	// get random number
		try {
		  float rand = Float.parseFloat(number); // get random number
		  randoms.add(rand);		// append random number
		}
		catch (NumberFormatException exception) { // format exception?
		  System.err.println(		// report problem
		    "random number '" + number + "' is not in float format");
		}
	      }
	      protocol.setRandomNumbers(randoms); // set random numbers
	    }
	    else 				// no randoms after protocol
	      scenProtocol =			// get scenario protocol
		line.substring(startPos);
	  }
	  else					// header line not found
	    System.err.println(
	      fileName + " does not start with correct header");
	}
	if (!fileName.endsWith(scenSuffix) ||	// file does not have suffix?
	  linePos == -1)
	  System.err.println(fileName + " is not a scenario file");
	else if (!scenProtocol.equals(protocolType)) // differing protocols?
	  System.err.println(fileName + " does not match current protocol");
	else {					// scenario file matches
	  restart();				// restart simulation
	  while ((line = file.readLine()) != null) { // go through file
	    if (line.matches(".*\\S")) {	// non-empty line?
	      for (enumeration = protocol.getServices().elements();
		   enumeration.hasMoreElements(); ) // go through services
		enumeration.nextElement(); 	// do nothing - to next services
	      actions.addElement(line);		// store action
	      seqDiagram.addEvents(		// add action
		protocol.performService(line));
	    }
	    // else
	    //   System.err.println("ignored " + line);
	  }
	  updateActionList();			// update possible events
	  sequenceDiagram.validate();		// validate sequence diagram
	  sequenceDiagram.setScrollPosition(	// to end of sequence diagram
	    0, seqDiagram.getVerticalPosition());
	  clear.setEnabled(!actions.isEmpty());	// enable clear
	  redo.setEnabled(!savedActions.isEmpty()); // enable redo
	  undo.setEnabled(!actions.isEmpty());	// enable undo
	}
	file.close();				// close file
      }
      catch (IOException exception) {		// catch I/O exception
	System.err.println("Could not load file " + exception);
      }

  }

  /**
    Print time sequence diagram.

    @param actionEvent	action event
  */
  void actionPrint(ActionEvent actionEvent) {
    PrintJob printJob = getToolkit().getPrintJob(frame, "Print Diagram", null);
    if (printJob != null) {
      seqDiagram.printStart();			// start page graphics
      do {					// loop through pages
	Graphics printGraphics = printJob.getGraphics();
	if (printGraphics != null) {
	  seqDiagram.print(printGraphics);	// get page graphics
	  printGraphics.dispose();		// print page graphics
	}
      } while (!seqDiagram.printStopped());	// as long as more
      printJob.end();
    }
  }

  /**
    Redo last user selection.

    @param actionEvent	action event
  */
  void actionRedo(ActionEvent actionEvent) {
    if (!savedActions.isEmpty()) {
      String action = (String) savedActions.firstElement();
      actions.addElement(action);
      savedActions.removeElementAt(0);
      seqDiagram.addEvents(protocol.performService(action));
      sequenceDiagram.validate();
      sequenceDiagram.setScrollPosition(0, seqDiagram.getVerticalPosition());
      updateActionList();
      undo.setEnabled(true);
      redo.setEnabled(!savedActions.isEmpty());
      clear.setEnabled(true);
    }
  }

  /**
    Run simulation automatically

    @param actionEvent	action event
  */
  void actionRun(ActionEvent ae) {		// start/stop auto run
    boolean empty = actions.isEmpty();
    if (running) {				// currently running?
      running = false;				// set not running
      run.setText("Run");			// set label to run
      clear.setEnabled(!actions.isEmpty());	// enable clear
      load.setEnabled(isStandalone);		// enable load
      print.setEnabled(isStandalone);		// enable print
      redo.setEnabled(!savedActions.isEmpty());	// enable redo
      save.setEnabled(isStandalone);		// enable save
      undo.setEnabled(!actions.isEmpty());	// enable undo
    }
    else {					// currently stopped
      running = true;				// set running
      run.setText("Stop");			// set label to stop
      clear.setEnabled(false);			// disable clear
      load.setEnabled(false);			// disable load
      print.setEnabled(false);			// disable print
      redo.setEnabled(false);			// disable redo
      save.setEnabled(false);			// disable save
      load.setEnabled(false);			// disable clear
      runAction();				// choose random action
    }
  }

  /**
    Save current actions into scenario file.

    @param actionEvent	action event
  */
  void actionSave(ActionEvent ae) {
    String action;				// user-selected action
    Enumeration enumeration;			// enumerated item
    String dirName;				// enumerated item
    String fileName;				// chosen directory/file
    BufferedWriter file;			// output file
    FileDialog dialogue =			// file dialogue
      new FileDialog(frame, "Save Scenario (name" + scenSuffix + ")",
      FileDialog.SAVE);
    dialogue.setVisible(true);			// display file dialogue
    dirName = dialogue.getDirectory();		// get directory name
    fileName = dialogue.getFile();		// get file name
    if (dirName != null && fileName != null)	// directory/file selected?
      try {					// try to save file
	if (!fileName.endsWith(scenSuffix))	// file does not have suffix?
	  fileName += scenSuffix;		// append it
	file = new BufferedWriter(		// open file
	  new FileWriter(dirName + fileName));
	Vector<Float> randoms =			// get protocol random numbers
	  protocol.getRandomNumbers();
	String line =				// initialise header line
	  scenPrefix + " " + protocolType;
	if (randoms != null) {			// random numbers to save?
	  line += " ";				// append a space
	  for (int i = 0; i < randoms.size(); i++) { // go through randoms
	    float random = (Float) randoms.get(i); // get next random
	    if (i > 0)				// not first random?
	      line += ",";			// append comma to line
	    line += random;			// append random
	  }
	}
	file.write(line);			// output header line
	file.newLine();				// append newline
	for (enumeration = actions.elements();	// go through actions
	     enumeration.hasMoreElements(); ) {	// as long as more
	  action = (String) enumeration.nextElement(); // get action
	  file.write(action);			// output action
	  file.newLine();			// append newline
	}
	file.close();				// close file
      }
      catch (IOException exception) {		// catch I/O exception
	System.err.println("Could not save file " + exception);
      }

  }

  /**
    Undo last user selection and reconstruct event sequence.

    @param actionEvent	action event
  */
  void actionUndo(ActionEvent ae) {
    boolean empty;

    if (!actions.isEmpty()) {
      savedActions.insertElementAt(actions.lastElement(), 0);
      actions.removeElementAt(actions.size() - 1);
      seqDiagram.clear();
      options.removeAll();
      protocol.init();
      for (Enumeration e = actions.elements(); e.hasMoreElements(); ) {
	for (Enumeration dummy = protocol.getServices().elements();
	  dummy.hasMoreElements(); )		// get services before redoing
	  dummy.nextElement();			// do nothing - to next services
	String action = (String) e.nextElement();
	seqDiagram.addEvents(protocol.performService(action));
      }
      updateActionList();			// update possible events
      sequenceDiagram.validate();
      sequenceDiagram.setScrollPosition(0, seqDiagram.getVerticalPosition());
      empty = actions.isEmpty();
      undo.setEnabled(!empty);
      redo.setEnabled(true);
      clear.setEnabled(!empty);
    }
  }

  /**
    Initialise the simulation.

    @exception		any exception
  */
  private void appInit()
    throws Exception {
    protocol = createProtocol(protocolType);	// create protocol
    seqDiagram = new TimeSequenceDiagram(protocol);
    actions = new Vector<String>();
    savedActions = new Vector<String>();
    // component initialisation
    setBackground(Color.lightGray);
    panelOptions.setFont(TimeSequenceDiagram.labelFont);
    panelOptions.setLayout(borderLayout4);
    gridLayout1.setRows(4);
    gridLayout1.setHgap(10);
    gridLayout1.setColumns(2);
    gridLayout1.setVgap(5);
    setLayout(borderLayout1);
    panelControl.setLayout(borderLayout2);
    panelMain.setLayout(borderLayout3);
    add(panelMain, BorderLayout.CENTER);
    panelMain.add(sequenceDiagram, BorderLayout.CENTER);
    panelMain.add(panelControl, BorderLayout.SOUTH);
    panelButtons.setLayout(gridLayout1);
    panelButtons.add(undo, null);
    panelButtons.add(redo, null);
    panelButtons.add(run, null);
    panelButtons.add(clear, null);
    panelButtons.add(load, null);
    load.setEnabled(isStandalone);
    panelButtons.add(save, null);
    save.setEnabled(isStandalone);
    panelButtons.add(print, null);
    print.setEnabled(isStandalone);
    add(panelControl, BorderLayout.SOUTH);
    panelControl.add(panelButtons, BorderLayout.WEST);
    panelControl.add(panelOptions, BorderLayout.CENTER);
    panelOptions.add(options, BorderLayout.CENTER);
    sequenceDiagram.add(seqDiagram, null);
    clear.addActionListener(
      new ActionListener() {			// add clear button
	public void actionPerformed(ActionEvent e) {
	  actionClear(e);
	}
      });
    load.addActionListener(
      new ActionListener() {			// action load button
	public void actionPerformed(ActionEvent e) {
	  actionLoad(e);
	}
      });
    print.addActionListener(
      new ActionListener() {			// add print button
	public void actionPerformed(ActionEvent e) {
	  actionPrint(e);
	}
      });
    redo.addActionListener(
      new ActionListener() {			// add redo button
	public void actionPerformed(ActionEvent e) {
	  actionRedo(e);
	}
      });
    run.addActionListener(
      new ActionListener() {			// add run button
	public void actionPerformed(ActionEvent e) {
	  actionRun(e);
	}
      });
    save.addActionListener(
      new ActionListener() {			// add save button
	public void actionPerformed(ActionEvent e) {
	  actionSave(e);
	}
      });
    undo.addActionListener(
      new ActionListener() {			// add undo button
	public void actionPerformed(ActionEvent e) {
	  actionUndo(e);
	}
      });
    options.addItemListener(
      new ItemListener() {			// add options list
	public void itemStateChanged(ItemEvent e) {
	  itemOptions(e);
	  if (isStandalone)			// running as application?
	    options.select(0);			// set item for any double-click
	}
      });
  }

  /**
    Create protocol for type.

    @param type		protocol type
    @return		protocol entity
    @exception		exception if invalid type
  */
  public static Protocol createProtocol(String type) throws Exception {
    Protocol protocol = null;			// protocol instance
    Class<?> subClass;				// protocol instance
    Class<?> superClass;			// protocol subclass/superclass
    Constructor subCons;			// subclass constructor
    String subType = "";			// protocol subtype
    int subPos;					// subtype position
    Class consPars[] = new Class[1];		// constructor parameters
    Object instPars[] = new Object[1];		// instance parameters

    try {					// try to create protocol
      subPos = type.indexOf("/");		// look for protocol subtype
      if (subPos != -1) {			// subtype present?
	subType = type.substring(subPos + 1);	// set subtype
	type = type.substring(0, subPos);	// set type
      }
      subClass = Class.forName("protocol." + type); // get class for type name
      superClass = subClass.getSuperclass();	// get its superclass
      if (superClass != null &&			// superclass is Protocol?
	superClass.getName().equals("support.Protocol")) {
	if (subType.equals(""))			// subtype absent?
	  protocol =				// create subclass instance
	    (Protocol) subClass.newInstance();
	else {					// subtype present
	  consPars[0] = subType.getClass();	// set up constructor params
	  try {					// try to construct
	    subCons =				// get subclass constructor
	      subClass.getConstructor(consPars);
	    instPars[0] = subType;		// set up instance params
	    protocol =				// create subclass instance
	      (Protocol) subCons.newInstance(instPars);
	  }
	  catch (Exception exception) {		// cannot construct
	    exception.printStackTrace();
	    throw new Exception(
	      "protocol '" + type + "' does not have subtype '" +
	      subType + "'");
	  }
	}
      }
      else					// superclass not Protocol
	throw new Exception("invalid protocol type '" + type + "'");
    }
    catch (Throwable thrown) {			// cannot create protocol
      throw new Exception(			// report exception
	"Cannot create protocol: " + thrown.getMessage());
    }
    return (protocol);				// return protocol instance
  }

  /**
    Return a parameter value.

    @param key  Parameter
    @param def  Parameter
    @return     The parameter value
  */
  public String getParameter(String key, String def) {
    return isStandalone ? System.getProperty(key, def) :
      (getParameter(key) != null ? getParameter(key) : def);
  }

  /**
    Return parameter information.

    @return   The parameterInfo value
  */
  public String[][] getParameterInfo() {
    String pinfo[][] = {
      {"Protocol", "String", "Protocol type"},
    };
    return pinfo;
  }

  /**
    Initialise the simulation.
  */
  public void init() {
    if (!isStandalone)
      try {
	protocolType = getParameter("protocol", "ABP");
	autoStart = getParameter("start", "true").equals("true");
      }
      catch (Exception e) {
	e.printStackTrace();
      }

    try {
      appInit();
    }
    catch (Exception e) {
      System.err.println(e.getMessage());
      // e.printStackTrace();
      System.exit(1);
    }
  }

  /**
    Show effect of user choice in diagram.

    @param ie  Parameter
  */
  void itemOptions(ItemEvent ie) {
    String action;				// user-selected action

    if (!listUpdate) {				// list not being updated?
      action = options.getSelectedItem();
      actions.addElement(action);
      savedActions.removeAllElements();
      seqDiagram.addEvents(protocol.performService(action));
      sequenceDiagram.validate();
      sequenceDiagram.setScrollPosition(0, seqDiagram.getVerticalPosition());
      updateActionList();
      if (running)				// currently running?
	runAction();				// choose random action
      else {					// not currently running
	undo.setEnabled(true);
	redo.setEnabled(false);
	clear.setEnabled(true);
      }
    }
  }

  /**
    The main program for the protocol simulator class.

    @param args		command-line arguments
  */
  public static void main(String[] args) {
    ProtocolSimulator applet = new ProtocolSimulator();
    applet.isStandalone = true;
    protocolType = args.length > 0 ? args[0] : "ABP";
    frame = new JFrame();
    frame.setTitle(protocolType + " Protocol Simulator");
    frame.add(applet, BorderLayout.CENTER);
    frame.addWindowListener(new FrameWindowAdapter(applet));
    autoStart = true;
    applet.init();
    // parse and set parameters from command-line arguments
    int i = 1;
    while (i < args.length) {
      int eq = args[i].indexOf('=');
      if (eq > 0) {
	String param = args[i].substring(0, eq);
	String value = args[i].substring(eq + 1);
	protocol.setParameter(param, value);
	i++;
      }
    }
    applet.start();
    frame.pack();
    frame.setSize(winWidth, winHeight);
    Dimension d = Toolkit.getDefaultToolkit().getScreenSize();
    frame.setLocation(
      (d.width - frame.getSize().width) / 2,
      (d.height - frame.getSize().height) / 2);
    frame.setVisible(true);
  }

  /**
    Restart simulation from scratch.
  */
  public void restart() {
    seqDiagram.clear();
    sequenceDiagram.setScrollPosition(0, seqDiagram.getVerticalPosition());
    sequenceDiagram.validate();
    actions.removeAllElements();
    savedActions.removeAllElements();
    protocol.init();
    updateActionList();
    undo.setEnabled(false);
    redo.setEnabled(false);
    clear.setEnabled(false);
  }

  /**
    Perform a random action during running
  */
  void runAction() {
    Thread runChoice =
      new Thread() {				// make thread for auto-run
	public void run() {			// run thread
	  int numChoices =			// get number of choices
	    options.getItemCount();
	  int selChoice;			// selected choice
	  if (numChoices >= 1) {		// at least one choice?
	    try {				// try to wait
	      Thread.sleep(runWait);		// wait before selecting
	    }
	    catch (InterruptedException exception) { // catch interrupt
	    }
	    do {
	      selChoice =			// choose random action
		(int) (Math.random() * numChoices);
	    }
	    while (numChoices > 1 &&		// more than one choice?
	      options.getItem(selChoice).	// choice is message loss?
		indexOf(": Lose") != -1 &&
	      Math.random() > lossProb);	// exceeds loss probability?
	    options.select(selChoice);		// select random action
	    options.dispatchEvent(		// simulate list click
	      new ItemEvent(options, ItemEvent.ITEM_STATE_CHANGED,
	      options, ItemEvent.SELECTED));
	  }
	}
      };
    runChoice.start();				// start auto-run thread
  }

  /**
    Generic method to pass parameter strings supplied from JavaScript on the web
    page into the protocol class.

    @param parameter	parameter name
    @param value	parameter value
  */
  public void setParameter(String parameter, String value) {
    protocol.setParameter(parameter, value);
  }

  /**
    Start simulation.
  */
  public void start() {
    if (autoStart)
      updateActionList();
    undo.setEnabled(false);
    redo.setEnabled(false);
    clear.setEnabled(false);
  }

  /**
    Update list of possible events.
  */
  public void updateActionList() {
    listUpdate = true;
    options.removeAll();
    for (Enumeration e = protocol.getServices().elements();
      e.hasMoreElements(); )
      options.add((String) e.nextElement());
    listUpdate = false;
  }

  /**
    Handle window closing event.

    @param windowEvent		window event
  */
  void windowClosing(WindowEvent windowEvent) {
    System.exit(0);
  }

}

/**
  This is a supporting class for adapting windows.

  @author	Iain A. Robin, Kenneth J. Turner
  @version	1.0 (1st September 1999, IAR): initial version
  <br/>		1.4 (9th March 2006, KJT): updated for JDK 1.5
  <br/>		1.5 (27th July 2010, KJT): minor tidying
*/
class FrameWindowAdapter extends java.awt.event.WindowAdapter {

  /** Adapted protocol simulator window */
  ProtocolSimulator adaptee;

  /**
    Constructor for a frame window adaptor.

    @param adaptee  Parameter
  */
  FrameWindowAdapter(ProtocolSimulator adaptee) {
    this.adaptee = adaptee;
  }

  /**
    Handle window closing event.

    @param windowEvent		window event
  */
  public void windowClosing(WindowEvent windowEvent) {
    adaptee.windowClosing(windowEvent);
  }

}

