/* EDF Teleinformation Embedded Agent
--  Copyright (C) 2013 Stephane Carrez
--  Written by Stephane Carrez (Stephane.Carrez@gmail.com)
--
--  Licensed under the Apache License, Version 2.0 (the "License");
--  you may not use this file except in compliance with the License.
--  You may obtain a copy of the License at
--
--      http://www.apache.org/licenses/LICENSE-2.0
--
--  Unless required by applicable law or agreed to in writing, software
--  distributed under the License is distributed on an "AS IS" BASIS,
--  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
--  See the License for the specific language governing permissions and
--  limitations under the License.
*/
#include <sys/ioctl.h>
#include <sys/time.h>
#include <termios.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <time.h>
#include <math.h>
#include <errno.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/poll.h>
#include <syslog.h>
#include <netdb.h>
#include <stdio.h>

/**
 * @brief Maximum number of entries in the queues (20 min).
 */
#define Q_LENGTH           600

/**
 * @brief Flush the queue each 5 minutes (5*60/2).
 */
#define Q_FLUSH_LIMIT      150

/**
 * @brief Retry count/delay after a server connect fails.
 */
#define RETRY_DELAY         10

/**
 * @brief Maximum size of the POST message.
 */
#define BUFSIZE           8192

typedef struct edf_teleinfo
{
  const char* device;
  const char* server;
  char* server_link;
  int fd;
  int server_fd;
  int baud;
  long iinst;
  long papp;
  long hchc;
  long hchp;
  long base;
  int frame_pos;
  char frame[128];
  int retry_count;
  struct addrinfo* mon_server;
  int wait_response;
  unsigned queue_size;
  long hp_queue[Q_LENGTH];
  long hc_queue[Q_LENGTH];
  long ic_queue[Q_LENGTH];
  long pap_queue[Q_LENGTH];
  time_t start_time;
  time_t last_time;
} edf_teleinfo_t;

/**
 * @brief Submit the values that were identified from the EDF teleinformation frame.
 *
 * This implementation just enqueues the new values.
 *
 * @param mon the teleinfo data.
 * @param hp, hc, pap, icurr teleinformation values.
 */
static void submit (edf_teleinfo_t* mon, long hp, long hc, long pap, long icurr)
{
  int pos = mon->queue_size;

  mon->hp_queue[pos] = hp;
  mon->hc_queue[pos] = hc;
  mon->ic_queue[pos] = icurr;
  mon->pap_queue[pos] = pap;
    
  if (pos >= Q_LENGTH - 1)
    {
      memmove (&mon->hp_queue[0], &mon->hp_queue[1], sizeof(long) * (Q_LENGTH - 1));
      memmove (&mon->hc_queue[0], &mon->hc_queue[1], sizeof(long) * (Q_LENGTH - 1));
      memmove (&mon->ic_queue[0], &mon->ic_queue[1], sizeof(long) * (Q_LENGTH - 1));
      memmove (&mon->pap_queue[0], &mon->pap_queue[1], sizeof(long) * (Q_LENGTH - 1));
    }
  else
    {
      mon->queue_size = pos + 1;
    }
  // printf("%d: HP=%10ld     HC=%10ld      PAP=%ld    I=%ld\n", pos, hp, hc, pap, icurr);
}

/**
 * @brief Extract the information from a teleinfo line.
 *
 * @param mon the teleinfo data.
 * @param frame the frame that we received.
 */
static void edf_teleinfo_process_frame (edf_teleinfo_t* mon, char* frame)
{
  char* p = strchr (frame, ' ');
  long value = 0;
  
  if (p != NULL)
    {
      *p++ = 0;
      value = strtol (p, &p, 10);
    }

  // DEBUG ("edf_teleinfo: %s = %ld", frame, (long) value);
  
  if (strcmp (frame, "IINST") == 0)
    {
      mon->iinst = value;
    }
  else if (strcmp (frame, "PAPP") == 0)
    {
      mon->papp = value;
    }
  else if (strcmp (frame, "HCHC") == 0)
    {
      mon->hchc = value;
    }
  else if (strcmp (frame, "HCHP") == 0)
    {
      mon->hchp = value;
    }
  else if (strcmp (frame, "BASE") == 0)
    {
      mon->base = value;
    }
}

/**
 * @brief Open the serial line or make sure it is opened.
 *
 * @param mon the monitoring info.
 * @return 0 if the serial line is opened.
 */
static int edf_teleinfo_open (edf_teleinfo_t* mon)
{
  struct termios options;

  if (mon->fd >= 0)
    {
      return 0;
    }
  
  mon->frame_pos = 0;
  mon->papp = 0;
  mon->hchc = 0;
  mon->hchp = 0;
  mon->base = 0;
  if (mon->device == NULL)
    {
      mon->device = strdup ("/dev/ttyUSB0");
    }
  
  mon->fd = open (mon->device, O_RDWR | O_NOCTTY | O_NDELAY | O_NONBLOCK);
  if (mon->fd < 0)
    {
      syslog (LOG_ERR, "edf_teleinfo: Cannot open device %s: %s", mon->device, strerror(errno));
      sleep (10);
      return -1;
    }

  tcgetattr (mon->fd, &options);
  options.c_cflag = B9600 | CS8 | CSTOPB | CREAD | CLOCAL;
  options.c_iflag = IGNBRK | IGNPAR;
  options.c_oflag = 0;
  options.c_lflag = 0;
  options.c_cc[VTIME] = 20;
  options.c_cc[VMIN]  = 250;

  /* Set the new options for the port... */
  tcflush (mon->fd, TCIFLUSH);
  tcsetattr (mon->fd, TCSANOW, &options);

  syslog (LOG_INFO, "Teleinfo serial line %s opened", mon->device);
  return 0;
}

/*
 * @brief Read the EDF teleinformation sent over the serial line.
 *
 * This operation is called each second 
 */
static int edf_teleinfo_read (edf_teleinfo_t* mon)
{
  char buffer[512 + 1];
  int pos;
  char* ptr;
  int result;

  if (edf_teleinfo_open (mon) != 0)
    {
      return 0;
    }
  
  result = read (mon->fd, buffer, sizeof (buffer));
  if (result < 0)
    {
      if (errno != EAGAIN && errno != EWOULDBLOCK)
        {
          syslog (LOG_WARNING, "Error while reading EDF teleinformation: %s", strerror (errno));
          close (mon->fd);
          mon->fd = -1;
        }
      return 0;
    }
  
  if (result <= 0)
    {
      return 0;
    }

  buffer[result + 1] = 0;

  pos = mon->frame_pos;
  ptr = buffer;
  while (result > 0)
    {
      char c = *ptr++;

      if (c == 0x02)
        {
          // start frame
          pos = 0;
        }
      else if (c == 0x03)
        {
          // end frame
          submit (mon, mon->hchp, mon->hchc, mon->papp, mon->iinst);
          pos = 0;
        }
      else if (c == 0x04)
        {
          // stop frame
          pos = 0;
        }
      else if (c == 0x0a)
        {
          // start group
          pos = 0;
        }
      else if (c == 0x0d)
        {
          // end group
          if (pos > 0)
            {
              mon->frame[pos] = 0;
              edf_teleinfo_process_frame (mon, mon->frame);
            }
        }
      else if (pos < (int) sizeof (mon->frame))
        {
          mon->frame[pos++] = c;
        }
      result--;
    }
  mon->frame_pos = pos;
  return 0;
}

/*
 * @brief Initialize the teleinformation.
 *
 * @param mon the monitoring info.
 * @param device the serial device to read.
 * @param url the server URL where the values are posted.
 */
static int edf_teleinfo_init (edf_teleinfo_t* mon, const char* device, const char* url)
{
  struct addrinfo hints;
  char* port;
  char* link;

  memset (mon, 0, sizeof (edf_teleinfo_t));
  mon->baud = 9600;
  mon->fd   = -1;
  mon->server_fd = -1;
  if (device == NULL)
    {
      mon->device = "/dev/ttyUSB0";
    }
  else
    {
      mon->device = device;
    }
  
  if (strncmp(url, "http://", sizeof("http://") - 1) != 0)
    {
      syslog (LOG_ERR, "Invalid URL: %s", url);
      return -1;
    }
  mon->server = &url[sizeof("http://") - 1];
  link = strchr (mon->server, '/');
  if (link == NULL)
    {
      syslog (LOG_ERR, "Missing link page in URL: %s", url);
      return -1;
    }
  port = strchr (mon->server, ':');
  if (port != NULL)
    {
      *port++ = 0;
    }
  else
    {
      port = "80";
    }
  mon->server_link = strdup (link);
  *link = 0;

  memset (&hints, 0, sizeof (hints));
  hints.ai_family   = AF_INET;
  hints.ai_socktype = SOCK_STREAM;
  hints.ai_protocol = 0;

  if (getaddrinfo (mon->server, port, &hints, &mon->mon_server) != 0)
    {
      syslog (LOG_ERR, "Cannot resolve server '%s': %s", mon->server, strerror (errno));
      return -1;
    }
  return 0;
}

/**
 * @brief Write the values in the POST message.
 *
 * Values are written in differential form.  The first value is written and
 * other values are written as an offset from the previous value.
 *
 * @param p the buffer.
 * @param size the size of the buffer.
 * @param name the parameter name.
 * @param queue the values to write.
 * @param count the number of values to write.
 * @return the size written in the buffer.
 */
static size_t write_queue (char* p, size_t size, const char* name, long* queue, int count)
{
    int i;
    size_t len = 0;
   
    len += snprintf (p, size - len, "%s=%ld", name, queue[0]);
    for (i = 1; i < count; i++) {
        len += snprintf (p + len, size - len, ",%ld", (long) queue[i] - (long) queue[i-1]);
    }
    return len;
}

/**
 * @brief Create the POST HTTP/1.0 message.
 *
 * @param mon the teleinformation data.
 * @param buffer the buffer to write.
 * @param size the buffer size.
 * @return the message size.
 */
static int create_post (edf_teleinfo_t* mon, char* buffer, size_t size)
{
    char* p = buffer;
    size_t len;
    size_t length_pos;
    size_t content_pos;
    
    len = snprintf (p, size, "POST %s HTTP/1.0\r\n", mon->server_link);
    len += snprintf (p + len, size - len, "Host: %s\r\n", mon->server);
    len += snprintf (p + len, size - len, "Content-Type: application/x-www-form-urlencoded\r\n");
    len += snprintf (p + len, size - len, "User-Agent: EDF-Monitor\r\n");
    len += snprintf (p + len, size - len, "Content-Length: ");
    length_pos = len;
    len += snprintf (p + len, size - len, "00000\r\n\r\n");

    content_pos = len;
    len += snprintf (p + len, size - len, "date=%ld&", mon->start_time);
    len += snprintf (p + len, size - len, "end=%ld&", mon->last_time);
    len += write_queue (p + len, size - len, "hp", mon->hp_queue, mon->queue_size);
    len += snprintf (p + len, size - len, "&");
    len += write_queue (p + len, size - len, "hc", mon->hc_queue, mon->queue_size);
    len += snprintf (p + len, size - len, "&");
    len += write_queue (p + len, size - len, "ic", mon->ic_queue, mon->queue_size);
    len += snprintf (p + len, size - len, "&");
    len += write_queue (p + len, size - len, "pap", mon->pap_queue, mon->queue_size);

    /* Patch the good length.  */
    length_pos += snprintf (p + length_pos, size - length_pos, "%05d", len - content_pos);

    /* Restore the \r separator.  */
    p[length_pos] = '\r';
    return len;
}

/**
 * @brief Flush the data collected in the queue.
 *
 * The POST message is created from the values that have been queued.  The
 * connection to the server is opened and the message sent to it.
 * If we fail to connect to the server, the queue position does not change
 * and we can retry next time.  The queue is flushed only after we successfully
 * send the POST message.  Caveat: we don't wait nor check for the server response.
 *
 * @param mon the teleinformation data.
 */
static void flush_queue (edf_teleinfo_t* mon)
{
    size_t len;
    int res;
    char buffer[BUFSIZE];
    
    if (mon->server_fd < 0)
      {
        if (mon->retry_count > 0)
          {
            mon->retry_count--;
            return;
          }
        
        mon->server_fd = socket (AF_INET, SOCK_STREAM, 0);
        if (mon->server_fd < 0)
          {
            syslog (LOG_ERR, "Cannot create socket: %s", strerror (errno));
            return;
          }

        res = connect (mon->server_fd, mon->mon_server->ai_addr, mon->mon_server->ai_addrlen);
        if (res < 0)
          {
            close (mon->server_fd);
            mon->server_fd = -1;
            mon->retry_count = RETRY_DELAY;
            return;
          }
      }
    
    time (&mon->last_time);
    len = create_post (mon, buffer, sizeof (buffer));
    if (len >= sizeof (buffer))
      {
        syslog (LOG_ERR, "POST size exceeds %d bytes and was truncated", sizeof (buffer));
      }
    
    res = write (mon->server_fd, buffer, len);
    if (res < 0)
      {
        close (mon->server_fd);
        mon->server_fd = -1;
        return;
      }
    time (&mon->start_time);
    mon->wait_response = 1;
    mon->queue_size = 0;
}

/**
 * @brief Wait and consume the server POST response.
 *
 * @param mon the teleinformation data.
 */
static void wait_response (edf_teleinfo_t* mon)
{
  struct pollfd fds[2];
  int res;

  if (mon->server_fd >= 0)
    {
      fds[0].fd = mon->server_fd;
      fds[0].events = POLLIN;

      res = poll (fds, 1, 1000);
      if (res > 0)
        {
          char buf[1024];

          res = read (mon->server_fd, buf, sizeof(buf));
          if ((res < 0 && errno != EAGAIN && errno != EWOULDBLOCK) || (res == 0))
            {
              close (mon->server_fd);
              mon->server_fd = -1;
              mon->wait_response = 0;
            }
        }
    }
}

void usage()
{
  fprintf (stderr, "Usage: edf-teleinfo /dev/ttyUSB0 http://server:port/path\n");
  exit (1);
}

int main(int argc, char** argv)
{
  edf_teleinfo_t mon;
  const char* device;
  const char* url;

  if (argc <= 2)
    {
      usage();
    }
  device = argv[1];
  url = argv[2];

  openlog ("edf-teleinfo", LOG_PERROR | LOG_PID, LOG_DAEMON);
  if (edf_teleinfo_init (&mon, device, url) != 0)
    {
      usage();
    }

  time (&mon.start_time);
  while (1)
    {
      sleep (1);
      edf_teleinfo_read (&mon);

      wait_response (&mon);
      if (mon.wait_response == 0 && mon.queue_size > Q_FLUSH_LIMIT)
        {
          flush_queue (&mon);
        }
    }
}
