/* 
A* -------------------------------------------------------------------
B* This file contains source code for the PyMOL computer program
C* Copyright (c) Schrodinger, LLC. 
D* -------------------------------------------------------------------
E* It is unlawful to modify or remove this copyright notice.
F* -------------------------------------------------------------------
G* Please see the accompanying LICENSE file for further information. 
H* -------------------------------------------------------------------
I* Additional authors of this source file include:
-* TJO: TJ O'Donnell, under contract for Schrodinger, LLC
-* 
-*
Z* -------------------------------------------------------------------
*/
#include"os_predef.h"
#include"MemoryDebug.h"
#include"pymol/memory.h"


/* backwards compatibility */

#ifdef _HAVE_LIBPNG
#ifndef _PYMOL_LIBPNG
#define _PYMOL_LIBPNG
#endif
#endif


/* do we have libpng? */

#ifdef _PYMOL_LIBPNG
#include<png.h>


/* The png_jmpbuf() macro, used in error handling, became available in

 * libpng version 1.0.6.  If you want to be able to run your code with older

 * versions of libpng, you must define the macro yourself (but only if it

 * is not already defined by libpng!).

 */

#ifndef png_jmpbuf
#  define png_jmpbuf(png_ptr) ((png_ptr)->jmpbuf)
#endif

#endif

#include"os_std.h"

#include"Base.h"
#include "MyPNG.h"
#include"MemoryDebug.h"
#include "Setting.h"
#include "Err.h"
#include "File.h"

/*
 * base64 decoding
 * http://stackoverflow.com/questions/342409/how-do-i-base64-encode-decode-in-c
 */

static unsigned char base64_decoding_table[] = {
  62,   0,   0,   0,  63,  52,  53,  54,
  55,  56,  57,  58,  59,  60,  61,   0,
   0,   0,   0,   0,   0,   0,   0,   1,
   2,   3,   4,   5,   6,   7,   8,   9,
  10,  11,  12,  13,  14,  15,  16,  17,
  18,  19,  20,  21,  22,  23,  24,  25,
   0,   0,   0,   0,   0,   0,  26,  27,
  28,  29,  30,  31,  32,  33,  34,  35,
  36,  37,  38,  39,  40,  41,  42,  43,
  44,  45,  46,  47,  48,  49,  50,  51};

static unsigned char *base64_decode(const char *data,
				    size_t input_length=0) {
  unsigned char *decoded_data = nullptr;
  unsigned int triple;
  unsigned int i = 0, j = 0, k;
  char c;

  if (input_length < 1)
    input_length = strlen(data);

  ok_assert(1, decoded_data = pymol::malloc<unsigned char>(input_length / 4 * 3));

  while (i < input_length) {
    triple = 0;

    for (k = 4; k && i < input_length;) {
      c = data[i++];
      if (c < 43 || c > 122)
        continue;
      triple += base64_decoding_table[c - 43] << (--k) * 6;
    }

    ok_assert(1, k == 0);

    for (k = 3; k;)
      decoded_data[j++] = (triple >> (--k) * 8) & 0xFF;
  }

  return decoded_data;

ok_except1:
  mfree(decoded_data);
  return nullptr;
}

typedef struct {
  unsigned char *c; // moving pointer
  unsigned char *h; // for freeing
} uchar2p;

#ifdef _PYMOL_LIBPNG
/**
 * Use with png_set_read_fn, for reading a PNG file from memory.
 * This simply copies from the memory pointer to outBytes while
 * incrementing the memory pointer.
 */
void read_data_from_buffer(png_structp png_ptr,
                           png_bytep outBytes,
                           png_size_t byteCountToRead) {
  uchar2p *io_ptr = (uchar2p*) png_get_io_ptr(png_ptr);

  if(!io_ptr)
    return;

  while(byteCountToRead--) {
    *(outBytes++) = *(io_ptr->c++);
  }
}

/**
 * Use with png_set_write_fn, for writing a PNG file to memory.
 */
static void write_data_to_buffer(png_structp png_ptr,
                                 png_bytep data,
                                 png_size_t length) {
  auto io_ptr = reinterpret_cast<png_outbuf_t*>(png_get_io_ptr(png_ptr));
  io_ptr->insert(io_ptr->end(), data, data + length);
}

/**
 * Use with png_set_read_fn instead of png_init_io, allows mixing of
 * Visual Studio versions
 */
static void read_data_from_file(
    png_structp png_ptr,
    png_bytep buffer,
    png_size_t count) {
  auto fp = static_cast<FILE*>(png_get_io_ptr(png_ptr));
  fread(buffer, 1, count, fp);
}

/**
 * Use with png_set_write_fn instead of png_init_io, allows mixing of
 * Visual Studio versions
 */
static void write_data_to_file(
    png_structp png_ptr,
    png_bytep buffer,
    png_size_t count) {
  auto fp = static_cast<FILE*>(png_get_io_ptr(png_ptr));
  fwrite(buffer, 1, count, fp);
}
#endif

int MyPNGWrite(pymol::zstring_view file_name_view, const pymol::Image& img,
    const float dpi, const int format, const int quiet,
    const float screen_gamma, const float file_gamma, png_outbuf_t* io_ptr)
{
  const char* file_name = file_name_view.c_str();
  const unsigned char* data_ptr = img.bits();
  int width = img.getWidth();
  int height = img.getHeight();
  switch (format) {
  case cMyPNG_FormatPNG:
    {
#ifdef _PYMOL_LIBPNG
      int ok = true;
      FILE *fp = nullptr;
      png_structp png_ptr;
      png_infop info_ptr;
      int bit_depth = 8;
      int bytes_per_pixel = 4;
      png_uint_32 k;
      png_byte *image = (png_byte *) data_ptr;
      png_bytep *row_pointers;
      int fd = 0;

      row_pointers = pymol::malloc<png_bytep>(height);

      /* open the file, allowing use of an encoded file descriptor, with
         approach adapted from TJO: chr(1) followed by ascii-format integer */
      if (!io_ptr) {
        if(file_name[0] == 1) {
          if(sscanf(file_name + 1, "%d", &fd) == 1) {
            fp = fdopen(fd, "wb");
          }
        } else {
          fp = pymol_fopen(file_name, "wb");
        }
        if(fp == nullptr) {
          ok = false;
          goto cleanup;
        } else if(feof(fp)) {
          ok = false;
          goto cleanup;
        }
      }
      /* Create and initialize the png_struct with the desired error handler
       * functions.  If you want to use the default stderr and longjump method,
       * you can supply nullptr for the last three parameters.  We also check that
       * the library version is compatible with the one used at compile time,
       * in case we are using dynamically linked libraries.  REQUIRED.
       */
      png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);

      if(png_ptr == nullptr) {
        ok = false;
        goto cleanup;
      }

      /* Allocate/initialize the image information data.  REQUIRED */
      info_ptr = png_create_info_struct(png_ptr);
      if(info_ptr == nullptr) {
        png_destroy_write_struct(&png_ptr, (png_infopp) nullptr);
        ok = false;
        goto cleanup;
      }

      /* Set error handling.  REQUIRED if you aren't supplying your own
       * error handling functions in the png_create_write_struct() call.
       */
      if(setjmp(png_jmpbuf(png_ptr))) {
        /* If we get here, we had a problem reading the file */
        png_destroy_write_struct(&png_ptr, (png_infopp) nullptr);
        ok = false;
        goto cleanup;
      }

      if (io_ptr) {
        png_set_write_fn(png_ptr, (void*) io_ptr, write_data_to_buffer, nullptr);
      } else {
        /* set up the output control if you are using standard C streams */
        png_set_write_fn(png_ptr, (void*) fp, write_data_to_file, nullptr);
      }

      /* Set the image information here.  Width and height are up to 2^31,
       * bit_depth is one of 1, 2, 4, 8, or 16, but valid values also depend on
       * the color_type selected. color_type is one of PNG_COLOR_TYPE_GRAY,
       * PNG_COLOR_TYPE_GRAY_ALPHA, PNG_COLOR_TYPE_PALETTE, PNG_COLOR_TYPE_RGB,
       * or PNG_COLOR_TYPE_RGB_ALPHA.  interlace is either PNG_INTERLACE_NONE or
       * PNG_INTERLACE_ADAM7, and the compression_type and filter_type MUST
       * currently be PNG_COMPRESSION_TYPE_BASE and PNG_FILTER_TYPE_BASE. REQUIRED
       */
      png_set_IHDR(png_ptr, info_ptr, width, height, bit_depth, PNG_COLOR_TYPE_RGB_ALPHA,
                   PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE);

      if(dpi > 0.0F) {          /* only set resolution if dpi is positive */
        int dots_per_meter = (int) (dpi * 39.3700787);
        png_set_pHYs(png_ptr, info_ptr, dots_per_meter, dots_per_meter,
                     PNG_RESOLUTION_METER);
      }

      png_set_gamma(png_ptr, screen_gamma, file_gamma);

      /* stamp the image as being created by PyMOL we could consider
       * supporting optional annotations as well: PDB codes, canonical
       * smiles, INCHIs, and other common identifiers */

      {
        png_text text;
        text.compression = PNG_TEXT_COMPRESSION_NONE;
        text.key = (png_charp) "Software";
        text.text = (png_charp) "PyMOL";
        text.text_length = 5;
        png_set_text(png_ptr, info_ptr, &text, 1);
      }
      {
        png_text text;
        text.compression = PNG_TEXT_COMPRESSION_NONE;
        text.key = (png_charp) "URL";
        text.text = (png_charp) "http://www.pymol.org";
        text.text_length = 5;
        png_set_text(png_ptr, info_ptr, &text, 1);
      }

      /* Write the file header information.  REQUIRED */
      png_write_info(png_ptr, info_ptr);

      /* The easiest way to write the image (you may have a different memory
       * layout, however, so choose what fits your needs best).  You need to
       * use the first method if you aren't handling interlacing yourself.
       */
      for(k = 0; k < height; k++)
        row_pointers[(height - k) - 1] = image + k * width * bytes_per_pixel;

      png_write_image(png_ptr, row_pointers);

      /* It is REQUIRED to call this to finish writing the rest of the file */
      png_write_end(png_ptr, info_ptr);

      /* clean up after the write, and free any memory allocated */
      png_destroy_write_struct(&png_ptr, &info_ptr);

    cleanup:
      if(fp) {
        /* close the file */
        fclose(fp);
      }

      mfree(row_pointers);
      /* that's it */

      return ok;
#else

      return 0;
#endif
    }
    break;
  case cMyPNG_FormatPPM:
    {
      FILE *fil = pymol_fopen(file_name, "wb");
      unsigned char *buffer = pymol::malloc<unsigned char>(3 * width * height);

      if(fil && buffer) {
        fprintf(fil, "P6\n");
        fprintf(fil, "%d %d\n", width, height);
        fprintf(fil, "255\n");
        {
          unsigned int a, b;
          unsigned char *q = buffer;
          const unsigned char *p;
          p = data_ptr + width * 4 * (height - 1);
          for(b = 0; b < height; b++) {
            for(a = 0; a < width; a++) {
              *(q++) = *(p++);  /* platform-specific ordering? */
              *(q++) = *(p++);
              *(q++) = *(p++);
              p++;
            }
            p -= width * 8;
          }
          fwrite(buffer, width, height * 3, fil);
        }
      }
      if(fil) {
        fclose(fil);
      }
      FreeP(buffer);
    }
    return 1;
    break;
  }
  return 0;
}

std::unique_ptr<pymol::Image> MyPNGRead(const char *file_name)
{
  std::unique_ptr<pymol::Image> img;
#ifdef _PYMOL_LIBPNG

  FILE *png_file = nullptr;
  png_struct *png_ptr = nullptr;
  png_info *info_ptr = nullptr;
  png_byte buf[8];
  png_byte *png_pixels = nullptr;
  png_byte **row_pointers = nullptr;
  png_byte *pix_ptr = nullptr;
  png_uint_32 row_bytes = 0;

  png_uint_32 width;
  png_uint_32 height;
  int bit_depth;
  int color_type;
  int row, col;
  int ret;
  int i;
  int ok = true;
  double file_gamma;
  uchar2p data = {NULL, nullptr};

  if(!file_name)
    return nullptr;

  if(!strncmp(file_name, "data:image/png;base64,", 22)) {
    const char *base64str = file_name + 22;
    data.h = data.c = base64_decode(base64str);
    memcpy(buf, data.c, 8);
    data.c += 8;

  } else {
    png_file = pymol_fopen(file_name, "rb");
    if(png_file == nullptr)
      return nullptr;

    /* read and check signature in PNG file */
    ret = fread(buf, 1, 8, png_file);
    if(ret != 8)
      ok = false;
  }

  if(ok) {
    ret = !png_sig_cmp(buf, 0, 8);
    if(!ret)
      ok = false;
  }
  /* create png and info structures */
  if(ok) {
    png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
    if(!png_ptr)
      ok = false;
  }

  if(ok) {
    info_ptr = png_create_info_struct(png_ptr);
    if(!info_ptr)
      ok = false;
  }

  if(ok && setjmp(png_jmpbuf(png_ptr)))
    ok = false;

  if(ok) {
    /* set up the input control for C streams */

    if(data.h) {
      png_set_read_fn(png_ptr, (void*) &data, read_data_from_buffer);
    } else {
      png_set_read_fn(png_ptr, (void*) png_file, read_data_from_file);
    }

    png_set_sig_bytes(png_ptr, 8);      /* we already read the 8 signature bytes */

    /* read the file information */
    png_read_info(png_ptr, info_ptr);

    /* get size and bit-depth of the PNG-image */
    png_get_IHDR(png_ptr, info_ptr,
                 &width, &height, &bit_depth, &color_type, nullptr, nullptr, nullptr);

    /* set-up the transformations */

    if(color_type != PNG_COLOR_TYPE_RGB_ALPHA) {
      png_set_expand(png_ptr);
      png_set_filler(png_ptr, 0xFF, PNG_FILLER_AFTER);
    }

    if(color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA)
      png_set_gray_to_rgb(png_ptr);
    /* only if file has a file gamma, we do a correction */
    if(png_get_gAMA(png_ptr, info_ptr, &file_gamma))
      png_set_gamma(png_ptr, (double) 2.2, file_gamma);

    /* all transformations have been registered; now update info_ptr data,
     * get rowbytes and channels, and allocate image memory */

    png_read_update_info(png_ptr, info_ptr);

    /* get the new color-type and bit-depth (after expansion/stripping) */
    png_get_IHDR(png_ptr, info_ptr, &width, &height, &bit_depth, &color_type,
                 nullptr, nullptr, nullptr);

    /* row_bytes is the width x number of channels x (bit-depth / 8) */
    row_bytes = png_get_rowbytes(png_ptr, info_ptr);
    if((png_pixels = (png_byte *) malloc(row_bytes * height * sizeof(png_byte))) == nullptr) {
      ok = false;
    }
  }

  if(ok) {

    if((row_pointers = (png_byte **) malloc(height * sizeof(png_bytep))) == nullptr) {
      png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
      free(png_pixels);
      png_pixels = nullptr;
      ok = false;
    }
  }

  if(ok) {

    /* set the individual row_pointers to point at the correct offsets */
    for(i = 0; i < ((signed) height); i++)
      row_pointers[i] = png_pixels + i * row_bytes;

    /* now we can go ahead and just read the whole image */
    png_read_image(png_ptr, row_pointers);

    /* read rest of file, and get additional chunks in info_ptr - REQUIRED */
    png_read_end(png_ptr, info_ptr);
  }

  img = std::make_unique<pymol::Image>(width, height);
  if(ok) {
    auto p = img->bits();
    for(row = 0; row < (signed) height; row++) {
      pix_ptr = row_pointers[(height - 1) - row];
      for(col = 0; col < (signed) width; col++) {
        *p++ = *pix_ptr++;
        *p++ = *pix_ptr++;
        *p++ = *pix_ptr++;
        *p++ = *pix_ptr++;
      }
    }

  }

  if(row_pointers != (unsigned char **) nullptr)
    free(row_pointers);
  if(png_pixels != (unsigned char *) nullptr)
    free(png_pixels);

  if(png_ptr) {
    png_destroy_read_struct(&png_ptr, &info_ptr, (png_infopp) nullptr);
  }
  if(png_file)
    fclose(png_file);
  if(data.h)
    mfree(data.h);

  return img;
#else
  return nullptr;
#endif

}                               /* end of source */
