/* csv2png - Make a png from a csv log file */

#define PROG_VERSION "1.0.1"

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <ctype.h>
#include <time.h>
#include <float.h>
#include <math.h>

#ifdef RISCOS
  #include "kernel.h"
  static _kernel_osfile_block inout ;
#endif
#include "gd.h"
#include "gdfonts.h"

#ifdef CSV2JPEG
#define JPEG_QUALITY -1 /* use IJG default */
#endif

#define BOOL int
#define text_offset 12
#define MAX_VALUES 32
#define BUFFSIZE 512

#define DEFAULT_TFORM "dmyHMS"

#define COMMENT_CHAR '#'

/* Value and flag storage */
static float values1[MAX_VALUES], values2[MAX_VALUES] ;
static int flags1[MAX_VALUES], flags2[MAX_VALUES],
  active[MAX_VALUES], time1, time2, num_active = 0,
  start_time = 0, end_time = 0 ;
static float min = FLT_MAX, max = -FLT_MAX ;

static char line_buf[BUFFSIZE] ;
static char colours[32]  ;
static int white, grid ;
static int margin, width, height, plot_width ;
static int ch = 0;
static gdImagePtr im_out ; /* The png file as a gd image */

static char *outname ;

#ifdef CSV2PNG
static void write_outfile(void)
{
  /* Open png file for o/p and get GD to write it */

  FILE *out ;

  out = fopen(outname, "wb");
  /* Write png */
  if (!out)
  {
    /* Report _this_ error to stderr! */
    fprintf(stderr, "Could not create output file.\n") ;
    exit(0) ;
  }

  gdImageInterlace(im_out, 1); /* Interlaced png */
  gdImagePng(im_out, out);
  fclose(out);
  #ifdef RISCOS
    inout.load = 0xb60 ; /* Filetype for png */
    /* settype */
    _kernel_osfile(18, outname, &inout) ;
  #endif
  /* forget image */
  gdImageDestroy(im_out);
}
#endif

#ifdef CSV2JPEG
static void write_outfile(void)
{
  /* Open png file for o/p and get GD to write it */

  FILE *out ;

  out = fopen(outname, "wb");
  /* Write png */
  if (!out)
  {
    /* Report _this_ error to stderr! */
    fprintf(stderr, "Could not create output file.\n") ;
    exit(0) ;
  }

  gdImageInterlace(im_out, 0); /* Not interlaced - not all apps handle it */
  gdImageJpeg(im_out, out, JPEG_QUALITY);
  fclose(out);
  #ifdef RISCOS
    inout.load = 0xc85 ; /* Filetype for jpeg */
    /* settype */
    _kernel_osfile(18, outname, &inout) ;
  #endif
  /* forget image */
  gdImageDestroy(im_out);
}
#endif

static void gerr(int fatal, char* format, ...)
{
  /* Print error message to the o/p png */

  char temp[256] ;

   va_list va;

   va_start(va, format);
   vsprintf(temp, format, va);
   va_end(va);

   gdImageString(im_out, gdFontSmall,
     margin,
     height - margin/2 - gdFontSmall->h/2 - 2,
     (unsigned char *) temp, colours[0]) ;

   if (fatal) {
     write_outfile() ;
     exit(0) ;
   }
}

static void get_value(char *string, int i, float *values, int *flags)
{
  /* Try to read a value from string, delimited by ',' or '\n'
     We start just after the previous ','
  */

  char *endp ;

  values[i] = (float) strtod(string, &endp) ; /* string to double */
  flags[i] = (endp != string) ; /* Any conversion? */
}

static int get_time(char *s, struct tm *tt, char *time_format)
{
  /* Given a string in some date/time format, decode it to tt */

  char *digits_start, *digits_end, tc ;
  int i = 0, num ;

  tt->tm_sec = tt->tm_min = tt->tm_hour = tt->tm_mday = tt->tm_mon
             = tt->tm_year = tt->tm_wday = tt->tm_yday = 0 ;
  tt->tm_isdst = -1 ;

  digits_start = s ;

  if (!strpbrk(s, "/:-")) return 1 ; /* No punctuaction
                                       => not a time - ignore */

  for (i=0; i<6; ++i)
  {
    if (*digits_start == '\0')  return 2 ; /* Ran out of numbers - bad */
    if (*digits_start == ',')   break ;    /* End of time */
    if (time_format[i] == '\0') break ;    /* Ran out of conversions */

    while (!isdigit(*digits_start)) {
      if (*digits_start == ',') {
        if (i==0) return 1 ; /* Not a time / date */
        return 0 ; /* We got some values */
      }
      ++digits_start ;
    }
    digits_end = &digits_start[1] ;
    while (isdigit(*digits_end)) ++digits_end ;

    /* Terminate number */
    tc = *digits_end ;
    *digits_end = '\0' ;

    num = atoi(digits_start) ;

    /* Store converted value to part of tt */
    switch (time_format[i]) {
      case 'd':
        /* Day of the month */
        tt->tm_mday = num ;
        break ;

      case 'H':
        /* Hour - 24-hr clock */
        tt->tm_hour = num ;
        break ;

      case 'm':
        /* Month */
        tt->tm_mon = num ;
        break ;

      case 'M':
        /* Minute */
        tt->tm_min = num ;
        break ;

      case 'S':
        /* Second */
        tt->tm_sec = num ;
        break ;

      case 'Y':
        /* Year with century */
        tt->tm_year = num - 1900 ;
        break ;

      case 'y':
        /* Year - no century */
        if (num < 70) num += 100 ;
        tt->tm_year = num ;
        break ;

      default:
        /* Uknown conversion */
        gerr(1, "%s: '%c' is not a valid conversion code.",
          time_format, time_format[i]);
        return 2 ;
    }

    *digits_end = tc ;
    /*printf("Read %d.\n", num[i]) ;*/

    /* Ready for next number */
    digits_start = ++digits_end ;
  }

  /*printf("Read %d time values.\n\n", i) ;*/

  if (i==0) return 2 ; /* No time values - bad */

  return 0 ;
}

static int read_line(FILE *fp, float *values, int *flags,
                         int line, int *time, char *tform)
{
  /* Read a line from the log file
     Save any read values into values
     Ditto for flags (indicate if a value is missing)
     line is the line number
     Save the time to time */

  int i=0, count = 0 ;
  char *next ;
  struct tm tm_time ;

  /* Read the next line */
  if (!fgets(line_buf, BUFFSIZE, fp)) return 2 ; /* EOF */

  /* Check for comment character */
  while (isspace(line_buf[i])) ++i ; /* Skip spaces */
  if (line_buf[i] == COMMENT_CHAR) return 1 ; /* Ignore comment lines */

  /* Now get the time value */

  /* Read the time */
  switch (get_time(line_buf, &tm_time, tform)) {
    case 0:
      /* Everything is OK */
      break ;

    case 1:
      /* Doesn't look like a time. Ignore it. */
      return 1 ;

    case 2:
      /* Error */
      gerr(1, "Failed to read line %d.", line) ;
      return 2 ;
  }

  tm_time.tm_mon -= 1 ;            /* January is month 0 */
  *time = (int) mktime(&tm_time) ; /* Convert to unix time */

/*
  printf("%d/%d/%d %d:%d:%d > %d. isdst: %d\n",
    tm_time.tm_mday, tm_time.tm_mon,
    tm_time.tm_year, tm_time.tm_hour, tm_time.tm_min, tm_time.tm_sec,
    *time, tm_time.tm_isdst) ;
*/

  /* Flag all to inactive */
  for (i=0; i<MAX_VALUES; flags[i++] = 0) ;

  next = line_buf ; /* Start looking for commas on the line */

  while ((next = strchr(next, ',')) && (count < MAX_VALUES))
  {
    ++next  ; /* Value comes after comma */
    get_value(next, count, values, flags) ; /* Read it */
    ++count ; /* No. values found */
  }

  /* Run out of values */

  return 0 ; /* Normal read */
}

static void plot_to_point(float offset, float scale, int ch, int x1,
  int x0, int ypix, int basepix, float *valuesa, float *valuesb,
  int *flagsa, int *flagsb)
{
  int y0, y1 ;

  /* Check flag for this point */
  if (!flagsa[ch]) return ;

  /* Convert data value to pixel position */
  y1 = basepix + ypix +
    (int) (((float) ypix-1) * (valuesa[ch] - offset) * scale) ;

  /* Was there no valid previous point? */
  if (!flagsb[ch])
  {
    /* Just plot a point here */
    gdImageSetPixel(im_out, x1, y1, colours[ch]) ;
    return ;
  }

  /* Draw to last point */
  /* Convert previous data value to pixel position */
  y0 = basepix + ypix +
    (int) (((float)ypix-1) * (valuesb[ch] - offset) * scale) ;

  /* Draw line */
  gdImageLine(im_out, x0, y0, x1, y1, colours[ch]) ;
}

static size_t format_time(char *buffer, time_t t, time_t dt)
{
  struct tm *tms ;
  tms = localtime(&t) ;

  if (dt < 100) /* 10 seconds */
    return strftime(buffer, 31, "%H:%M:%S", tms) ;

  if (dt < 86400) /* 1 day */
    return strftime(buffer, 31, "%H:%M", tms) ;

  return strftime(buffer, 31, "%d,%H:%M", tms) ;
}

static void allocate_colours(void)
{
  /* First color allocated is background. */
  white = gdImageColorAllocate(im_out, 255, 255, 255) ;
  grid  = gdImageColorAllocate(im_out, 187, 187, 187) ;

  colours[0] = gdImageColorAllocate(im_out, 0, 0, 0) ;
  colours[1] = gdImageColorAllocate(im_out, 119, 119, 119) ;
  colours[2] = gdImageColorAllocate(im_out, 0, 68, 153) ;
  colours[3] = gdImageColorAllocate(im_out, 0, 204, 0) ;
  colours[4] = gdImageColorAllocate(im_out, 221, 0, 0) ;
  colours[5] = gdImageColorAllocate(im_out, 85, 136, 0) ;
  colours[6] = gdImageColorAllocate(im_out, 255, 187, 0) ;
  colours[7] = gdImageColorAllocate(im_out, 0, 187, 255) ;

  colours[8] = gdImageColorAllocate(im_out, 0, 0, 0) ;
  colours[9] = gdImageColorAllocate(im_out, 119, 119, 119) ;
  colours[10] = gdImageColorAllocate(im_out, 0, 68, 153) ;
  colours[11] = gdImageColorAllocate(im_out, 0, 204, 0) ;
  colours[12] = gdImageColorAllocate(im_out, 221, 0, 0) ;
  colours[13] = gdImageColorAllocate(im_out, 85, 136, 0) ;
  colours[14] = gdImageColorAllocate(im_out, 255, 187, 0) ;
  colours[15] = gdImageColorAllocate(im_out, 0, 187, 255) ;

  colours[16] = gdImageColorAllocate(im_out, 0, 0, 0) ;
  colours[17] = gdImageColorAllocate(im_out, 119, 119, 119) ;
  colours[18] = gdImageColorAllocate(im_out, 0, 68, 153) ;
  colours[19] = gdImageColorAllocate(im_out, 0, 204, 0) ;
  colours[20] = gdImageColorAllocate(im_out, 221, 0, 0) ;
  colours[21] = gdImageColorAllocate(im_out, 85, 136, 0) ;
  colours[22] = gdImageColorAllocate(im_out, 255, 187, 0) ;
  colours[23] = gdImageColorAllocate(im_out, 0, 187, 255) ;

  colours[24] = gdImageColorAllocate(im_out, 0, 0, 0) ;
  colours[25] = gdImageColorAllocate(im_out, 119, 119, 119) ;
  colours[26] = gdImageColorAllocate(im_out, 0, 68, 153) ;
  colours[27] = gdImageColorAllocate(im_out, 0, 204, 0) ;
  colours[28] = gdImageColorAllocate(im_out, 221, 0, 0) ;
  colours[29] = gdImageColorAllocate(im_out, 85, 136, 0) ;
  colours[30] = gdImageColorAllocate(im_out, 255, 187, 0) ;
  colours[31] = gdImageColorAllocate(im_out, 0, 187, 255) ;
}

static void draw_key(int width)
{
  /* Draw a key accross the top */

  int key_unit_width, text_indent, line_len, i, j=0, line_y, text_y ;
  char key[4] ;

  if (num_active == 0) return ; /* No data => no key */

  /* The width of one entry in the key... */
  key_unit_width = (width - margin) / ((num_active<4)?4:num_active) ;
  text_indent = key_unit_width / 2 ; /* Text in RH half */
  line_len =    key_unit_width / 3 ; /* Line for 1st 1/3 */
  /* line vertically centered in key area */
  line_y = margin / 2 ;
  /* Text vertically centered on line - take off an extra h/2 */
  text_y = margin / 2 - gdFontSmall->h / 2 ;

  for (i=0; i<MAX_VALUES; ++i)
  {
    if (!active[i])
      continue ; /* Don't add key for inactive sensors */
    /* Draw the line */
    gdImageLine(im_out, margin + j * key_unit_width,
      line_y, margin + j * key_unit_width + line_len, line_y, colours[i]) ;
    /* Key text */
    sprintf(key, "%d", i) ;
    /* Add text */
    gdImageString(im_out, gdFontSmall,
      margin + j * key_unit_width + text_indent,
      text_y,
      (unsigned char *) key, colours[0]) ;
    ++j ;
  }
}

static int scan_file(FILE *fp, char *tform)
{
  int line = 1, ch, st ;

  do {
    st = read_line(fp, values1, flags1, line, &time1, tform) ;
    switch (st) {
      case 0: /* Normal read line */
         /* Remember 1st time value */
        if (start_time == 0) start_time = time1 ;
        for (ch=0; ch<MAX_VALUES; ++ch)
        {
          if (!flags1[ch]) continue ;
          if (values1[ch] > max) max = values1[ch] ;
          if (values1[ch] < min) min = values1[ch] ;
          if (!active[ch]) {
            active[ch] = 1 ; /* This channel IS active */
            ++num_active ;   /* One more active channel */
          }
        }
        ++line ;
        break ;

      case 1: /* Nothing read - e.g. title line */
        break ;

      case 2: /* EOF - will drop out now */
        break ;
    }
  } while (st < 2) ;

  rewind(fp) ;

  end_time = time1 ; /* Remember last time value */

/*  printf("Start: %s", ctime((time_t *) &start_time)) ;
  printf("End:   %s", ctime((time_t *) &end_time)) ;*/
  return 1 ;
}

static void set_up(void)
{
  int i ;

  for (i=0; i<MAX_VALUES; ++i) {
    values1[i] = values2[i] = 0 ;
    flags1[i] = flags2[i] = 0 ;
  }
}

int main(int argc, char *argv[])
{
  int x, xx, row, x2, x1, y0, y1, line = 1, dt, which1 = 1, st ;
  float npw, scale ;
  char *tform ;

  FILE *data ;

  /* Check for correct usage */
  if (argc < 5)
  {
    fprintf(stderr, "csv2png / csv2jpeg version %s\n", PROG_VERSION) ;
    fprintf(stderr, "Usage: csv2png in_csv out_png width height [tform]\n") ;
    fprintf(stderr, "tform is a string of time format speciers, with no\
 punctuation.\nd date\nm month\ny year without century\n\
Y year with century\nH hour (0-23)\nM minute\nS second\n\
e.g. \"dmyHMS\" for British format, \"mdyHMS\" for US.\n") ;
    exit(0) ;
  }

  outname = argv[2] ; /* Name of png */

  /* Determine dimensions */
  width  = atoi(argv[3]) ;
  height = atoi(argv[4]) ;

  /* Final argument is optional date/time format */
  if (argc>5) tform = argv[5] ;
  else tform = DEFAULT_TFORM ;

  row = (width + 1) / 2 ;
  if (row % 4) row += 4 - row % 4 ;

  margin = (3 * gdFontSmall->h) / 2 ;
  plot_width = width - 1 - margin ;

  /* Create the image inb GD */

  im_out = gdImageCreate(width, height) ;
  if (!im_out)
  {
    fprintf(stderr, "Failed to create image.\n") ;
    exit(0) ;
  }

  /* Image now exists - all further user messages to image */

  /* Set up all the colours we will need */
  allocate_colours() ;

  /* Set up defaults */
  set_up() ;

  /* Open input file */
  data = fopen(argv[1], "rb") ;

  if (!data) /* Did input file open? */
  {
    gerr(1, "%s - open failed", argv[1]) ;
  }
  else
  {
    /* Parse file for min / max, &c */
    if (!scan_file(data, tform))
    {
      fclose(data) ;
      gerr(1, "Failed to scan file") ;
    }

    if (min == max) {
      --min ;
      ++max ;
    }
    if (min >= max) gerr(1, "Strange y limits") ;

    if (start_time == end_time) {
      --start_time ;
      ++end_time ;
    }
    if (start_time >= end_time) gerr(1, "Strange time limits") ;

    /* File will be closed later */

    xx = line = x = 0 ;

    dt = end_time - start_time ;
    npw = (float) plot_width / (float) dt ;
    scale = 1 / (min - max) ;


    x2 = margin ;
    x1 = width - 1 ;
    y0 = margin ;
    y1 = margin + (height - 2 * margin - 1) ;

    /* Plot the grid */
    gdImageDashedLine(im_out, x2, y0, x1, y0, grid) ;
    gdImageDashedLine(im_out, x1, y0, x1, y1, grid) ;
    gdImageDashedLine(im_out, x1, y1, x2, y1, grid) ;
    gdImageDashedLine(im_out, x2, y1, x2, y0, grid) ;

    /* Plot the data
       read_line does nothing for MON data
       but populates mag from log file */

    x1 = x2 = margin ;

    /*while (read_line(data, (which1)?values1:values2,
      (which1)?flags1:flags2, line, (which1)?&time1:&time2))*/
    do
    {
      st = read_line(data, (which1)?values1:values2,
      (which1)?flags1:flags2, line, (which1)?&time1:&time2, tform) ;
      switch (st) {
        case 0: /* Normal read */
          /* Horizontal pixel position for this x */
          if (time1 > 0) x1 = margin +
            (int) floor(0.5+npw * (float) (time1 - start_time)) ;
          /* Horizontal pixel position for previous x  */
          if (time2 > 0) x2 = margin +
            (int) floor(0.5+npw * (float) (time2 - start_time)) ;
          for (ch=0; ch<MAX_VALUES; ++ch) /* Loop over channels */
          {
            if (!active[ch]) continue ;

            /* Now draw line or point */
            if (which1) { /* From 2 to 1 */
              if (!flags1[ch]) continue ;
              plot_to_point(min, scale, ch, x1, x2, height - 2 * margin,
                margin, values1, values2, flags1, flags2) ;
            } else {      /* From 1 to 2 */
              if (!flags2[ch]) continue ;
              plot_to_point(min, scale, ch, x2, x1, height - 2 * margin,
                margin, values2, values1, flags2, flags1) ;
            }
          }
          ++line ;
          which1 = !which1 ; /* Current values become the previous */
          break ;

        case 1: /* No data - title? */
        case 2: /* EOF */
          break ;
      }
    } while (st < 2) ;

    fclose(data) ; /* Log file must be closed */

    /* Add the key */
    draw_key(width) ;

    /* Add min / max */
    sprintf(line_buf, "%g", min) ;
    gdImageStringUp(im_out, gdFontSmall,
      margin / 2 - gdFontSmall->h / 2,
      height - margin  + (strlen(line_buf) * gdFontSmall->w) / 3,
      (unsigned char *) line_buf, colours[0]) ;
    sprintf(line_buf, "%g", max) ;
    gdImageStringUp(im_out, gdFontSmall,
      margin / 2 - gdFontSmall->h / 2,
      margin  + (2 * strlen(line_buf) * gdFontSmall->w) / 3,
      (unsigned char *) line_buf, colours[0]) ;
    format_time(line_buf, start_time, dt) ;
    gdImageString(im_out, gdFontSmall,
      margin,
      height - margin/2 - gdFontSmall->h/2 + 2,
      (unsigned char *) line_buf, colours[0]) ;
    format_time(line_buf, end_time, dt) ;
    gdImageString(im_out, gdFontSmall,
      width - strlen(line_buf)*gdFontSmall->w - 2,
      height - margin/2 - gdFontSmall->h/2 + 2,
      (unsigned char *) line_buf, colours[0]) ;
  }

  write_outfile() ;

  return 0 ;
}
