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

// Original code copyright 2014 Foxit Software Inc. http://www.foxitsoftware.com

#include "core/fpdfapi/edit/cpdf_npagetooneexporter.h"

#include <algorithm>
#include <memory>
#include <sstream>
#include <utility>

#include "constants/page_object.h"
#include "core/fpdfapi/edit/cpdf_contentstream_write_utils.h"
#include "core/fpdfapi/page/cpdf_page.h"
#include "core/fpdfapi/parser/cpdf_array.h"
#include "core/fpdfapi/parser/cpdf_dictionary.h"
#include "core/fpdfapi/parser/cpdf_document.h"
#include "core/fpdfapi/parser/cpdf_name.h"
#include "core/fpdfapi/parser/cpdf_number.h"
#include "core/fpdfapi/parser/cpdf_object.h"
#include "core/fpdfapi/parser/cpdf_reference.h"
#include "core/fpdfapi/parser/cpdf_stream.h"
#include "core/fpdfapi/parser/cpdf_stream_acc.h"
#include "core/fxcrt/check_op.h"
#include "core/fxcrt/fx_safe_types.h"
#include "core/fxcrt/fx_string_wrappers.h"
#include "core/fxcrt/span.h"

namespace {

// Calculates the N-up parameters.  When importing multiple pages into one page.
// The space of output page is evenly divided along the X axis and Y axis based
// on the input `pages_on_x_axis` and `pages_on_y_axis`.
class NupState {
 public:
  NupState(const CFX_SizeF& pagesize,
           size_t pages_on_x_axis,
           size_t pages_on_y_axis);

  // Calculate sub page origin and scale with the source page of `pagesize` and
  // new page of `sub_page_size_`.
  CPDF_NPageToOneExporter::NupPageSettings CalculateNewPagePosition(
      const CFX_SizeF& pagesize);

 private:
  // Helper function to get the `sub_x`, `sub_y` pair based on
  // `sub_page_index_`. The space of output page is evenly divided into slots
  // along x and y axis. `sub_x` and `sub_y` are 0-based indices that indicate
  // which allocation slot to use.
  std::pair<size_t, size_t> ConvertPageOrder() const;

  // Given the `sub_x` and `sub_y` subpage position within a page, and a source
  // page with dimensions of `pagesize`, calculate the sub page's origin and
  // scale.
  CPDF_NPageToOneExporter::NupPageSettings CalculatePageEdit(
      size_t sub_x,
      size_t sub_y,
      const CFX_SizeF& pagesize) const;

  const CFX_SizeF dest_page_size_;
  const size_t pages_on_x_axis_;
  const size_t pages_on_y_axis_;
  const size_t pages_per_sheet_;
  CFX_SizeF sub_page_size_;

  // A 0-based index, in range of [0, pages_per_sheet_ - 1).
  size_t sub_page_index_ = 0;
};

NupState::NupState(const CFX_SizeF& pagesize,
                   size_t pages_on_x_axis,
                   size_t pages_on_y_axis)
    : dest_page_size_(pagesize),
      pages_on_x_axis_(pages_on_x_axis),
      pages_on_y_axis_(pages_on_y_axis),
      pages_per_sheet_(pages_on_x_axis * pages_on_y_axis) {
  DCHECK_GT(pages_on_x_axis_, 0);
  DCHECK_GT(pages_on_y_axis_, 0);
  DCHECK_GT(dest_page_size_.width, 0);
  DCHECK_GT(dest_page_size_.height, 0);

  sub_page_size_.width = dest_page_size_.width / pages_on_x_axis_;
  sub_page_size_.height = dest_page_size_.height / pages_on_y_axis_;
}

std::pair<size_t, size_t> NupState::ConvertPageOrder() const {
  size_t sub_x = sub_page_index_ % pages_on_x_axis_;
  size_t sub_y = sub_page_index_ / pages_on_x_axis_;

  // Y Axis, pages start from the top of the output page.
  sub_y = pages_on_y_axis_ - sub_y - 1;

  return {sub_x, sub_y};
}

CPDF_NPageToOneExporter::NupPageSettings NupState::CalculatePageEdit(
    size_t sub_x,
    size_t sub_y,
    const CFX_SizeF& pagesize) const {
  CPDF_NPageToOneExporter::NupPageSettings settings;
  settings.sub_page_start_point.x = sub_x * sub_page_size_.width;
  settings.sub_page_start_point.y = sub_y * sub_page_size_.height;

  const float x_scale = sub_page_size_.width / pagesize.width;
  const float y_scale = sub_page_size_.height / pagesize.height;
  settings.scale = std::min(x_scale, y_scale);

  float sub_width = pagesize.width * settings.scale;
  float sub_height = pagesize.height * settings.scale;
  if (x_scale > y_scale) {
    settings.sub_page_start_point.x += (sub_page_size_.width - sub_width) / 2;
  } else {
    settings.sub_page_start_point.y += (sub_page_size_.height - sub_height) / 2;
  }
  return settings;
}

CPDF_NPageToOneExporter::NupPageSettings NupState::CalculateNewPagePosition(
    const CFX_SizeF& pagesize) {
  if (sub_page_index_ >= pages_per_sheet_) {
    sub_page_index_ = 0;
  }

  auto [sub_x, sub_y] = ConvertPageOrder();
  ++sub_page_index_;
  return CalculatePageEdit(sub_x, sub_y, pagesize);
}

// Helper that generates the content stream for a sub-page.
ByteString GenerateSubPageContentStream(
    const ByteString& xobject_name,
    const CPDF_NPageToOneExporter::NupPageSettings& settings) {
  CFX_Matrix matrix;
  matrix.Scale(settings.scale, settings.scale);
  matrix.Translate(settings.sub_page_start_point.x,
                   settings.sub_page_start_point.y);

  fxcrt::ostringstream content_stream;
  content_stream << "q\n";
  WriteMatrix(content_stream, matrix) << " cm\n"
                                      << "/" << xobject_name << " Do Q\n";
  return ByteString(content_stream);
}

}  // namespace

CPDF_NPageToOneExporter::CPDF_NPageToOneExporter(CPDF_Document* dest_doc,
                                                 CPDF_Document* src_doc)
    : CPDF_PageOrganizer(dest_doc, src_doc) {}

CPDF_NPageToOneExporter::~CPDF_NPageToOneExporter() = default;

bool CPDF_NPageToOneExporter::ExportNPagesToOne(
    pdfium::span<const uint32_t> page_indices,
    const CFX_SizeF& dest_page_size,
    size_t pages_on_x_axis,
    size_t pages_on_y_axis) {
  if (!Init()) {
    return false;
  }

  FX_SAFE_SIZE_T safe_pages_per_sheet = pages_on_x_axis;
  safe_pages_per_sheet *= pages_on_y_axis;
  if (!safe_pages_per_sheet.IsValid()) {
    return false;
  }

  ClearObjectNumberMap();
  src_page_xobject_map_.clear();
  size_t pages_per_sheet = safe_pages_per_sheet.ValueOrDie();
  NupState n_up_state(dest_page_size, pages_on_x_axis, pages_on_y_axis);

  FX_SAFE_INT32 curpage = 0;
  const CFX_FloatRect dest_page_rect(0, 0, dest_page_size.width,
                                     dest_page_size.height);
  for (size_t outer_page_index = 0; outer_page_index < page_indices.size();
       outer_page_index += pages_per_sheet) {
    xobject_name_to_number_map_.clear();

    RetainPtr<CPDF_Dictionary> dest_page_dict =
        dest()->CreateNewPage(curpage.ValueOrDie());
    if (!dest_page_dict) {
      return false;
    }

    dest_page_dict->SetRectFor(pdfium::page_object::kMediaBox, dest_page_rect);
    ByteString content;
    size_t inner_page_max =
        std::min(outer_page_index + pages_per_sheet, page_indices.size());
    for (size_t i = outer_page_index; i < inner_page_max; ++i) {
      RetainPtr<CPDF_Dictionary> src_page_dict =
          src()->GetMutablePageDictionary(page_indices[i]);
      if (!src_page_dict) {
        return false;
      }

      auto src_page = pdfium::MakeRetain<CPDF_Page>(src(), src_page_dict);
      src_page->AddPageImageCache();
      NupPageSettings settings =
          n_up_state.CalculateNewPagePosition(src_page->GetPageSize());
      content += AddSubPage(src_page, settings);
    }

    FinishPage(dest_page_dict, content);
    ++curpage;
  }

  return true;
}

// static
ByteString CPDF_NPageToOneExporter::GenerateSubPageContentStreamForTesting(
    const ByteString& xobject_name,
    const NupPageSettings& settings) {
  return GenerateSubPageContentStream(xobject_name, settings);
}

ByteString CPDF_NPageToOneExporter::AddSubPage(
    const RetainPtr<CPDF_Page>& src_page,
    const NupPageSettings& settings) {
  uint32_t src_page_obj_num = src_page->GetDict()->GetObjNum();
  const auto it = src_page_xobject_map_.find(src_page_obj_num);
  ByteString xobject_name = it != src_page_xobject_map_.end()
                                ? it->second
                                : MakeXObjectFromPage(src_page);
  return GenerateSubPageContentStream(xobject_name, settings);
}

RetainPtr<CPDF_Stream> CPDF_NPageToOneExporter::MakeXObjectFromPageRaw(
    RetainPtr<CPDF_Page> src_page) {
  RetainPtr<const CPDF_Dictionary> src_page_dict = src_page->GetDict();
  RetainPtr<const CPDF_Object> src_contents =
      src_page_dict->GetDirectObjectFor(pdfium::page_object::kContents);

  auto new_xobject =
      dest()->NewIndirect<CPDF_Stream>(dest()->New<CPDF_Dictionary>());
  RetainPtr<CPDF_Dictionary> new_xobject_dict = new_xobject->GetMutableDict();
  static const char kResourceString[] = "Resources";
  if (!CPDF_PageOrganizer::CopyInheritable(new_xobject_dict, src_page_dict,
                                           kResourceString)) {
    // Use a default empty resources if it does not exist.
    new_xobject_dict->SetNewFor<CPDF_Dictionary>(kResourceString);
  }
  uint32_t src_page_obj_num = src_page_dict->GetObjNum();
  uint32_t new_xobject_obj_num = new_xobject_dict->GetObjNum();
  AddObjectMapping(src_page_obj_num, new_xobject_obj_num);
  UpdateReference(new_xobject_dict);
  new_xobject_dict->SetNewFor<CPDF_Name>("Type", "XObject");
  new_xobject_dict->SetNewFor<CPDF_Name>("Subtype", "Form");
  new_xobject_dict->SetNewFor<CPDF_Number>("FormType", 1);
  new_xobject_dict->SetRectFor("BBox", src_page->GetBBox());
  new_xobject_dict->SetMatrixFor("Matrix", src_page->GetPageMatrix());
  if (!src_contents) {
    return new_xobject;
  }
  const CPDF_Array* src_contents_array = src_contents->AsArray();
  if (!src_contents_array) {
    RetainPtr<const CPDF_Stream> stream(src_contents->AsStream());
    auto acc = pdfium::MakeRetain<CPDF_StreamAcc>(std::move(stream));
    acc->LoadAllDataFiltered();
    new_xobject->SetDataAndRemoveFilter(acc->GetSpan());
    return new_xobject;
  }
  ByteString src_content_stream;
  for (size_t i = 0; i < src_contents_array->size(); ++i) {
    RetainPtr<const CPDF_Stream> stream = src_contents_array->GetStreamAt(i);
    auto acc = pdfium::MakeRetain<CPDF_StreamAcc>(std::move(stream));
    acc->LoadAllDataFiltered();
    src_content_stream += ByteStringView(acc->GetSpan());
    src_content_stream += "\n";
  }
  new_xobject->SetDataAndRemoveFilter(src_content_stream.unsigned_span());
  return new_xobject;
}

ByteString CPDF_NPageToOneExporter::MakeXObjectFromPage(
    RetainPtr<CPDF_Page> src_page) {
  RetainPtr<CPDF_Stream> new_xobject = MakeXObjectFromPageRaw(src_page);

  // TODO(xlou): A better name schema to avoid possible object name collision.
  ByteString xobject_name = ByteString::Format("X%d", ++object_number_);
  xobject_name_to_number_map_[xobject_name] = new_xobject->GetObjNum();
  src_page_xobject_map_[src_page->GetDict()->GetObjNum()] = xobject_name;
  return xobject_name;
}

std::unique_ptr<XObjectContext>
CPDF_NPageToOneExporter::CreateXObjectContextFromPage(int src_page_index) {
  RetainPtr<CPDF_Dictionary> src_page_dict =
      src()->GetMutablePageDictionary(src_page_index);
  if (!src_page_dict) {
    return nullptr;
  }

  auto src_page = pdfium::MakeRetain<CPDF_Page>(src(), src_page_dict);
  auto xobject = std::make_unique<XObjectContext>();
  xobject->dest_doc = dest();
  xobject->xobject.Reset(MakeXObjectFromPageRaw(src_page));
  return xobject;
}

void CPDF_NPageToOneExporter::FinishPage(
    RetainPtr<CPDF_Dictionary> dest_page_dict,
    const ByteString& content) {
  RetainPtr<CPDF_Dictionary> resources =
      dest_page_dict->GetOrCreateDictFor(pdfium::page_object::kResources);
  RetainPtr<CPDF_Dictionary> xobject = resources->GetOrCreateDictFor("XObject");
  for (auto& it : xobject_name_to_number_map_) {
    xobject->SetNewFor<CPDF_Reference>(it.first, dest(), it.second);
  }

  auto stream =
      dest()->NewIndirect<CPDF_Stream>(dest()->New<CPDF_Dictionary>());
  stream->SetData(content.unsigned_span());
  dest_page_dict->SetNewFor<CPDF_Reference>(pdfium::page_object::kContents,
                                            dest(), stream->GetObjNum());
}

XObjectContext::XObjectContext() = default;

XObjectContext::~XObjectContext() = default;
