#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <ctype.h>
#include <math.h>


#include <setjmp.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#include "vectors.h"
#include "image.h"

/* Declare local data types
 */

typedef struct _PNMScanner
{
  int    fd;                 /* The file descriptor of the file being read */
  char   cur;                /* The current character in the input stream */
  int    eof;                /* Have we reached end of file? */
  char  *inbuf;              /* Input buffer - initially 0 */
  int    inbufsize;          /* Size of input buffer */
  int    inbufvalidsize;     /* Size of input buffer with valid data */
  int    inbufpos;           /* Position in input buffer */
} PNMScanner;

typedef struct _PNMInfo
{
  int       xres, yres;        /* The size of the image */
  int       maxval;            /* For ascii image files, the max value
                                 * which we need to normalize to */
  int       np;                /* Number of image planes (0 for pbm) */
  int       asciibody;         /* 1 if ascii body, 0 if raw body */
  jmp_buf    jmpbuf;            /* Where to jump to on an error loading */
  /* Routine to use to load the pnm body */
  unsigned char *    (* loader) (PNMScanner *, struct _PNMInfo *);
} PNMInfo;

#define BUFLEN 512              /* The input buffer size for data returned
                                 * from the scanner.  Note that lines
                                 * aren't allowed to be over 256 characters
                                 * by the spec anyways so this shouldn't
                                 * be an issue. */

/* Declare some local functions.
 */
static unsigned char *pnm_load_ascii   (PNMScanner   *scan,
                                        PNMInfo      *info);
static unsigned char *pnm_load_raw     (PNMScanner   *scan,
                                        PNMInfo      *info);
static unsigned char *pnm_load_rawpbm  (PNMScanner   *scan,
                                        PNMInfo      *info);


static void   pnmscanner_destroy       (PNMScanner *s);
static void   pnmscanner_createbuffer  (PNMScanner *s,
                                        int         bufsize);
static void   pnmscanner_getchar       (PNMScanner *s);
static void   pnmscanner_eatwhitespace (PNMScanner *s);
static void   pnmscanner_gettoken      (PNMScanner *s,
                                        char       *buf,
                                        int         bufsize);
static void   pnmscanner_getsmalltoken (PNMScanner *s,
                                        char       *buf);

static PNMScanner * pnmscanner_create  (int         fd);


#define pnmscanner_eof(s) ((s)->eof)
#define pnmscanner_fd(s) ((s)->fd)

/* Checks for a fatal error */
#define CHECK_FOR_ERROR(predicate, jmpbuf, errmsg) \
        if ((predicate)) \
        { puts ((errmsg)); longjmp((jmpbuf),1); }

static struct struct_pnm_types
{
  char   name;
  int    np;
  int    asciibody;
  int    maxval;
  unsigned char * (* loader) (PNMScanner *, struct _PNMInfo *);
} pnm_types[] =
{
  { '1', 0, 1,   1, pnm_load_ascii },  /* ASCII PBM */
  { '2', 1, 1, 255, pnm_load_ascii },  /* ASCII PGM */
  { '3', 3, 1, 255, pnm_load_ascii },  /* ASCII PPM */
  { '4', 0, 0,   1, pnm_load_rawpbm }, /* RAW   PBM */
  { '5', 1, 0, 255, pnm_load_raw },    /* RAW   PGM */
  { '6', 3, 0, 255, pnm_load_raw },    /* RAW   PPM */
  {  0 , 0, 0,   0, NULL}
};

Image *
image_load_pnm (const char *filename)
{
  int             fd;           /* File descriptor */
  char            buf[BUFLEN];  /* buffer for random things like scanning */
  PNMInfo        *pnminfo;
  PNMScanner * volatile scan;
  int             ctr;
  Image          *image = NULL;

  /* open the file */
  fd = open (filename, O_RDONLY);

  if (fd == -1)
    {
      printf ("Could not open '%s' for reading: %s", filename, strerror (errno));
      return NULL;
    }

  /* allocate the necessary structures */
  pnminfo = malloc (sizeof (PNMInfo));
  image   = malloc (sizeof (Image));

  scan = NULL;
  /* set error handling */
  if (setjmp (pnminfo->jmpbuf))
    goto error;


  if (!(scan = pnmscanner_create (fd)))
    goto error;

  /* Get magic number */
  pnmscanner_gettoken (scan, buf, BUFLEN);
  if (pnmscanner_eof (scan))
    {
      puts ("Premature end of file.");
      goto error;
    }
  if (buf[0] != 'P' || buf[2])
    {
      puts ("Invalid file.");
      goto error;
    }

  /* Look up magic number to see what type of PNM this is */
  for (ctr = 0; pnm_types[ctr].name; ctr++)
    if (buf[1] == pnm_types[ctr].name)
      {
        pnminfo->np        = pnm_types[ctr].np;
        pnminfo->asciibody = pnm_types[ctr].asciibody;
        pnminfo->maxval    = pnm_types[ctr].maxval;
        pnminfo->loader    = pnm_types[ctr].loader;
      }

  if (!pnminfo->loader)
    {
      puts ("File not in a supported format.");
      longjmp (pnminfo->jmpbuf, 1);
    }

  pnmscanner_gettoken (scan, buf, BUFLEN);
  if (pnmscanner_eof (scan))
    {
      puts ("Premature end of file.");
      goto error;
    }
  pnminfo->xres = isdigit(*buf) ? atoi (buf) : 0;
  if (pnminfo->xres <= 0)
    {
      puts ("Invalid X resolution.");
      goto error;
    }

  pnmscanner_gettoken (scan, buf, BUFLEN);
  if (pnmscanner_eof (scan))
    {
      puts ("Premature end of file.");
      goto error;
    }
  pnminfo->yres = isdigit (*buf) ? atoi (buf) : 0;
  if (pnminfo->yres <= 0)
    {
      puts ("Invalid Y resolution.");
      goto error;
    }

  if (pnminfo->np != 0)         /* pbm's don't have a maxval field */
    {
      pnmscanner_gettoken (scan, buf, BUFLEN);
      if (pnmscanner_eof (scan))
        {
          puts ("Premature end of file.");
          goto error;
        }

      pnminfo->maxval = isdigit (*buf) ? atoi (buf) : 0;
      if ((pnminfo->maxval<=0) ||
          (pnminfo->maxval>255 && !pnminfo->asciibody))
        {
          puts ("Invalid maximum value.");
          goto error;
        }
    }

  image->data = pnminfo->loader (scan, pnminfo);
  image->width  = pnminfo->xres;
  image->height = pnminfo->yres;

  /* Destroy the scanner */
  pnmscanner_destroy (scan);

  /* free the structures */
  free (pnminfo);

  /* close the file */
  close (fd);
  return image;

error:
  /* If we get here, we had a problem reading the file */
  if (scan)
    pnmscanner_destroy (scan);
  close (fd);
  free (pnminfo);
  free (image);
  return NULL;
}

static unsigned char *
pnm_load_ascii (PNMScanner   *scan,
                PNMInfo      *info)
{
  unsigned char *data, *d;
  int            x, y, b;
  int            np;
  char           buf[BUFLEN];

  np = (info->np) ? (info->np) : 1;
  data = malloc (sizeof (info->yres * info->xres * np));

  /* Buffer reads to increase performance */
  pnmscanner_createbuffer (scan, 4096);

  for (y = 0; y < info->yres; )
    {
      for (x = 0; x < info->xres; x++)
        {
          for (b = 0; b < np; b++)
            {
              /* Truncated files will just have all 0's at the end of the images */
              if (pnmscanner_eof (scan))
                puts ("Premature end of file.");
              if (info->np)
                pnmscanner_gettoken (scan, buf, BUFLEN);
              else
                pnmscanner_getsmalltoken (scan, buf);

              switch (info->maxval)
                {
                case 255:
                  d[b] = isdigit (*buf) ? atoi (buf) : 0;
                  break;

                case 1:
                  d[b] = (*buf == '0') ? 0xff : 0x00;
                  break;

                default:
                  d[b] = (255.0 * (((double)(isdigit (*buf) ? atoi (buf) : 0))
                                   / (double)(info->maxval)));
                }
            }

          d += np;
        }
    }
  return data;
}

static unsigned char *
pnm_load_raw (PNMScanner   *scan,
              PNMInfo      *info)
{
  unsigned char *data, *d;
  int    x, y;
  int    fd;

  data = malloc (info->yres * info->xres * info->np);
  fd = pnmscanner_fd (scan);
  d = data;

  for (y = 0; y < info->yres; y++)
    {
      CHECK_FOR_ERROR ((info->xres*info->np
                        != read(fd, d, info->xres*info->np)),
                       info->jmpbuf,
                       "Premature end of file.");

      if (info->maxval != 255)      /* Normalize if needed */
        {
          for (x = 0; x < info->xres * info->np; x++)
            {
              d[x] = MIN (d[x], info->maxval); /* guard against overflow */
              d[x] = 255.0 * (double) d[x] / (double) info->maxval;
            }
        }

      d += info->xres * info->np;
    }
  return data;
}

static unsigned char *
pnm_load_rawpbm (PNMScanner   *scan,
                 PNMInfo      *info)
{
  unsigned char *buf;
  unsigned char  curbyte;
  unsigned char *data, *d;
  int    x, y;
  int    fd;
  int    rowlen, bufpos;

  fd = pnmscanner_fd (scan);
  rowlen = (int)ceil ((double)(info->xres)/8.0);
  data = malloc (info->yres * info->xres);
  buf = malloc (rowlen);

  for (y = 0; y < info->yres; )
    {
      CHECK_FOR_ERROR ((rowlen != read(fd, buf, rowlen)),
                       info->jmpbuf, "Error reading file.");
      bufpos = 0;
      curbyte = buf[0];

      for (x = 0; x < info->xres; x++)
        {
          if ((x % 8) == 0)
            curbyte = buf[bufpos++];
          d[x] = (curbyte & 0x80) ? 0x00 : 0xff;
          curbyte <<= 1;
        }

      d += info->xres;
    }
  free (buf);
  return data;
}

/**************** FILE SCANNER UTILITIES **************/

/* pnmscanner_create ---
 *    Creates a new scanner based on a file descriptor.  The
 *    look ahead buffer is one character initially.
 */
static PNMScanner *
pnmscanner_create (int fd)
{
  PNMScanner *s;

  s = malloc (sizeof (PNMScanner));
  s->fd = fd;
  s->inbuf = NULL;
  s->eof = !read (s->fd, &(s->cur), 1);
  return s;
}

/* pnmscanner_destroy ---
 *    Destroys a scanner and its resources.  Doesn't close the fd.
 */
static void
pnmscanner_destroy (PNMScanner *s)
{
  if (s->inbuf)
    free (s->inbuf);
  free (s);
}

/* pnmscanner_createbuffer ---
 *    Creates a buffer so we can do buffered reads.
 */
static void
pnmscanner_createbuffer (PNMScanner *s,
                         int         bufsize)
{
  s->inbuf = malloc (bufsize);
  s->inbufsize = bufsize;
  s->inbufpos = 0;
  s->inbufvalidsize = read (s->fd, s->inbuf, bufsize);
}

/* pnmscanner_gettoken ---
 *    Gets the next token, eating any leading whitespace.
 */
static void
pnmscanner_gettoken (PNMScanner *s,
                     char       *buf,
                     int         bufsize)
{
  int ctr=0;

  pnmscanner_eatwhitespace (s);
  while (! s->eof           &&
         ! isspace (s->cur) &&
         (s->cur != '#')    &&
         (ctr < bufsize))
    {
      buf[ctr++] = s->cur;
      pnmscanner_getchar (s);
    }
  buf[ctr] = '\0';
}

/* pnmscanner_getsmalltoken ---
 *    Gets the next char, eating any leading whitespace.
 */
static void
pnmscanner_getsmalltoken (PNMScanner *s,
                          char      *buf)
{
  pnmscanner_eatwhitespace (s);
  if (!(s->eof) && !isspace (s->cur) && (s->cur != '#'))
    {
      *buf = s->cur;
      pnmscanner_getchar (s);
    }
}

/* pnmscanner_getchar ---
 *    Reads a character from the input stream
 */
static void
pnmscanner_getchar (PNMScanner *s)
{
  if (s->inbuf)
    {
      s->cur = s->inbuf[s->inbufpos++];
      if (s->inbufpos >= s->inbufvalidsize)
        {
          if (s->inbufpos > s->inbufvalidsize)
            s->eof = 1;
          else
            s->inbufvalidsize = read (s->fd, s->inbuf, s->inbufsize);
          s->inbufpos = 0;
        }
    }
  else
    s->eof = !read (s->fd, &(s->cur), 1);
}

/* pnmscanner_eatwhitespace ---
 *    Eats up whitespace from the input and returns when done or eof.
 *    Also deals with comments.
 */
static void
pnmscanner_eatwhitespace (PNMScanner *s)
{
  int state = 0;

  while (!(s->eof) && (state != -1))
    {
      switch (state)
        {
        case 0:  /* in whitespace */
          if (s->cur == '#')
            {
              state = 1;  /* goto comment */
              pnmscanner_getchar(s);
            }
          else if (!isspace (s->cur))
            state = -1;
          else
            pnmscanner_getchar (s);
          break;

        case 1:  /* in comment */
          if (s->cur == '\n')
            state = 0;  /* goto whitespace */
          pnmscanner_getchar (s);
          break;
        }
    }
}
