// Copyright 2026 The PDFium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "core/fxcodec/png/libpng_png_context.h"

#include <setjmp.h>
#include <string.h>

#include <utility>

#include "core/fxcodec/cfx_codec_memory.h"
#include "core/fxcodec/progressive_decoder_context_delegate.h"
#include "core/fxcrt/check.h"
#include "core/fxcrt/compiler_specific.h"
#include "core/fxcrt/retain_ptr.h"
#include "core/fxcrt/span.h"
#include "core/fxge/dib/cfx_dibitmap.h"

#ifdef USE_SYSTEM_LIBPNG
#include <png.h>
#else
#include "third_party/libpng/png.h"
#endif

namespace {

constexpr double kPngGamma = 2.2;

}  // namespace

extern "C" {

void _png_error_data(png_structp png_ptr, png_const_charp error_msg) {
  if (png_get_error_ptr(png_ptr)) {
    UNSAFE_TODO(strncpy(static_cast<char*>(png_get_error_ptr(png_ptr)),
                        error_msg,
                        fxcodec::LibpngPngContext::kPngErrorSize - 1));
  }

  longjmp(png_jmpbuf(png_ptr), 1);
}

void _png_warning_data(png_structp png_ptr, png_const_charp error_msg) {}

void _png_get_header_func(png_structp png_ptr, png_infop info_ptr) {
  auto* context = reinterpret_cast<fxcodec::LibpngPngContext*>(
      png_get_progressive_ptr(png_ptr));
  if (!context) {
    return;
  }

  png_uint_32 width = 0;
  png_uint_32 height = 0;
  int bits_per_component = 0;
  int libpng_color_type = 0;
  png_get_IHDR(png_ptr, info_ptr, &width, &height, &bits_per_component,
               &libpng_color_type, nullptr, nullptr, nullptr);
  if (bits_per_component > 8) {
    png_set_strip_16(png_ptr);
  } else if (bits_per_component < 8) {
    png_set_expand_gray_1_2_4_to_8(png_ptr);
  }

  if (libpng_color_type == PNG_COLOR_TYPE_PALETTE) {
    png_set_palette_to_rgb(png_ptr);
  }

  context->number_of_passes_ = png_set_interlace_handling(png_ptr);
  context->height_ = height;

  // Notifies the delegate of image dimensions and metadata.
  if (!context->delegate_->PrepareDirectOutput(
          width, height,
          fxcodec::ProgressiveDecoderContextDelegate::Format::kArgb)) {
    // Note that `png_error` function is marked as `PNG_NORETURN`.
    png_error(context->png_, "Read Header Callback Error");
  }
  int intent;
  if (png_get_sRGB(png_ptr, info_ptr, &intent)) {
    png_set_gamma(png_ptr, kPngGamma, 0.45455);
  } else {
    double image_gamma;
    if (png_get_gAMA(png_ptr, info_ptr, &image_gamma)) {
      png_set_gamma(png_ptr, kPngGamma, image_gamma);
    } else {
      png_set_gamma(png_ptr, kPngGamma, 0.45455);
    }
  }
  if (!(libpng_color_type & PNG_COLOR_MASK_COLOR)) {
    png_set_gray_to_rgb(png_ptr);
  }
  png_set_bgr(png_ptr);
  if (!(libpng_color_type & PNG_COLOR_MASK_ALPHA)) {
    png_set_filler(png_ptr, 0xff, PNG_FILLER_AFTER);
  }
  png_read_update_info(png_ptr, info_ptr);
}

void _png_get_end_func(png_structp png_ptr, png_infop info_ptr) {}

void _png_get_row_func(png_structp png_ptr,
                       png_bytep new_row,
                       png_uint_32 row_num,
                       int pass) {
  auto* context = reinterpret_cast<fxcodec::LibpngPngContext*>(
      png_get_progressive_ptr(png_ptr));
  if (!context || !context->bitmap_) {
    return;
  }

  pdfium::span<uint8_t> dst_buf = context->delegate_->AskScanlineBuf(row_num);
  CHECK(!dst_buf.empty());
  png_progressive_combine_row(png_ptr, dst_buf.data(), new_row);

  if ((pass == (context->number_of_passes_ - 1)) &&
      (row_num == (context->height_ - 1))) {
    context->finished_ = true;
  }
}

int _png_set_read_and_error_fns(png_structrp png_ptr,
                                void* user_ctx,
                                char* error_buf) {
  if (setjmp(png_jmpbuf(png_ptr))) {
    return 0;
  }
  png_set_progressive_read_fn(png_ptr, user_ctx, _png_get_header_func,
                              _png_get_row_func, _png_get_end_func);
  png_set_error_fn(png_ptr, error_buf, _png_error_data, _png_warning_data);
  return 1;
}

}  // extern "C"

namespace fxcodec {

LibpngPngContext::LibpngPngContext(ProgressiveDecoderContextDelegate* delegate)
    : delegate_(delegate) {
  png_ =
      png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
  if (!png_) {
    return;
  }
  info_ = png_create_info_struct(png_);
  if (!info_) {
    return;
  }
  if (!_png_set_read_and_error_fns(png_, this, last_error_)) {
    png_destroy_read_struct(&png_, &info_, nullptr);
    png_ = nullptr;
    info_ = nullptr;
  }
}

LibpngPngContext::~LibpngPngContext() {
  png_destroy_read_struct(png_ ? &png_ : nullptr, info_ ? &info_ : nullptr,
                          nullptr);
}

void LibpngPngContext::Input(RetainPtr<CFX_CodecMemory> codec_memory) {
  codec_memory_ = std::move(codec_memory);
}

FXCODEC_STATUS LibpngPngContext::StartDecode(RetainPtr<CFX_DIBitmap> bitmap) {
  bitmap_ = std::move(bitmap);
  CHECK_EQ(bitmap_->GetFormat(), FXDIB_Format::kBgra);
  FXCODEC_STATUS status = FXCODEC_STATUS::kDecodeToBeContinued;
  if (!delegate_->ReadMoreData(0, &status)) {
    return status;
  }
  return FXCODEC_STATUS::kDecodeToBeContinued;
}

FXCODEC_STATUS LibpngPngContext::ContinueDecode() {
  FXCODEC_STATUS status = FXCODEC_STATUS::kDecodeFinished;
  while (!finished_) {
    if (!ProcessData(codec_memory_)) {
      status = FXCODEC_STATUS::kError;
      break;
    }
    if (finished_) {
      break;
    }
    status = FXCODEC_STATUS::kError;
    if (!delegate_->ReadMoreData(std::nullopt, &status)) {
      break;
    }
  }
  bitmap_ = nullptr;
  return finished_ ? FXCODEC_STATUS::kDecodeFinished : status;
}

bool LibpngPngContext::ReadHeader(RetainPtr<CFX_CodecMemory> codec_memory) {
  return ProcessData(std::move(codec_memory));
}

bool LibpngPngContext::ProcessData(RetainPtr<CFX_CodecMemory> codec_memory) {
  if (setjmp(png_jmpbuf(png_))) {
    return false;
  }
  pdfium::span<uint8_t> src_buf = codec_memory->GetUnconsumedSpan();
  png_process_data(png_, info_, src_buf.data(), src_buf.size());

  // `libpng` always consumes all the data from `src_buf`, so
  // advance/seek `codec_memory` to the end of the buffer.
  codec_memory->Seek(codec_memory->GetSize());
  CHECK(codec_memory->GetUnconsumedSpan().empty());

  return true;
}

}  // namespace fxcodec
