/*
 * UPIWIN - Micro Pi Windowing Framework Kernel
 * Copyright (C) 2019 Amy Bowersox/Erbosoft Metaverse Design Solutions
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *-------------------------------------------------------------------------
 */
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <png.h>

typedef unsigned char uch;
typedef unsigned short ush;
typedef unsigned long ulg;

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

png_structp png_ptr = NULL;
png_infop info_ptr = NULL;

png_uint_32  width, height;
int  bit_depth, color_type;
uch  *image_data = NULL;

/* returns 0 on success, -1 on error */
int readpng_init(const char *fname, FILE *infile, ulg *pWidth, ulg *pHeight)
{
  uch sig[8];

  fread(sig, 1, 8, infile);
  if (!png_check_sig(sig, 8))
  {
    fprintf(stderr, "%s: invalid PNG file signature\n", fname);
    return -1;
  }

  png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
  if (!png_ptr)
  {
    fprintf(stderr, "%s: out of memory\n", fname);
    return -1;
  }

  info_ptr = png_create_info_struct(png_ptr);
  if (!info_ptr)
  {
    png_destroy_read_struct(&png_ptr, NULL, NULL);
    fprintf(stderr, "%s: out of memory\n", fname);
    return -1;
  }

  if (setjmp(png_jmpbuf(png_ptr)))
  {
    png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
    fprintf(stderr, "%s: bad header in PNG image\n", fname);
    return -1;
  }

  png_init_io(png_ptr, infile);
  png_set_sig_bytes(png_ptr, 8);
  png_read_info(png_ptr, info_ptr);

  png_get_IHDR(png_ptr, info_ptr, &width, &height, &bit_depth, &color_type, NULL, NULL, NULL);
  *pWidth = width;
  *pHeight = height;

  return 0;
}

/* returns -1 on error, 0 otherwise (out vars not touched if we can't find background */
int readpng_get_bgcolor(const char *fname, uch *red, uch *green, uch *blue)
{
  png_color_16p pBackground;

  if (setjmp(png_jmpbuf(png_ptr))) {
    png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
    fprintf(stderr, "%s: unspecified error in background reading\n", fname);
    return -1;
  }

  if (!png_get_valid(png_ptr, info_ptr, PNG_INFO_bKGD))
    return 0;

  png_get_bKGD(png_ptr, info_ptr, &pBackground);

  if (bit_depth == 16)
  {
    *red   = pBackground->red   >> 8;
    *green = pBackground->green >> 8;
    *blue  = pBackground->blue  >> 8;
  }
  else if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8)
  {
    if (bit_depth == 1)
      *red = *green = *blue = pBackground->gray ? 255 : 0;
    else if (bit_depth == 2)
      *red = *green = *blue = (255/3) * pBackground->gray;
    else /* bit_depth == 4 */
      *red = *green = *blue = (255/15) * pBackground->gray;
  }
  else
  {
    *red   = (uch)pBackground->red;
    *green = (uch)pBackground->green;
    *blue  = (uch)pBackground->blue;
  }

  return 0;
}

/* display_exponent == LUT_exponent * CRT_exponent */
uch *readpng_get_image(const char *fname, double display_exponent, int *pChannels, ulg *pRowbytes)
{
  double  gamma;
  png_uint_32  i, rowbytes;
  png_bytepp  row_pointers = NULL;

  if (setjmp(png_jmpbuf(png_ptr))) {
    png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
    fprintf(stderr, "%s: unspecified error in image data reading\n", fname);
    return NULL;
  }

  if (color_type == PNG_COLOR_TYPE_PALETTE)
    png_set_expand(png_ptr);
  if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8)
    png_set_expand(png_ptr);
  if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS))
    png_set_expand(png_ptr);
  if (bit_depth == 16)
    png_set_strip_16(png_ptr);
  if (color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA)
    png_set_gray_to_rgb(png_ptr);

  if (png_get_gAMA(png_ptr, info_ptr, &gamma))
    png_set_gamma(png_ptr, display_exponent, gamma);

  png_read_update_info(png_ptr, info_ptr);

  *pRowbytes = rowbytes = png_get_rowbytes(png_ptr, info_ptr);
  *pChannels = (int)png_get_channels(png_ptr, info_ptr);

  if ((image_data = (uch *)malloc(rowbytes*height)) == NULL)
  {
    png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
    fprintf(stderr, "%s: could not allocate image buffer\n", fname);
    return NULL;
  }

  if ((row_pointers = (png_bytepp)malloc(height*sizeof(png_bytep))) == NULL)
  {
    png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
    free(image_data);
    image_data = NULL;
    fprintf(stderr, "%s: could not allocate image row buffer\n", fname);
    return NULL;
  }

  fprintf(stderr, "image %s: chans=%d rowbytes=%u height=%u\n", fname, *pChannels,
	  rowbytes, height);

  for (i = 0; i < height; ++i)
    row_pointers[i] = image_data + i * rowbytes;

  png_read_image(png_ptr, row_pointers);

  free(row_pointers);
  row_pointers = NULL;

  png_read_end(png_ptr, NULL);

  return image_data;
}

void readpng_cleanup(int free_image_data)
{
  if (free_image_data && image_data)
  {
    free(image_data);
    image_data = NULL;
  }

  if (png_ptr && info_ptr)
  {
    png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
    png_ptr = NULL;
    info_ptr = NULL;
  }
}

int do_convert(const char *infilename, const char *outfilename)
{
  ulg image_width, image_height, image_rowbytes, row, i;
  FILE *fpin;
  uch *image_data, *src;
  uint16_t bred = 0, bgreen = 0, bblue = 0, buf = 0;
  int image_channels, fdout;
  uch bg_red=0, bg_green=0, bg_blue=0, br, bg, bb, ba;
  double display_exponent = 2.2; /* a guesstimate */

  if (!(fpin = fopen(infilename, "rb")))
  {
     fprintf(stderr, "%s: could not open file\n", infilename);
     return -1;
  }

  if (readpng_init(infilename, fpin, &image_width, &image_height))
  {
    readpng_cleanup(1);
    fclose(fpin);
    return -1;
  }

  if (readpng_get_bgcolor(infilename, &bg_red, &bg_green, &bg_blue))
  {
    readpng_cleanup(1);
    fclose(fpin);
    return -1;
  }

  image_data = readpng_get_image(infilename, display_exponent, &image_channels, &image_rowbytes);
  readpng_cleanup(0);
  fclose(fpin);
  if (!image_data)
    return -1;

  if ((fdout = creat(outfilename, S_IREAD|S_IWRITE)) < 0)
  {
    free(image_data);
    fprintf(stderr, "%s: could not open file\n", outfilename);
    return -1;
  }

  for (row = 0; row < image_height; ++row)
  {
    src = image_data + (row * image_rowbytes);
    for (i = image_width; i > 0; --i)
    {
      if (image_channels == 3)
      {
	       bred = *src++;
	       bgreen = *src++;
	       bblue = *src++;
      }
      else if (image_channels == 4)
      {
	       br = *src++;
	       bg = *src++;
	       bb = *src++;
         ba = *src++;
         if (ba == 255)
         {
           bred = br;
           bgreen = bg;
           bblue = bb;
         }
	       else if (ba == 0)
         {
           bred = bg_red;
           bgreen = bg_green;
           bblue = bg_blue;
         }
         else
         {
           png_composite(bred, br, ba, bg_red);
           png_composite(bgreen, bg, ba, bg_green);
           png_composite(bblue, bb, ba, bg_blue);
         }
      }
      bred = (bred >> 3) & 0x1F;
      bgreen = (bgreen >> 2) & 0x3F;
      bblue = (bblue >> 3) & 0x1F;
      buf = (bred << 11) | (bgreen << 5) | bblue;
      if (write(fdout, &buf, sizeof(uint16_t)) < 0)
      {
        close(fdout);
        free(image_data);
        fprintf(stderr, "%s: error writing image data\n", outfilename);
        return -1;
      }
    }
  }

  close(fdout);
  free(image_data);
  return 0;
}

int main(int argc, char *argv[])
{
  if (argc < 3)
  {
    fprintf(stderr, "usage: mksplash inputfile outputfile\n");
    return EXIT_FAILURE;
  }

  if (do_convert(argv[1], argv[2]))
    return EXIT_FAILURE;
  return EXIT_SUCCESS;
}
