// pngWriter.C
// Copyright (c) 2009 The Foundry Visionmongers Ltd.  All Rights Reserved.

#include "png.h"
#include "DDImage/DDWindows.h"
#undef FAR  // suppress warning in zconf.h caused by windows.h
#include "DDImage/FileWriter.h"
#include "DDImage/Row.h"
#include "DDImage/ARRAY.h"
#include "DDImage/Knobs.h"

#include <cmath>
#include <utility>

using namespace DD::Image;

class pngWriter : public FileWriter
{

protected:
public:
  int datatype;
  pngWriter(Write* iop) : FileWriter(iop), datatype(0) {}
  ~pngWriter() override {}
  void execute() override;
  static const Writer::Description d;

  void knobs(Knob_Callback f) override
  {
    static const char* const dtypes[] = { "8 bit", "16 bit", nullptr };
    Enumeration_knob(f, &datatype, dtypes, "datatype", "data type");
  }

  const char* help() override { return "Portable Network Graphics format"; }
};

static Writer* build(Write* iop) { return new pngWriter(iop); }
const Writer::Description pngWriter::d("png\0", build);

int color_type_lookup[] = {
  PNG_COLOR_TYPE_GRAY, PNG_COLOR_TYPE_GRAY_ALPHA,
  PNG_COLOR_TYPE_RGB, PNG_COLOR_TYPE_RGB_ALPHA
};


void pngWriter::execute()
{
  if (!open())
    return;
  int wdt = width(), hgt = height(), depth = 0;
  Channel ch[4]; // channel lookup

  // find the channels that we want to write (4 max, mapping to RGBA)
  depth = iop->depth();
  if (depth > 4)
    depth = 4;
  for (int i = 0; i < 4; i++)
    ch[i] = iop->channel_written_to(i);
  ChannelSet channels = channel_mask(depth);

  png_struct* png_ptr =
    png_create_write_struct (PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
  png_info* info_ptr =
    png_ptr ? png_create_info_struct (png_ptr) : nullptr;
  if (!png_ptr || !info_ptr) {
    iop->critical("Failed to allocate png structures");
    png_destroy_write_struct (&png_ptr, &info_ptr);
    return;
  }

  // allocate everything else before the setjmp!
  input0().request(0, 0, width(), height(), channels, 1);
  Row row(0, wdt);
  ARRAY(png_byte, png_pixels, (datatype ? 2 : 1) * wdt * depth);

  if (!setjmp(png_jmpbuf(png_ptr))) {

    png_init_io (png_ptr, (FILE*)file);

    png_set_IHDR (png_ptr, info_ptr, wdt, hgt, datatype ? 16 : 8,
                  color_type_lookup[depth - 1],
                  PNG_INTERLACE_NONE,
                  PNG_COMPRESSION_TYPE_BASE,
                  PNG_FILTER_TYPE_BASE);
 
    // Write the pixel aspect ratio information as xres/yres.
    // Note that when unit type is specified as PNG_RESOLUTION_UNKNOWN,
    // the xres/yres only represents pixel aspect ratio.
    // Skip if ratio is 1 since readers will assume that this section is missing.
    // Link: https://www.w3.org/TR/2003/REC-PNG-20031110/#11pHYs
    if (getPixelAspect() != 1) {
      const auto& res = getPixelAspectAsResolution();
      png_set_pHYs(png_ptr, info_ptr, res.first, res.second, PNG_RESOLUTION_UNKNOWN);
    }

    // write the file header information
    png_write_info (png_ptr, info_ptr);

    // Read each row and tell png to write it out:
    for (int y = 0; y < hgt; y++) {
      iop->status(double(y) / height());
      get(hgt - y - 1, 0, wdt, channels, row);
      const float* alpha = depth > 3 ? row[ch[3]] : nullptr;
      if (aborted())
        break;
      if (datatype) {
        U16* buffer16 = (U16*)(&png_pixels[0]);
        for (int i = 0; i < depth; i++)
          to_short(i, buffer16 + i, row[ch[i]], alpha, wdt, 16, depth);
        tomsb(buffer16, wdt * depth);
      }
      else {
        unsigned char* buffer8 = png_pixels;
        for (int i = 0; i < depth; i++)
          to_byte(i, buffer8 + i, row[ch[i]], alpha, wdt, depth);
      }
      png_write_row(png_ptr, png_pixels);
    }

    /* write the additional chuncks to the PNG file (not really needed) */
    if (!aborted())
      png_write_end (png_ptr, info_ptr);

  }
  else {   // longjmp to here from png library:

    iop->critical("Error from libpng");

  }

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

class png16Writer : public pngWriter
{
public:
  png16Writer(Write* iop) : pngWriter(iop) { datatype = 1; }
  static const Writer::Description d;
};

static Writer* build16(Write* iop) { return new png16Writer(iop); }
const Writer::Description png16Writer::d("png16\0", build16);