/*
 * Created on Jun 22, 2005
 *
 * TODO To change the template for this generated file go to
 * Window - Preferences - Java - Code Style - Code Templates
 */
package com.broadsoft.clients.oci.bcct;

/**
 * @author imported from AS
 *
 * TODO To change the template for this generated type comment go to
 * Window - Preferences - Java - Code Style - Code Templates
 */

import java.nio.*;

/**
 * As its name implies, objects of this class represent a transmission element
 * between the BCCT server and clients. 
 */
public class Packet implements Comparable
{
  public static final byte TYPE_PROT_REG_REQUEST = 0;
  public static final byte TYPE_PROT_REG_RESPONSE = 1;
  public static final byte TYPE_MESSAGE = 2;
  public static final byte TYPE_RESPONSE = 3;
  public static final byte TYPE_KEEP_ALIVE = 4;
  public static final byte TYPE_PROT_UNREG = 5;

  public static final byte STATUS_SUCCESS = 0;
  public static final byte STATUS_ERROR = 1;
  public static final byte STATUS_EXCEPTION_MESSAGE = 2;

  public static final byte ERROR_CODE_NONE = 0;
  public static final byte ERROR_CODE_HOST_NOT_AUTHORIZED = 1;
  public static final byte ERROR_CODE_PROTOCOL_NOT_AUTHORIZED = 2;
  public static final byte ERROR_CODE_INTERFACE_NOT_AUTHORIZED = 3;
  
  public static class Header
  {
    public static byte[] packetDelimiter = {'B','C','C','T'};
    
    /** Used internally */
    public static long NOT_A_REPLY = -1;
    
    public static String typeToString(int type) {
      switch(type) {
         case TYPE_PROT_REG_REQUEST:
           return "PROTOCOL REGISTRATION REQUEST";
         case TYPE_PROT_REG_RESPONSE:
           return "PROTOCOL REGISTRATION RESPONSE";
         case TYPE_MESSAGE:
           return "MESSAGE";
         case TYPE_RESPONSE:
           return "RESPONSE";
         case TYPE_KEEP_ALIVE:
           return "KEEP_ALIVE";
         case TYPE_PROT_UNREG:
           return "PROTOCOL UNREGISTRATION";
         default:
           return "UNKNOWN TYPE";
      }
    }

    public static String statusToString(int status) {
      switch(status) {
      case STATUS_SUCCESS:
        return "SUCCESS";
      case STATUS_ERROR:
        return "ERROR";
      case STATUS_EXCEPTION_MESSAGE:
        return "EXCEPTION MESSAGE";
      default:
        return "UNKNOWN TYPE";
      }
    }

    private static int      nextMsgId = 0;
    private static final Object lockNextMsgId = new Object();
    
    // *exact* serialized buffer size (no padding)
    public static final int HEADER_SERIALIZED_SIZE = 36;
    public byte[]   delimiter = packetDelimiter;
    public short    versionMajor = 1;
    public short    versionMinor = 0;
    private long    msgId;          // read-only field
    public byte     type;
    public boolean  replyExpected = false;        // converted to a byte when serialized
    public byte     status = STATUS_SUCCESS;      // Status detail is carried in payload when in error
    public byte     errorCode = ERROR_CODE_NONE;
    public short    protocol;
    public short    nextKeepAlive;                // in sec
    public long     repliesTo = NOT_A_REPLY;      // NOT_A_REPLY if not a reply
    public int      payloadSize;                  // size of data not including header

    public Header() 
    {
      synchronized(lockNextMsgId) {
        msgId = nextMsgId++;
      }
    }    

    /** Internal method */
    protected Header(long msgId) 
    {
      this.msgId = msgId;
    }    

    public long getMsgId()
    {
      return msgId;
    }
    
    public String toString() {
      return "Version: " + versionMajor + "." + versionMinor + "\n" +
             "Message ID: " + msgId + "\n" +
             "Type: " + typeToString(type) + "\n" +
             "Protocol: " + protocol + "\n" + 
             "Status: " + statusToString(status) + "\n" +
             "Replies to: " + repliesTo + "\n" +
             "Reply expected: " + replyExpected + "\n" +
             "Payload size: " + payloadSize + "\n";
    }
  }

  public Header header;
  public ByteBuffer payload = null;

  private int priority;

  // Used when receiving a packet whose header has been deserialized
  protected Packet(Header header, ByteBuffer payload)
  {
    this.header = header;
    this.payload = payload;
  }

  public Packet()
  {
    this.header = new Header();
  }

  
  public Packet(ByteBuffer msg)
  {
    header  = unserializeHeader(msg);
    payload = ByteBuffer.allocate(msg.limit() - msg.position());
    payload.put(msg);
    payload.flip();
  }
  
  public Packet(byte type, short protocol) throws Exception
  {
    init(type, protocol, null);
  }
  
  // Used when crafting a new packet before sending
  public Packet(byte type, short protocol, ByteBuffer payload) throws Exception
  {
    init(type, protocol, payload);
  }
  
  // Used when crafting a new packet before sending
  private void init(byte type, short protocol, ByteBuffer payload) throws Exception
  {
    header = new Header();
    header.type = type;
    header.replyExpected = false;
    header.repliesTo = Header.NOT_A_REPLY; // we don't know yet

    header.status = STATUS_SUCCESS;
    header.errorCode = 0;
    header.nextKeepAlive = 0;

    header.protocol = protocol;

    // very important: at this point, we DO NOT know the datasize!
    header.payloadSize = 0;
    this.payload = payload;
  }
 
  public void setNextKeepAlive(short nextKeepAlive) {
    header.nextKeepAlive = nextKeepAlive;
  }
  
  // Used when crafting a new packet before sending
  public Packet(byte type, short protocol, Exception e) throws Exception
  {
    header = new Header();
    header.type = type;
    header.replyExpected = false;
    header.repliesTo = Header.NOT_A_REPLY; // we don't know yet

    header.status = STATUS_EXCEPTION_MESSAGE;
    header.errorCode = ERROR_CODE_NONE;
    header.nextKeepAlive = 0;

    header.protocol = protocol;

    ByteBuffer msg = ByteBuffer.wrap(e.getMessage().getBytes());
    this.payload = msg;
    header.payloadSize = msg.remaining();
  }

  public void assignPriority(int priority) {
    this.priority = priority;
  }

  // in our case, higher priority means less than 
  // i.e. top priority = 0
  public int compareTo(Object o) {
    Packet compared = (Packet) o;
    return compared.priority - this.priority;
  }
  
  public int getType()
  {
    return header.type;
  }
  
  public short getProtocol()
  {
    return header.protocol;
  }
  
  /**
   * Specify that this packet is a reply to another packet.
   * @param origPacket Original packet this packet replies to.
   */
  public void isReplyTo(Packet origPacket)
  {
    this.header.repliesTo = origPacket.header.msgId;
  }

  /**
   * Specify that the sender expects a reply to this packet.
   */
  public void setReplyExpected(boolean expected)
  {
    this.header.replyExpected = expected;
  }
  

  /**
   * @param buffer
   * @throws Exception
   */
  private void serializeHeader(ByteBuffer buffer) throws Exception
  {   
    buffer.put(header.delimiter);
    buffer.putShort(header.versionMajor);
    buffer.putShort(header.versionMinor);
    buffer.putLong(header.msgId);
    buffer.put(header.type);
    buffer.put(header.replyExpected?(byte)1:(byte)0);
    buffer.put(header.status);
    buffer.put(header.errorCode);
    buffer.putShort(header.protocol);
    buffer.putShort(header.nextKeepAlive);
    buffer.putLong(header.repliesTo);
    buffer.putInt(header.payloadSize);
  }

  /**
   * @param buffer The buffer must be large enough
   * @throws Exception
   */
  public void serialize(ByteBuffer buffer) throws Exception
  {
    // go after the header and serialize payload first to know its size
    int headerPos = buffer.position();
    int payloadStartPos = headerPos + Header.HEADER_SERIALIZED_SIZE;
    buffer.position(payloadStartPos);

    if (payload != null)
      buffer.put(payload);

    // calculate datasize
    int payloadEndPos = buffer.position();
    header.payloadSize = payloadEndPos - payloadStartPos;

    // serialize header
    buffer.position(headerPos);
    serializeHeader(buffer);
    buffer.position(payloadEndPos);
  }

  /**
   * Attempts to create a Packet.Header from a data stream. 
   * If the data is incomplete, returns null.
   * Null is returned instead of throwing an exception for performance purposes
   * (having an incomplete buffer is deemed to be frequent).
   * <p>Precondition: the buffer is ready for additional relative put operations.
   * <p>Postconditions: 
   * <p>  1. The buffer and its state are unmodified if no header were created (null is returned)
   * <p>  2. The buffer is in an unspecified state if a header was created 
   * @return Null or newly created Packet
   */
  public static Packet.Header unserializeHeader(ByteBuffer buffer)
  {
    // bail out if we didn't receive a full packet
    if (buffer.position() < Header.HEADER_SERIALIZED_SIZE) {
      return null;
    }
    // at this point, we know we have a full header: decode it
    
    // ready buffer for relative gets
    buffer.flip();

    Header header = new Header(0);

    buffer.get(header.delimiter, 0, 4);
    
    if (!header.delimiter.equals(Header.packetDelimiter)) {
      // Socket transmission is desynchronized
      return null;
    }

    header.versionMajor = buffer.getShort();
    header.versionMinor = buffer.getShort();
    header.msgId = buffer.getLong();
    header.type = buffer.get();
    header.replyExpected = buffer.get() != (byte)0;
    header.status = buffer.get();
    header.errorCode = buffer.get();
    header.protocol = buffer.getShort();
    header.nextKeepAlive = buffer.getShort();
    header.repliesTo = buffer.getLong();
    header.payloadSize = buffer.getInt();
    
    return header;
  }
}
