package stamp.peripheral.x10.insteon;

import stamp.core.*;
import stamp.math.Int32;  // http://www.parallax.com/javelin/applications.asp#AN011
//DEBUG import stamp.util.text.*; // http://www.parallax.com/javelin/applications.asp#AN012

/**
 * This class provides access to the INSTEON PowerLinc V2 Controller
 * (http://www.cls01.com/insteon)
 *
 * @see <a href="http://www.smarthome.com/2414s.html">Smarthome INSTEON Controller</a>
 * @see <a href="http://www.smarthome.com/insteon/sdk2600S.html">INSTEON Development Kit</a>
 * @author Scott Herr (see <a href="http://www.cls01.com/insteon">http://www.cls01.com/insteon</a>)
 * @version 0.03 April 23, 2006
 */

public class Insteon {

  //****************************************************************************
  // PUBLIC DEFINITIONS AND METHODS
  //****************************************************************************

  //----------------------------------------------------------------------------
  // TYPICAL DEFINITIONS
  //  static final int PLCpinTX = CPU.pin9;
  //  static final int PLCpinRX = CPU.pin10;
  //  static Uart PLCtx = new Uart(Uart.dirTransmit, PLCpinTX, Uart.dontInvert, Uart.speed4800, Uart.stop1);
  //  static Uart PLCrx = new Uart(Uart.dirReceive, PLCpinRX, Uart.dontInvert, Uart.speed4800, Uart.stop1);
  //  static Timer PLCtimer = new Timer();
  //  static Insteon PLC = new Insteon(PLCtx,PLCrx,PLCtimer);
  //---------------------------------------------------------------------------/

  // Command types
  public final static char CMDTYPE_DIRECT = 0x00;              // Direct command
  // Command 1
  public final static char CMD_ON = 0x11;              // On command
  public final static char CMD_OFF = 0x13;             // Off command
  // Command 2
  public final static char CMD2_FULL_ON = 0xff;        // Full On level
  // X10 commands
  public final static char X10_ON = 2;
  public final static char X10_OFF = 3;
  public final static char X10_BRIGHT = 5;
  public final static char X10_DIM = 4;
  public final static char X10_ALL_LIGHTS_ON = 1;
  public final static char X10_ALL_LIGHTS_OFF = 6;
  public final static char X10_ALL_UNITS_OFF = 0;
  public final static char X10_HAIL_REQ = 8;
  public final static char X10_HAIL_ACK = 9;
  public final static char X10_STATUS_ON = 13;
  public final static char X10_STATUS_OFF = 14;
  public final static char X10_STATUS_REQUEST = 15;
  // Event types
  public final static char IBIOS_EVENT_REPORT = 0x45;
  public final static char IBIOS_SIMULATED_EVENT = 0x47;
  public final static char IBIOS_X10_BYTE_RECD = 0x4A;
  public final static char IBIOS_INSTEON_MSG_RECD = 0x4F;
  // Event handles
  public final static char EVNT_INIT = 0x00;
  public final static char EVNT_IRX_MYMSG = 0x01;
  public final static char EVNT_IRX_MSG = 0x02;
  public final static char EVNT_IRX_PKT = 0x03;
  public final static char EVNT_ITX_ACK = 0x04;
  public final static char EVNT_ITX_NACK = 0x05;
  public final static char EVNT_IRX_BADID = 0x06;
  public final static char EVNT_IRX_ENROLL = 0x07;
  public final static char EVNT_IRX_XRX_MSG = 0x08;
  public final static char EVNT_IRX_XRX_XMSG = 0x09;
  public final static char EVNT_IRX_BTN_TAP = 0x0A;
  public final static char EVNT_IRX_BTN_HOLD = 0x0B;
  public final static char EVNT_IRX_BTN_REL = 0x0C;
  public final static char EVNT_TICK = 0x0D;
  public final static char EVNT_ALARM = 0x0E;
  public final static char EVNT_MIDNIGHT = 0x0F;
  public final static char EVNT_2AM = 0x10;
  public final static char EVNT_RX = 0x11;
  public final static char EVNT_SRX_COM = 0x12;
  public final static char EVNT_DAUGHTER = 0x13;
  public final static char EVNT_LOAD_ON = 0x14;
  public final static char EVNT_LOAD_OFF = 0x15;
  public final static char EVNT_NULL = 0xFF;

  //----------------------------------------------------------------------------
  /**
   * Constructor for INSTEON PowerLinc V2 Controller
   * @param tx Uart for transmitting from Javelin to PLC
   * @param rx Uart for receiving to Javelin from PLC
   * @param t2 Timer to use within Insteon class
   */
  public Insteon(Uart tx, Uart rx, Timer t2) {
    this.tx = tx;
    this.rx = rx;
    this.t2 = t2;
    this.t2.mark();
    char addrHigh = 0;
  } // END Insteon
  //----------------------------------------------------------------------------
  /**
   * Send INSTEON Command
   * @param type INSTEON command type (CMDTYPE_xxx)
   * @param addr INSTEON address
   * @param command1 INSTEON command (CMD_xxx)
   * @param command2 INSTEON command2 (CMD2_xxx or group or value)
   * @return
   *    0 = success,
   *    -1xxx = error downloading command to PLC (xxx = error from downloadData),
   *    -2xxx = error requesting PLC to send command (xxx = error from sendMask),
   *    (common error -1111 = PLC disconnected or unplugged)
   */
  public int sendInsteonCmd(char type, char[] addr, char command1, char command2) {
    int rtnCode;
    // Send INSTEON Command
    cmdBuffer[0] = addr[0];
    cmdBuffer[1] = addr[1];
    cmdBuffer[2] = addr[2];
    cmdBuffer[3] = (char)((type<<4) | 0x0f);
    cmdBuffer[4] = command1;
    cmdBuffer[5] = command2;
    rtnCode = downloadData(0x01,0xa4,6,cmdBuffer);
    if (rtnCode < 0) return rtnCode - 1000;
    rtnCode = sendMask(0x01,0x42,0x10,0xff);
    if (rtnCode < 0) return rtnCode - 2000;
    return 0;
  } // END sendInsteonCmd
  //----------------------------------------------------------------------------
  /**
   * Send X10 Command - Single Command
   * @param houseCode X10 House Code ('A'-'P')
   * @param unitNumber X10 Unit Number (1-16)
   * @param X10Command X10 Command (see X10_[command])
   * @return
   *    0 = success,
   *    -1xxx = error downloading X10 house/unit to PLC (xxx = error from downloadData),
   *    -2xxx = error requesting PLC to send X10 house/unit (xxx = error from sendMask),
   *    -3xxx = error downloading command to PLC (xxx = error from downloadData),
   *    -4xxx = error requesting PLC to send X10 command (xxx = error from sendMask),
   *    (common error -1111 = PLC disconnected or unplugged)
   */
  public int sendX10(char houseCode, int unitNumber, char X10Command) {
    return sendX10(houseCode, unitNumber, X10Command, 1);
  }
  /**
   * Send X10 Command - Multiple Commands (typically X10_DIM)
   * @param houseCode X10 House Code ('A'-'P')
   * @param unitNumber X10 Unit Number (1-16)
   * @param X10Command X10 Command (see X10_[command])
   * @param count Number of times to send command
   * @return
   *    0 = success,
   *    -1xxx = error downloading X10 house/unit to PLC (xxx = error from downloadData),
   *    -2xxx = error requesting PLC to send X10 house/unit (xxx = error from sendMask),
   *    -3xxx = error downloading command to PLC (xxx = error from downloadData),
   *    -4xxx = error requesting PLC to send X10 command (xxx = error from sendMask),
   *    (common error -1111 = PLC disconnected or unplugged)
   */
  public int sendX10(char houseCode, int unitNumber, char X10Command, int count) {
    int rtnCode;
    int i;
    // Send House Code and Device Code
    X10Cmd[0] = (char)((X10_CODE[houseCode-'A']<<4) + X10_CODE[unitNumber-1]);
    rtnCode = downloadData(0x01,0x65,1,X10Cmd);
    if (rtnCode < 0) return rtnCode - 1000;
    rtnCode = sendMask(0x01,0x66,0x80,0xf7);
    if (rtnCode < 0) return rtnCode - 2000;
    // Send Command "count" times
    X10Cmd[0] = (char)((X10_CODE[houseCode-'A']<<4) + X10Command);
    if (count==0) count = 1;
    for (i=0;i<count;i++) {
      if (i>0) CPU.delay(IBIOS_X10_DELAY);
      rtnCode = downloadData(0x01,0x65,1,X10Cmd);
      if (rtnCode < 0) return rtnCode - 3000;
      rtnCode = sendMask(0x01,0x66,0x88,0xff);
      if (rtnCode < 0) return rtnCode - 4000;
    }
    return 0;
  } // END sendX10
  //----------------------------------------------------------------------------
  /**
   * Poll INSTEON Controller for Events
   * (Insert into main loop to regularly check for events and
   * read into buffer; then use readNextEvent to read events
   * from buffer)
   *
   * @return
   *    0 = Success,
   *    -1 = Event buffer overflowed,
   *    -1x = Error while reading command (x = error from readCommand)
   */
  public int poll() {
    int saveBufEnd;
    int thisCommand;
    int i;
    boolean saveError;
    do {
      thisCommand = readCommand(tempBuffer);
      if (thisCommand > 0) {
        saveBufEnd = cBufEnd;
        saveError = false;
        if (saveData(tempBuffer[0]+1) < 0) saveError = true;
        if (saveData(thisCommand) < 0) saveError = true;
        for (i=1;i<=tempBuffer[0];i++) {
          if (saveData(tempBuffer[i]) < 0) saveError = true;
        }
        if (saveError) {
          cBufEnd = saveBufEnd;       // buffer overflow - discard event
          return -1;
        }
      }
    } while (thisCommand > 0);
    if (thisCommand == -1) return 0;
    else return thisCommand - 10;
  } // END poll
  //----------------------------------------------------------------------------
  /**
   * Read Next Event
   * @param rtnBuffer Character array to store event that is read
   * @return 0 = Event read OK into rtnBuffer,
   *    -1 = No more events available,
   *    -2 = Buffer internal error - attempted to read past end
   */
  public int readNextEvent(char[] rtnBuffer) {
    int i,j;
    if (cBufStart == cBufEnd) return -1;
    j = cBuffer[cBufStart]+1;
    for (i=0;i<j;i++) {
      if (cBufStart==cBufEnd) return -2;  // trying to read past end
      rtnBuffer[i] = cBuffer[cBufStart];
      cBufStart++;
      if (cBufStart == cBuffer.length) cBufStart = 0;
    }
    eventType = rtnBuffer[1];               // Event Type
    switch (eventType) {                    // Event Handle
      case IBIOS_EVENT_REPORT:
      case IBIOS_SIMULATED_EVENT:
      case IBIOS_INSTEON_MSG_RECD:
        eventHandle = rtnBuffer[2];
        break;
      default:
        eventHandle = EVNT_NULL;
    }
    if (eventType == IBIOS_INSTEON_MSG_RECD) {
      eventFromAddress[0] = rtnBuffer[3];
      eventFromAddress[1] = rtnBuffer[4];
      eventFromAddress[2] = rtnBuffer[5];
      eventToAddress[0] = rtnBuffer[6];
      eventToAddress[1] = rtnBuffer[7];
      eventToAddress[2] = rtnBuffer[8];
      eventMsgType = (char)((rtnBuffer[9]&0xE0)>>>5);
      eventMsgExtended = ((rtnBuffer[9]&0x10)>0);
      eventHopsLeft = (byte)((rtnBuffer[9]&0x0C)>>>2);
      eventMaxHops = (byte)(rtnBuffer[9]&0x03);
      eventCmd1 = rtnBuffer[10];
      eventCmd2 = rtnBuffer[11];
    }
    else {
      eventFromAddress[0] = (char)0;
      eventFromAddress[1] = (char)0;
      eventFromAddress[2] = (char)0;
      eventToAddress[0] = (char)0;
      eventToAddress[1] = (char)0;
      eventToAddress[2] = (char)0;
      eventMsgType = (char)0;
      eventMsgExtended = false;
      eventHopsLeft = 0;
      eventMaxHops = 0;
      eventCmd1 = (char)0;
      eventCmd2 = (char)0;
   }
    return 0;
  } // END readNextEvent
  /**
   * Event Type
   */
  public char eventType;
  /**
   * Event Handle
   */
  public char eventHandle;
  /**
   * Event From Address
   */
  public char[] eventFromAddress = new char[3];
  /**
   * Event To Address
   */
  public char[] eventToAddress = new char[3];
  /**
   * Event Message Type
   */
  public char eventMsgType;
  /**
   * Event Message Extended
   */
  public boolean eventMsgExtended;
  /**
   * Event Hops Left
   */
  public byte eventHopsLeft;
  /**
   * Event Max Hops
   */
  public byte eventMaxHops;
  /**
   * Event Command 1
   */
  public char eventCmd1;
  /**
   * Event Command 2
   */
  public char eventCmd2;
  //----------------------------------------------------------------------------
  /**
   * Get PowerLinc Version Information
   * (address, deviceType, firmware)
   *
   * @return
   *    0 = Success,
   *    -1xx = Error reading version information (xx = error from sendCommand),
   *    (common error -111 = PLC disconnected or unplugged)
   */
  public int getVersion() {
    int rtnCode;
    int checksum;
    int i;
    rtnCode = sendCommand(IBIOS_GET_VERSION,tempBuffer);
    if (rtnCode < 0) return rtnCode - 100;
    this.address[0] = tempBuffer[1];
    this.address[1] = tempBuffer[2];
    this.address[2] = tempBuffer[3];
    this.deviceType = (tempBuffer[4]<<8) | (tempBuffer[5]);
    this.firmware = tempBuffer[6];
    return 0;
  } // END getVersion
  /**
   * INSTEON address of PowerLinc controller (PLC)
   */
  public char[] address = new char[3];
  /**
   * INSTEON device type
   */
  public int deviceType;
  /**
   * PLC firmware version
   */
  public char firmware;
  //----------------------------------------------------------------------------
  /**
   * Check if INSTEON Addresses are Equal
   *
   * @param addr1 INSTEON Address #1
   * @param addr2 INSTEON Address #2
   * @return true = equal, false = not equal
   */
  public boolean addrEquals(char[] addr1, char[] addr2) {
    if ((addr1[0] == addr2[0])
     && (addr1[1] == addr2[1])
     && (addr1[2] == addr2[2])) return true;
    else return false;
  } // END addrEquals

  //****************************************************************************
  // PRIVATE DEFINITIONS, VARIABLES AND METHODS
  //****************************************************************************

  final static int IBIOS_DELAY = 0;
  final static int IBIOS_STX = 0x02;
  final static int IBIOS_ACK = 0x06;
  final static int IBIOS_NAK = 0x15;
  final static int IBIOS_DOWNLOAD = 0x40;
  final static int IBIOS_MASK = 0x46;
  final static int IBIOS_GET_VERSION = 0x48;
  final static char[] X10_CODE = {6,14,2,10,1,9,5,13,7,15,3,11,0,8,4,12};
  final static int IBIOS_X10_DELAY = 2000;

  // VARIABLES
  static char[] cBuffer = new char[250];
  static int cBufStart = 0;
  static int cBufEnd = 0;
  static char[] tempBuffer = new char[20];
  static char[] cmdBuffer = new char[20];
  static char[] X10Cmd = new char[1];
  static Int32 checksum = new Int32();
  static Timer t1 = new Timer();
  private Timer t2;
  private Uart tx;
  private Uart rx;

  /**
   * Send a Mask (in order to send data over power line)
   *
   * @param command Command to send to the PLC
   * @param buffer Character buffer to store command data read
   * @return
   *    0 = Success,
   *    -1xx = Error sending command to the PLC (xx = error from sendCommand),
   *    -200 = NAK received after sending command to PLC,
   *    -300 = Error reading data from the PLC (xx = error from readData),
   *    -400 = Unexpected data received from PLC
   */
  private int sendMask(int addrHigh, int addrLow,
      int orMask, int andMask) {
    int rtnCode;
    int checksum;
    int i;
    rtnCode = sendCommand(IBIOS_MASK,tempBuffer);
    if (rtnCode < 0) return rtnCode - 100;
    sendByte(addrHigh,IBIOS_DELAY);
    sendByte(addrLow,IBIOS_DELAY);
    sendByte(orMask,IBIOS_DELAY);
    sendByte(andMask,IBIOS_DELAY);
    rtnCode = readData(5,tempBuffer);
    if (rtnCode == IBIOS_ACK) return 0;
    if (rtnCode == IBIOS_NAK) return -200;
    if (rtnCode < 0) return -300;
    return -400;
  } // END sendMask

  /**
   * Download Data to the PLC
   *
   * @param addrHigh High data address
   * @param addrLow Low data address
   * @param length Number of bytes
   * @param buff Character buffer holding data
   * @return
   *    0 = Success,
   *    -1xx = Error sending command to the PLC (xx = error from sendCommand),
   *    -200 = NAK received after sending command to PLC,
   *    -300 = Error reading data from the PLC (xx = error from readData),
   *    -400 = Unexpected data received from PLC
   */
  private int downloadData(int addrHigh, int addrLow,
      int length, char[] buff) {
    int rtnCode;
    int checksum;
    int i;
    rtnCode = sendCommand(IBIOS_DOWNLOAD,tempBuffer);
    if (rtnCode < 0) return rtnCode - 100;
    sendByte(addrHigh,IBIOS_DELAY);
    sendByte(addrLow,IBIOS_DELAY);
    sendByte((length>>>8),IBIOS_DELAY);
    sendByte((length&0xff),IBIOS_DELAY);
    checksum = calcChecksum(addrHigh, addrLow, length, buff);
    sendByte(checksum >>> 8,IBIOS_DELAY);
    sendByte(checksum & 0xff,IBIOS_DELAY);
    for (i=0;i<length;i++) {
      sendByte(buff[i]&0xff,IBIOS_DELAY);
    }
    rtnCode = readData(5,tempBuffer);
    if (rtnCode == IBIOS_ACK) return 0;
    if (rtnCode == IBIOS_NAK) return -200;
    if (rtnCode < 0) return -300;
    return -400;
  } // END downloadData

  /**
   * Send a Command to the PLC
   *
   * @param command Command to send to the PLC
   * @param buffer Character buffer to store command data read
   * @return
   *    0 = Success,
   *    -1x = Error reading command from PLC (x = error from readCommand),
   *    -20 = Command echo from PLC doesn't match command sent
   */
  private int sendCommand(int command, char[] buffer) {
    int lastCommand;
    while (rx.byteAvailable()) {
      poll();
    }
    sendByte(IBIOS_STX,IBIOS_DELAY);
    sendByte(command,IBIOS_DELAY);
    lastCommand = readCommand(buffer);
    if (lastCommand < 0) return lastCommand - 10;
    if (lastCommand != command) return -20;
    return 0;
  } // END sendCommand

  /**
   * Send a Byte to the PLC
   *
   * @param byteToSend Byte to send to PLC
   * @param delayTime Time in ms since sending last byte to wait before sending
   */
  private void sendByte(int byteToSend, int delayTime) {
    while (!this.t2.timeout(delayTime));
    tx.sendByte(byteToSend);
    this.t2.mark();
    //DEBUG Format.printf("<%02x>",byteToSend);
  } // END sendByte

  /**
   * Read a Command from the PLC
   *
   * @param buffer Character buffer to store command data read
   * @return
   *    >0 = Command read from the PLC,
   *    -1 = No data available from PLC,
   *    -2 = First byte not STX,
   *    -3 = Error reading 2nd byte from PLC,
   *    -4 = Unknown command read,
   *    -5 = Error reading command data from PLC
   */
  private int readCommand(char[] buffer) {
    int nextByte;
    int thisCommand;
    int dataLength;
    int rtnCode;
    boolean receivedSTX = false;
    nextByte = getByte(200);
    if (nextByte < 0) return -1;
    if (nextByte != IBIOS_STX) return -2; // not STX
    nextByte = getByte(200);
    if (nextByte < 0) return -3;
    thisCommand = nextByte;
    switch (thisCommand) {
      case IBIOS_DOWNLOAD:
        dataLength = 0;
        break;
      case IBIOS_MASK:
        dataLength = 0;
        break;
      case IBIOS_X10_BYTE_RECD:
        dataLength = 2;
        break;
      case IBIOS_INSTEON_MSG_RECD:
        dataLength = 10;
        break;
      case IBIOS_EVENT_REPORT:
        dataLength = 1;
        break;
      case IBIOS_GET_VERSION:
        dataLength = 7;
        break;
      default:                         // unknown command
        return -4;
    }
    rtnCode = readData(dataLength,buffer);
    if (rtnCode < 0) return -5;
    return thisCommand;
  } // END readCommand

  /**
   * Read Data from the PLC
   * @param bytesToRead Number of bytes to read
   * @param buffer Character buffer to store data read
   * @return
   *    >=0 = Last byte read,
   *    -1 = Error while reading from PLC
   */
  private int readData(int bytesToRead, char[] buffer) {
    int nextByte;
    int i = 0;
    nextByte = 0;
    while (bytesToRead>0) {
      nextByte = getByte(2000);
      if (nextByte < 0) return -1;
      i++;
      buffer[i] = (char)nextByte;
      buffer[0] = (char)i;
      --bytesToRead;
    }
    return nextByte;
  } // END readData

  /**
   * Get a Byte from the PLC
   * @param waitTime Number of ms to wait for data
   * @param buffer Character buffer to store data read
   * @return
   *    >=0 = Last byte read,
   *    -1 = No data available before timeout
   */
  private int getByte(int waitTime) {
    int byteRead;
    t1.mark();
    while(!t1.timeout(waitTime) && !rx.byteAvailable());
    if (!rx.byteAvailable()) return -1;
    byteRead = rx.receiveByte() & 0xff;
    //DEBUG Format.printf("{%02x}",byteRead);
    return byteRead;
  } // END getByte

  /**
   * Save Data in Buffer
   * @param data Data byte (integer) to save in buffer
   * @return
   *    0 = Success,
   *    -1 = Buffer full
   */
  private int saveData(int data) {
    int newEnd;
    newEnd = cBufEnd + 1;
    if (newEnd == cBuffer.length) newEnd = 0;
    if (newEnd == cBufStart) {              // Buffer full
      return -1;
    }
    cBuffer[cBufEnd] = (char)data;
    //DEBUG Format.printf("(%d:",cBufEnd);
    //DEBUG Format.printf("%02x)",data&0xff);
    cBufEnd = newEnd;
    return 0;
  } // END saveData

  /**
   * Calculate Checksum
   *
   * @param addrHigh High data address
   * @param addrLow Low data address
   * @param length Number of bytes
   * @param buff Character buffer holding data
   * @return Checksum value
   */
  private int calcChecksum(int addrHigh, int addrLow,
      int length, char[] buff) {
    int i;
    checksum.set(0);
    checksum.add(addrHigh);
    checksum.add(addrLow);
    checksum.add(length >>> 8);
    checksum.add(length & 0xff);
    for (i=0;i<length;i++) {
      checksum.add(buff[i]&0xff);
    }
    checksum.high = ~checksum.high;
    checksum.low = ~checksum.low;
    checksum.add(1);
    return checksum.low;
  } // END calcChecksum

} // END class