/*
 * PNG - Portable Network Graphics
 *
 * Alpha channle is linear.
 * Color   \   Bit Depth      1   2   4   8  16  palette  bKGD
 * PNG_COLOR_TYPE_PALETTE     O   O   O   O      must      8 bit
 * PNG_COLOR_TYPE_GRAY        O   O   O   O   O           16
 * PNG_COLOR_TYPE_GRAY_ALPHA              O   O           16
 * PNG_COLOR_TYPE_RGB                     O   O  possible 16 * 3
 * PNG_COLOR_TYPE_RGB_ALPHA               O   O  possible 16 * 3
 * 
 * This code is based on jpeg.c and sample code from the libpng-1.0.5.
 * 2000/01/10: YOSHIDA Hiroshi
 *
 * TODO:
 * pngLoad(): Alpha channel, Transparency palette.
 * pngDump(): Dump.
 * 
 */


#include "image.h"	/* xloadimage declarations */
#ifdef HAVE_LIBPNG
#include "options.h"
#include <png.h>
#include <setjmp.h>

#undef  DEBUG
/* #define  DEBUG */
#undef  debug

#ifdef DEBUG
# define debug(xx)	fprintf(stderr,xx)
#else
# define debug(xx)
#endif

#define PNG_BYTES_TO_CHECK 4

static Image *image;    /* xloadimage image being returned */
static ZFILE *zinput_file;
static char *filename;

int pngIdent(char *fullname, char *name);
Image *pngLoad(char *fullname, char *name, unsigned int verbose);
/* void pngDump(Image *image, char *options, char *file, int verbose); */


/*
 * png read handler
 */
static void png_read_data(png_structp png_ptr,
			  png_bytep data, png_size_t length)
{
  if (zread(zinput_file, data, length) != length)
    png_error(png_ptr, "Read Error");
}


/*
 * png warn handler
 */
static void output_warn(png_structp png_ptr, png_const_charp str)
{
  debug(" #warn ");
  fprintf(stderr, " PNG file: %s - %s\n", filename, str);
  fflush(stderr);
}


/*
 * png error handler
 */
static void output_error(png_structp png_ptr, png_const_charp str)
{
  debug(" #error ");
  output_warn( png_ptr, str);
#if PNG_LIBPNG_VER_MAJOR >= 1 && PNG_LIBPNG_VER_MINOR >= 4
  longjmp(png_jmpbuf((png_ptr)),1);
#else
  longjmp(png_ptr->jmpbuf, 1);	/* return control to outer routine */
#endif
}


static const char *pngColor(int color_type)
{
  const char *str;

  switch (color_type) {
  case PNG_COLOR_TYPE_GRAY:
    str = "GRAY";
    break;
  case PNG_COLOR_TYPE_GRAY_ALPHA:
    str = "GRAY_ALPHA";
    break;
  case PNG_COLOR_TYPE_PALETTE:
    str = "PALETTE";
    break;
  case PNG_COLOR_TYPE_RGB:
    str = "RGB";
    break;
  case PNG_COLOR_TYPE_RGB_ALPHA:
    str = "RGB_ALPHA";
    break;
  default:
    str = "UNKNOWN_COLOR_TYPE";
  }
  return str;
}


static const char *pngInterlace(int interlace_type)
{
  const char *str;

  switch (interlace_type) {
  case PNG_INTERLACE_NONE:
    str = "NONE";
    break;
  case PNG_INTERLACE_ADAM7:
    str = "ADAM7";
    break;
  default:
    str = "UNKNOWN_TYPE";
  }
  return str;
}


/*
 * Output PNG file infomation.
 */
static void pngInfo( png_uint_32 width, png_uint_32 height,
		     int bit_depth, int color_type, int interlace_type,
		     double file_gamma)
{
  printf("%s is %ldx%ld PNG image, color type %s, %d bit",
	 filename, width, height, pngColor(color_type), bit_depth);
  if( interlace_type != PNG_INTERLACE_NONE)
    printf(", interlace %s", pngInterlace(interlace_type));
  if( file_gamma >= 0.0)
    printf(", file gamma %.4f", file_gamma);
  putchar('\n');
}


/*
 * pngIdent(), pngLoad()
 * Read PNG header & allocate png's struct:
 * return 1: success
 */
static int pngHeader(png_structpp png_pp,
		     png_infopp info_pp, png_infopp end_pp)
{
  *info_pp = *end_pp = NULL;
  *png_pp = png_create_read_struct(PNG_LIBPNG_VER_STRING,
				   NULL, output_error, output_warn);
  if (!*png_pp)
    return 0;
  *info_pp = png_create_info_struct(*png_pp);
  if (!*info_pp) {
    png_destroy_read_struct(png_pp, info_pp, end_pp);
    return 0;
  }
  *end_pp = png_create_info_struct(*png_pp);
  if (!*end_pp) {
    png_destroy_read_struct(png_pp, info_pp, end_pp);
    return 0;
  }
#if PNG_LIBPNG_VER_MAJOR >= 1 && PNG_LIBPNG_VER_MINOR >= 4
  if (setjmp(png_jmpbuf(*png_pp))) {
#else
  if (setjmp((*png_pp)->jmpbuf)) {
#endif
    /* On error */
    png_destroy_read_struct(png_pp, info_pp, end_pp);
    return 0;
  }
  png_set_sig_bytes(*png_pp, PNG_BYTES_TO_CHECK);
  png_set_read_fn(*png_pp, NULL, png_read_data);
  png_read_info(*png_pp, *info_pp);

  return 1;
}


/*
 * return !0: It is a PNG.
 */
static int is_png(ZFILE *zf)
{
  byte png_read_buff[PNG_BYTES_TO_CHECK];

  /* Read in some of the signature bytes */
  if (zread(zf, png_read_buff,PNG_BYTES_TO_CHECK) != PNG_BYTES_TO_CHECK)
    return 0;
  return !png_sig_cmp(png_read_buff, (png_size_t)0, PNG_BYTES_TO_CHECK);
}


/*
 * Main control routine for identifying PNG without loading
 * return 1: PNG file.
 */
int pngIdent(char *fullname, char *name)
{
  png_structp png_ptr;
  png_infop info_ptr, end_info;
  png_uint_32 width, height;
  int color_type, bit_depth, interlace_type;
  double file_gamma;

  zinput_file = zopen(fullname);
  if (zinput_file == NULL) {
    zclose(zinput_file);
    return 0;
  }
  /* check at early timing */
  if (is_png(zinput_file) == 0) {
    zclose(zinput_file);
    return 0;
  }
  filename = name;

  /* read infomation header */
  if (!pngHeader(&png_ptr, &info_ptr, &end_info)) {
    zclose(zinput_file);
    return 0;
  }

#if PNG_LIBPNG_VER_MAJOR >= 1 && PNG_LIBPNG_VER_MINOR >= 4
  if (setjmp(png_jmpbuf(png_ptr))) {
#else
  if (setjmp(png_ptr->jmpbuf)) {
#endif
    /* On error */
    freeImage(image);
    png_destroy_read_struct(&png_ptr, &info_ptr, &end_info);
    zclose(zinput_file);
    return 0;
  }
  png_get_IHDR(png_ptr, info_ptr, &width, &height, &bit_depth,
	       &color_type, &interlace_type, NULL, NULL);
  if(!png_get_gAMA( png_ptr, info_ptr, &file_gamma))
    file_gamma = -1.0;
  /* print out PNG infomation */
  pngInfo( width, height, bit_depth, color_type, interlace_type, file_gamma);

  znocache(zinput_file);
  png_destroy_read_struct(&png_ptr, &info_ptr, &end_info);
  zclose(zinput_file);
  return 1;
}


/*
 * Main control routine for loading
 */
Image *pngLoad(char *fullname, char *name, unsigned int verbose)
{
  png_structp png_ptr;
  png_infop info_ptr, end_info;
  png_colorp palette;
  png_color_16p background;
  png_bytep bufp, *row_pointers;
  png_uint_32 width, height;
  int i, row_stride, color_type, bit_depth, num_palette, interlace_type;
  double file_gamma;

  zinput_file = zopen(fullname);
  if (zinput_file == NULL) {
    zclose(zinput_file);
    return NULL;
  }
  /* check at early timing */
  if (is_png(zinput_file) == 0) {
    zclose(zinput_file);
    return NULL;
  }
  filename = name;

  /* read infomation header */
  if (!pngHeader(&png_ptr, &info_ptr, &end_info)) {
    zclose(zinput_file);
    return NULL;
  }
  png_get_IHDR(png_ptr, info_ptr, &width, &height, &bit_depth,
	       &color_type, &interlace_type, NULL, NULL);
  if(!png_get_gAMA( png_ptr, info_ptr, &file_gamma))
    file_gamma = -1.0;
  /* print out PNG infomation */
  if (verbose)
    pngInfo( width, height, bit_depth, color_type, interlace_type, file_gamma);
  znocache(zinput_file);
  image = NULL;

  if (file_gamma <= 0.0)
    file_gamma = 1.0;
  png_set_gamma(png_ptr, 1.0, file_gamma);
  if (bit_depth > 8)
    png_set_strip_16(png_ptr);      /* 16 bit -> 8 bit */
  /*  if (color_type & PNG_COLOR_MASK_ALPHA) */
  png_set_strip_alpha(png_ptr);
  if (png_get_bKGD(png_ptr, info_ptr, &background))
    png_set_background(png_ptr, background, file_gamma, 1, 1.0);
  switch (color_type) {
  case PNG_COLOR_TYPE_PALETTE:
    if (bit_depth < 8)
      png_set_packing(png_ptr);     /* 1 pixlel 1 byte */
    image = newRGBImage(width, height, 8);
    png_get_PLTE(png_ptr, info_ptr, &palette, &num_palette);
    image->rgb.used = num_palette;
    for (i = 0; i < num_palette; i++) {
      *(image->rgb.red + i) = palette->red << 8;
      *(image->rgb.green + i) = palette->green << 8;
      *(image->rgb.blue + i) = palette->blue << 8;
      palette++;
    }
    break;
  case PNG_COLOR_TYPE_GRAY_ALPHA:
  case PNG_COLOR_TYPE_GRAY:
    if (bit_depth < 8)
#if PNG_LIBPNG_VER_MAJOR >= 1 && PNG_LIBPNG_VER_MINOR >= 4
      png_set_expand_gray_1_2_4_to_8(png_ptr);  /* 1 pixlel 1 byte */
#else
      png_set_gray_1_2_4_to_8(png_ptr);  /* 1 pixlel 1 byte */
#endif
    image = newRGBImage(width, height, 8);
    image->rgb.used = 256;
    for (i = 0; i < 256; i++) {
      *(image->rgb.red + i) = 
	*(image->rgb.green + i) = 
	*(image->rgb.blue + i) = i << 8;
    }
    break;
  case PNG_COLOR_TYPE_RGB_ALPHA:
  case PNG_COLOR_TYPE_RGB:
    image = newTrueImage(width, height);
    break;
  default:
    fprintf(stderr, "Unknown color type PNG.");
    png_destroy_read_struct(&png_ptr, &info_ptr, &end_info);
    zclose(zinput_file);
    return NULL;
  }
  image->title = dupString(filename);

  bufp = image->data;
  png_read_update_info(png_ptr, info_ptr);
  row_stride = png_get_rowbytes(png_ptr, info_ptr);
  row_pointers = (png_bytep *)lmalloc(sizeof(png_bytep) * height);
  for (i = 0; i < height; i++) {
    *(row_pointers + i) = bufp;
    bufp += row_stride;
  }
  png_read_image(png_ptr, row_pointers);
  lfree((byte *)row_pointers);
  png_destroy_read_struct(&png_ptr, &info_ptr, &end_info);
  zclose(zinput_file);

  return image;
}

#else /* !HAVE_LIBPNG */
static int unused;
#endif /* !HAVE_LIBPNG */
