// TimeSequenceDiagram.java

package simulator;				// simulator package

import java.awt.*;				// import Java AWT classes

import java.util.*;				// import Java utility classes

import support.*;				// import Jasper support classes

/**
  This is the class for a time sequence diagram.

  @author	Iain A. Robin, Kenneth J. Turner
  @version	1.0 (1st September 1999, IAR): initial version <br/>
  <br/>		1.4 (9th March 2006, KJT): updated for JDK 1.5, use of Swing
		  graphics, minor tidying
  <br/>		1.5 (27th July 2010, KJT): minor tidying; vertical space before
		  DELIVER; vertical space before COMMENT not just for first
		  event; addition of TRAVERSE for events that span columns
*/
public class TimeSequenceDiagram extends Canvas {

  /** Vertical extent of primitive */
  private final static int primitiveHeight = 10;

  /** Width of primitive arrow */
  private final static int arrowWidth = 30;

  /** Top/left insert of diagram */
  private final static int inset = 15;

  /** Page left-hand margin */
  private final static int margin = 15;	//

  /** Maximum printing height */
  private final static int maxHeight = 800;

  /** Max symbols to print */
  private final static int maxSymbols = 64;

  /** Diagram/page header font */
  public final static Font headerFont = new Font("Sans serif", Font.BOLD, 14);

  /** Page footer font */
  public final static Font footerFont = new Font("Sans serif", Font.BOLD, 14);

  /** Diagram label font */
  public final static Font labelFont = new Font("Sans serif", Font.PLAIN, 13);

  /** Mid green comments */
  public final static Color commentColour =   new Color(0, 128, 0);

  /** Red last symbol */
  public final static Color lastColour = Color.red;

  /** Tan timeouts */
  public final static Color timeoutColour = new Color(128, 64, 48);

  /** Tan timeouts */
  private String[] columnHeaders;

  /** Header text height */
  private int headerTextHeight;

  /** Column width in pixels */
  private int columnWidth;

  /** Number of columns */
  private int columns;

  /** Y-coordinate for symbol area top */
  private int topOfBlock;

  /** Default symbol height */
  private int defaultSymbolHeight;

  /** Vertical scroll position */
  private int verticalPosition;

  /** List of symbols */
  private Vector<TSDSymbol> symbols;

  /** List of protocol entities */
  private Vector<ProtocolEntity> entities;

  /** Printing starting */
  private boolean starting;

  /** Printing finished */
  private boolean stopped;

  /** Y-coordinate for initial symbol */
  private int initialHeight;

  /** Medium column number */
  private int mediumColumn;

  /** Last column number */
  private int lastColumn;

  /** Y-coordinate for start of SEND arrow */
  private int start = 0;

  /** Current printing top */
  private int currentHeight;

  /** Current page number */
  private int page;

  /**
    Constructor for the TimeSequenceDiagram object

    @param protocol  Parameter
  */
  public TimeSequenceDiagram(Protocol protocol) {
    setBackground(Color.white);
    entities = protocol.getEntities();
    mediumColumn = protocol.getMediumColumn();
    columns = entities.size();
    columnHeaders = new String[columns];
    for (int i = 0; i < columns; i++)
      columnHeaders[i] = ((ProtocolEntity) entities.elementAt(i)).getName();
    symbols = new Vector<TSDSymbol>();
    verticalPosition = 0;
    lastColumn = -1;
    starting = true;
  }

  /**
    Adds a feature to the CommentSymbol attribute of the TimeSequenceDiagram
    object.

    @param comment	comment text
    @param lToR		true = left-to-right
    @param sourceCol	source column
  */
  private void addCommentSymbol(String comment, boolean lToR, int sourceCol) {
    CommentSymbol st =
      new CommentSymbol(null, comment, lToR, sourceCol, topOfBlock);
    addSymbol(st);
  }

  // determine types of protocol events
  // and add appropriate symbols to diagram

  /**
    Add events to time sequence diagram.

    @param events	event list
  */
  public void addEvents(Vector<ProtocolEvent> events) {
    int eventNo = 0;
    int source = 0;
    int destination = 0;
    for (Enumeration e = events.elements(); e.hasMoreElements(); ) {
      ProtocolEvent event = (ProtocolEvent) e.nextElement();
      PDU pdu = null;
      if (event != null) {
	int eventType = event.getType();
	pdu = event.getPDU();
	if (pdu != null) {
	  source = entities.indexOf(pdu.getSource());
	  destination = entities.indexOf(pdu.getDestination());
	  boolean lToR = destination > source;
	  int col = lToR ? source + 1 : source - 1;
	  String label = pdu.getLabel();
	  int transmissionHeight = defaultSymbolHeight;
	  switch (eventType) {
	      case ProtocolEvent.SEND:
		addSpace(source);
		addServicePrimitive(
		  event, lToR, col, topOfBlock, label, ServicePrimitive.TAIL);
		break;
	      case ProtocolEvent.TRANSMIT:
		addSpace(source);
		addServicePrimitive(
		  event, lToR, col, topOfBlock, label, ServicePrimitive.TAIL);
		break;
	      case ProtocolEvent.RECEIVE:
		start = getTransmissionStart(pdu);
		if (start != topOfBlock)
		  transmissionHeight = Math.max(
		    defaultSymbolHeight, topOfBlock - start + primitiveHeight);
		if (destination == lastColumn)
		  transmissionHeight += 7;
		addSuccessfulTransmission(
		  event, lToR, mediumColumn, start, transmissionHeight);
		addServicePrimitive(
		  event, lToR, destination, start + transmissionHeight,
		  label, ServicePrimitive.HEAD);
		lastColumn = destination;
		break;
	      case ProtocolEvent.DELIVER:
		addSpace(source);
		addServicePrimitive(
		  event, lToR, destination, topOfBlock, label,
		  ServicePrimitive.HEAD);
		break;
	      case ProtocolEvent.FRAGMENT:
		if (eventNo == 0)
		  start = getTransmissionStart(pdu);
		if (start != topOfBlock)
		  transmissionHeight = Math.max(
		    defaultSymbolHeight, topOfBlock - start + primitiveHeight);
		if (destination == lastColumn)
		  transmissionHeight += 7;
		addSuccessfulTransmission(
		  event, lToR, mediumColumn, start, transmissionHeight);
		addServicePrimitive(
		  event, lToR, destination, start + transmissionHeight,
		  label, ServicePrimitive.HEAD);
		lastColumn = destination;
		break;
	      case ProtocolEvent.LOSE:
		if (eventNo == 0)
		  start = getTransmissionStart(pdu);
		if (start != topOfBlock)
		  transmissionHeight = Math.max(
		    defaultSymbolHeight, topOfBlock - start + primitiveHeight);
		if (destination == lastColumn)
		  transmissionHeight += 7;
		addFailedTransmission(
		  event, lToR, mediumColumn, start, transmissionHeight);
		lastColumn = destination;
		break;
	      case ProtocolEvent.TIMEOUT:
		if (eventNo == 0)
		  addSpace(source);
		addSenderTimeout(event, lToR, source);
		addServicePrimitive(
		  event, lToR, col, topOfBlock, label, ServicePrimitive.TAIL);
		break;
	      case ProtocolEvent.TRAVERSE:
		source++;				// incr. source column
		int columns = destination - source;	// set number of columns
		start = getTransmissionStart(pdu); 	// get y for start
		if (start != topOfBlock)		// not top of block?
		  transmissionHeight = Math.max(	// set trans. height
		    defaultSymbolHeight, topOfBlock - start + primitiveHeight);
		if (columns > 1)
		  transmissionHeight =
		    columns*(transmissionHeight + primitiveHeight);
		if (destination == lastColumn)		// last column?
		  transmissionHeight += 7;		// allow extra space
		addTraverseTransmission(		// add traverse trans.
		  event, lToR, source, columns, start, transmissionHeight);
		addServicePrimitive(			// add service prim.
		  event, lToR, destination, start + transmissionHeight,
		  label, ServicePrimitive.HEAD);
		lastColumn = destination;		// set last column
		break;
	  }
	}
	if (eventType == ProtocolEvent.COMMENT) {
	  // if (eventNo == 0)
	  addSpace(source);
	  int col = entities.indexOf(event.getTarget());
	  addCommentSymbol(event.getComment(), col < mediumColumn, col);
	}
      }
      eventNo++;
    }
  }

  /**
    Adds a failed transmission to a time sequence diagram.

    @param event	protocol event
    @param lToR		true = left-to-right
    @param mediumColumn	medium column
    @param top		top of service primitive
    @param height	height of successful transmission
  */
  private void addFailedTransmission(
    ProtocolEvent event, boolean lToR, int mediumColumn, int top, int height) {
    FailedTransmission t =
      new FailedTransmission(event, lToR, mediumColumn, top, height);
    addSymbol(t);
  }

  /**
    Adds a feature to the SenderTimeout attribute of the TimeSequenceDiagram
    object.

    @param event	protocol event
    @param lToR		true = left-to-right
    @param sourceCol	source column
  */
  private void addSenderTimeout(
    ProtocolEvent event, boolean lToR, int sourceCol) {
    CommentSymbol st =
      new CommentSymbol(event, "timeout", lToR, sourceCol, topOfBlock);
    addSymbol(st);
  }

  /**
    Adds a feature to the ServicePrimitive attribute of the TimeSequenceDiagram
    object.

    @param event	protocol event
    @param lToR		true = left-to-right
    @param destination	destination column
    @param top		top of service primitive
    @param label	label for service primitive
    @param pos		position for label
  */
  private void addServicePrimitive(
    ProtocolEvent event, boolean lToR, int destination, int top,
    String label, int pos) {
    ServicePrimitive sp =
      new ServicePrimitive(event, lToR, destination, top, primitiveHeight);
    sp.setLabel(label, pos);
    addSymbol(sp);
  }

  /**
    Adds a feature to the Space attribute of the TimeSequenceDiagram object

    @param col		column
  */
  private void addSpace(int col) {
    addSymbol(new SpaceFiller(col, topOfBlock));
  }

  /**
    Adds a successful transmission to a time sequence diagram.

    @param event	protocol event
    @param lToR		true = left-to-right
    @param mediumColumn	medium column
    @param top		top of service primitive
    @param height	height of successful transmission
  */
  private void addSuccessfulTransmission(
    ProtocolEvent event, boolean lToR, int mediumColumn, int top, int height) {
    SuccessfulTransmission t =
      new SuccessfulTransmission(event, lToR, mediumColumn, top, height);
    addSymbol(t);
  }

  /**
    Adds a feature to the Symbol attribute of the TimeSequenceDiagram object

    @param symbol  The feature to be added to the Symbol attribute
  */
  private void addSymbol(TSDSymbol symbol) {
    int height = getSize().height;
    int symbolHeight = symbol.getHeight();
    int top = symbol.getTop();
    topOfBlock = top + symbolHeight;
    symbols.addElement(symbol);
    if (topOfBlock + symbolHeight + 2 * inset >= height) {
      int width = getSize().width;
      setSize(width, topOfBlock + symbolHeight + 2 * inset);
    }
    verticalPosition = getSize().height - initialHeight;
    repaint();
  }

  /**
    Adds a traverse transmission to a time sequence diagram.

    @param event	protocol event
    @param lToR		true = left-to-right
    @param source	source column
    @param columns	columns
    @param top		top of service primitive
    @param height	height of traverse transmission
  */
  private void addTraverseTransmission(
    ProtocolEvent event, boolean lToR, int source, int columns, int top,
     int height) {
    TraverseTransmission t =
      new TraverseTransmission(event, lToR, source, columns, top, height);
    addSymbol(t);
  }

  /**
    Remove everything from the time sequence diagram.
  */
  public void clear() {
    symbols.removeAllElements();
    verticalPosition = 0;
    setSize(getSize().width, initialHeight);
    topOfBlock = 3 * inset + headerTextHeight;
    repaint();
  }

  /**
    Draw the columns of the time sequence diagram.

    @param g		graphics object
    @param margin	column margin
    @param width	column width
    @param height	column height
  */
  private void drawColumns(Graphics g, int margin, int width, int height) {
    int headerWidth;
    int columnCentre;
    g.setFont(headerFont);				// column headings
    FontMetrics fm = getFontMetrics(headerFont);
    headerTextHeight = fm.getHeight();
    for (int c = 0; c < columns; c++) {
      columnCentre = margin + inset + (2 * c + 1) * columnWidth / 2;
      headerWidth = fm.stringWidth(columnHeaders[c]);
      g.drawString(
	columnHeaders[c], columnCentre - headerWidth / 2,
	inset + fm.getAscent());
    }
    g.drawLine(
      margin + inset, inset + headerTextHeight,
      margin + width - inset, inset + headerTextHeight);
    for (int c = 1; c < columns; c++)			// column separators
      g.drawLine(
	margin + inset + c * columnWidth, inset,
	margin + inset + c * columnWidth, height - inset);

  }

  /**
    Gets the transmissionStart attribute of the TimeSequenceDiagram object.

    @param pdu		PDU associated with the TRANSMIT event
    @return		Y-coordinate of the end of the SEND event arrow from
			which the corresponding TRANSMIT event symbol will start
  */
  private int getTransmissionStart(PDU pdu) {
    int y = topOfBlock;
    for (Enumeration e = symbols.elements(); e.hasMoreElements(); ) {
      TSDSymbol symbol = (TSDSymbol) e.nextElement();
      ProtocolEvent event = symbol.getEvent();
      if (event != null) {
	PDU pduSent = event.getPDU();
	if (pdu != null && pduSent.matches(pdu))
	  y = symbol.getTop() + symbol.getHeight();
      }
    }
    return(y);
  }

  /**
    Get the verticalPosition attribute of the TimeSequenceDiagram object.

    @return		vertical position
  */
  public int getVerticalPosition() {
    return(verticalPosition);
  }

  /**
    Paint the time sequene diagram.

    @param g		graphics object
  */
  public void paint(Graphics g) {
    TSDSymbol symbol;
    int width = getSize().width;
    int height = getSize().height;
    columnWidth = (width - 2 * inset) / columns;
    TSDSymbol.setParameters(inset, columnWidth, arrowWidth);
    defaultSymbolHeight =
      (columnWidth - arrowWidth) * primitiveHeight / arrowWidth;
    drawColumns(g, 0, width, height);
    if (starting) {
      initialHeight = height;
      topOfBlock = 3 * inset + headerTextHeight;
      starting = false;
    }
    for (Enumeration e = symbols.elements(); e.hasMoreElements(); ) {
      symbol = (TSDSymbol) e.nextElement();
      if (!e.hasMoreElements())				// colour last symbol
	g.setColor(lastColour);
      symbol.draw(g);
    }
  }

  /**
    Print the time sequence diagram.

    @param g		graphcs object
  */
  public void print(Graphics g) {
    TSDSymbol symbol = null;				// current symbol
    TSDSymbol symbolBuff[] =				// symbols to print
      new TSDSymbol[maxSymbols];
    int nextSymbol = 0;					// next symbol position
    int sym;						// symbol count
    int width = getSize().width;			// diagram width
    int height = getSize().height;			// diagram height
    int offset;						// offset from page top
    int nextHeight;					// next printing top
    int nullHeight = 0;					// height at last null

    if (starting) {					// first page?
      starting = false;					// note not first page
      stopped = false;					// printing not finished
      offset = 0;					// set first page offset
      currentHeight = 0;				// set symbol height
      page = 0;						// set page number
      System.out.print("Printing " +			// start page listing
	ProtocolSimulator.protocolType + " page");
    }
    else						// not first page
      offset =						// set later page offset
	3 * inset + headerTextHeight - currentHeight;
    nextHeight = currentHeight + maxHeight;		// next printing top
    columnWidth = (width - 2 * inset) / columns;	// set column width
    TSDSymbol.setParameters(				// set symbol details
      margin + inset, columnWidth, arrowWidth);
    defaultSymbolHeight =				// set transm. height
      (columnWidth - arrowWidth) * primitiveHeight / arrowWidth;
    drawColumns(g, margin, width, height);		// draw column headers
    System.err.print(" " + ++page);			// print page number
    for (Enumeration e = symbols.elements();		// go through symbols
      e.hasMoreElements(); ) {
      symbol = (TSDSymbol) e.nextElement();		// get next symbol
      if (symbol.getClass() == SpaceFiller.class) {	// symbol gap?
	if (symbol.top + symbol.height >= nextHeight) {	// past end of page?
	  printFooter(g);				// print page footer
	  currentHeight = nullHeight;			// set next page top
	  return;					// stop rendering
	}
	else {						// still within page
	  nullHeight = symbol.top;			// save last null height
	  for (sym = 0; sym < nextSymbol; sym++) {	// go through symbols
	    symbol = symbolBuff[sym];			// get stored symbol
	    symbol.top += offset;			// move symbol up page
	    symbol.draw(g);				// draw symbol
	    symbol.top -= offset;			// move symbol back
	  }
	  nextSymbol = 0;				// back to buffer start
	}
      }
      else if (symbol.top >= currentHeight) {		// inside current page?
	if (nextSymbol < maxSymbols)			// room for symbol?
	  symbolBuff[nextSymbol++] = symbol;		// store symbol
	else {						// no room for symbol
	  System.err.println(				// report error
	    "Cannot print more than " + maxSymbols + " symbols");
	  System.exit(1);				// leave programme
	}
      }
    }
    if (symbol != null &&				// non-null symbol?
      symbol.top >= nextHeight)				// past end of page?
      currentHeight = nullHeight;			// set next page top
    else {						// still within page
      for (sym = 0; sym < nextSymbol; sym++) {		// go through symbols
	symbol = symbolBuff[sym];			// get stored symbol
	symbol.top += offset;				// move symbol up page
	symbol.draw(g);					// draw symbol
	symbol.top -= offset;				// move symbol back
      }
      System.err.println();				// end page listing
      stopped = true;					// printing finished
    }
    printFooter(g);					// print page footer
  }

  /**
    Print footer of time sequence diagram.

    @param g		graphics object
  */
  public void printFooter(Graphics g) {			// print page footer
    String footer =					// set page footer
      ProtocolSimulator.protocolType + " Page " + page;
    FontMetrics fm = g.getFontMetrics(footerFont);	// get font metrics
    int diagWidth = getSize().width;			// diagram width
    int footerWidth = fm.stringWidth(footer);		// get footer width
    g.drawString(ProtocolSimulator.protocolType +	// print page footer
      " Page " + page,
      margin + (diagWidth - footerWidth) / 2, inset + maxHeight);
  }

  /**
    Note printing as started.
  */
  public void printStart() {				// start printing
    starting = true;					// set not started
  }

  /**
    Check if printing has stopped.

    @return		true/false if printing has/has not stopped
  */
  public boolean printStopped() {			// return print status
    return((stopped));					// get printing stopped
  }

}

