content: /**
   * Copyright (c) 2008 Andrew Rapp. All rights reserved.
   *  
   * This file is part of XBee-API.
   *  
   * XBee-API is free software: you can redistribute it and/or modify
   * it under the terms of the GNU General Public License as published by
   * the Free Software Foundation, either version 3 of the License, or
   * (at your option) any later version.
   *  
   * XBee-API is distributed in the hope that it will be useful,
   * but WITHOUT ANY WARRANTY; without even the implied warranty of
   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   * GNU General Public License for more details.
   *  
   * You should have received a copy of the GNU General Public License
   * along with XBee-API.  If not, see <http://www.gnu.org/licenses/>.
   */
  
  package com.rapplogic.xbee.api;
  
  import java.util.EnumSet;
  import java.util.HashMap;
  import java.util.Map;
  
  import org.apache.log4j.Logger;
  
  import com.rapplogic.xbee.util.ByteUtils;
  
  /**
   * Packages a frame data array into an XBee packet.
   * <p/>
   * @author andrew
   *
   */
  public class XBeePacket {
  
  	public enum SpecialByte {
  		START_BYTE (0x7e), // ~
  		ESCAPE (0x7d), // }
  		XON (0x11),
  		XOFF (0x13);
  		
  		private static final Map<Integer,SpecialByte> lookup = new HashMap<Integer,SpecialByte>();
  		
  		static {
  			for(SpecialByte s : EnumSet.allOf(SpecialByte.class)) {
  				lookup.put(s.getValue(), s);
  			}
  		}
  		
  		public static SpecialByte get(int value) { 
  			return lookup.get(value); 
  		}
  		
  	    private final int value;
  	    
  	    SpecialByte(int value) {
  	        this.value = value;
  	    }
  
  		public int getValue() {
  			return value;
  		}
  	}
  	
  	private final static Logger log = Logger.getLogger(XBeePacket.class);
  		
  	private int[] packet;
  	
  	/**
  	 * Performs the necessary activities to construct an XBee packet from the frame data.
  	 * This includes: computing the checksum, escaping the necessary bytes, adding the start byte and length bytes.
  	 * 
  	 * The format of a packet is as follows:
  	 * 
  	 * start byte - msb length byte - lsb length byte - frame data - checksum byte
  	 * 
  	 * @param frameData
  	 */
  	public XBeePacket(int[] frameData) {
  		
  		// checksum is always computed on pre-escaped packet
  		Checksum checksum = new Checksum();
  
          for (int aFrameData : frameData) {
              checksum.addByte(aFrameData);
          }
  		
  		checksum.compute();
  		
  		// packet size is frame data + start byte + 2 length bytes + checksum byte
  		packet = new int[frameData.length + 4];
  		packet[0] = SpecialByte.START_BYTE.getValue();
  		
  		// Packet length does not include escape bytes or start, length and checksum bytes
  		XBeePacketLength length = new XBeePacketLength(frameData.length);
  		
  		// msb length (will be zero until maybe someday when > 255 bytes packets are supported)
  		packet[1] = length.getMsb();
  		// lsb length
  		packet[2] = length.getLsb();
  		
  		for (int i = 0; i < frameData.length; i++) {
  			if (frameData[i] > 255) {
  				throw new RuntimeException("Packet values must not be greater than one byte (255): " + frameData[i]);
  			}
  			
  			packet[3 + i] = frameData[i];
  		}
  		
  		// set last byte as checksum
  		// note: if checksum is not correct, XBee won't send out packet or return error.  ask me how I know.
  
  		packet[packet.length - 1] = checksum.getChecksum();
  		
  //		for (int i = 0; i < packet.length; i++) {
  //			log.debug("XBeeApi pre-escape packet byte " + i + " is " + ByteUtils.toBase16(packet[i]));
  //		}
  		
  		int preEscapeLength = packet.length;
  		
  		// TODO save escaping for the serial out method. this is an unnecessary operation
  		packet = escapePacket(packet);
  		
  		if (log.isDebugEnabled()) {
  			StringBuilder stringBuilder = new StringBuilder();
  			stringBuilder.append("Packet: ");
  			for (int i = 0; i < packet.length; i++) {
  				stringBuilder.append(ByteUtils.toBase16(packet[i]));
  				if (i < packet.length - 1) {
  					stringBuilder.append(" ");
  				}
  			}	
  			log.debug(stringBuilder);
  
  			log.debug("pre-escape packet size is " + preEscapeLength + ", post-escape packet size is " + packet.length);
  		}
  	}
  	
  	/**
  	 * Escape all bytes in packet after start byte, and including checksum
  	 * 
  	 * @param packet
  	 * @return
  	 */
  	private static int[] escapePacket(int[] packet) {
  		int escapeBytes = 0;
  		
  		// escape packet.  start at one so we don't escape the start byte 
  		for (int i = 1; i < packet.length; i++) {
  			if (isSpecialByte(packet[i])) {
  				log.debug("escapeFrameData: packet byte requires escaping byte " + ByteUtils.toBase16(packet[i]));
  				escapeBytes++;
  			}
  		}
  
  		if (escapeBytes == 0) {
  			return packet;
  		} else {
  			log.debug("packet requires escaping");
  			
  			int[] escapePacket = new int[packet.length + escapeBytes];
  			
  			int pos = 1;
  			
  			escapePacket[0] = SpecialByte.START_BYTE.getValue();
  				
  			for (int i = 1; i < packet.length; i++) {
  				if (isSpecialByte(packet[i])) {
  					escapePacket[pos] = SpecialByte.ESCAPE.getValue();
  					escapePacket[++pos] = 0x20 ^ packet[i];
  					
  					log.debug("escapeFrameData: xor'd byte is 0x" + Integer.toHexString(escapePacket[pos]));
  				} else {
  					escapePacket[pos] = packet[i];
  				}
  				
  				pos++;
  			}
  			
  			return escapePacket;
  		}
  	}
  	
  	/**
  	 * @deprecated use getByteArray
  	 * @return
  	 */
  	public int[] getPacket() {
  		return this.getByteArray();
  	}
  	
  	public int[] getByteArray() {
  		return packet;
  	}
  	
  	public static boolean isSpecialByte(int b) {
          return b == SpecialByte.START_BYTE.getValue() || b == SpecialByte.ESCAPE.getValue() || b == SpecialByte.XON.getValue() ||
                  b == SpecialByte.XOFF.getValue();
  
      }
  	
  	public String toString() {
  		return ByteUtils.toBase16(this.packet);
  	}
  	
  	/**
  	 * Must be called with unescapted packet
  	 * 
  	 * @param packet
  	 * @return
  	 */
  	public static int getPacketLength(int[] packet) {
  		return new XBeePacketLength(packet[1], packet[2]).getLength();
  	}
  	
  	/**
  	 * Returns true if the packet is valid, Verifies both escaped and un-escaped packets
       *
  	 * @param packet
  	 * @return
  	 */
  	public static boolean verify(int[] packet) {
  		boolean valid = true;
  		
  		try {
  			if (packet[0] != SpecialByte.START_BYTE.getValue()) {
  				return false;
  			}
  
  			if (packetEndsWithEscapeByte(packet)) {
  				// packet can never end on a escape byte since the following byte is xor to unescape and will result in a array out of bounds exception
  				// this is an incomplete packet
  				return false;
  			}
  			
  			// first need to un-escape packet since escape bytes complicate things
  			int[] unEscaped = unEscapePacket(packet);
  			
  			// in theory 3 bytes are the minimum for 0 byte packet with no escape bytes. any less it can't be valid
  			if (unEscaped.length < 3) {
  				return false;
  			}
  			
  			int len = getPacketLength(unEscaped);
  			
  			// total packet length = stated length + 1 start byte + 1 checksum byte + 2 length bytes
  			// stated packet length does include escaping bytes, so actual packet size can be much larger, almost double
  			int expectedPacketLength = len + 4;
  			
  			// if we are less bytes than expected packet length it can't be valid
  			if (unEscaped.length != expectedPacketLength) {
  				return false;
  			}
  			
  			int[] frameData = new int[len];
  			
  			Checksum checksum = new Checksum();
  			
  			for (int i = 3; i < unEscaped.length - 1; i++) {
  				frameData[i - 3] = unEscaped[i];
  				checksum.addByte(frameData[i - 3]);
  			}
  			
  			// add checksum byte to verify -- the last byte
  			checksum.addByte(unEscaped[unEscaped.length - 1]);
  			
  			if (!checksum.verify()) {
  				valid = false;
  			}
  		} catch (Exception e) {
  			throw new RuntimeException("Packet verification failed with error: ", e);
  		}
  
  		return valid;
  	}
  	
  	public static boolean packetEndsWithEscapeByte(int[] packet) {
  		return (packet[packet.length - 1] == SpecialByte.ESCAPE.getValue());
  	}
  	
  	/**
  	 * 
  	 * @param packet
  	 * @return
  	 * @throws IncompletePacketException 
  	 */
  	public static int[] unEscapePacket(int[] packet) {
  	
  		int escapeBytes = 0;
  		
  		if (packetEndsWithEscapeByte(packet)) {
  			//packet can never end on a escape byte since the following byte is xor to unescape and will result in a array out of bounds exception
  			throw new RuntimeException("Invalid packet -- packet cannot end with an escape byte " + ByteUtils.toBase16(packet));
  		}
  
  		// first check if escape byte exists, if not we don't allocate a new array
          for (int b : packet) {
              if (b == SpecialByte.ESCAPE.getValue()) {
                  escapeBytes++;
              }
          }
  		
  		if (escapeBytes == 0) {
  			return packet;
  		}
  		
  		int[] unEscapedPacket = new int[packet.length - escapeBytes];
  		
  		int pos = 0;
  		
  		for (int i = 0; i < packet.length; i++) {
  			if (packet[i] == SpecialByte.ESCAPE.getValue()) {
  				// discard escape byte and un-escape following byte
  				if (i >= packet.length - 1) {
  					return packet;
  				}
  				
  				unEscapedPacket[pos] = 0x20 ^ packet[++i];
  			} else {
  				unEscapedPacket[pos] = packet[i];
  			}
  			
  			pos++;
  		}
  		
  		return unEscapedPacket;
  	}
  }