/*
 * Copyright (c) 2017 Shaun Feakes - All rights reserved
 *
 * You may use redistribute and/or modify this code under the terms of
 * the GNU General Public License version 2 as published by the 
 * Free Software Foundation. For the terms of this license, 
 * see <http://www.gnu.org/licenses/>.
 *
 * You are free to use this software under the terms of the GNU General
 * Public License, 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.
 *
 *  https://github.com/sfeakes/aqualinkd
 */


#include <stdio.h>
#include <stdarg.h>
#include <termios.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <string.h>
#include <sys/ioctl.h>
#include <stdbool.h>

#include "aq_serial.h"
#include "utils.h"
#include "config.h"

//#define BLOCKING_MODE

static struct termios _oldtio;

//static struct aqconfig *_config_parameters;
//static struct aqconfig *_aqualink_config;
bool _pda_mode = false;

void set_pda_mode(bool mode)
{
  if (mode)
    logMessage(LOG_NOTICE, "AqualinkD is using PDA mode\n");

  _pda_mode = mode;
}

bool pda_mode()
{
  return _pda_mode;
}

void log_packet(int level, char *init_str, unsigned char* packet, int length)
{
  
  if ( getLogLevel() < level) {
    //logMessage(LOG_INFO, "Send '0x%02hhx'|'0x%02hhx' to controller\n", packet[5] ,packet[6]);
    return;
  }
  
  int cnt;
  int i;
  char buff[MAXLEN];

  cnt = sprintf(buff, "%s", init_str);
  cnt += sprintf(buff+cnt, " | HEX: ");
  //printHex(packet_buffer, packet_length);
  for (i=0;i<length;i++)
    cnt += sprintf(buff+cnt, "0x%02hhx|",packet[i]);

  cnt += sprintf(buff+cnt, "\n");
  logMessage(level, buff);
  //logMessage(LOG_DEBUG_SERIAL, buff);
/*
  int i;
  char temp_string[64];
  char message_buffer[MAXLEN];

  sprintf(temp_string, "Send 0x%02hhx|", packet[0]);
  strcpy(message_buffer, temp_string);

  for (i = 1; i < length; i++) {
    sprintf(temp_string, "0x%02hhx|", packet[i]);
    strcat(message_buffer, temp_string);
  }

  strcat(message_buffer, "\n");
  logMessage(LOG_DEBUG, message_buffer);
  */
}

const char* get_packet_type(unsigned char* packet , int length)
{
  static char buf[15];

  if (length <= 0 )
    return "";

  switch (packet[PKT_CMD]) {
    case CMD_ACK:
      return "Ack";
    break;
    case CMD_STATUS:
      return "Status";
    break;
    case CMD_MSG:
      return "Message";
    break;
    case CMD_MSG_LONG:
      return "Lng Message";
    break;
    case CMD_PROBE:
      return "Probe";
    break;
    case CMD_GETID:
      return "GetID";
    break;
    case CMD_PERCENT:
      return "AR %%";
    break;
    case CMD_PPM:
      return "AR PPM";
    break;
    case CMD_PDA_0x05:
      return "PDA Unknown";
    break;
    case CMD_PDA_HIGHLIGHT:
      return "PDA Hlight";
    break;
    case CMD_PDA_CLEAR:
      return "PDA Clear";
    break;
    case CMD_PDA_SHIFTLINES:
      return "PDA Shiftlines";
    break;
    case CMD_PDA_HIGHLIGHTCHARS:
      return "PDA C_HlightChar";
    break;
    default:
      sprintf(buf, "Unknown '0x%02hhx'", packet[PKT_CMD]);
      return buf;
    break;
  }
}


/*
Open and Initialize the serial communications port to the Aqualink RS8 device.
Arg is tty or port designation string
returns the file descriptor
*/
int init_serial_port(char* tty)
{
  long BAUD = B9600;
  long DATABITS = CS8;
  long STOPBITS = 0;
  long PARITYON = 0;
  long PARITY = 0;

  struct termios newtio;       //place for old and new port settings for serial port

  //int fd = open(tty, O_RDWR | O_NOCTTY | O_NONBLOCK);
  int fd = open(tty, O_RDWR | O_NOCTTY | O_NONBLOCK | O_NDELAY);
  if (fd < 0)  {
    logMessage(LOG_ERR, "Unable to open port: %s\n", tty);
    return -1;
  }

  logMessage(LOG_DEBUG_SERIAL, "Openeded serial port %s\n",tty);
  
#ifdef BLOCKING_MODE
  fcntl(fd, F_SETFL, 0);
  newtio.c_cc[VMIN]= 1;
  newtio.c_cc[VTIME]= 0;
  logMessage(LOG_DEBUG_SERIAL, "Set serial port %s to blocking mode\n",tty);
#else
  int flags = fcntl(fd, F_GETFL, 0);
  fcntl(fd, F_SETFL, flags | O_NONBLOCK | O_NDELAY);
  newtio.c_cc[VMIN]= 0;
  newtio.c_cc[VTIME]= 1;
  logMessage(LOG_DEBUG_SERIAL, "Set serial port %s to non blocking mode\n",tty);
#endif
/*
// Need to change this to flock, but that depends on good exit.
  if ( ioctl(fd, TIOCEXCL) != 0) {
    displayLastSystemError("Locking serial port.");
  }

  struct flock fl;
  fl.l_type = F_WRLCK;
  fl.l_whence = SEEK_SET;
  fl.l_start = 0;
  fl.l_len = 0;

  if (fcntl(fd, F_SETLK, &fl) == -1)
  {
    if (errno == EACCES || errno == EAGAIN)
    {
      displayLastSystemError("Locking serial port.");
    }
    else
    {
      displayLastSystemError("Unknown error Locking serial port.");
    }
  }
*/
  tcgetattr(fd, &_oldtio); // save current port settings
    // set new port settings for canonical input processing
  newtio.c_cflag = BAUD | DATABITS | STOPBITS | PARITYON | PARITY | CLOCAL | CREAD;
  newtio.c_iflag = IGNPAR;
  newtio.c_lflag = 0;       // ICANON;  
  newtio.c_oflag = 0;
    
  tcflush(fd, TCIFLUSH);
  tcsetattr(fd, TCSANOW, &newtio);
  
  logMessage(LOG_DEBUG_SERIAL, "Set serial port %s io attributes\n",tty);

  return fd;
}

/* close tty port */
void close_serial_port(int fd)
{
  tcsetattr(fd, TCSANOW, &_oldtio);
  close(fd);
  logMessage(LOG_DEBUG_SERIAL, "Closed serial port\n");
}

// Generate and return checksum of packet.
int generate_checksum(unsigned char* packet, int length)
{
  int i, sum, n;

  n = length - 3;
  sum = 0;
  for (i = 0; i < n; i++)
  sum += (int) packet[i];
  return(sum & 0x0ff);
}





// Send an ack packet to the Aqualink RS8 master device.
// fd: the file descriptor of the serial port connected to the device
// command: the command byte to send to the master device, NUL if no command
// 
// NUL = '\x00'
// DLE = '\x10'
// STX = '\x02'
// ETX = '\x03'
// 
// masterAddr = '\x00'          # address of Aqualink controller
// 
//msg = DLE+STX+dest+cmd+args
//msg = msg+self.checksum(msg)+DLE+ETX
//      DLE+STX+DEST+CMD+ARGS+CHECKSUM+DLE+ETX


void print_hex(char *pk, int length)
{
  int i=0;
  for (i=0;i<length;i++)
  {
    printf("0x%02hhx|",pk[i]);
  }
  printf("\n");
}

void test_cmd()
{
  const int length = 11;
  unsigned char ackPacket[] = { NUL, DLE, STX, DEV_MASTER, CMD_ACK, NUL, NUL, 0x13, DLE, ETX, NUL };
  //send_cmd(fd, CMD_ACK, command);
  
  print_hex((char *)ackPacket, length);
  
  ackPacket[7] = generate_checksum(ackPacket, length-1);
  print_hex((char *)ackPacket, length);
  
  ackPacket[6] = 0x02;
  ackPacket[7] = generate_checksum(ackPacket, length-1);
  print_hex((char *)ackPacket, length);
}

void send_test_cmd(int fd, unsigned char destination, unsigned char b1, unsigned char b2, unsigned char b3)
{
  const int length = 11;
  unsigned char ackPacket[] = { NUL, DLE, STX, DEV_MASTER, CMD_ACK, NUL, NUL, 0x13, DLE, ETX, NUL };
  //unsigned char ackPacket[] = { NUL, DLE, STX, DEV_MASTER, NUL, NUL, NUL, 0x13, DLE, ETX, NUL };

  // Update the packet and checksum if command argument is not NUL.
  ackPacket[3] = destination;
  ackPacket[4] = b1;
  ackPacket[5] = b2;
  ackPacket[6] = b3;
  ackPacket[7] = generate_checksum(ackPacket, length-1);

#ifdef BLOCKING_MODE
  write(fd, ackPacket, length);
#else
  int nwrite, i;
  for (i=0; i<length; i += nwrite) {        
    nwrite = write(fd, ackPacket + i, length - i);
    if (nwrite < 0) 
      logMessage(LOG_ERR, "write to serial port failed\n");
  }
  //logMessage(LOG_DEBUG_SERIAL, "Send %d bytes to serial\n",length);
  //tcdrain(fd);
  //logMessage(LOG_DEBUG, "Send '0x%02hhx' to '0x%02hhx'\n", command, destination);
#endif  

  log_packet(LOG_DEBUG_SERIAL, "Sent ", ackPacket, length);

}
void send_command(int fd, unsigned char destination, unsigned char b1, unsigned char b2, unsigned char b3)
{
  const int length = 11;
  unsigned char ackPacket[] = { NUL, DLE, STX, DEV_MASTER, CMD_ACK, NUL, NUL, 0x13, DLE, ETX, NUL };
  //unsigned char ackPacket[] = { NUL, DLE, STX, DEV_MASTER, NUL, NUL, NUL, 0x13, DLE, ETX, NUL };

  // Update the packet and checksum if command argument is not NUL.
  ackPacket[3] = destination;
  ackPacket[4] = b1;
  ackPacket[5] = b2;
  ackPacket[6] = b3;
  ackPacket[7] = generate_checksum(ackPacket, length-1);

#ifdef BLOCKING_MODE
  write(fd, ackPacket, length);
#else
  int nwrite, i;
  for (i=0; i<length; i += nwrite) {        
    nwrite = write(fd, ackPacket + i, length - i);
    if (nwrite < 0) 
      logMessage(LOG_ERR, "write to serial port failed\n");
  }
  //logMessage(LOG_DEBUG_SERIAL, "Send %d bytes to serial\n",length);
  //tcdrain(fd);
  //logMessage(LOG_DEBUG, "Send '0x%02hhx' to '0x%02hhx'\n", command, destination);
#endif  

  if ( getLogLevel() >= LOG_DEBUG_SERIAL) {
    char buf[30];
    sprintf(buf, "Sent     %8.8s ", get_packet_type(ackPacket+1 , length));
    log_packet(LOG_DEBUG_SERIAL, buf, ackPacket, length);
  }
}

void send_messaged(int fd, unsigned char destination, char *message)
{
  const int length = 24;
  int i;
  //unsigned char ackPacket[] = { NUL, DLE, STX, DEV_MASTER, CMD_ACK, NUL, NUL, 0x13, DLE, ETX, NUL };
  unsigned char msgPacket[] = { DLE,STX,DEV_MASTER,CMD_MSG,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,NUL,DLE,ETX };
  //unsigned char ackPacket[] = { NUL, DLE, STX, DEV_MASTER, NUL, NUL, NUL, 0x13, DLE, ETX, NUL };

  // Update the packet and checksum if command argument is not NUL.
  msgPacket[2] = destination;
  for (i=0; i < strlen(message) && i < AQ_MSGLEN; i++)
    msgPacket[4+i] = message[i];

  msgPacket[length-3] = generate_checksum(msgPacket, length-1);

#ifdef BLOCKING_MODE
  write(fd, msgPacket, length);
#else
  int nwrite;
  for (i=0; i<length; i += nwrite) {        
    nwrite = write(fd, msgPacket + i, length - i);
    if (nwrite < 0) 
      logMessage(LOG_ERR, "write to serial port failed\n");
  }
  //logMessage(LOG_DEBUG_SERIAL, "Send %d bytes to serial\n",length);
  //tcdrain(fd);
  //logMessage(LOG_DEBUG, "Send '0x%02hhx' to '0x%02hhx'\n", command, destination);
#endif  

  log_packet(LOG_DEBUG_SERIAL, "Sent ", msgPacket, length);
}

void _send_ack(int fd, unsigned char ack_type, unsigned char command);

void send_extended_ack(int fd, unsigned char ack_type, unsigned char command)
{
  // ack_typ should only be ACK_NORMAL, ACK_SCREEN_BUSY, ACK_SCREEN_BUSY_DISPLAY
  _send_ack(fd, ack_type, command);
}

void send_ack(int fd, unsigned char command)
{
  if (! _pda_mode)
    _send_ack(fd, ACK_NORMAL, command);
  else
    _send_ack(fd, ACK_PDA, command);
}

void _send_ack(int fd, unsigned char ack_type, unsigned char command)
{
  const int length = 11;
  // Default null ack with checksum generated, don't mess with it, just over right                    
  unsigned char ackPacket[] = { NUL, DLE, STX, DEV_MASTER, CMD_ACK, NUL, NUL, 0x13, DLE, ETX, NUL }; 
  //unsigned char ackPacket[] = { NUL, DLE, STX, DEV_MASTER, NUL, NUL, NUL, 0x13, DLE, ETX, NUL };

  // Update the packet and checksum if command argument is not NUL.
  if(command != NUL || ack_type != NUL) {
    //ackPacket[5] = 0x00 normal, 0x03 some pause, 0x01 some pause ending  (0x01 = Screen Busy (also return from logn message))
    ackPacket[5] = ack_type;
    ackPacket[6] = command;
    ackPacket[7] = generate_checksum(ackPacket, length-1);

    // NULL out the command byte if it is the same. Difference implies that
    // a new command has come in, and is awaiting processing.
    /*
    if(aqualink_cmd == command) {
      aqualink_cmd = NUL;
    }
    */
    
    // In debug mode, log the packet to the private log file.
    //log_packet(ackPacket, length);
  }

  // Send the packet to the master device.
  //write(fd, ackPacket, length);
  //logMessage(LOG_DEBUG, "Send '0x%02hhx' to controller\n", command);

#ifdef BLOCKING_MODE
  write(fd, ackPacket, length);
#else
  int nwrite, i;
  for (i=0; i<length; i += nwrite) {        
    nwrite = write(fd, ackPacket + i, length - i);
    if (nwrite < 0) 
      logMessage(LOG_ERR, "write to serial port failed\n");
  }
  logMessage(LOG_DEBUG_SERIAL, "Send %d bytes to serial\n",length);
  //tcdrain(fd);
#endif  
  
  log_packet(LOG_DEBUG_SERIAL, "Sent ", ackPacket, length);
}


int get_packet(int fd, unsigned char* packet)
{
  unsigned char byte;
  int bytesRead;
  int index = 0;
  bool endOfPacket = FALSE;
  bool packetStarted = FALSE;
  bool lastByteDLE = FALSE;
  int retry = 0;

  // Read packet in byte order below
  // DLE STX ........ ETX DLE
  // sometimes we get ETX DLE and no start, so for now just ignoring that.  Seem to be more applicable when busy RS485 traffic

  while (!endOfPacket) {
    bytesRead = read(fd, &byte, 1);
    if (bytesRead < 0 && errno == EAGAIN && packetStarted == FALSE && lastByteDLE == FALSE) {
      // We just have nothing to read
      return 0;
    } else if (bytesRead < 0 && errno == EAGAIN) {
      // If we are in the middle of reading a packet, keep going
      if (retry > 20) {
        logMessage(LOG_WARNING, "Serial read timeout\n");
        log_packet(LOG_WARNING, "Bad receive packet ", packet, index);
        return 0;
      }
      retry++;
      delay(10);
    } else if (bytesRead == 1) {
//printf("Byte 0x%02hhx\n",byte);
      if (lastByteDLE==TRUE && byte == NUL) {
        // Check for DLE | NULL (that's escape DLE so delete the NULL)
        //printf("IGNORE THIS PACKET\n");
        lastByteDLE = FALSE;
      } else if (lastByteDLE==TRUE) {
        if (index == 0)
          index++;

        packet[index] = byte;
        index++;
        if (byte == STX && packetStarted == FALSE) {
          packetStarted = TRUE;
        } else if (byte == ETX && packetStarted == TRUE) {
          endOfPacket = TRUE;
        }
      } else if (packetStarted) {
        packet[index] = byte;
        index++;
      } else if (byte == DLE && packetStarted == FALSE) {
        packet[index] = byte;
      }

      // // reset index incase we have EOP before start
      if (packetStarted == FALSE)
        index=0;
      
      if (byte == DLE) {
        lastByteDLE = TRUE;
      } else {
        lastByteDLE = FALSE;
      }
    } else if(bytesRead < 0) {
      // Got a read error. Wait one millisecond for the next byte to
      // arrive.
      logMessage(LOG_WARNING, "Read error: %d - %s\n", errno, strerror(errno));
      if(errno == 9) {
        // Bad file descriptor. Port has been disconnected for some reason.
        // Return a -1.
        return -1;
      }
      delay(100);
    }

    // Break out of the loop if we exceed maximum packet
    // length.
    if (index >= AQ_MAXPKTLEN) {
      logMessage(LOG_WARNING, "Serial packet too large\n");
      log_packet(LOG_WARNING, "Bad receive packet ", packet, index);
      break;
    }
  }

  //logMessage(LOG_DEBUG, "Serial checksum, length %d got 0x%02hhx expected 0x%02hhx\n", index, packet[index-3], generate_checksum(packet, index));

  if (generate_checksum(packet, index) != packet[index-3]){
    logMessage(LOG_WARNING, "Serial read bad checksum, ignoring\n");
    log_packet(LOG_WARNING, "Bad receive packet ", packet, index);
    return 0;
  } else if (index < AQ_MINPKTLEN && packetStarted) { //NSF. Sometimes we get END sequence only, so just ignore.
  //} else if (index < AQ_MINPKTLEN) { //NSF. Sometimes we get END sequence only, so just ignore.
    logMessage(LOG_WARNING, "Serial read too small\n");
    log_packet(LOG_WARNING, "Bad receive packet ", packet, index);
    return 0;
  }

  logMessage(LOG_DEBUG_SERIAL, "Serial read %d bytes\n",index);
  // Return the packet length.
  return index;
}

// Reads the bytes of the next incoming packet, and
// returns when a good packet is available in packet
// fd: the file descriptor to read the bytes from
// packet: the unsigned char buffer to store the bytes in
// returns the length of the packet
int get_packet_old(int fd, unsigned char* packet)
{
  unsigned char byte;
  int bytesRead;
  int index = 0;
  int endOfPacket = FALSE;
  int packetStarted = FALSE;
  int foundDLE = FALSE;
  bool started = FALSE;
  int retry=0;

  while (!endOfPacket) {
    //printf("Read loop %d\n",++i);
    bytesRead = read(fd, &byte, 1);

    if (bytesRead < 0 && errno == EAGAIN && started == FALSE) {
      // We just have nothing to read
      return 0;
    } else if (bytesRead < 0 && errno == EAGAIN) {
      // If we are in the middle of reading a packet, keep going
      if (retry > 20) {
        logMessage(LOG_WARNING, "Serial read timeout\n");
        log_packet(LOG_WARNING, "Bad receive packet ", packet, index);
        return 0;
      }
      retry++;
      delay(10);
    } else if (bytesRead == 1) {
      started = TRUE;
      //if (bytesRead == 1) {
      if (byte == DLE) {
        // Found a DLE byte. Set the flag, and record the byte.
        foundDLE = TRUE;
        packet[index] = byte;
      }
      else if (byte == STX && foundDLE == TRUE) {
        // Found the DLE STX byte sequence. Start of packet detected.
        // Reset the DLE flag, and record the byte.
        foundDLE = FALSE;
        packetStarted = TRUE;
        packet[index] = byte;
      }
      else if (byte == NUL && foundDLE == TRUE) {
        // Found the DLE NUL byte sequence. Detected a delimited data byte.
        // Reset the DLE flag, and decrement the packet index to offset the
        // index increment at the end of the loop. The delimiter, [NUL], byte
        // is not recorded.
        foundDLE = FALSE;
        //trimmed = true;
        index--;
      }
      else if (byte == ETX && foundDLE == TRUE) {
        // Found the DLE ETX byte sequence. End of packet detected.
        // Reset the DLE flag, set the end of packet flag, and record
        // the byte.
        foundDLE = FALSE;
        packetStarted = FALSE;
        endOfPacket = TRUE;
        packet[index] = byte;
      }
      else if (packetStarted == TRUE) {
        // Found a data byte. Reset the DLE flag just in case it is set
        // to prevent anomalous detections, and record the byte.
        foundDLE = FALSE;
        packet[index] = byte;
      }
      else {
        // Found an extraneous byte. Probably a NUL between packets.
        // Ignore it, and decrement the packet index to offset the
        // index increment at the end of the loop.
        index--;
      }

      // Finished processing the byte. Increment the packet index for the
      // next byte.
      index++;

      // Break out of the loop if we exceed maximum packet
      // length.
      if (index >= AQ_MAXPKTLEN) {
        break;
      }
    }
    else if(bytesRead < 0) {
      // Got a read error. Wait one millisecond for the next byte to
      // arrive.
      logMessage(LOG_WARNING, "Read error: %d - %s\n", errno, strerror(errno));
      if(errno == 9) {
        // Bad file descriptor. Port has been disconnected for some reason.
        // Return a -1.
        return -1;
      }
      delay(100);
    }
  }

  if (generate_checksum(packet, index) != packet[index-3]){
    logMessage(LOG_WARNING, "Serial read bad checksum, ignoring\n");
    log_packet(LOG_WARNING, "Bad receive packet ", packet, index);
    return 0;
  } else if (index < AQ_MINPKTLEN) {
    logMessage(LOG_WARNING, "Serial read too small\n");
    log_packet(LOG_WARNING, "Bad receive packet ", packet, index);
    return 0;
  }

  logMessage(LOG_DEBUG_SERIAL, "Serial read %d bytes\n",index);
  // Return the packet length.
  return index;
}

