package com.tecspy.protocol;

import java.io.IOException;

import org.apache.log4j.Logger;

/**
 * Is fed with bytes from an input stream and issues complete messages. The
 * messages are bounded by a single start byte, "stx", and a single end byte,
 * "etx".
 * 
 * This class is coded to cope with the special case where stx and etx are the
 * same. Bytes that are received "outside" the message stx and etx are
 * considered to be erroneous and must be handled by the implementation.
 * 
 * Test byte streams:
 * 
 * normal : stx data etx | stx data etx | ...
 * 
 * restart: stx data stx data etx - report discarded bytes
 * 
 * quickrestart: stx stx data
 * 
 * when stx == etx then this is ignored
 * 
 * oob data: stx data etx data - report oob data
 * 
 * random: ??????? - not a real test but may stress test
 * 
 * join/part stream midway - quite possible at start of stream: data etx stx
 * data etx - special handling?
 * 
 * tests for maximum message length being exceeded
 * 
 * @author Michael Erskine
 * 
 */
public abstract class StxEtxMessageByteSink implements ByteSink {
	/** Standard log4j logger */
	protected static Logger log = Logger.getLogger(StxEtxMessageByteSink.class);

	/**
	 * Currently forming message buffer - should be constructed to the expected
	 * size of typical messages rather than the maximum
	 */
	protected StringBuilder msg;

	/** Message start byte */
	protected byte stx = (byte) 0x02;

	/** Message end byte */
	protected byte etx = (byte) 0x03;

	/** Maximum message length */
	protected int maxmsg = 1000;

	/**
	 * State of read process: false = waiting for stx, true = waiting for data
	 * or etx.
	 */
	private boolean inmsg = false;

	/**
	 * An indication of the number of bytes read. NB: not necessarily 100%
	 * correct.
	 */
	public long byteTotal = 0;

	protected byte lastChar;

	/**
	 * A subclass constructor must create the message buffer e.g.: -
	 * 
	 * <code>
	 * super();
	 * stx = (byte) 0xFE;
	 * etx = (byte) 0xFE;
	 * maxmsg = 1000;
	 * msg = new StringBuilder(1000);
	 * </code>
	 * 
	 */
	public StxEtxMessageByteSink() {
	}

	protected abstract void reportUnexpectedRestart();

	protected abstract void reportUnexpectedEtx();

	protected abstract void reportUnexpectedData(byte b);

	protected abstract void handleMsg();

	public int processBytes(byte[] buf, int len) {
		// incrementing the byte total here rather than for each byte
		byteTotal += len;
		for (int i = 0; i < len; i++) { // foreach byte
			if (!inmsg) { // !inmsg == waiting for stx
				// we are waiting for stx to start the message
				if (buf[i] == stx) {
					// stx is the normal condition
					inmsg = true;
				} else if (buf[i] == etx) {
					// we weren't expecting etx
					// in the case where etx == stx, the byte will have been
					// handled above
					reportUnexpectedEtx();
				} else {
					// we got unexpected data whilst waiting for stx
					reportUnexpectedData(buf[i]);
				}
			} else { // inmsg == true
				// we are waiting for data or etx
				if (buf[i] == etx) {
					// etx is a normal condition - flush
					handleMsg();
					msg.setLength(0);
					inmsg = false;
				} else if (buf[i] == stx) {
					// stx here is a message restart - report and reset
					// in the case where etx == stx, the byte will have been
					// handled above
					reportUnexpectedRestart();
					msg.setLength(0);
				} else {
					// normal condition add byte to message
					char c = (char) buf[i];
					if (msg.length() >= maxmsg) {
						log.error("maximum message length exceeded (" + maxmsg
								+ "): buffered data discarded");
						msg.setLength(0);
					}
					msg.append(c);
				}
			}
			lastChar = buf[i];
		} // end of foreach byte
		return len;
	}

	public abstract void processEof();

	public abstract void processException(IOException e);
}
