#include <errno.h>
#include <string.h>
#include <stdlib.h>

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

#include "libgimp/stdplugins-intl.h"

/* Declare local data types
 */

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

typedef struct _PNMInfo
{
  gint       xres, yres;        /* The size of the image */
  gint       maxval;            /* For ascii image files, the max value
                                 * which we need to normalize to */
  gint       np;                /* Number of image planes (0 for pbm) */
  gint       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 */
  void    (* loader) (PNMScanner *, struct _PNMInfo *, GimpPixelRgn *);
} PNMInfo;

/* Contains the information needed to write out PNM rows */
typedef struct _PNMRowInfo
{
  gint    fd;           /* File descriptor */
  gchar  *rowbuf;       /* Buffer for writing out rows */
  gint    xres;         /* X resolution */
  gint    np;           /* Number of planes */
  guchar *red;          /* Colormap red */
  guchar *grn;          /* Colormap green */
  guchar *blu;          /* Colormap blue */
} PNMRowInfo;

/* Save info  */
typedef struct
{
  gint  raw;  /*  raw or ascii  */
} PNMSaveVals;

#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 gint32 load_image (const gchar      *filename);

static void   pnm_load_ascii           (PNMScanner   *scan,
                                        PNMInfo      *info,
                                        GimpPixelRgn *pixel_rgn);
static void   pnm_load_raw             (PNMScanner   *scan,
                                        PNMInfo      *info,
                                        GimpPixelRgn *pixel_rgn);
static void   pnm_load_rawpbm          (PNMScanner   *scan,
                                        PNMInfo      *info,
                                        GimpPixelRgn *pixel_rgn);


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

static PNMScanner * pnmscanner_create  (gint        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)) \
        { g_message ((errmsg)); longjmp((jmpbuf),1); }

static struct struct_pnm_types
{
  gchar   name;
  gint    np;
  gint    asciibody;
  gint    maxval;
  void (* loader) (PNMScanner *, struct _PNMInfo *, GimpPixelRgn *pixel_rgn);
} 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}
};

static gint32
load_image (const gchar *filename)
{
  gint32 volatile image_ID = -1;
  GimpDrawable   *drawable;
  int             fd;           /* File descriptor */
  char            buf[BUFLEN];  /* buffer for random things like scanning */
  PNMInfo        *pnminfo;
  PNMScanner * volatile scan;
  int             ctr;

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

  if (fd == -1)
    {
      g_message (_("Could not open '%s' for reading: %s"),
                 gimp_filename_to_utf8 (filename), g_strerror (errno));
      return -1;
    }

  /* allocate the necessary structures */
  pnminfo = g_new (PNMInfo, 1);

  scan = NULL;
  /* set error handling */
  if (setjmp (pnminfo->jmpbuf))
    {
      /* If we get here, we had a problem reading the file */
      if (scan)
        pnmscanner_destroy (scan);
      close (fd);
      g_free (pnminfo);
      if (image_ID != -1)
        gimp_image_delete (image_ID);
      return -1;
    }

  if (!(scan = pnmscanner_create (fd)))
    longjmp (pnminfo->jmpbuf, 1);

  /* Get magic number */
  pnmscanner_gettoken (scan, buf, BUFLEN);
  CHECK_FOR_ERROR (pnmscanner_eof (scan), pnminfo->jmpbuf,
                   _("Premature end of file."));
  CHECK_FOR_ERROR ((buf[0] != 'P' || buf[2]), pnminfo->jmpbuf,
                   _("Invalid file."));

  /* 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)
    {
      g_message (_("File not in a supported format."));
      longjmp (pnminfo->jmpbuf,1);
    }

  pnmscanner_gettoken (scan, buf, BUFLEN);
  CHECK_FOR_ERROR (pnmscanner_eof (scan), pnminfo->jmpbuf,
                   _("Premature end of file."));
  pnminfo->xres = g_ascii_isdigit(*buf) ? atoi (buf) : 0;
  CHECK_FOR_ERROR (pnminfo->xres <= 0, pnminfo->jmpbuf,
                   _("Invalid X resolution."));

  pnmscanner_gettoken (scan, buf, BUFLEN);
  CHECK_FOR_ERROR (pnmscanner_eof (scan), pnminfo->jmpbuf,
                   _("Premature end of file."));
  pnminfo->yres = g_ascii_isdigit (*buf) ? atoi (buf) : 0;
  CHECK_FOR_ERROR (pnminfo->yres <= 0, pnminfo->jmpbuf,
                   _("Invalid Y resolution."));

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

      pnminfo->maxval = g_ascii_isdigit (*buf) ? atoi (buf) : 0;
      CHECK_FOR_ERROR (((pnminfo->maxval<=0)
                        || (pnminfo->maxval>255 && !pnminfo->asciibody)),
                       pnminfo->jmpbuf,
                       _("Invalid maximum value."));
    }

  pnminfo->loader (scan, pnminfo);

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

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

  /* close the file */
  close (fd);
}

static void
pnm_load_ascii (PNMScanner   *scan,
                PNMInfo      *info,
                GimpPixelRgn *pixel_rgn)
{
  guchar *data, *d;
  gint    x, y, i, b;
  gint    start, end, scanlines;
  gint    np;
  gchar   buf[BUFLEN];

  np = (info->np) ? (info->np) : 1;
  data = g_new (guchar, gimp_tile_height () * info->xres * np);

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

  for (y = 0; y < info->yres; )
    {
      start = y;
      end = y + gimp_tile_height ();
      end = MIN (end, info->yres);
      scanlines = end - start;
      d = data;

      for (i = 0; i < scanlines; i++)
        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))
                  g_message (_("Premature end of file."));
                if (info->np)
                  pnmscanner_gettoken (scan, buf, BUFLEN);
                else
                  pnmscanner_getsmalltoken (scan, buf);

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

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

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

            d += np;
          }
      gimp_pixel_rgn_set_rect (pixel_rgn, data, 0, y, info->xres, scanlines);
      y += scanlines;
    }
  g_free (data);
}

static void
pnm_load_raw (PNMScanner   *scan,
              PNMInfo      *info,
              GimpPixelRgn *pixel_rgn)
{
  guchar *data, *d;
  gint    x, y, i;
  gint    start, end, scanlines;
  gint    fd;

  data = g_new (guchar, gimp_tile_height () * info->xres * info->np);
  fd = pnmscanner_fd (scan);

  for (y = 0; y < info->yres; )
    {
      start = y;
      end = y + gimp_tile_height ();
      end = MIN (end, info->yres);
      scanlines = end - start;
      d = data;

      for (i = 0; i < scanlines; i++)
        {
          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 * (gdouble) d[x] / (gdouble) info->maxval;
                }
            }

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

      gimp_pixel_rgn_set_rect (pixel_rgn, data, 0, y, info->xres, scanlines);
      y += scanlines;
    }
  g_free (data);
}

static void
pnm_load_rawpbm (PNMScanner   *scan,
                 PNMInfo      *info,
                 GimpPixelRgn *pixel_rgn)
{
  guchar *buf;
  guchar  curbyte;
  guchar *data, *d;
  gint    x, y, i;
  gint    start, end, scanlines;
  gint    fd;
  gint    rowlen, bufpos;

  fd = pnmscanner_fd (scan);
  rowlen = (int)ceil ((double)(info->xres)/8.0);
  data = g_new (guchar, gimp_tile_height () * info->xres);
  buf = g_new (guchar, rowlen);

  for (y = 0; y < info->yres; )
    {
      start = y;
      end = y + gimp_tile_height ();
      end = MIN (end, info->yres);
      scanlines = end - start;
      d = data;

      for (i = 0; i < scanlines; i++)
        {
          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;
        }

      gimp_pixel_rgn_set_rect (pixel_rgn, data, 0, y, info->xres, scanlines);
      y += scanlines;
    }
  g_free (buf);
  g_free (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 (gint fd)
{
  PNMScanner *s;

  s = g_new (PNMScanner, 1);
  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)
    g_free (s->inbuf);
  g_free (s);
}

/* pnmscanner_createbuffer ---
 *    Creates a buffer so we can do buffered reads.
 */
static void
pnmscanner_createbuffer (PNMScanner *s,
                         gint        bufsize)
{
  s->inbuf = g_new (gchar, 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,
                     gchar      *buf,
                     gint        bufsize)
{
  int ctr=0;

  pnmscanner_eatwhitespace (s);
  while (! s->eof                   &&
         ! g_ascii_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,
                          gchar      *buf)
{
  pnmscanner_eatwhitespace (s);
  if (!(s->eof) && !g_ascii_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 (!g_ascii_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;
        }
    }
}
