// Copyright 2014 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 "public/fpdf_transformpage.h"

#include <memory>
#include <sstream>

#include "constants/page_object.h"
#include "core/fpdfapi/edit/cpdf_contentstream_write_utils.h"
#include "core/fpdfapi/page/cpdf_clippath.h"
#include "core/fpdfapi/page/cpdf_page.h"
#include "core/fpdfapi/page/cpdf_pageobject.h"
#include "core/fpdfapi/page/cpdf_path.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_number.h"
#include "core/fpdfapi/parser/cpdf_reference.h"
#include "core/fpdfapi/parser/cpdf_stream.h"
#include "core/fxcrt/fx_string_wrappers.h"
#include "core/fxcrt/numerics/safe_conversions.h"
#include "core/fxcrt/span.h"
#include "core/fxcrt/stl_util.h"
#include "core/fxge/cfx_fillrenderoptions.h"
#include "core/fxge/cfx_path.h"
#include "fpdfsdk/cpdfsdk_helpers.h"

namespace {

void SetBoundingBox(CPDF_Page* page,
                    const ByteString& key,
                    const CFX_FloatRect& rect) {
  if (!page) {
    return;
  }

  page->GetMutableDict()->SetRectFor(key, rect);
  page->UpdateDimensions();
}

bool GetBoundingBox(const CPDF_Page* page,
                    ByteStringView key,
                    float* left,
                    float* bottom,
                    float* right,
                    float* top) {
  if (!page || !left || !bottom || !right || !top) {
    return false;
  }

  RetainPtr<const CPDF_Array> pArray = page->GetDict()->GetArrayFor(key);
  if (!pArray) {
    return false;
  }

  *left = pArray->GetFloatAt(0);
  *bottom = pArray->GetFloatAt(1);
  *right = pArray->GetFloatAt(2);
  *top = pArray->GetFloatAt(3);
  return true;
}

RetainPtr<CPDF_Object> GetPageContent(CPDF_Dictionary* pPageDict) {
  return pPageDict->GetMutableDirectObjectFor(pdfium::page_object::kContents);
}

void OutputPath(fxcrt::ostringstream& buf, CPDF_Path path) {
  const CFX_Path* pPath = path.GetObject();
  if (!pPath) {
    return;
  }

  pdfium::span<const CFX_Path::Point> points = pPath->GetPoints();
  if (path.IsRect()) {
    CFX_PointF diff = points[2].point_ - points[0].point_;
    buf << points[0].point_.x << " " << points[0].point_.y << " " << diff.x
        << " " << diff.y << " re\n";
    return;
  }

  for (size_t i = 0; i < points.size(); ++i) {
    buf << points[i].point_.x << " " << points[i].point_.y;
    CFX_Path::Point::Type point_type = points[i].type_;
    if (point_type == CFX_Path::Point::Type::kMove) {
      buf << " m\n";
    } else if (point_type == CFX_Path::Point::Type::kBezier) {
      buf << " " << points[i + 1].point_.x << " " << points[i + 1].point_.y
          << " " << points[i + 2].point_.x << " " << points[i + 2].point_.y;
      buf << " c";
      if (points[i + 2].close_figure_) {
        buf << " h";
      }
      buf << "\n";

      i += 2;
    } else if (point_type == CFX_Path::Point::Type::kLine) {
      buf << " l";
      if (points[i].close_figure_) {
        buf << " h";
      }
      buf << "\n";
    }
  }
}

}  // namespace

FPDF_EXPORT void FPDF_CALLCONV FPDFPage_SetMediaBox(FPDF_PAGE page,
                                                    float left,
                                                    float bottom,
                                                    float right,
                                                    float top) {
  SetBoundingBox(CPDFPageFromFPDFPage(page), pdfium::page_object::kMediaBox,
                 CFX_FloatRect(left, bottom, right, top));
}

FPDF_EXPORT void FPDF_CALLCONV FPDFPage_SetCropBox(FPDF_PAGE page,
                                                   float left,
                                                   float bottom,
                                                   float right,
                                                   float top) {
  SetBoundingBox(CPDFPageFromFPDFPage(page), pdfium::page_object::kCropBox,
                 CFX_FloatRect(left, bottom, right, top));
}

FPDF_EXPORT void FPDF_CALLCONV FPDFPage_SetBleedBox(FPDF_PAGE page,
                                                    float left,
                                                    float bottom,
                                                    float right,
                                                    float top) {
  SetBoundingBox(CPDFPageFromFPDFPage(page), pdfium::page_object::kBleedBox,
                 CFX_FloatRect(left, bottom, right, top));
}

FPDF_EXPORT void FPDF_CALLCONV FPDFPage_SetTrimBox(FPDF_PAGE page,
                                                   float left,
                                                   float bottom,
                                                   float right,
                                                   float top) {
  SetBoundingBox(CPDFPageFromFPDFPage(page), pdfium::page_object::kTrimBox,
                 CFX_FloatRect(left, bottom, right, top));
}

FPDF_EXPORT void FPDF_CALLCONV FPDFPage_SetArtBox(FPDF_PAGE page,
                                                  float left,
                                                  float bottom,
                                                  float right,
                                                  float top) {
  SetBoundingBox(CPDFPageFromFPDFPage(page), pdfium::page_object::kArtBox,
                 CFX_FloatRect(left, bottom, right, top));
}

FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV FPDFPage_GetMediaBox(FPDF_PAGE page,
                                                         float* left,
                                                         float* bottom,
                                                         float* right,
                                                         float* top) {
  return GetBoundingBox(CPDFPageFromFPDFPage(page),
                        pdfium::page_object::kMediaBox, left, bottom, right,
                        top);
}

FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV FPDFPage_GetCropBox(FPDF_PAGE page,
                                                        float* left,
                                                        float* bottom,
                                                        float* right,
                                                        float* top) {
  return GetBoundingBox(CPDFPageFromFPDFPage(page),
                        pdfium::page_object::kCropBox, left, bottom, right,
                        top);
}

FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV FPDFPage_GetBleedBox(FPDF_PAGE page,
                                                         float* left,
                                                         float* bottom,
                                                         float* right,
                                                         float* top) {
  return GetBoundingBox(CPDFPageFromFPDFPage(page),
                        pdfium::page_object::kBleedBox, left, bottom, right,
                        top);
}

FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV FPDFPage_GetTrimBox(FPDF_PAGE page,
                                                        float* left,
                                                        float* bottom,
                                                        float* right,
                                                        float* top) {
  return GetBoundingBox(CPDFPageFromFPDFPage(page),
                        pdfium::page_object::kTrimBox, left, bottom, right,
                        top);
}

FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV FPDFPage_GetArtBox(FPDF_PAGE page,
                                                       float* left,
                                                       float* bottom,
                                                       float* right,
                                                       float* top) {
  return GetBoundingBox(CPDFPageFromFPDFPage(page),
                        pdfium::page_object::kArtBox, left, bottom, right, top);
}

FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV
FPDFPage_TransFormWithClip(FPDF_PAGE page,
                           const FS_MATRIX* matrix,
                           const FS_RECTF* clipRect) {
  if (!matrix && !clipRect) {
    return false;
  }

  CPDF_Page* pPage = CPDFPageFromFPDFPage(page);
  if (!pPage) {
    return false;
  }

  RetainPtr<CPDF_Dictionary> pPageDict = pPage->GetMutableDict();
  RetainPtr<CPDF_Object> pContentObj = GetPageContent(pPageDict.Get());
  if (!pContentObj) {
    return false;
  }

  CPDF_Document* doc = pPage->GetDocument();
  if (!doc) {
    return false;
  }

  fxcrt::ostringstream text_buf;
  text_buf << "q ";

  if (clipRect) {
    CFX_FloatRect rect = CFXFloatRectFromFSRectF(*clipRect);
    rect.Normalize();
    WriteRect(text_buf, rect) << " re W* n ";
  }
  if (matrix) {
    WriteMatrix(text_buf, CFXMatrixFromFSMatrix(*matrix)) << " cm ";
  }

  auto pStream = doc->NewIndirect<CPDF_Stream>(doc->New<CPDF_Dictionary>());
  pStream->SetDataFromStringstream(&text_buf);

  auto pEndStream = doc->NewIndirect<CPDF_Stream>(doc->New<CPDF_Dictionary>());
  pEndStream->SetData(ByteStringView(" Q").unsigned_span());

  RetainPtr<CPDF_Array> pContentArray = ToArray(pContentObj);
  if (pContentArray) {
    pContentArray->InsertNewAt<CPDF_Reference>(0, doc, pStream->GetObjNum());
    pContentArray->AppendNew<CPDF_Reference>(doc, pEndStream->GetObjNum());
  } else if (pContentObj->IsStream() && !pContentObj->IsInline()) {
    pContentArray = doc->NewIndirect<CPDF_Array>();
    pContentArray->AppendNew<CPDF_Reference>(doc, pStream->GetObjNum());
    pContentArray->AppendNew<CPDF_Reference>(doc, pContentObj->GetObjNum());
    pContentArray->AppendNew<CPDF_Reference>(doc, pEndStream->GetObjNum());
    pPageDict->SetNewFor<CPDF_Reference>(pdfium::page_object::kContents, doc,
                                         pContentArray->GetObjNum());
  }

  // Need to transform the patterns as well.
  RetainPtr<const CPDF_Dictionary> pRes =
      pPageDict->GetDictFor(pdfium::page_object::kResources);
  if (!pRes) {
    return true;
  }

  RetainPtr<const CPDF_Dictionary> pPatternDict = pRes->GetDictFor("Pattern");
  if (!pPatternDict) {
    return true;
  }

  CPDF_DictionaryLocker locker(pPatternDict);
  for (const auto& it : locker) {
    RetainPtr<CPDF_Object> pObj = it.second;
    if (pObj->IsReference()) {
      pObj = pObj->GetMutableDirect();
    }

    RetainPtr<CPDF_Dictionary> dict;
    if (pObj->IsDictionary()) {
      dict.Reset(pObj->AsMutableDictionary());
    } else if (CPDF_Stream* pObjStream = pObj->AsMutableStream()) {
      dict = pObjStream->GetMutableDict();
    } else {
      continue;
    }

    if (matrix) {
      CFX_Matrix m = CFXMatrixFromFSMatrix(*matrix);
      dict->SetMatrixFor("Matrix", dict->GetMatrixFor("Matrix") * m);
    }
  }

  return true;
}

FPDF_EXPORT void FPDF_CALLCONV
FPDFPageObj_TransformClipPath(FPDF_PAGEOBJECT page_object,
                              double a,
                              double b,
                              double c,
                              double d,
                              double e,
                              double f) {
  CPDF_PageObject* pPageObj = CPDFPageObjectFromFPDFPageObject(page_object);
  if (!pPageObj) {
    return;
  }

  CFX_Matrix matrix((float)a, (float)b, (float)c, (float)d, (float)e, (float)f);

  // Special treatment to shading object, because the ClipPath for shading
  // object is already transformed.
  if (!pPageObj->IsShading()) {
    pPageObj->TransformClipPath(matrix);
  }
}

FPDF_EXPORT FPDF_CLIPPATH FPDF_CALLCONV
FPDFPageObj_GetClipPath(FPDF_PAGEOBJECT page_object) {
  CPDF_PageObject* pPageObj = CPDFPageObjectFromFPDFPageObject(page_object);
  if (!pPageObj) {
    return nullptr;
  }

  return FPDFClipPathFromCPDFClipPath(&pPageObj->mutable_clip_path());
}

FPDF_EXPORT int FPDF_CALLCONV FPDFClipPath_CountPaths(FPDF_CLIPPATH clip_path) {
  CPDF_ClipPath* pClipPath = CPDFClipPathFromFPDFClipPath(clip_path);
  if (!pClipPath || !pClipPath->HasRef()) {
    return -1;
  }

  return pdfium::checked_cast<int>(pClipPath->GetPathCount());
}

FPDF_EXPORT int FPDF_CALLCONV
FPDFClipPath_CountPathSegments(FPDF_CLIPPATH clip_path, int path_index) {
  CPDF_ClipPath* pClipPath = CPDFClipPathFromFPDFClipPath(clip_path);
  if (!pClipPath || !pClipPath->HasRef()) {
    return -1;
  }

  if (path_index < 0 ||
      static_cast<size_t>(path_index) >= pClipPath->GetPathCount()) {
    return -1;
  }

  return fxcrt::CollectionSize<int>(pClipPath->GetPath(path_index).GetPoints());
}

FPDF_EXPORT FPDF_PATHSEGMENT FPDF_CALLCONV
FPDFClipPath_GetPathSegment(FPDF_CLIPPATH clip_path,
                            int path_index,
                            int segment_index) {
  CPDF_ClipPath* pClipPath = CPDFClipPathFromFPDFClipPath(clip_path);
  if (!pClipPath || !pClipPath->HasRef()) {
    return nullptr;
  }

  if (path_index < 0 ||
      static_cast<size_t>(path_index) >= pClipPath->GetPathCount()) {
    return nullptr;
  }

  pdfium::span<const CFX_Path::Point> points =
      pClipPath->GetPath(path_index).GetPoints();
  if (!fxcrt::IndexInBounds(points, segment_index)) {
    return nullptr;
  }

  return FPDFPathSegmentFromFXPathPoint(&points[segment_index]);
}

FPDF_EXPORT FPDF_CLIPPATH FPDF_CALLCONV FPDF_CreateClipPath(float left,
                                                            float bottom,
                                                            float right,
                                                            float top) {
  CPDF_Path Path;
  Path.AppendRect(left, bottom, right, top);

  auto pNewClipPath = std::make_unique<CPDF_ClipPath>();
  pNewClipPath->AppendPath(Path, CFX_FillRenderOptions::FillType::kEvenOdd);

  // Caller takes ownership.
  return FPDFClipPathFromCPDFClipPath(pNewClipPath.release());
}

FPDF_EXPORT void FPDF_CALLCONV FPDF_DestroyClipPath(FPDF_CLIPPATH clipPath) {
  // Take ownership back from caller and destroy.
  std::unique_ptr<CPDF_ClipPath>(CPDFClipPathFromFPDFClipPath(clipPath));
}

FPDF_EXPORT void FPDF_CALLCONV FPDFPage_InsertClipPath(FPDF_PAGE page,
                                                       FPDF_CLIPPATH clipPath) {
  CPDF_Page* pPage = CPDFPageFromFPDFPage(page);
  if (!pPage) {
    return;
  }

  RetainPtr<CPDF_Dictionary> pPageDict = pPage->GetMutableDict();
  RetainPtr<CPDF_Object> pContentObj = GetPageContent(pPageDict.Get());
  if (!pContentObj) {
    return;
  }

  fxcrt::ostringstream strClip;
  CPDF_ClipPath* pClipPath = CPDFClipPathFromFPDFClipPath(clipPath);
  for (size_t i = 0; i < pClipPath->GetPathCount(); ++i) {
    CPDF_Path path = pClipPath->GetPath(i);
    if (path.GetPoints().empty()) {
      // Empty clipping (totally clipped out)
      strClip << "0 0 m W n ";
    } else {
      OutputPath(strClip, path);
      if (pClipPath->GetClipType(i) ==
          CFX_FillRenderOptions::FillType::kWinding) {
        strClip << "W n\n";
      } else {
        strClip << "W* n\n";
      }
    }
  }
  CPDF_Document* doc = pPage->GetDocument();
  if (!doc) {
    return;
  }

  auto pStream = doc->NewIndirect<CPDF_Stream>(doc->New<CPDF_Dictionary>());
  pStream->SetDataFromStringstream(&strClip);

  RetainPtr<CPDF_Array> pArray = ToArray(pContentObj);
  if (pArray) {
    pArray->InsertNewAt<CPDF_Reference>(0, doc, pStream->GetObjNum());
  } else if (pContentObj->IsStream() && !pContentObj->IsInline()) {
    auto pContentArray = doc->NewIndirect<CPDF_Array>();
    pContentArray->AppendNew<CPDF_Reference>(doc, pStream->GetObjNum());
    pContentArray->AppendNew<CPDF_Reference>(doc, pContentObj->GetObjNum());
    pPageDict->SetNewFor<CPDF_Reference>(pdfium::page_object::kContents, doc,
                                         pContentArray->GetObjNum());
  }
}
