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

#include "chrome/browser/printing/pwg_raster_converter.h"

#include <algorithm>
#include <memory>
#include <optional>
#include <utility>

#include "base/cancelable_callback.h"
#include "base/check_op.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/notreached.h"
#include "chrome/browser/printing/printing_service.h"
#include "chrome/services/printing/public/mojom/pdf_to_pwg_raster_converter.mojom.h"
#include "chrome/services/printing/public/mojom/printing_service.mojom.h"
#include "components/cloud_devices/common/cloud_device_description.h"
#include "components/cloud_devices/common/printer_description.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/child_process_data.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "mojo/public/cpp/system/platform_handle.h"
#include "printing/mojom/print.mojom.h"
#include "printing/pdf_render_settings.h"
#include "printing/pwg_raster_settings.h"
#include "printing/units.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/size.h"

namespace printing {

namespace {

using content::BrowserThread;

// Converts PDF into PWG raster. Class lives on the UI thread.
class PwgRasterConverterHelper
    : public base::RefCounted<PwgRasterConverterHelper> {
 public:
  PwgRasterConverterHelper(const PdfRenderSettings& settings,
                           const PwgRasterSettings& bitmap_settings);

  PwgRasterConverterHelper(const PwgRasterConverterHelper&) = delete;
  PwgRasterConverterHelper& operator=(const PwgRasterConverterHelper&) = delete;

  void Convert(const std::optional<bool>& use_skia,
               const base::RefCountedMemory* data,
               PwgRasterConverter::ResultCallback callback);

 private:
  friend class base::RefCounted<PwgRasterConverterHelper>;

  ~PwgRasterConverterHelper();

  void RunCallback(base::ReadOnlySharedMemoryRegion region,
                   uint32_t page_count);

  PdfRenderSettings settings_;
  PwgRasterSettings bitmap_settings_;
  mojo::Remote<printing::mojom::PdfToPwgRasterConverter>
      pdf_to_pwg_raster_converter_remote_;
  PwgRasterConverter::ResultCallback callback_;
};

PwgRasterConverterHelper::PwgRasterConverterHelper(
    const PdfRenderSettings& settings,
    const PwgRasterSettings& bitmap_settings)
    : settings_(settings), bitmap_settings_(bitmap_settings) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
}

PwgRasterConverterHelper::~PwgRasterConverterHelper() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
}

void PwgRasterConverterHelper::Convert(
    const std::optional<bool>& use_skia,
    const base::RefCountedMemory* data,
    PwgRasterConverter::ResultCallback callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  callback_ = std::move(callback);

  GetPrintingService()->BindPdfToPwgRasterConverter(
      pdf_to_pwg_raster_converter_remote_.BindNewPipeAndPassReceiver());
  pdf_to_pwg_raster_converter_remote_.set_disconnect_handler(
      base::BindOnce(&PwgRasterConverterHelper::RunCallback, this,
                     base::ReadOnlySharedMemoryRegion(), /*page_count=*/0));

  base::MappedReadOnlyRegion memory =
      base::ReadOnlySharedMemoryRegion::Create(data->size());
  if (!memory.IsValid()) {
    RunCallback(base::ReadOnlySharedMemoryRegion(), /*page_count=*/0);
    return;
  }

  if (use_skia) {
    pdf_to_pwg_raster_converter_remote_->SetUseSkiaRendererPolicy(*use_skia);
  }

  // TODO(thestig): Write `data` into shared memory in the first place, to avoid
  // this copy.
  memory.mapping.GetMemoryAsSpan<uint8_t>().copy_prefix_from(*data);
  pdf_to_pwg_raster_converter_remote_->Convert(
      std::move(memory.region), settings_, bitmap_settings_,
      base::BindOnce(&PwgRasterConverterHelper::RunCallback, this));
}

void PwgRasterConverterHelper::RunCallback(
    base::ReadOnlySharedMemoryRegion region,
    uint32_t page_count) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  if (callback_) {
    if (region.IsValid() && page_count > 0) {
      std::move(callback_).Run(std::move(region));
    } else {
      std::move(callback_).Run(base::ReadOnlySharedMemoryRegion());
    }
  }
  pdf_to_pwg_raster_converter_remote_.reset();
}

class PwgRasterConverterImpl : public PwgRasterConverter {
 public:
  PwgRasterConverterImpl();

  PwgRasterConverterImpl(const PwgRasterConverterImpl&) = delete;
  PwgRasterConverterImpl& operator=(const PwgRasterConverterImpl&) = delete;

  ~PwgRasterConverterImpl() override;

  void Start(const std::optional<bool>& use_skia,
             const base::RefCountedMemory* data,
             const PdfRenderSettings& conversion_settings,
             const PwgRasterSettings& bitmap_settings,
             ResultCallback callback) override;

 private:
  scoped_refptr<PwgRasterConverterHelper> utility_client_;

  // Cancelable version of PwgRasterConverter::ResultCallback.
  base::CancelableOnceCallback<void(base::ReadOnlySharedMemoryRegion)>
      cancelable_callback_;
};

PwgRasterConverterImpl::PwgRasterConverterImpl() = default;

PwgRasterConverterImpl::~PwgRasterConverterImpl() = default;

void PwgRasterConverterImpl::Start(const std::optional<bool>& use_skia,
                                   const base::RefCountedMemory* data,
                                   const PdfRenderSettings& conversion_settings,
                                   const PwgRasterSettings& bitmap_settings,
                                   ResultCallback callback) {
  cancelable_callback_.Reset(std::move(callback));
  utility_client_ = base::MakeRefCounted<PwgRasterConverterHelper>(
      conversion_settings, bitmap_settings);
  utility_client_->Convert(use_skia, data, cancelable_callback_.callback());
}

}  // namespace

// static
std::unique_ptr<PwgRasterConverter> PwgRasterConverter::CreateDefault() {
  return std::make_unique<PwgRasterConverterImpl>();
}

// static
PdfRenderSettings PwgRasterConverter::GetConversionSettings(
    const cloud_devices::CloudDeviceDescription& printer_capabilities,
    const gfx::Size& page_size,
    bool use_color) {
  gfx::Size dpi = gfx::Size(kDefaultPdfDpi, kDefaultPdfDpi);
  cloud_devices::printer::DpiCapability dpis;
  if (dpis.LoadFrom(printer_capabilities))
    dpi = gfx::Size(dpis.GetDefault().horizontal, dpis.GetDefault().vertical);

  bool page_is_landscape =
      static_cast<double>(page_size.width()) / dpi.width() >
      static_cast<double>(page_size.height()) / dpi.height();

  // Pdfium assumes that page width is given in dpi.width(), and height in
  // dpi.height(). If we rotate the page, we need to also swap the DPIs.
  gfx::Size final_page_size = page_size;
  if (page_is_landscape) {
    final_page_size = gfx::Size(page_size.height(), page_size.width());
    dpi = gfx::Size(dpi.height(), dpi.width());
  }
  double scale_x = static_cast<double>(dpi.width()) / kPointsPerInch;
  double scale_y = static_cast<double>(dpi.height()) / kPointsPerInch;

  // Make vertical rectangle to optimize streaming to printer. Fix orientation
  // by autorotate.
  gfx::Rect area(final_page_size.width() * scale_x,
                 final_page_size.height() * scale_y);
  return PdfRenderSettings(area, gfx::Point(0, 0), dpi,
                           /*autorotate=*/true, use_color,
                           PdfRenderSettings::Mode::NORMAL);
}

// static
PwgRasterSettings PwgRasterConverter::GetBitmapSettings(
    const cloud_devices::CloudDeviceDescription& printer_capabilities,
    const cloud_devices::CloudDeviceDescription& ticket) {
  cloud_devices::printer::DuplexTicketItem duplex_item;
  cloud_devices::printer::DuplexType duplex_value =
      cloud_devices::printer::DuplexType::NO_DUPLEX;
  if (duplex_item.LoadFrom(ticket))
    duplex_value = duplex_item.value();

  // This assumes `ticket` contains a color ticket item. In case it does not, or
  // the color is invalid, `color_value` will default to AUTO_COLOR, which works
  // just fine. With AUTO_COLOR, it may be possible to better determine the
  // value for `use_color` based on `printer_capabilities`, rather than just
  // defaulting to the safe value of true. Parsing `printer_capabilities`
  // requires work, which this method is avoiding on purpose.
  cloud_devices::printer::Color color_value;
  cloud_devices::printer::ColorTicketItem color_item;
  if (color_item.LoadFrom(ticket) && color_item.IsValid())
    color_value = color_item.value();
  DCHECK(color_value.IsValid());
  bool use_color;
  switch (color_value.type) {
    case cloud_devices::printer::ColorType::STANDARD_MONOCHROME:
    case cloud_devices::printer::ColorType::CUSTOM_MONOCHROME:
      use_color = false;
      break;

    case cloud_devices::printer::ColorType::STANDARD_COLOR:
    case cloud_devices::printer::ColorType::CUSTOM_COLOR:
    case cloud_devices::printer::ColorType::AUTO_COLOR:
      use_color = true;
      break;

    default:
      NOTREACHED();
  }

  cloud_devices::printer::PwgRasterConfigCapability raster_capability;
  // If the raster capability fails to load, `raster_capability` will contain
  // the default value.
  raster_capability.LoadFrom(printer_capabilities);
  cloud_devices::printer::DocumentSheetBack document_sheet_back =
      raster_capability.value().document_sheet_back;

  PwgRasterSettings result;
  switch (duplex_value) {
    case cloud_devices::printer::DuplexType::NO_DUPLEX:
      result.duplex_mode = mojom::DuplexMode::kSimplex;
      result.odd_page_transform = TRANSFORM_NORMAL;
      break;
    case cloud_devices::printer::DuplexType::LONG_EDGE:
      result.duplex_mode = mojom::DuplexMode::kLongEdge;
      if (document_sheet_back ==
          cloud_devices::printer::DocumentSheetBack::ROTATED) {
        result.odd_page_transform = TRANSFORM_ROTATE_180;
      } else if (document_sheet_back ==
                 cloud_devices::printer::DocumentSheetBack::FLIPPED) {
        result.odd_page_transform = TRANSFORM_FLIP_VERTICAL;
      } else {
        result.odd_page_transform = TRANSFORM_NORMAL;
      }
      break;
    case cloud_devices::printer::DuplexType::SHORT_EDGE:
      result.duplex_mode = mojom::DuplexMode::kShortEdge;
      if (document_sheet_back ==
          cloud_devices::printer::DocumentSheetBack::MANUAL_TUMBLE) {
        result.odd_page_transform = TRANSFORM_ROTATE_180;
      } else if (document_sheet_back ==
                 cloud_devices::printer::DocumentSheetBack::FLIPPED) {
        result.odd_page_transform = TRANSFORM_FLIP_HORIZONTAL;
      } else {
        result.odd_page_transform = TRANSFORM_NORMAL;
      }
      break;
  }

  result.rotate_all_pages = raster_capability.value().rotate_all_pages;
  result.reverse_page_order = raster_capability.value().reverse_order_streaming;

  // No need to check for SRGB_8 support in `types`. CDD spec says:
  // "any printer that doesn't support SGRAY_8 must be able to perform
  // conversion from RGB to grayscale... "
  const auto& types = raster_capability.value().document_types_supported;
  result.use_color =
      use_color ||
      !std::ranges::contains(
          types, cloud_devices::printer::PwgDocumentTypeSupported::SGRAY_8);

  return result;
}

}  // namespace printing
