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

#include "cc/paint/display_item_list.h"

#include <stddef.h>

#include <map>
#include <string>

#include "base/trace_event/trace_event.h"
#include "base/trace_event/traced_value.h"
#include "cc/base/math_util.h"
#include "cc/debug/picture_debug_util.h"
#include "cc/paint/paint_op_buffer_iterator.h"
#include "cc/paint/solid_color_analyzer.h"
#include "third_party/skia/include/core/SkCanvas.h"
#include "third_party/skia/include/core/SkPictureRecorder.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/gfx/geometry/rect_f.h"
#include "ui/gfx/geometry/skia_conversions.h"

namespace cc {

namespace {

bool GetCanvasClipBounds(SkCanvas* canvas, gfx::Rect* clip_bounds) {
  SkRect canvas_clip_bounds;
  if (!canvas->getLocalClipBounds(&canvas_clip_bounds))
    return false;
  *clip_bounds = ToEnclosingRect(gfx::SkRectToRectF(canvas_clip_bounds));
  return true;
}

template <typename Function>
void IterateTextContent(const PaintOpBuffer& buffer,
                        const Function& yield,
                        const gfx::Rect& rect) {
  if (!buffer.has_draw_text_ops())
    return;
  for (const PaintOp& op : buffer) {
    if (op.GetType() == PaintOpType::DrawTextBlob) {
      yield(static_cast<const DrawTextBlobOp&>(op), rect);
    } else if (op.GetType() == PaintOpType::DrawRecord) {
      IterateTextContent(static_cast<const DrawRecordOp&>(op).record.buffer(),
                         yield, rect);
    }
  }
}

template <typename Function>
void IterateTextContentByOffsets(const PaintOpBuffer& buffer,
                                 const std::vector<size_t>& offsets,
                                 const std::vector<gfx::Rect>& rects,
                                 const Function& yield) {
  DCHECK(buffer.has_draw_text_ops());
  DCHECK_EQ(rects.size(), offsets.size());
  size_t index = 0;
  for (const PaintOp& op : PaintOpBuffer::OffsetIterator(buffer, offsets)) {
    if (op.GetType() == PaintOpType::DrawTextBlob) {
      yield(static_cast<const DrawTextBlobOp&>(op), rects[index]);
    } else if (op.GetType() == PaintOpType::DrawRecord) {
      IterateTextContent(static_cast<const DrawRecordOp&>(op).record.buffer(),
                         yield, rects[index]);
    }
    ++index;
  }
}
}  // namespace

DisplayItemList::DisplayItemList() {
  visual_rects_.reserve(1024);
  offsets_.reserve(1024);
  paired_begin_stack_.reserve(32);
}

DisplayItemList::~DisplayItemList() = default;

void DisplayItemList::Raster(SkCanvas* canvas,
                             ImageProvider* image_provider) const {
  gfx::Rect canvas_playback_rect;
  if (!GetCanvasClipBounds(canvas, &canvas_playback_rect))
    return;

  TRACE_EVENT_BEGIN1("cc", "DisplayItemList::Raster", "total_op_count",
                     TotalOpCount());
  std::vector<size_t> offsets;
  rtree_.Search(canvas_playback_rect, &offsets);
  paint_op_buffer_.Playback(canvas, PlaybackParams(image_provider), &offsets);

  bool trace_enabled = false;
  TRACE_EVENT_CATEGORY_GROUP_ENABLED("cc", &trace_enabled);
  if (trace_enabled) {
    size_t rastered_op_count = 0;
    for (PaintOpBuffer::PlaybackFoldingIterator it(paint_op_buffer_, &offsets);
         it; ++it) {
      rastered_op_count += 1 + it->AdditionalOpCount();
    }
    TRACE_EVENT_END1("cc", "DisplayItemList::Raster", "rastered_op_count",
                     rastered_op_count);
  }
}

void DisplayItemList::CaptureContent(const gfx::Rect& rect,
                                     std::vector<NodeInfo>* content) const {
  if (!paint_op_buffer_.has_draw_text_ops())
    return;
  std::vector<size_t> offsets;
  std::vector<gfx::Rect> rects;
  rtree_.Search(rect, &offsets, &rects);
  IterateTextContentByOffsets(
      paint_op_buffer_, offsets, rects,
      [content](const DrawTextBlobOp& op, const gfx::Rect& rect) {
        // Only union the rect if the current is the same as the last one.
        if (!content->empty() && content->back().node_id == op.node_id)
          content->back().visual_rect.Union(rect);
        else
          content->emplace_back(op.node_id, rect);
      });
}

double DisplayItemList::AreaOfDrawText(const gfx::Rect& rect) const {
  if (!paint_op_buffer_.has_draw_text_ops())
    return 0;
  std::vector<size_t> offsets;
  std::vector<gfx::Rect> rects;
  rtree_.Search(rect, &offsets, &rects);
  DCHECK_EQ(offsets.size(), rects.size());

  double area = 0;
  size_t index = 0;
  for (const PaintOp& op :
       PaintOpBuffer::OffsetIterator(paint_op_buffer_, offsets)) {
    if (op.GetType() == PaintOpType::DrawTextBlob ||
        // Don't walk into the record because the visual rect is already the
        // bounding box of the sub paint operations. This works for most paint
        // results for text generated by blink.
        (op.GetType() == PaintOpType::DrawRecord &&
         static_cast<const DrawRecordOp&>(op).record.has_draw_text_ops())) {
      area += static_cast<double>(rects[index].width()) * rects[index].height();
    }
    ++index;
  }
  return area;
}

void DisplayItemList::EndPaintOfPairedEnd() {
#if DCHECK_IS_ON()
  DCHECK(IsPainting());
  DCHECK_LT(current_range_start_, paint_op_buffer_.size());
  current_range_start_ = kNotPainting;
#endif
  DCHECK(paired_begin_stack_.size());
  size_t last_begin_index = paired_begin_stack_.back().first_index;
  size_t last_begin_count = paired_begin_stack_.back().count;
  DCHECK_GT(last_begin_count, 0u);

  // Copy the visual rect at |last_begin_index| to all indices that constitute
  // the begin item. Note that because we possibly reallocate the
  // |visual_rects_| buffer below, we need an actual copy instead of a const
  // reference which can become dangling.
  auto visual_rect = visual_rects_[last_begin_index];
  for (size_t i = 1; i < last_begin_count; ++i)
    visual_rects_[i + last_begin_index] = visual_rect;
  paired_begin_stack_.pop_back();

  // Copy the visual rect of the matching begin item to the end item(s).
  visual_rects_.resize(paint_op_buffer_.size(), visual_rect);

  // The block that ended needs to be included in the bounds of the enclosing
  // block.
  GrowCurrentBeginItemVisualRect(visual_rect);
}

void DisplayItemList::Finalize() {
  FinalizeImpl();
  paint_op_buffer_.ShrinkToFit();
}

void DisplayItemList::FinalizeImpl() {
  TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("cc.debug"),
               "DisplayItemList::Finalize");
#if DCHECK_IS_ON()
  // If this fails a call to StartPaint() was not ended.
  DCHECK(!IsPainting());
  // If this fails we had more calls to EndPaintOfPairedBegin() than
  // to EndPaintOfPairedEnd().
  DCHECK(paired_begin_stack_.empty());
  DCHECK_EQ(visual_rects_.size(), offsets_.size());
#endif

  rtree_.Build(
      visual_rects_,
      [](const std::vector<gfx::Rect>& rects, size_t index) {
        return rects[index];
      },
      [this](const std::vector<gfx::Rect>& rects, size_t index) {
        // Ignore the given rects, since the payload comes from
        // offsets. However, the indices match, so we can just index
        // into offsets.
        return offsets_[index];
      });
  visual_rects_.clear();
  visual_rects_.shrink_to_fit();
  offsets_.clear();
  offsets_.shrink_to_fit();
  paired_begin_stack_.shrink_to_fit();
}

PaintRecord DisplayItemList::FinalizeAndReleaseAsRecord() {
  FinalizeImpl();
  PaintRecord record = paint_op_buffer_.ReleaseAsRecord();
  Reset();
  return record;
}

void DisplayItemList::EmitTraceSnapshot() const {
  bool include_items;
  TRACE_EVENT_CATEGORY_GROUP_ENABLED(
      TRACE_DISABLED_BY_DEFAULT("cc.debug.display_items"), &include_items);
  TRACE_EVENT_OBJECT_SNAPSHOT_WITH_ID(
      TRACE_DISABLED_BY_DEFAULT("cc.debug.display_items") ","
      TRACE_DISABLED_BY_DEFAULT("cc.debug.picture") ","
      TRACE_DISABLED_BY_DEFAULT("devtools.timeline.picture"),
      "cc::DisplayItemList", TRACE_ID_LOCAL(this),
      CreateTracedValue(include_items));
}

std::string DisplayItemList::ToString() const {
  base::trace_event::TracedValueJSON value;
  AddToValue(&value, true);
  return value.ToFormattedJSON();
}

std::unique_ptr<base::trace_event::TracedValue>
DisplayItemList::CreateTracedValue(bool include_items) const {
  auto state = std::make_unique<base::trace_event::TracedValue>();
  AddToValue(state.get(), include_items);
  return state;
}

void DisplayItemList::AddToValue(base::trace_event::TracedValue* state,
                                 bool include_items) const {
  state->BeginDictionary("params");

  gfx::Rect bounds;
  if (rtree_.has_valid_bounds()) {
    bounds = rtree_.GetBoundsOrDie();
  } else {
    // For tracing code, just use the entire positive quadrant if the |rtree_|
    // has invalid bounds.
    bounds = gfx::Rect(INT_MAX, INT_MAX);
  }

  if (include_items) {
    state->BeginArray("items");

    PlaybackParams params(nullptr, SkM44());
    std::map<size_t, gfx::Rect> visual_rects = rtree_.GetAllBoundsForTracing();
    for (const PaintOp& op : paint_op_buffer_) {
      state->BeginDictionary();
      state->SetString("name", PaintOpTypeToString(op.GetType()));

      MathUtil::AddToTracedValue(
          "visual_rect",
          visual_rects[paint_op_buffer_.GetOpOffsetForTracing(op)], state);

      SkPictureRecorder recorder;
      SkCanvas* canvas = recorder.beginRecording(gfx::RectToSkRect(bounds));
      op.Raster(canvas, params);
      sk_sp<SkPicture> picture = recorder.finishRecordingAsPicture();

      if (picture->approximateOpCount()) {
        std::string b64_picture;
        PictureDebugUtil::SerializeAsBase64(picture.get(), &b64_picture);
        state->SetString("skp64", b64_picture);
      }

      state->EndDictionary();
    }

    state->EndArray();  // "items".
  }

  MathUtil::AddToTracedValue("layer_rect", bounds, state);
  state->EndDictionary();  // "params".

  {
    SkPictureRecorder recorder;
    SkCanvas* canvas = recorder.beginRecording(gfx::RectToSkRect(bounds));
    canvas->translate(-bounds.x(), -bounds.y());
    canvas->clipRect(gfx::RectToSkRect(bounds));
    Raster(canvas);
    sk_sp<SkPicture> picture = recorder.finishRecordingAsPicture();

    std::string b64_picture;
    PictureDebugUtil::SerializeAsBase64(picture.get(), &b64_picture);
    state->SetString("skp64", b64_picture);
  }
}

void DisplayItemList::GenerateDiscardableImagesMetadata() {
  gfx::Rect bounds;
  if (rtree_.has_valid_bounds()) {
    bounds = rtree_.GetBoundsOrDie();
  } else {
    // Bounds are only used to size an SkNoDrawCanvas, pass INT_MAX.
    bounds = gfx::Rect(INT_MAX, INT_MAX);
  }

  image_map_.Generate(paint_op_buffer_, bounds);
}

void DisplayItemList::Reset() {
#if DCHECK_IS_ON()
  DCHECK(!IsPainting());
  DCHECK(paired_begin_stack_.empty());
#endif

  rtree_.Reset();
  image_map_.Reset();
  paint_op_buffer_.Reset();
  visual_rects_.clear();
  visual_rects_.shrink_to_fit();
  offsets_.clear();
  offsets_.shrink_to_fit();
  paired_begin_stack_.clear();
  paired_begin_stack_.shrink_to_fit();
}

bool DisplayItemList::GetColorIfSolidInRect(const gfx::Rect& rect,
                                            SkColor4f* color,
                                            int max_ops_to_analyze) {
  std::vector<size_t>* offsets_to_use = nullptr;
  std::vector<size_t> offsets;
  if (rtree_.has_valid_bounds() && !rect.Contains(rtree_.GetBoundsOrDie())) {
    rtree_.Search(rect, &offsets);
    offsets_to_use = &offsets;
  }

  absl::optional<SkColor4f> solid_color =
      SolidColorAnalyzer::DetermineIfSolidColor(
          paint_op_buffer_, rect, max_ops_to_analyze, offsets_to_use);
  if (solid_color) {
    *color = *solid_color;
    return true;
  }
  return false;
}

namespace {

absl::optional<DisplayItemList::DirectlyCompositedImageResult>
DirectlyCompositedImageResultForPaintOpBuffer(const PaintOpBuffer& op_buffer) {
  // A PaintOpBuffer for an image may have 1 (a DrawImageRect or a DrawRecord
  // that recursively contains a PaintOpBuffer for an image) or 4 paint
  // operations:
  //  (1) Save
  //  (2) Translate which applies an offset of the image in the layer
  //   or Concat with a transformation rotating the image by +/-90 degrees for
  //      image orientation
  //  (3) DrawImageRect or DrawRecord (see the 1 operation case above)
  //  (4) Restore
  // The following algorithm also supports Translate and Concat in the same
  // PaintOpBuffer (i.e. 5 operations).
  constexpr size_t kMaxDrawImageOps = 5;
  if (op_buffer.size() > kMaxDrawImageOps)
    return absl::nullopt;

  bool transpose_image_size = false;
  absl::optional<DisplayItemList::DirectlyCompositedImageResult> result;
  for (const PaintOp& op : op_buffer) {
    switch (op.GetType()) {
      case PaintOpType::Save:
      case PaintOpType::Restore:
      case PaintOpType::Translate:
        break;
      case PaintOpType::Concat: {
        // We only expect a single transformation. If we see another one, then
        // this image won't be eligible for directly compositing.
        if (transpose_image_size)
          return absl::nullopt;
        // The transformation must be before the DrawImageRect operation.
        if (result)
          return absl::nullopt;

        const ConcatOp& concat_op = static_cast<const ConcatOp&>(op);
        if (!MathUtil::SkM44Preserves2DAxisAlignment(concat_op.matrix))
          return absl::nullopt;

        // If the image has been rotated +/-90 degrees we'll need to transpose
        // the width and height dimensions to account for the same transform
        // applying when the layer bounds were calculated. Since we already
        // know that the transformation preserves axis alignment, we only
        // need to confirm that this is not a scaling operation.
        transpose_image_size = (concat_op.matrix.rc(0, 0) == 0);
        break;
      }
      case PaintOpType::DrawImageRect: {
        if (result)
          return absl::nullopt;
        const auto& draw_image_rect_op =
            static_cast<const DrawImageRectOp&>(op);
        const SkRect& src = draw_image_rect_op.src;
        const SkRect& dst = draw_image_rect_op.dst;
        if (src.isEmpty() || dst.isEmpty())
          return absl::nullopt;
        result.emplace();
        result->default_raster_scale = gfx::Vector2dF(
            src.width() / dst.width(), src.height() / dst.height());
        // Ensure the layer will use nearest neighbor when drawn by the display
        // compositor, if required.
        result->nearest_neighbor =
            draw_image_rect_op.flags.getFilterQuality() ==
            PaintFlags::FilterQuality::kNone;
        break;
      }
      case PaintOpType::DrawRecord:
        if (result)
          return absl::nullopt;
        result = DirectlyCompositedImageResultForPaintOpBuffer(
            static_cast<const DrawRecordOp&>(op).record.buffer());
        if (!result)
          return absl::nullopt;
        break;
      default:
        // Disqualify the layer as a directly composited image if any other
        // paint op is detected.
        return absl::nullopt;
    }
  }

  if (result && transpose_image_size)
    result->default_raster_scale.Transpose();
  return result;
}

}  // anonymous namespace

absl::optional<DisplayItemList::DirectlyCompositedImageResult>
DisplayItemList::GetDirectlyCompositedImageResult() const {
  return DirectlyCompositedImageResultForPaintOpBuffer(paint_op_buffer_);
}

}  // namespace cc