// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
// SPDX-License-Identifier: BSD-3-Clause
#include "vtkPNGWriter.h"

#include "vtkAlgorithmOutput.h"
#include "vtkEndian.h"
#include "vtkErrorCode.h"
#include "vtkImageData.h"
#include "vtkObjectFactory.h"
#include "vtkStreamingDemandDrivenPipeline.h"
#include "vtkUnsignedCharArray.h"
#include "vtk_png.h"
#include <vtksys/SystemTools.hxx>

#include <vector>

VTK_ABI_NAMESPACE_BEGIN
class vtkPNGWriter::vtkInternals
{
public:
  std::vector<std::pair<std::string, std::string>> TextKeyValue;
};

vtkStandardNewMacro(vtkPNGWriter);

vtkCxxSetObjectMacro(vtkPNGWriter, Result, vtkUnsignedCharArray);

const char* vtkPNGWriter::TITLE = "Title";
const char* vtkPNGWriter::AUTHOR = "Author";
const char* vtkPNGWriter::DESCRIPTION = "Description";
const char* vtkPNGWriter::COPYRIGHT = "Copyright";
const char* vtkPNGWriter::CREATION_TIME = "Creation Time";
const char* vtkPNGWriter::SOFTWARE = "Software";
const char* vtkPNGWriter::DISCLAIMER = "Disclaimer";
const char* vtkPNGWriter::WARNING = "Warning";
const char* vtkPNGWriter::SOURCE = "Source";
const char* vtkPNGWriter::COMMENT = "Comment";

vtkPNGWriter::vtkPNGWriter()
{
  this->FileLowerLeft = 1;
  this->FileDimensionality = 2;
  this->CompressionLevel = 5;
  this->Result = nullptr;
  this->TempFP = nullptr;
  this->Internals = new vtkInternals();
}

vtkPNGWriter::~vtkPNGWriter()
{
  if (this->Result)
  {
    this->Result->Delete();
    this->Result = nullptr;
  }
  delete this->Internals;
}

//------------------------------------------------------------------------------
// Writes all the data from the input.
void vtkPNGWriter::Write()
{
  this->SetErrorCode(vtkErrorCode::NoError);

  // Error checking
  if (this->GetInput() == nullptr)
  {
    vtkErrorMacro(<< "Write:Please specify an input!");
    return;
  }
  if (!this->WriteToMemory && !this->FileName && !this->FilePattern)
  {
    vtkErrorMacro(<< "Write:Please specify either a FileName or a file prefix and pattern");
    this->SetErrorCode(vtkErrorCode::NoFileNameError);
    return;
  }

  // Make sure the file name is allocated
  size_t internalFileNameSize = (this->FileName ? strlen(this->FileName) : 1) +
    (this->FilePrefix ? strlen(this->FilePrefix) : 1) +
    (this->FilePattern ? strlen(this->FilePattern) : 1) + 256;
  this->InternalFileName = new char[internalFileNameSize];
  this->InternalFileName[0] = 0;

  // Fill in image information.
  this->GetInputExecutive(0, 0)->UpdateInformation();
  int* wExtent;
  wExtent = vtkStreamingDemandDrivenPipeline::GetWholeExtent(this->GetInputInformation(0, 0));
  this->FileNumber = wExtent[4];
  this->MinimumFileNumber = this->MaximumFileNumber = this->FileNumber;
  this->FilesDeleted = 0;
  this->UpdateProgress(0.0);
  // loop over the z axis and write the slices
  for (this->FileNumber = wExtent[4]; this->FileNumber <= wExtent[5]; ++this->FileNumber)
  {
    this->MaximumFileNumber = this->FileNumber;
    int uExt[6];
    memcpy(uExt, wExtent, 4 * sizeof(int));
    uExt[4] = uExt[5] = this->FileNumber;
    if (!this->WriteToMemory)
    {
      int bytes_printed = 0;
      // determine the name
      if (this->FileName)
      {
        bytes_printed =
          snprintf(this->InternalFileName, internalFileNameSize, "%s", this->FileName);
      }
      else
      {
        if (this->FilePrefix)
        {
          bytes_printed = snprintf(this->InternalFileName, internalFileNameSize, this->FilePattern,
            this->FilePrefix, this->FileNumber);
        }
        else
        {
          bytes_printed = snprintf(
            this->InternalFileName, internalFileNameSize, this->FilePattern, this->FileNumber);
        }
      }
      if (static_cast<size_t>(bytes_printed) >= internalFileNameSize)
      {
        // add null terminating character just to be safe.
        this->InternalFileName[internalFileNameSize - 1] = 0;
        vtkWarningMacro("Filename has been truncated.");
      }
    }
    this->GetInputAlgorithm()->UpdateExtent(uExt);
    this->WriteSlice(this->GetInput(), uExt);
    if (this->ErrorCode == vtkErrorCode::OutOfDiskSpaceError)
    {
      this->DeleteFiles();
      break;
    }
    this->UpdateProgress((this->FileNumber - wExtent[4]) / (wExtent[5] - wExtent[4] + 1.0));
  }
  delete[] this->InternalFileName;
  this->InternalFileName = nullptr;
}

extern "C"
{
  static void vtkPNGWriteInit(png_structp png_ptr, png_bytep data, png_size_t sizeToWrite)
  {
    vtkPNGWriter* self =
      vtkPNGWriter::SafeDownCast(static_cast<vtkObject*>(png_get_io_ptr(png_ptr)));
    if (self)
    {
      vtkUnsignedCharArray* uc = self->GetResult();
      // write to the uc array
      unsigned char* ptr =
        uc->WritePointer(uc->GetMaxId() + 1, static_cast<vtkIdType>(sizeToWrite));
      memcpy(ptr, data, sizeToWrite);
    }
  }
}

extern "C"
{
  static void vtkPNGWriteFlush(png_structp vtkNotUsed(png_ptr)) {}
}

extern "C"
{
  static void vtkPNGWriteWarningFunction(png_structp /*png_ptr*/, png_const_charp warning_msg)
  {
    fprintf(stderr, "libpng warning: %s\n", warning_msg);
  }
}

extern "C"
{
  /* The PNG library does not expect the error function to return.
     Therefore we must use this ugly longjmp call.  */
  static void vtkPNGWriteErrorFunction(png_structp png_ptr, png_const_charp error_msg)
  {
#if PNG_LIBPNG_VER >= 10400
    vtkPNGWriteWarningFunction(png_ptr, error_msg);
#else
    (void)error_msg;
    longjmp(png_ptr->jmpbuf, 1);
#endif
  }
}

// we disable this warning because even though this is a C++ file, between
// the setjmp and resulting longjmp there should not be any C++ constructors
// or destructors.
#if defined(_MSC_VER) && !defined(VTK_DISPLAY_WIN32_WARNINGS)
#pragma warning(disable : 4611)
#endif

static constexpr unsigned int VTK_MAXIMUM_UNCOMPRESSED_TEXT_SIZE = 10000;

void vtkPNGWriter::WriteSlice(vtkImageData* data, int* uExtent)
{
  vtkInternals* impl = this->Internals;
  // Call The correct templated function for the output
  unsigned int ui;

  // Call the correct templated function for the input
  if (data->GetScalarType() != VTK_UNSIGNED_SHORT && data->GetScalarType() != VTK_UNSIGNED_CHAR)
  {
    vtkWarningMacro("PNGWriter only supports unsigned char and unsigned short inputs");
    return;
  }

  png_structp png_ptr =
    png_create_write_struct(PNG_LIBPNG_VER_STRING, (png_voidp) nullptr, nullptr, nullptr);
  if (!png_ptr)
  {
    vtkErrorMacro(<< "Unable to write PNG file!");
    return;
  }

  png_set_compression_level(png_ptr, this->CompressionLevel);

  png_infop info_ptr = png_create_info_struct(png_ptr);
  if (!info_ptr)
  {
    png_destroy_write_struct(&png_ptr, (png_infopp) nullptr);
    vtkErrorMacro(<< "Unable to write PNG file!");
    return;
  }

  this->TempFP = nullptr;
  if (this->WriteToMemory)
  {
    vtkUnsignedCharArray* uc = this->GetResult();
    if (!uc || uc->GetReferenceCount() > 1)
    {
      uc = vtkUnsignedCharArray::New();
      this->SetResult(uc);
      uc->Delete();
    }
    // start out with 10K as a guess for the image size
    uc->Allocate(10000);
    png_set_write_fn(png_ptr, static_cast<png_voidp>(this), vtkPNGWriteInit, vtkPNGWriteFlush);
  }
  else
  {
    this->TempFP = vtksys::SystemTools::Fopen(this->InternalFileName, "wb");
    if (!this->TempFP)
    {
      vtkErrorMacro("Unable to open file " << this->InternalFileName);
      this->SetErrorCode(vtkErrorCode::CannotOpenFileError);
      return;
    }
    png_init_io(png_ptr, this->TempFP);
    png_set_error_fn(png_ptr, nullptr, vtkPNGWriteErrorFunction, vtkPNGWriteWarningFunction);
    if (setjmp(png_jmpbuf((png_ptr))))
    {
      fclose(this->TempFP);
      png_destroy_write_struct(&png_ptr, &info_ptr);
      this->SetErrorCode(vtkErrorCode::UnknownError);
      return;
    }
  }

  void* outPtr;
  outPtr = data->GetScalarPointer(uExtent[0], uExtent[2], uExtent[4]);
  png_uint_32 width, height;
  width = uExtent[1] - uExtent[0] + 1;
  height = uExtent[3] - uExtent[2] + 1;
  int bit_depth = 8;
  if (data->GetScalarType() == VTK_UNSIGNED_SHORT)
  {
    bit_depth = 16;
  }
  int color_type;
  switch (data->GetNumberOfScalarComponents())
  {
    case 1:
      color_type = PNG_COLOR_TYPE_GRAY;
      break;
    case 2:
      color_type = PNG_COLOR_TYPE_GRAY_ALPHA;
      break;
    case 3:
      color_type = PNG_COLOR_TYPE_RGB;
      break;
    default:
      color_type = PNG_COLOR_TYPE_RGB_ALPHA;
      break;
  }

  png_set_IHDR(png_ptr, info_ptr, width, height, bit_depth, color_type, PNG_INTERLACE_NONE,
    PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);

  // add latin1, uncompressed text chunks to the PNG file
  if (!impl->TextKeyValue.empty())
  {
    std::vector<png_text> pngText(impl->TextKeyValue.size());
    for (size_t i = 0; i < pngText.size(); ++i)
    {
      pngText[i].key = const_cast<char*>(impl->TextKeyValue[i].first.c_str());
      pngText[i].text = const_cast<char*>(impl->TextKeyValue[i].second.c_str());
      pngText[i].text_length = impl->TextKeyValue[i].second.length();
      if (pngText[i].text_length < VTK_MAXIMUM_UNCOMPRESSED_TEXT_SIZE)
      {
        pngText[i].compression = PNG_TEXT_COMPRESSION_NONE;
      }
      else
      {
        pngText[i].compression = PNG_TEXT_COMPRESSION_zTXt;
      }
#ifdef PNG_iTXt_SUPPORTED
      pngText[i].itxt_length = 0;
      pngText[i].lang = nullptr;
      pngText[i].lang_key = nullptr;
#endif
    }
    png_set_text(png_ptr, info_ptr, pngText.data(), static_cast<int>(pngText.size()));
  }

  // interlace_type - PNG_INTERLACE_NONE or
  //                 PNG_INTERLACE_ADAM7

  png_write_info(png_ptr, info_ptr);
  // default is big endian
  if (bit_depth > 8)
  {
#ifndef VTK_WORDS_BIGENDIAN
    png_set_swap(png_ptr);
#endif
  }
  std::vector<png_byte*> row_pointers(height);
  vtkIdType* outInc = data->GetIncrements();
  vtkIdType rowInc = outInc[1] * bit_depth / 8;
  for (ui = 0; ui < height; ui++)
  {
    // computing the offset explicitly in a temporary variable as there seems to
    // be some bug in intel compilers on longhorn (thanks to Greg Abram) when
    // the offset is computed directly in the []'s.
    unsigned int offset = height - ui - 1;
    row_pointers[offset] = (png_byte*)outPtr;
    outPtr = (unsigned char*)outPtr + rowInc;
  }
  png_write_image(png_ptr, row_pointers.data());
  row_pointers.clear();
  png_write_end(png_ptr, info_ptr);

  png_destroy_write_struct(&png_ptr, &info_ptr);

  if (this->TempFP)
  {
    fflush(this->TempFP);
    if (ferror(this->TempFP))
    {
      this->SetErrorCode(vtkErrorCode::OutOfDiskSpaceError);
    }
  }

  if (this->TempFP)
  {
    fclose(this->TempFP);
  }
}

void vtkPNGWriter::PrintSelf(ostream& os, vtkIndent indent)
{
  this->Superclass::PrintSelf(os, indent);

  os << indent << "Result: " << this->Result << "\n";
}

void vtkPNGWriter::AddText(const char* key, const char* value)
{
  vtkInternals* impl = this->Internals;
  const size_t MAX_KEY_LENGTH = 79;
  if (!key || key[0] == '\0')
  {
    vtkWarningMacro("Trying to add PNG text chunk with a null or empty key");
    return;
  }
  size_t keyLength = strlen(key);
  if (keyLength > MAX_KEY_LENGTH)
  {
    vtkWarningMacro("Trying to add a PNG text chunk with a key longer than "
      << MAX_KEY_LENGTH << " characters: " << key << " Truncating ...");
    keyLength = MAX_KEY_LENGTH;
  }
  size_t index = impl->TextKeyValue.size();
  impl->TextKeyValue.resize(index + 1);
  impl->TextKeyValue[index].first.assign(key, keyLength);
  impl->TextKeyValue[index].second.assign(value);
  this->Modified();
}

void vtkPNGWriter::ClearText()
{
  vtkInternals* impl = this->Internals;
  if (!impl->TextKeyValue.empty())
  {
    impl->TextKeyValue.clear();
    this->Modified();
  }
}
VTK_ABI_NAMESPACE_END
