// Copyright 2013 The Flutter 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 "flutter/flow/raster_cache.h"

#include <vector>

#include "flutter/flow/layers/layer.h"
#include "flutter/flow/paint_utils.h"
#include "flutter/fml/logging.h"
#include "flutter/fml/trace_event.h"
#include "include/core/SkCanvas.h"
#include "include/core/SkImage.h"
#include "include/core/SkPicture.h"
#include "include/core/SkSurface.h"

namespace flutter {

RasterCacheResult::RasterCacheResult() {}

RasterCacheResult::RasterCacheResult(const RasterCacheResult& other) = default;

RasterCacheResult::~RasterCacheResult() = default;

RasterCacheResult::RasterCacheResult(sk_sp<SkImage> image,
                                     const SkRect& logical_rect)
    : image_(std::move(image)), logical_rect_(logical_rect) {}

void RasterCacheResult::draw(SkCanvas& canvas, const SkPaint* paint) const {
  TRACE_EVENT0("flutter", "RasterCacheResult::draw");
  SkAutoCanvasRestore auto_restore(&canvas, true);
  SkIRect bounds =
      RasterCache::GetDeviceBounds(logical_rect_, canvas.getTotalMatrix());
  FML_DCHECK(bounds.size() == image_->dimensions());
  canvas.resetMatrix();
  canvas.drawImage(image_, bounds.fLeft, bounds.fTop, paint);
}

RasterCache::RasterCache(size_t access_threshold,
                         size_t picture_cache_limit_per_frame)
    : access_threshold_(access_threshold),
      picture_cache_limit_per_frame_(picture_cache_limit_per_frame),
      checkerboard_images_(false),
      weak_factory_(this) {}

RasterCache::~RasterCache() = default;

static bool CanRasterizePicture(SkPicture* picture) {
  if (picture == nullptr) {
    return false;
  }

  const SkRect cull_rect = picture->cullRect();

  if (cull_rect.isEmpty()) {
    // No point in ever rasterizing an empty picture.
    return false;
  }

  if (!cull_rect.isFinite()) {
    // Cannot attempt to rasterize into an infinitely large surface.
    return false;
  }

  return true;
}

static bool IsPictureWorthRasterizing(SkPicture* picture,
                                      bool will_change,
                                      bool is_complex) {
  if (will_change) {
    // If the picture is going to change in the future, there is no point in
    // doing to extra work to rasterize.
    return false;
  }

  if (!CanRasterizePicture(picture)) {
    // No point in deciding whether the picture is worth rasterizing if it
    // cannot be rasterized at all.
    return false;
  }

  if (is_complex) {
    // The caller seems to have extra information about the picture and thinks
    // the picture is always worth rasterizing.
    return true;
  }

  // TODO(abarth): We should find a better heuristic here that lets us avoid
  // wasting memory on trivial layers that are easy to re-rasterize every frame.
  return picture->approximateOpCount() > 5;
}

static RasterCacheResult Rasterize(
    GrContext* context,
    const SkMatrix& ctm,
    SkColorSpace* dst_color_space,
    bool checkerboard,
    const SkRect& logical_rect,
    std::function<void(SkCanvas*)> draw_function) {
  TRACE_EVENT0("flutter", "RasterCachePopulate");
  SkIRect cache_rect = RasterCache::GetDeviceBounds(logical_rect, ctm);

  const SkImageInfo image_info = SkImageInfo::MakeN32Premul(
      cache_rect.width(), cache_rect.height(), sk_ref_sp(dst_color_space));

  sk_sp<SkSurface> surface =
      context
          ? SkSurface::MakeRenderTarget(context, SkBudgeted::kYes, image_info)
          : SkSurface::MakeRaster(image_info);

  if (!surface) {
    return {};
  }

  SkCanvas* canvas = surface->getCanvas();
  canvas->clear(SK_ColorTRANSPARENT);
  canvas->translate(-cache_rect.left(), -cache_rect.top());
  canvas->concat(ctm);
  draw_function(canvas);

  if (checkerboard) {
    DrawCheckerboard(canvas, logical_rect);
  }

  return {surface->makeImageSnapshot(), logical_rect};
}

RasterCacheResult RasterizePicture(SkPicture* picture,
                                   GrContext* context,
                                   const SkMatrix& ctm,
                                   SkColorSpace* dst_color_space,
                                   bool checkerboard) {
  return Rasterize(context, ctm, dst_color_space, checkerboard,
                   picture->cullRect(),
                   [=](SkCanvas* canvas) { canvas->drawPicture(picture); });
}

static inline size_t ClampSize(size_t value, size_t min, size_t max) {
  if (value > max) {
    return max;
  }

  if (value < min) {
    return min;
  }

  return value;
}

void RasterCache::Prepare(PrerollContext* context,
                          Layer* layer,
                          const SkMatrix& ctm) {
  LayerRasterCacheKey cache_key(layer->unique_id(), ctm);
  Entry& entry = layer_cache_[cache_key];
  entry.access_count = ClampSize(entry.access_count + 1, 0, access_threshold_);
  entry.used_this_frame = true;
  if (!entry.image.is_valid()) {
    entry.image = Rasterize(context->gr_context, ctm, context->dst_color_space,
                            checkerboard_images_, layer->paint_bounds(),
                            [layer, context](SkCanvas* canvas) {
                              SkISize canvas_size = canvas->getBaseLayerSize();
                              SkNWayCanvas internal_nodes_canvas(
                                  canvas_size.width(), canvas_size.height());
                              internal_nodes_canvas.addCanvas(canvas);
                              Layer::PaintContext paintContext = {
                                  (SkCanvas*)&internal_nodes_canvas,
                                  canvas,
                                  context->gr_context,
                                  nullptr,
                                  context->raster_time,
                                  context->ui_time,
                                  context->texture_registry,
                                  context->raster_cache,
                                  context->checkerboard_offscreen_layers};
                              if (layer->needs_painting()) {
                                layer->Paint(paintContext);
                              }
                            });
  }
}

bool RasterCache::Prepare(GrContext* context,
                          SkPicture* picture,
                          const SkMatrix& transformation_matrix,
                          SkColorSpace* dst_color_space,
                          bool is_complex,
                          bool will_change) {
  if (picture_cached_this_frame_ >= picture_cache_limit_per_frame_) {
    return false;
  }
  if (!IsPictureWorthRasterizing(picture, will_change, is_complex)) {
    // We only deal with pictures that are worthy of rasterization.
    return false;
  }

  // Decompose the matrix (once) for all subsequent operations. We want to make
  // sure to avoid volumetric distortions while accounting for scaling.
  const MatrixDecomposition matrix(transformation_matrix);

  if (!matrix.IsValid()) {
    // The matrix was singular. No point in going further.
    return false;
  }

  PictureRasterCacheKey cache_key(picture->uniqueID(), transformation_matrix);

  Entry& entry = picture_cache_[cache_key];
  entry.access_count = ClampSize(entry.access_count + 1, 0, access_threshold_);
  entry.used_this_frame = true;

  if (entry.access_count < access_threshold_ || access_threshold_ == 0) {
    // Frame threshold has not yet been reached.
    return false;
  }

  if (!entry.image.is_valid()) {
    entry.image = RasterizePicture(picture, context, transformation_matrix,
                                   dst_color_space, checkerboard_images_);
  }
  picture_cached_this_frame_++;
  return true;
}

RasterCacheResult RasterCache::Get(const SkPicture& picture,
                                   const SkMatrix& ctm) const {
  PictureRasterCacheKey cache_key(picture.uniqueID(), ctm);
  auto it = picture_cache_.find(cache_key);
  return it == picture_cache_.end() ? RasterCacheResult() : it->second.image;
}

RasterCacheResult RasterCache::Get(Layer* layer, const SkMatrix& ctm) const {
  LayerRasterCacheKey cache_key(layer->unique_id(), ctm);
  auto it = layer_cache_.find(cache_key);
  return it == layer_cache_.end() ? RasterCacheResult() : it->second.image;
}

void RasterCache::SweepAfterFrame() {
  using PictureCache = PictureRasterCacheKey::Map<Entry>;
  using LayerCache = LayerRasterCacheKey::Map<Entry>;
  SweepOneCacheAfterFrame<PictureCache, PictureCache::iterator>(picture_cache_);
  SweepOneCacheAfterFrame<LayerCache, LayerCache::iterator>(layer_cache_);
  picture_cached_this_frame_ = 0;
  TraceStatsToTimeline();
}

void RasterCache::Clear() {
  picture_cache_.clear();
  layer_cache_.clear();
}

void RasterCache::SetCheckboardCacheImages(bool checkerboard) {
  if (checkerboard_images_ == checkerboard) {
    return;
  }

  checkerboard_images_ = checkerboard;

  // Clear all existing entries so previously rasterized items (with or without
  // a checkerboard) will be refreshed in subsequent passes.
  Clear();
}

void RasterCache::TraceStatsToTimeline() const {
#if FLUTTER_RUNTIME_MODE != FLUTTER_RUNTIME_MODE_RELEASE

  size_t layer_cache_count = 0;
  size_t layer_cache_bytes = 0;
  size_t picture_cache_count = 0;
  size_t picture_cache_bytes = 0;

  for (const auto& item : layer_cache_) {
    const auto dimensions = item.second.image.image_dimensions();
    layer_cache_count++;
    layer_cache_bytes += dimensions.width() * dimensions.height() * 4;
  }

  for (const auto& item : picture_cache_) {
    const auto dimensions = item.second.image.image_dimensions();
    picture_cache_count++;
    picture_cache_bytes += dimensions.width() * dimensions.height() * 4;
  }

  FML_TRACE_COUNTER("flutter", "RasterCache",
                    reinterpret_cast<int64_t>(this),             //
                    "LayerCount", layer_cache_count,             //
                    "LayerMBytes", layer_cache_bytes * 1e-6,     //
                    "PictureCount", picture_cache_count,         //
                    "PictureMBytes", picture_cache_bytes * 1e-6  //
  );

#endif  // FLUTTER_RUNTIME_MODE != FLUTTER_RUNTIME_MODE_RELEASE
}

}  // namespace flutter