// ABRAProtocol.java

package protocol;

import java.util.*;
import support.*;

/**
  This is the class for the Abracadabra protocol sender.

  @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
*/
public class ABRAProtocol implements ProtocolEntity, Timeouts {

  // Protocol state constants
  private static final int CLOSED  = 0; // waiting for connection request
  private static final int CR_SENT = 1; // waiting for connection confirmation
  private static final int CR_RECV = 2; // waiting for ConResp from user
  private static final int SEND    = 3; // prepared to send data PDU
  private static final int WAIT    = 4; // waiting for acknowledgement of data
  private static final int DR_SENT = 5; // waiting for disconnection confirm

  // Protocol data unit types
  public static final String CR = "Connection Request";
  public static final String CC = "Connection Confirmation";
  public static final String DT = "Data Transfer";
  public static final String AK = "Acknowledgement";
  public static final String DR = "Disconnection Request";
  public static final String DC = "Disconnection Confirmation";

  public static final int maxRetries = 3;

  private PDU pduSent;
  private PDU lastDTpdu;
  private int sendSeq;
  private int recvSeq;
  private ProtocolEntity peer;
  private ABRAService user;
  private Vector<ProtocolEvent> entityEvents;		// events from entity
  private Medium medium;
  private String name;
  private boolean timerEnabled;
  private int crRemaining;
  private int drRemaining;
  private int dtRemaining;
  private boolean dtOrAk;
  private int state;

  public ABRAProtocol(Medium m, String name) {
    this.name = name;
    medium = m;
    initialise();
  }

  public String getName() {
    return(name);
  }

  public Vector<String> getServices() {
    Vector<String> list = new Vector<String>();
    switch (state) {
      case CR_SENT:        // waiting for connection confirmation
	if (timerEnabled) {
	  if (crRemaining > 1)
	    list.addElement("Timeout - resend " + CR);
	  else
	    list.addElement("Retry limit reached - send " + DR);
	}
	break;
      case WAIT:           // waiting for acknowledgement of data
	if (timerEnabled) {
	  if (dtRemaining > 1)
	    list.addElement("Timeout - resend DT(" + lastDTpdu.seq + ")" );
	  else
	    list.addElement("Retry limit reached - send " + DR);
	}
	break;
      case DR_SENT:        // waiting for disconnection confirmation
	if (timerEnabled) {
	  if (drRemaining > 1) {
	    list.addElement("Timeout - resend " + DR);
	  }
	  else {
	    // Retry limit reached
	    dtRemaining--;
	    state = CLOSED;
	  }
	}
	break;
      }
    return(list);
  }

  /** Increment PDU sequence number (alternating bit) */

  private int inc(int seq) {
    return(1 - seq);
  }

  public void initialise() {
    entityEvents = new Vector<ProtocolEvent>();
    state = CLOSED;
  }

  public boolean hasTimer(String type) {
    return(type.equals("DT") || type.equals("DR") || type.equals("CR"));
  }

  public Vector<ProtocolEvent> performService(String s) {
    Vector<ProtocolEvent> events = new Vector<ProtocolEvent>();
    switch (state) {
      case CR_SENT:
	if (s.startsWith("Timeout")) {
	  crRemaining--;
	  transmitPDU(new PDU("CR"), peer);  // resend connection request PDU
	  events.addElement(
	    new ProtocolEvent(ProtocolEvent.TIMEOUT, pduSent));
	}
	if (s.startsWith("Retry limit")) {
	  crRemaining--;
	  transmitPDU(new PDU("DisInd"), user);
	  events.addElement(
	    new ProtocolEvent(ProtocolEvent.DELIVER, pduSent));
	  transmitPDU(new PDU("DR"), peer);  // send disconnection request PDU
	  events.addElement(
	    new ProtocolEvent(ProtocolEvent.TRANSMIT, pduSent));
	  drRemaining = maxRetries;
	  state = DR_SENT;
	}
	break;
      case WAIT:
	if (s.startsWith("Timeout")) {
	  dtRemaining--;
	  transmitPDU(lastDTpdu, peer);
	  events.addElement(
	    new ProtocolEvent(ProtocolEvent.TIMEOUT, pduSent));
	}
	if (s.startsWith("Retry limit")) {
	  transmitPDU(new PDU("DisInd"), user);
	  events.addElement(
	    new ProtocolEvent(ProtocolEvent.TRANSMIT, pduSent));
	  dtRemaining--;
	  transmitPDU(new PDU("DR"), peer);  // send disconnection request PDU
	  events.addElement(
	    new ProtocolEvent(ProtocolEvent.TRANSMIT, pduSent));
	  drRemaining = maxRetries;
	  state = DR_SENT;
	}
	break;
      case DR_SENT:
	if (s.startsWith("Timeout")) {
	  drRemaining--;
	  transmitPDU(new PDU("DR"), peer);
	  events.addElement(
	    new ProtocolEvent(ProtocolEvent.TIMEOUT, pduSent));
	}
	break;
    }
    for (Enumeration e = entityEvents.elements(); e.hasMoreElements(); )
      events.addElement((ProtocolEvent) e.nextElement());
    return(events);
  }

  public Vector<ProtocolEvent> receivePDU(PDU pdu) {
    Vector<ProtocolEvent> events = new Vector<ProtocolEvent>();
    String pduType = "";
    if (pdu != null)
      pduType = pdu.type;
    switch (state) {
      case CLOSED:
	if (pduType.equals("ConReq")) {		// ConReq primitive?
	  transmitPDU(new PDU("CR"), peer);	// send connection request PDU
	  dtOrAk = false;
	  sendSeq = 0;
	  recvSeq = 0;
	  crRemaining = maxRetries;
	  state = CR_SENT;
	  events.addElement(new ProtocolEvent(ProtocolEvent.TRANSMIT, pduSent));
	}
	else if (pduType.equals("CR")) {	// connection request PDU?
	  transmitPDU(new PDU("ConInd"), user);
	  events.addElement(new ProtocolEvent(ProtocolEvent.DELIVER, pduSent));
	  dtOrAk = false;
	  sendSeq = 0;
	  recvSeq = 0;
	  state = CR_RECV;
	}
	else if (pduType.equals("DR")) {	// got disconnect request PDU
	  transmitPDU(new PDU("DC"), peer);	// send disconnect confirm PDU
	  events.addElement(new ProtocolEvent(ProtocolEvent.TRANSMIT, pduSent));
	}
	break;
      case CR_SENT:
	if (pduType.equals("CC") ||		// connection confirmation PDU?
	    pduType.equals("CR")) {		// connection request PDU?
	  // (both stations have requested connections at the same time)
	  transmitPDU(new PDU("ConConf"), user);
	  events.addElement(new ProtocolEvent(ProtocolEvent.DELIVER, pduSent));
	  timerEnabled = false;
	  state = SEND;
	}
	else if (pduType.equals("DR")) {   	 // disconnection request PDU?
	  transmitPDU(new PDU("DisInd"), user);
	  events.addElement(new ProtocolEvent(ProtocolEvent.DELIVER, pduSent));
	  timerEnabled = false;
	  state = CLOSED;
	}
	else if (pduType.equals("DisReq")) {	// disconnection request prim?
	  // transmitPDU(new PDU("DisInd"), user);
	  // events.addElement(
	  //  new ProtocolEvent(ProtocolEvent.DELIVER, pduSent));
	  transmitPDU(new PDU("DR"), peer);	// send disconnect request PDU
	  events.addElement(new ProtocolEvent(ProtocolEvent.TRANSMIT, pduSent));
	  drRemaining = maxRetries;
	  state = DR_SENT;
	}
	break;
      case CR_RECV:
	if (pduType.equals("ConResp")) {
	  transmitPDU(new PDU("CC"), peer);  // send connection confirm PDU
	  events.addElement(new ProtocolEvent(ProtocolEvent.TRANSMIT, pduSent));
	  state = SEND;
	}
	else if (pduType.equals("DR")) {
	  transmitPDU(new PDU("DisInd"), user);
	  events.addElement(
	    new ProtocolEvent(ProtocolEvent.DELIVER, pduSent));
	  transmitPDU(new PDU("DC"), peer);  // send disconnection confirm PDU
	  events.addElement(new ProtocolEvent(ProtocolEvent.TRANSMIT, pduSent));
	  state = CLOSED;
	}
	else if (pduType.equals("ConReq")) {
	  transmitPDU(new PDU("ConConf"), user);
	  events.addElement(
	    new ProtocolEvent(ProtocolEvent.DELIVER, pduSent));
	  transmitPDU(new PDU("CC"), peer);	// send connection confirm PDU
	  events.addElement(new ProtocolEvent(ProtocolEvent.TRANSMIT, pduSent));
	  state = SEND;
	}
	else if (pduType.equals("DisReq")) {
	  transmitPDU(new PDU("DR"), peer);	// send disconnect request PDU
	  events.addElement(new ProtocolEvent(ProtocolEvent.TRANSMIT, pduSent));
	  state = CLOSED;
	}
	break;
      case WAIT:
	if (pduType.equals("AK")) {
	  if (pdu.seq == sendSeq) {
	    dtOrAk = true;
	    timerEnabled = false;
	    user.setOKToSend(true);
	    state = SEND;
	  }
	  else {				// wrong AK : enter error phase
	    transmitPDU(new PDU("DisInd"), user);
	    events.addElement(
	      new ProtocolEvent(ProtocolEvent.DELIVER, pduSent));
	    transmitPDU(new PDU("DR"), peer);  // send disconnect request PDU
	    events.addElement(
	      new ProtocolEvent(ProtocolEvent.TRANSMIT, pduSent));
	    drRemaining = maxRetries;
	    state = DR_SENT;
	  }
	}
	// break intentionally omitted
      case SEND:
	if (state == SEND			// avoid drop-through from WAIT
	    && pduType.equals("DatReq")) {	// DatReq received from user
	  transmitPDU(new PDU("DT", sendSeq, pdu.getSDU()), peer);
	  events.addElement(
	    new ProtocolEvent(ProtocolEvent.TRANSMIT, pduSent));
	  user.setOKToSend(false);
	  sendSeq = inc(sendSeq);
	  dtRemaining = maxRetries;
	  state = WAIT;
	}
	else if (pduType.equals("DR")) {	// disconnection request PDU?
	  transmitPDU(new PDU("DisInd"), user);
	  events.addElement(
	    new ProtocolEvent(ProtocolEvent.DELIVER, pduSent));
	  transmitPDU(new PDU("DC"), peer);
	  events.addElement(new ProtocolEvent(ProtocolEvent.TRANSMIT, pduSent));
	  timerEnabled = false;
	  state = CLOSED;
	}
	else if (pduType.equals("DT")) {	// data PDU received
	  dtOrAk = true;
	  if (pdu.seq == recvSeq) {		// check sequence number correct
	    recvSeq = inc(recvSeq);
	    transmitPDU(new PDU("DatInd", pdu.getSDU()), user);
	    events.addElement(
	      new ProtocolEvent(ProtocolEvent.DELIVER, pduSent));
	  }
	  transmitPDU(new PDU("AK", recvSeq), peer);
	  events.addElement(new ProtocolEvent(ProtocolEvent.TRANSMIT, pduSent));
	  // no state transition
	}
	if (pduType.equals("CC") || pduType.equals("DC")) {
	  transmitPDU(new PDU("DisInd"), user);
	  events.addElement(new ProtocolEvent(ProtocolEvent.DELIVER, pduSent));
	  transmitPDU(new PDU("DR"), peer);
	  events.addElement(new ProtocolEvent(ProtocolEvent.TRANSMIT, pduSent));
	  drRemaining = maxRetries;
	  state = DR_SENT;
	}
	if (pduType.equals("CR")) {		// connection request PDU?
	  if (!dtOrAk) {
	    transmitPDU(new PDU("CC"), peer);
	    events.addElement(
	      new ProtocolEvent(ProtocolEvent.TRANSMIT, pduSent));
	  // no state transition
	  }
	  else {
	    transmitPDU(new PDU("DisInd"), user);
	    events.addElement(
	      new ProtocolEvent(ProtocolEvent.DELIVER, pduSent));
	    transmitPDU(new PDU("DR"), peer);
	    events.addElement(
	      new ProtocolEvent(ProtocolEvent.TRANSMIT, pduSent));
	    drRemaining = maxRetries;
	    state = DR_SENT;
	  }
	}
	if (pduType.equals("DisReq")) {
	  transmitPDU(new PDU("DR"), peer);
	  events.addElement(new ProtocolEvent(ProtocolEvent.TRANSMIT, pduSent));
	  drRemaining = maxRetries;
	  state = DR_SENT;
	}
	break;
      case DR_SENT:				// awaiting disconnect confirm
	if (pduType.equals("DC") ||		// disconnect confirm PDU?
	    pduType.equals("DR")) {		// disconnect request PDU?
	  // both stations have requested disconnection at the same time
	  timerEnabled = false;
	  state = CLOSED;
	}
	if (pduType.equals("ConReq")) {
	  transmitPDU(new PDU("DisInd"), user);
	  events.addElement(new ProtocolEvent(ProtocolEvent.DELIVER, pduSent));
	}
	if (timerEnabled && drRemaining <= 1) {
	  drRemaining--;
	  state = CLOSED;
	}
	break;
      }
    return(events);
  }

  public void setPeer(ProtocolEntity peer) {
    this.peer = peer;
  }

  /**
    Set the timer for a specified PDU.

    @param pdu		PDU
    @param enabled	whether the timer is enabled
  */
  public void setTimer(PDU pdu, boolean enabled) {
    timerEnabled = enabled;
  }

  public void setUser(ProtocolEntity user) {
    this.user = (ABRAService)user;
  }

  public void transmitPDU(PDU pdu, ProtocolEntity destination) {
    pdu.setSource(this);
    pdu.setDestination(destination);
    if (destination == peer)
      entityEvents = medium.receivePDU(pdu);
    else
      entityEvents = user.receivePDU(pdu);
    pduSent = pdu;
    if (pdu.type.equals("DT"))
      lastDTpdu = pdu;
for (Enumeration e = entityEvents.elements(); e.hasMoreElements(); )
  System.err.println("ABRAP transmitPDU: " + (ProtocolEvent) e.nextElement());
  }

}
