// 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_pageorganizer.h"

#include <set>
#include <utility>
#include <vector>

#include "constants/catalog.h"
#include "constants/page_object.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_string.h"
#include "core/fxcrt/bytestring.h"
#include "core/fxcrt/check.h"

CPDF_PageOrganizer::CPDF_PageOrganizer(CPDF_Document* dest_doc,
                                       CPDF_Document* src_doc)
    : dest_doc_(dest_doc), src_doc_(src_doc) {}

CPDF_PageOrganizer::~CPDF_PageOrganizer() = default;

bool CPDF_PageOrganizer::Init() {
  DCHECK(dest_doc_);
  DCHECK(src_doc_);

  return InitDestDoc();
}

bool CPDF_PageOrganizer::InitDestDoc() {
  RetainPtr<CPDF_Dictionary> root = dest()->GetMutableRoot();
  if (!root) {
    return false;
  }

  RetainPtr<CPDF_Dictionary> info = dest()->GetInfo();
  if (info) {
    info->SetNewFor<CPDF_String>("Producer", "PDFium");
  }

  if (root->GetByteStringFor("Type", ByteStringView()).IsEmpty()) {
    root->SetNewFor<CPDF_Name>("Type", "Catalog");
  }

  RetainPtr<CPDF_Dictionary> pages;
  if (RetainPtr<CPDF_Object> current_pages =
          root->GetMutableObjectFor(pdfium::catalog::kPages);
      current_pages) {
    pages = ToDictionary(current_pages->GetMutableDirect());
  }
  if (!pages) {
    pages = dest()->NewIndirect<CPDF_Dictionary>();
    root->SetNewFor<CPDF_Reference>(pdfium::catalog::kPages, dest(),
                                    pages->GetObjNum());
  }
  if (pages->GetByteStringFor("Type", ByteStringView()).IsEmpty()) {
    pages->SetNewFor<CPDF_Name>("Type", "Pages");
  }

  if (!pages->GetArrayFor("Kids")) {
    auto kids_array = dest()->NewIndirect<CPDF_Array>();
    pages->SetNewFor<CPDF_Number>("Count", 0);
    pages->SetNewFor<CPDF_Reference>("Kids", dest(), kids_array->GetObjNum());
  }
  return true;
}

bool CPDF_PageOrganizer::UpdateReference(RetainPtr<CPDF_Object> obj) {
  switch (obj->GetType()) {
    case CPDF_Object::kReference: {
      CPDF_Reference* reference = obj->AsMutableReference();
      uint32_t newobjnum = GetNewObjId(reference);
      if (newobjnum == 0) {
        return false;
      }
      reference->SetRef(dest(), newobjnum);
      return true;
    }
    case CPDF_Object::kDictionary: {
      CPDF_Dictionary* dict = obj->AsMutableDictionary();
      std::vector<ByteString> bad_keys;
      {
        CPDF_DictionaryLocker locker(dict);
        for (const auto& it : locker) {
          const ByteString& key = it.first;
          if (key == "Parent" || key == "Prev" || key == "First") {
            continue;
          }
          RetainPtr<CPDF_Object> next_obj = it.second;
          if (!UpdateReference(next_obj)) {
            bad_keys.push_back(key);
          }
        }
      }
      for (const auto& key : bad_keys) {
        dict->RemoveFor(key.AsStringView());
      }
      return true;
    }
    case CPDF_Object::kArray: {
      CPDF_Array* array = obj->AsMutableArray();
      for (size_t i = 0; i < array->size(); ++i) {
        if (!UpdateReference(array->GetMutableObjectAt(i))) {
          return false;
        }
      }
      return true;
    }
    case CPDF_Object::kStream: {
      return UpdateReference(obj->AsMutableStream()->GetMutableDict());
    }
    default:
      return true;
  }
}

uint32_t CPDF_PageOrganizer::GetNewObjId(CPDF_Reference* ref) {
  if (!ref) {
    return 0;
  }

  uint32_t obj_num = ref->GetRefObjNum();
  uint32_t new_obj_num = 0;
  const auto it = object_number_map_.find(obj_num);
  if (it != object_number_map_.end()) {
    new_obj_num = it->second;
  }
  if (new_obj_num) {
    return new_obj_num;
  }

  RetainPtr<const CPDF_Object> direct = ref->GetDirect();
  if (!direct) {
    return 0;
  }

  RetainPtr<CPDF_Object> clone = direct->Clone();
  const CPDF_Dictionary* dict_clone = clone->AsDictionary();
  if (dict_clone && dict_clone->KeyExist("Type")) {
    ByteString type = dict_clone->GetByteStringFor("Type");
    if (type.EqualNoCase("Pages")) {
      return 4;
    }
    if (type.EqualNoCase("Page")) {
      return 0;
    }
  }

  new_obj_num = dest()->AddIndirectObject(clone);
  AddObjectMapping(obj_num, new_obj_num);
  if (!UpdateReference(std::move(clone))) {
    return 0;
  }

  return new_obj_num;
}

// static
bool CPDF_PageOrganizer::CopyInheritable(
    RetainPtr<CPDF_Dictionary> dest_page_dict,
    RetainPtr<const CPDF_Dictionary> src_page_dict,
    ByteStringView key) {
  if (dest_page_dict->KeyExist(key)) {
    return true;
  }

  RetainPtr<const CPDF_Object> inheritable =
      PageDictGetInheritableTag(std::move(src_page_dict), key);
  if (!inheritable) {
    return false;
  }

  dest_page_dict->SetFor(ByteString(key), inheritable->Clone());
  return true;
}

// static
RetainPtr<const CPDF_Object> CPDF_PageOrganizer::PageDictGetInheritableTag(
    RetainPtr<const CPDF_Dictionary> dict,
    ByteStringView src_tag) {
  if (!dict || src_tag.IsEmpty()) {
    return nullptr;
  }
  if (!dict->KeyExist(pdfium::page_object::kParent) ||
      !dict->KeyExist(pdfium::page_object::kType)) {
    return nullptr;
  }

  RetainPtr<const CPDF_Name> name =
      ToName(dict->GetObjectFor(pdfium::page_object::kType)->GetDirect());
  if (!name || name->GetString() != "Page") {
    return nullptr;
  }

  RetainPtr<const CPDF_Dictionary> pp = ToDictionary(
      dict->GetObjectFor(pdfium::page_object::kParent)->GetDirect());
  if (!pp) {
    return nullptr;
  }

  if (dict->KeyExist(src_tag)) {
    return dict->GetObjectFor(src_tag);
  }

  std::set<const CPDF_Dictionary*> visited_parent_dicts;
  while (pp) {
    if (!visited_parent_dicts.insert(pp.Get()).second) {
      return nullptr;
    }

    if (pp->KeyExist(src_tag)) {
      return pp->GetObjectFor(src_tag);
    }
    if (!pp->KeyExist(pdfium::page_object::kParent)) {
      break;
    }

    pp = ToDictionary(
        pp->GetObjectFor(pdfium::page_object::kParent)->GetDirect());
  }
  return nullptr;
}
