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

#include "printing/common/metafile_utils.h"

#include "base/strings/stringprintf.h"
#include "base/time/time.h"
#include "printing/buildflags/buildflags.h"
#include "third_party/skia/include/core/SkCanvas.h"
#include "third_party/skia/include/core/SkPicture.h"
#include "third_party/skia/include/core/SkPictureRecorder.h"
#include "third_party/skia/include/core/SkTime.h"
#include "third_party/skia/include/docs/SkPDFDocument.h"
#include "ui/accessibility/ax_node.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/accessibility/ax_role_properties.h"
#include "ui/accessibility/ax_tree.h"
#include "ui/accessibility/ax_tree_update.h"

namespace {

#if BUILDFLAG(ENABLE_TAGGED_PDF)

// Table 333 in PDF 32000-1:2008 spec, section 14.8.4.2
const char kPDFStructureTypeDocument[] = "Document";
const char kPDFStructureTypeParagraph[] = "P";
const char kPDFStructureTypeDiv[] = "Div";
const char kPDFStructureTypeHeading[] = "H";
const char kPDFStructureTypeLink[] = "Link";
const char kPDFStructureTypeList[] = "L";
const char kPDFStructureTypeListItemLabel[] = "Lbl";
const char kPDFStructureTypeListItemBody[] = "LI";
const char kPDFStructureTypeTable[] = "Table";
const char kPDFStructureTypeTableRow[] = "TR";
const char kPDFStructureTypeTableHeader[] = "TH";
const char kPDFStructureTypeTableCell[] = "TD";
const char kPDFStructureTypeFigure[] = "Figure";
const char kPDFStructureTypeNonStruct[] = "NonStruct";

// Standard attribute owners from PDF 32000-1:2008 spec, section 14.8.5.2
// (Attribute owners are kind of like "categories" for structure node
// attributes.)
const char kPDFTableAttributeOwner[] = "Table";

// Table Attributes from PDF 32000-1:2008 spec, section 14.8.5.7
const char kPDFTableCellColSpanAttribute[] = "ColSpan";
const char kPDFTableCellHeadersAttribute[] = "Headers";
const char kPDFTableCellRowSpanAttribute[] = "RowSpan";
const char kPDFTableHeaderScopeAttribute[] = "Scope";
const char kPDFTableHeaderScopeColumn[] = "Column";
const char kPDFTableHeaderScopeRow[] = "Row";

SkString GetHeadingStructureType(int heading_level) {
  // From Table 333 in PDF 32000-1:2008 spec, section 14.8.4.2,
  // "H1"..."H6" are valid structure types.
  if (heading_level >= 1 && heading_level <= 6)
    return SkString(base::StringPrintf("H%d", heading_level).c_str());

  // If we don't have a valid heading level, use the generic heading role.
  return SkString(kPDFStructureTypeHeading);
}

#endif  // BUILDFLAG(ENABLE_TAGGED_PDF)

SkTime::DateTime TimeToSkTime(base::Time time) {
  base::Time::Exploded exploded;
  time.UTCExplode(&exploded);
  SkTime::DateTime skdate;
  skdate.fTimeZoneMinutes = 0;
  skdate.fYear = exploded.year;
  skdate.fMonth = exploded.month;
  skdate.fDayOfWeek = exploded.day_of_week;
  skdate.fDay = exploded.day_of_month;
  skdate.fHour = exploded.hour;
  skdate.fMinute = exploded.minute;
  skdate.fSecond = exploded.second;
  return skdate;
}

sk_sp<SkPicture> GetEmptyPicture() {
  SkPictureRecorder rec;
  SkCanvas* canvas = rec.beginRecording(100, 100);
  // Add some ops whose net effects equal to a noop.
  canvas->save();
  canvas->restore();
  return rec.finishRecordingAsPicture();
}

// Convert an AXNode into a SkPDF::StructureElementNode in order to make a
// tagged (accessible) PDF. Returns true on success and false if we don't
// have enough data to build a valid tree.
bool RecursiveBuildStructureTree(const ui::AXNode* ax_node,
                                 SkPDF::StructureElementNode* tag) {
#if BUILDFLAG(ENABLE_TAGGED_PDF)
  bool valid = false;

  tag->fNodeId = ax_node->GetIntAttribute(ax::mojom::IntAttribute::kDOMNodeId);
  switch (ax_node->data().role) {
    case ax::mojom::Role::kRootWebArea:
      tag->fTypeString = kPDFStructureTypeDocument;
      break;
    case ax::mojom::Role::kParagraph:
      tag->fTypeString = kPDFStructureTypeParagraph;
      break;
    case ax::mojom::Role::kGenericContainer:
      tag->fTypeString = kPDFStructureTypeDiv;
      break;
    case ax::mojom::Role::kHeading:
      tag->fTypeString = GetHeadingStructureType(ax_node->GetIntAttribute(
          ax::mojom::IntAttribute::kHierarchicalLevel));
      break;
    case ax::mojom::Role::kLink:
      tag->fTypeString = kPDFStructureTypeLink;
      break;
    case ax::mojom::Role::kList:
      tag->fTypeString = kPDFStructureTypeList;
      break;
    case ax::mojom::Role::kListMarker:
      tag->fTypeString = kPDFStructureTypeListItemLabel;
      break;
    case ax::mojom::Role::kListItem:
      tag->fTypeString = kPDFStructureTypeListItemBody;
      break;
    case ax::mojom::Role::kTable:
      tag->fTypeString = kPDFStructureTypeTable;
      break;
    case ax::mojom::Role::kRow:
      tag->fTypeString = kPDFStructureTypeTableRow;
      break;
    case ax::mojom::Role::kColumnHeader:
      tag->fTypeString = kPDFStructureTypeTableHeader;
      tag->fAttributes.appendName(kPDFTableAttributeOwner,
                                  kPDFTableHeaderScopeAttribute,
                                  kPDFTableHeaderScopeColumn);
      break;
    case ax::mojom::Role::kRowHeader:
      tag->fTypeString = kPDFStructureTypeTableHeader;
      tag->fAttributes.appendName(kPDFTableAttributeOwner,
                                  kPDFTableHeaderScopeAttribute,
                                  kPDFTableHeaderScopeRow);
      break;
    case ax::mojom::Role::kCell: {
      tag->fTypeString = kPDFStructureTypeTableCell;

      // Append an attribute consisting of the string IDs of all of the
      // header cells that correspond to this table cell.
      std::vector<ui::AXNode*> header_nodes;
      ax_node->GetTableCellColHeaders(&header_nodes);
      ax_node->GetTableCellRowHeaders(&header_nodes);
      std::vector<int> header_ids;
      header_ids.reserve(header_nodes.size());
      for (ui::AXNode* header_node : header_nodes) {
        header_ids.push_back(header_node->GetIntAttribute(
            ax::mojom::IntAttribute::kDOMNodeId));
      }
      tag->fAttributes.appendNodeIdArray(
          kPDFTableAttributeOwner, kPDFTableCellHeadersAttribute, header_ids);
      break;
    }
    case ax::mojom::Role::kFigure:
    case ax::mojom::Role::kImage: {
      tag->fTypeString = kPDFStructureTypeFigure;
      std::string alt =
          ax_node->GetStringAttribute(ax::mojom::StringAttribute::kName);
      tag->fAlt = SkString(alt.c_str());
      break;
    }
    case ax::mojom::Role::kStaticText:
      // Currently we're only marking text content, so we can't generate
      // a nonempty structure tree unless we have at least one kStaticText
      // node in the tree.
      tag->fTypeString = kPDFStructureTypeNonStruct;
      valid = true;
      break;
    default:
      tag->fTypeString = kPDFStructureTypeNonStruct;
  }

  if (ui::IsCellOrTableHeader(ax_node->data().role)) {
    base::Optional<int> row_span = ax_node->GetTableCellRowSpan();
    if (row_span.has_value()) {
      tag->fAttributes.appendInt(kPDFTableAttributeOwner,
                                 kPDFTableCellRowSpanAttribute,
                                 row_span.value());
    }
    base::Optional<int> col_span = ax_node->GetTableCellColSpan();
    if (col_span.has_value()) {
      tag->fAttributes.appendInt(kPDFTableAttributeOwner,
                                 kPDFTableCellColSpanAttribute,
                                 col_span.value());
    }
  }

  std::string lang = ax_node->GetLanguage();
  std::string parent_lang =
      ax_node->parent() ? ax_node->parent()->GetLanguage() : "";
  if (!lang.empty() && lang != parent_lang)
    tag->fLang = lang.c_str();

  size_t children_count = ax_node->GetUnignoredChildCount();
  tag->fChildVector.resize(children_count);
  for (size_t i = 0; i < children_count; i++) {
    tag->fChildVector[i] = std::make_unique<SkPDF::StructureElementNode>();
    bool success = RecursiveBuildStructureTree(
        ax_node->GetUnignoredChildAtIndex(i), tag->fChildVector[i].get());

    if (success)
      valid = true;
  }

  return valid;
#else  // BUILDFLAG(ENABLE_TAGGED_PDF)
  return false;
#endif
}

}  // namespace

namespace printing {

sk_sp<SkDocument> MakePdfDocument(const std::string& creator,
                                  const ui::AXTreeUpdate& accessibility_tree,
                                  SkWStream* stream) {
  SkPDF::Metadata metadata;
  SkTime::DateTime now = TimeToSkTime(base::Time::Now());
  metadata.fCreation = now;
  metadata.fModified = now;
  metadata.fCreator = creator.empty()
                          ? SkString("Chromium")
                          : SkString(creator.c_str(), creator.size());
  metadata.fRasterDPI = 300.0f;

  SkPDF::StructureElementNode tag_root = {};
  if (!accessibility_tree.nodes.empty()) {
    ui::AXTree tree(accessibility_tree);
    if (RecursiveBuildStructureTree(tree.root(), &tag_root))
      metadata.fStructureElementTreeRoot = &tag_root;
  }

  return SkPDF::MakeDocument(stream, metadata);
}

sk_sp<SkData> SerializeOopPicture(SkPicture* pic, void* ctx) {
  const auto* context = reinterpret_cast<const ContentToProxyTokenMap*>(ctx);
  uint32_t pic_id = pic->uniqueID();
  auto iter = context->find(pic_id);
  if (iter == context->end())
    return nullptr;

  return SkData::MakeWithCopy(&pic_id, sizeof(pic_id));
}

sk_sp<SkPicture> DeserializeOopPicture(const void* data,
                                       size_t length,
                                       void* ctx) {
  uint32_t pic_id;
  if (length < sizeof(pic_id)) {
    NOTREACHED();  // Should not happen if the content is as written.
    return GetEmptyPicture();
  }
  memcpy(&pic_id, data, sizeof(pic_id));

  auto* context = reinterpret_cast<PictureDeserializationContext*>(ctx);
  auto iter = context->find(pic_id);
  if (iter == context->end() || !iter->second) {
    // When we don't have the out-of-process picture available, we return
    // an empty picture. Returning a nullptr will cause the deserialization
    // crash.
    return GetEmptyPicture();
  }
  return iter->second;
}

sk_sp<SkData> SerializeOopTypeface(SkTypeface* typeface, void* ctx) {
  auto* context = reinterpret_cast<TypefaceSerializationContext*>(ctx);
  SkFontID typeface_id = typeface->uniqueID();
  bool data_included = context->insert(typeface_id).second;

  // Need the typeface ID to identify the desired typeface.  Include an
  // indicator for when typeface data actually follows vs. when the typeface
  // should already exist in a cache when deserializing.
  SkDynamicMemoryWStream stream;
  stream.write32(typeface_id);
  stream.writeBool(data_included);
  if (data_included) {
    typeface->serialize(&stream, SkTypeface::SerializeBehavior::kDoIncludeData);
  }
  return stream.detachAsData();
}

sk_sp<SkTypeface> DeserializeOopTypeface(const void* data,
                                         size_t length,
                                         void* ctx) {
  SkStream* stream = *(reinterpret_cast<SkStream**>(const_cast<void*>(data)));
  if (length < sizeof(stream)) {
    NOTREACHED();  // Should not happen if the content is as written.
    return nullptr;
  }

  SkFontID id;
  if (!stream->readU32(&id)) {
    return nullptr;
  }
  bool data_included;
  if (!stream->readBool(&data_included)) {
    return nullptr;
  }

  auto* context = reinterpret_cast<TypefaceDeserializationContext*>(ctx);
  auto iter = context->find(id);
  if (iter != context->end()) {
    DCHECK(!data_included);
    return iter->second;
  }

  // Typeface not encountered before, expect it to be present in the stream.
  DCHECK(data_included);
  sk_sp<SkTypeface> typeface = SkTypeface::MakeDeserialize(stream);
  context->emplace(id, typeface);
  return typeface;
}

SkSerialProcs SerializationProcs(PictureSerializationContext* picture_ctx,
                                 TypefaceSerializationContext* typeface_ctx) {
  SkSerialProcs procs;
  procs.fPictureProc = SerializeOopPicture;
  procs.fPictureCtx = picture_ctx;
  procs.fTypefaceProc = SerializeOopTypeface;
  procs.fTypefaceCtx = typeface_ctx;
  return procs;
}

SkDeserialProcs DeserializationProcs(
    PictureDeserializationContext* picture_ctx,
    TypefaceDeserializationContext* typeface_ctx) {
  SkDeserialProcs procs;
  procs.fPictureProc = DeserializeOopPicture;
  procs.fPictureCtx = picture_ctx;
  procs.fTypefaceProc = DeserializeOopTypeface;
  procs.fTypefaceCtx = typeface_ctx;
  return procs;
}

}  // namespace printing
