/*
  This program 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.
  
  This program 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 this program.  If not, see <http://www.gnu.org/licenses/>. 
*/

/**
 * Dynamic content for nweb -stg
 * This file should be saved as utf-8!
 **/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include "nweb.h"

#define QUEUE_SIZE   10 // Max number of commands in queue
#define COMMAND_SIZE 20 // Max number of bytes in command
#define RESPONSE_TIME 5 // Device MUST respond within 0.5 seconds and
                        // MUST deliver at least 2 characters/second
#define LINE_SIZE    50 // Maximum size of command/response line,
                        // including <CR> and null-termination
#define MAX_RETRY     3 // Maximum number of tries for a single command

typedef enum {
  CMD_BREW = 'B',
  CMD_ABORT = 'A',
  // special commands for queue posting and error handling
  CMD_POSTED = 124,
  CMD_FULL = 125,
  CMD_ERROR = 126,
  CMD_UNKNOWN = 127,
  CMD_FREE = 0,
} cmd_e;

typedef struct {
  volatile char command;
} queue_t;

typedef struct {
  shared_access_t m;
  volatile unsigned int brewtime;
  volatile queue_t queue[QUEUE_SIZE];
  volatile size_t queue_count;
  volatile bool forked;
  volatile queue_t queue_post;
  volatile unsigned char commerror;
} shared_t;

static const char dev[] = "/dev/ttyATH0";
static volatile shared_t *sh;

// command io
//     tries MAX_RETRY times
//     returns true if successful
// *io points to buffer of min LINE_SIZE bytes
//     MUST be null-terminated char string
//     containing command on entry
//     overwritten by response
static bool command(int fd, char *io) {
  char line[LINE_SIZE];
  unsigned char retry;
  char c;
  int ret;
  unsigned int n;
  sprintf(line, "%s\n", io); // make command
  for(retry = 0; retry < MAX_RETRY; retry++) { // try several times
    tcflush(fd, TCIFLUSH); // flush pending data
    write(fd, line, strlen(line)); // send command
    n = 0; // reset
    while(1) { // worst-case block is LINE_SIZE * RESPONSE_TIME
      if((ret = read(fd, &c, 1)) == -1) ret = 0; // read character
      if(ret == 0) break; // timed out
      if(c == '\n' || c == '\r') { // accept \r or \n
        io[n] = 0; // null-terminate
        return true; // success
      } else if(n < LINE_SIZE) {
        io[n++] = c; // place character
        if(n == LINE_SIZE) break; // response length exceeded
      }
    }
  }
  return false;
}

// serial process
//     MUST not return
static void serial() {
  struct termios tio;
  int fd;
  unsigned char state = 0;
  char io[LINE_SIZE];
  unsigned int qix;

  queue_t queue[QUEUE_SIZE];
  size_t queue_count;

  wlog(LOG, "SERIAL", (char*)dev, 0);

  memset(&tio, 0, sizeof(tio));
  tio.c_cflag = CS8 | CREAD | CLOCAL; // 8N1
  tio.c_cc[VMIN] = 0;
  tio.c_cc[VTIME] = RESPONSE_TIME;
  cfsetospeed(&tio, B115200);            // 115200 baud
  cfsetispeed(&tio, B115200);            // 115200 baud

  fd = open(dev, O_RDWR);

  if(fd == -1)
    wlog(ERROR, "unable to open serial port", "", 0);

  tcsetattr(fd, TCSANOW, &tio);

  while(1) {
    usleep(50000);
    if(qix == 0) sprintf(io, "ATQ"); // query status
    else sprintf(io, "ATQ%u", qix); // query queue
    if(command(fd, io)) {
      if(qix == 0) {
        if(!strcmp(io, "IDLE")) {
          sh->m.lock();
          sh->brewtime = 0; // set idle
          sh->commerror = 0; // reset error
          sh->m.unlock();
          qix = 1; // start reading queue
          queue_count = 0; // reset local queue
        } else if(!strncmp(io, "BREW", 4)) {
          if(strlen(io) > 5) {
            if(io[4] == ' ') {
              sh->m.lock();
              sh->brewtime = strtoul(&io[5], NULL, 10); // set brewing
              sh->commerror = 0; // reset error
              sh->m.unlock();
              qix = 1; // start reading queue
              queue_count = 0; // reset local queue
            } else {
              sh->m.lock();
              sh->commerror = 1;
              sh->m.unlock();
            }
          } else {
            sh->m.lock();
            sh->commerror = 2;
            sh->m.unlock();
          }
        } else {
          sh->m.lock();
          sh->commerror = 6; // unknown state
          sh->m.unlock();
        }
        
        sh->m.lock();
        if(sh->queue_post.command != CMD_FREE
        && sh->queue_post.command != CMD_POSTED
        && sh->queue_post.command != CMD_ERROR
        && sh->queue_post.command != CMD_FULL) {
          sprintf(io, "ATP%c", sh->queue_post.command);
          sh->m.unlock();
          if(command(fd, io)) {
            if(!strcmp(io, "OK")) {
              sh->m.lock();
              sh->queue_post.command = CMD_POSTED;
              sh->m.unlock();
            } else if(!strcmp(io, "FULL")) {
              sh->m.lock();
              sh->queue_post.command = CMD_FULL;
              sh->m.unlock();
            } else {
              sh->m.lock();
              sh->commerror = 6; // unknown response
              sh->queue_post.command = CMD_ERROR;
              sh->m.unlock();
            }
          } else {
            sh->m.lock();
            sh->commerror = 5; // no response
            sh->queue_post.command = CMD_ERROR;
            sh->m.unlock();
          }
        } else {
          sh->m.unlock();
        }
        
      } else {
        if(!strncmp(io, "BREW", 4)) {
          if(queue_count >= QUEUE_SIZE) {
            // Device queue is too big
            sh->m.lock();
            sh->commerror = 3;
            sh->m.unlock();
            qix = 0; // reset
          } else {
            // Store in local queue
            queue[queue_count].command = CMD_BREW;
            queue_count++;
            qix++; // next item
          }
        } else if(!strcmp(io, "END")) {
          qix = 0; // queue downloaded, reset
          sh->m.lock();
          sh->commerror = 0; // reset error
          // Copy local queue to shared memory = publish to web
          sh->queue_count = queue_count;
          memcpy((void*)sh->queue, queue, sizeof(queue));
          sh->m.unlock();
        } else {
          sh->m.lock();
          sh->commerror = 4; // unknown command in queue
          sh->m.unlock();
          qix = 0; // reset due to error
        }          
      }
    } else {
      sh->m.lock();
      sh->commerror = 5; // no response
      sh->m.unlock();
    }
  }
}

// post item to queue
static char queue_post(queue_t *post) {
  char ret;
  
  while(1) {
    sh->m.lock();
    if(sh->queue_post.command == CMD_FREE) break;
    sh->m.unlock();
    usleep(50000);
  }
  memcpy((void*)&sh->queue_post, post, sizeof(queue_t));
  sh->m.unlock();
  
  do {
    usleep(50000);
    sh->m.lock();
    ret = sh->queue_post.command;
    sh->m.unlock();
  } while(ret != CMD_POSTED && ret != CMD_FULL && ret != CMD_ERROR);
  
  sh->m.lock();
  sh->queue_post.command = CMD_FREE;
  sh->m.unlock();
  return ret;
}  

// wwrite null terminated string convenience function
static wswrite(int fd, char *str) {
  wwrite(fd, str, strlen(str));
}

// dynamic content generator for /interface.xml
static void interface_xml(int fs, char *params) {
  char command[COMMAND_SIZE];
  size_t n;
  char time[6] = "00:00";
  unsigned int brewtime;
  unsigned char commerror;
  queue_t queue[QUEUE_SIZE];
  queue_t post;
  size_t queue_count;

  // Make local copy to prevent unnecessary lock time
  sh->m.lock();
  commerror = sh->commerror;
  brewtime = sh->brewtime;
  queue_count = sh->queue_count;
  memcpy(queue, (void*)sh->queue, sizeof(queue));
  sh->m.unlock();

  
  // Headsie
  wswrite(fs, "<?xml version=\"1.0\" encoding=\"iso-8859-1\"?>\n");
  wswrite(fs, "<sands>\n");

  // Posting of new commands to queue
  if(wgets(command, COMMAND_SIZE, "command", params)) {
    int posterror = 0;
    if(!strcmp(command, "brew")) {
      post.command = CMD_BREW;
      posterror = queue_post(&post);
    } else if(!strcmp(command, "abort")) {
      post.command = CMD_ABORT;
      posterror = queue_post(&post);
    } else {
      posterror = CMD_UNKNOWN;
    }
    if(posterror != CMD_POSTED) {
      wswrite(fs, "\t<message>");
      switch(posterror) {
        case CMD_FULL: wswrite(fs, "Unable to queue command, queue is full");
          break;
        case CMD_ERROR: wswrite(fs, "An error occurred while processing the command");
          break;
        case CMD_UNKNOWN: wswrite(fs, "This command is not supported");
          break;
        default: wswrite(fs, "An unknown error has occurred");
          break;
      }
      wswrite(fs, "</message>\n");
    }
  }   

  // Status response
  wswrite(fs, "\t<status>");
  if(commerror) {
    wswrite(fs, "Serial communication problem");
  } else if(brewtime) {
    wswrite(fs, "Brewing coffee, ");
    time[4] = (brewtime % 10) + '0';
    time[3] = ((brewtime / 10) % 6) + '0';
    time[1] = ((brewtime / 60) % 10) + '0';
    time[0] = ((brewtime / 600) % 6) + '0';
    wswrite(fs, time);
    wswrite(fs, " until done");
  }
  wswrite(fs, "</status>\n");

  // Queue response
  for(n = 0; n < queue_count; n++) {
    wswrite(fs, "\t<queue>");
    switch(queue[n].command) {
      case CMD_BREW:
        wswrite(fs, "Start brewing");
        break;
      case CMD_ABORT:
        wswrite(fs, "Abort all commands");
        break;
      default:
        wswrite(fs, "Unknown command");
    }
    wswrite(fs, "</queue>\n");
  }

  // Footsie  
  wswrite(fs, "</sands>\n");
}

dfile wopen(const char *path, int fd, void *shm) {
  int pid;
  bool forked;
  sh = (shared_t*)shm;

  sh->m.lock();
  forked = sh->forked;
  if(!forked) sh->forked = true;
  sh->m.unlock();
  
  if(!forked) {
    wlog(LOG, "SERIAL", "forking", 0);
    if((pid = fork()) == 0) {
      serial(); // never returns... EVER
      exit(10);
    }
  }

  // Dynamic files
  if(!strcmp(path, "interface.xml")) return interface_xml;
  // Not found
  return NULL;
}

void wclose() {
}

size_t wmem() {
  return sizeof(shared_t);
}