// 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 "cc/paint/solid_color_analyzer.h"

#include <cmath>

#include "base/trace_event/trace_event.h"
#include "build/build_config.h"
#include "cc/paint/paint_op_buffer.h"
#include "third_party/skia/include/core/SkTypes.h"
#include "third_party/skia/include/utils/SkNoDrawCanvas.h"

namespace cc {
namespace {

SkColor DoSrcOverAlphaBlend(SkColor src, SkColor dst) {
  if (SkColorGetA(src) == 0)
    return dst;
  if (SkColorGetA(src) == 255)
    return src;

  // Note: using alpha blending formulas adapted from
  // https://en.wikipedia.org/wiki/Alpha_compositing:
  //
  // outA = srcA + dstA * (1 - srcA)
  // outRGB = srcRGB * (srcA / outA) + dstRGB * [dstA * (1 - srcA) / outA]
  const float src_alpha = SkColorGetA(src) / 255.0f;
  const float src_alpha_complement = (255.0f - SkColorGetA(src)) / 255.0f;
  const float dst_alpha = SkColorGetA(dst) / 255.0f;
  const float out_alpha = src_alpha + dst_alpha * src_alpha_complement;
  if (out_alpha == 0.0f)
    return SK_ColorTRANSPARENT;

  const float inverse_out_alpha = 1.0f / out_alpha;
  const float src_weight = src_alpha * inverse_out_alpha;
  const float dst_weight = dst_alpha * src_alpha_complement * inverse_out_alpha;
  const float out_red =
      (SkColorGetR(src) * src_weight + SkColorGetR(dst) * dst_weight);
  const float out_green =
      (SkColorGetG(src) * src_weight + SkColorGetG(dst) * dst_weight);
  const float out_blue =
      (SkColorGetB(src) * src_weight + SkColorGetB(dst) * dst_weight);

  return SkColorSetARGB(static_cast<U8CPU>(std::floor(out_alpha * 255.0f)),
                        static_cast<U8CPU>(std::floor(out_red)),
                        static_cast<U8CPU>(std::floor(out_green)),
                        static_cast<U8CPU>(std::floor(out_blue)));
}

bool ActsLikeClear(SkBlendMode mode, unsigned src_alpha) {
  switch (mode) {
    case SkBlendMode::kClear:
      return true;
    case SkBlendMode::kSrc:
    case SkBlendMode::kSrcIn:
    case SkBlendMode::kDstIn:
    case SkBlendMode::kSrcOut:
    case SkBlendMode::kDstATop:
      return src_alpha == 0;
    case SkBlendMode::kDstOut:
      return src_alpha == 0xFF;
    default:
      return false;
  }
}

bool IsSolidColorBlendMode(SkBlendMode blendmode) {
  return blendmode == SkBlendMode::kSrc || blendmode == SkBlendMode::kSrcOver;
}

bool IsSolidColorPaint(const PaintFlags& flags) {
  SkBlendMode blendmode = flags.getBlendMode();

  // Paint is solid color if the following holds:
  // - Style is fill, and there are no special effects.
  // - Blend mode is either kSrc or kSrcOver.
  bool is_solid_color =
      IsSolidColorBlendMode(blendmode) && !flags.HasShader() &&
      !flags.getLooper() && !flags.getMaskFilter() && !flags.getColorFilter() &&
      !flags.getImageFilter() && flags.getStyle() == PaintFlags::kFill_Style;

#if defined(OS_MACOSX)
  // Additionally, on Mac, we require that the color is opaque due to
  // https://crbug.com/922899.
  // TODO(andrescj): remove this condition once that bug is fixed.
  is_solid_color = (is_solid_color && SkColorGetA(flags.getColor()) == 255);
#endif  // OS_MACOSX

  return is_solid_color;
}

// Returns true if the specified |drawn_shape| will cover the entire canvas
// and that the canvas is not clipped (i.e. it covers ALL of the canvas).
template <typename T>
bool IsFullQuad(const SkCanvas& canvas, const T& drawn_shape) {
  if (!canvas.isClipRect())
    return false;

  SkIRect clip_irect;
  if (!canvas.getDeviceClipBounds(&clip_irect))
    return false;

  // if the clip is smaller than the canvas, we're partly clipped, so abort.
  if (!clip_irect.contains(SkIRect::MakeSize(canvas.getBaseLayerSize())))
    return false;

  const SkMatrix& matrix = canvas.getTotalMatrix();
  // If the transform results in a non-axis aligned
  // rect, then be conservative and return false.
  if (!matrix.rectStaysRect())
    return false;

  SkMatrix inverse;
  if (!matrix.invert(&inverse))
    return false;

  SkRect clip_rect = SkRect::Make(clip_irect);
  inverse.mapRect(&clip_rect, clip_rect);
  return drawn_shape.contains(clip_rect);
}

void CalculateSolidColor(SkColor src_color,
                         SkBlendMode blendmode,
                         SkColor* dst_color,
                         bool* is_solid_color) {
  if (blendmode == SkBlendMode::kSrc) {
    // In the Src mode, we don't have to worry about what's in the canvas
    // because we'll replace it with |src_color|.
    *dst_color = src_color;
    *is_solid_color = true;
  } else {
    DCHECK_EQ(SkBlendMode::kSrcOver, blendmode);

    // When using the SrcOver mode, we must ensure that either a) we're
    // completely occluding what's in the canvas with an opaque color, or
    // b) whatever is in the canvas is already a solid color.
    if (SkColorGetA(src_color) == 255 || *is_solid_color) {
      *dst_color = DoSrcOverAlphaBlend(src_color, *dst_color);
      *is_solid_color = true;
    }
  }
}

void CheckIfSolidColor(const SkCanvas& canvas,
                       SkColor color,
                       SkBlendMode blendmode,
                       bool* is_solid_color,
                       bool* is_transparent,
                       SkColor* out_color) {
  SkRect rect;
  if (!canvas.getLocalClipBounds(&rect)) {
    *is_transparent = false;
    *is_solid_color = false;
    return;
  }

  bool does_cover_canvas = IsFullQuad(canvas, rect);
  uint8_t alpha = SkColorGetA(color);
  if (does_cover_canvas && ActsLikeClear(blendmode, alpha))
    *is_transparent = true;
  else if (alpha != 0 || blendmode != SkBlendMode::kSrc)
    *is_transparent = false;

  bool solid_color_candidate =
      does_cover_canvas && IsSolidColorBlendMode(blendmode);

#if defined(OS_MACOSX)
  // Additionally, on Mac, we require that the color is opaque due to
  // https://crbug.com/922899.
  // TODO(andrescj): remove this condition once that bug is fixed.
  solid_color_candidate = (solid_color_candidate && alpha == 255);
#endif  // OS_MACOSX

  if (solid_color_candidate) {
    CalculateSolidColor(color /* src_color */, blendmode,
                        out_color /* dst_color */, is_solid_color);
  } else {
    *is_solid_color = false;
  }
}

template <typename T>
void CheckIfSolidShape(const SkCanvas& canvas,
                       const T& shape,
                       const PaintFlags& flags,
                       bool* is_solid_color,
                       bool* is_transparent,
                       SkColor* color) {
  TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("cc.debug"),
               "SolidColorAnalyzer::CheckIfSolidShape");
  if (flags.nothingToDraw())
    return;

  bool does_cover_canvas = IsFullQuad(canvas, shape);
  SkBlendMode blendmode = flags.getBlendMode();
  if (does_cover_canvas && ActsLikeClear(blendmode, flags.getAlpha()))
    *is_transparent = true;
  else if (flags.getAlpha() != 0 || blendmode != SkBlendMode::kSrc)
    *is_transparent = false;

  if (does_cover_canvas && IsSolidColorPaint(flags)) {
    CalculateSolidColor(flags.getColor() /* src_color */, flags.getBlendMode(),
                        color /* dst_color */, is_solid_color);
  } else {
    *is_solid_color = false;
  }
}

bool CheckIfRRectClipCoversCanvas(const SkCanvas& canvas,
                                  const SkRRect& rrect) {
  TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("cc.debug"),
               "SolidColorAnalyzer::CheckIfRRectClipCoversCanvas");
  return IsFullQuad(canvas, rrect);
}

}  // namespace

base::Optional<SkColor> SolidColorAnalyzer::DetermineIfSolidColor(
    const PaintOpBuffer* buffer,
    const gfx::Rect& rect,
    int max_ops_to_analyze,
    const std::vector<size_t>* offsets) {
  if (buffer->size() == 0 || (offsets && offsets->empty()))
    return SK_ColorTRANSPARENT;

  bool is_solid = true;
  bool is_transparent = true;
  SkColor color = SK_ColorTRANSPARENT;

  struct Frame {
    Frame(PaintOpBuffer::CompositeIterator iter,
          const SkMatrix& original_ctm,
          int save_count)
        : iter(iter), original_ctm(original_ctm), save_count(save_count) {}

    PaintOpBuffer::CompositeIterator iter;
    const SkMatrix original_ctm;
    int save_count = 0;
  };

  SkNoDrawCanvas canvas(rect.width(), rect.height());
  canvas.translate(-rect.x(), -rect.y());
  canvas.clipRect(gfx::RectToSkRect(rect), SkClipOp::kIntersect, false);

  std::vector<Frame> stack;
  // We expect to see at least one DrawRecordOp because of the way items are
  // constructed. Reserve this to 2, and go from there.
  stack.reserve(2);
  stack.emplace_back(PaintOpBuffer::CompositeIterator(buffer, offsets),
                     canvas.getTotalMatrix(), canvas.getSaveCount());

  int num_draw_ops = 0;
  while (!stack.empty()) {
    auto& frame = stack.back();
    if (!frame.iter) {
      canvas.restoreToCount(frame.save_count);
      stack.pop_back();
      if (!stack.empty())
        ++stack.back().iter;
      continue;
    }

    const PaintOp* op = *frame.iter;
    PlaybackParams params(nullptr, frame.original_ctm);
    switch (op->GetType()) {
      case PaintOpType::DrawRecord: {
        const DrawRecordOp* record_op = static_cast<const DrawRecordOp*>(op);
        stack.emplace_back(
            PaintOpBuffer::CompositeIterator(record_op->record.get(), nullptr),
            canvas.getTotalMatrix(), canvas.getSaveCount());
        continue;
      }

      // Any of the following ops result in non solid content.
      case PaintOpType::DrawDRRect:
      case PaintOpType::DrawImage:
      case PaintOpType::DrawImageRect:
      case PaintOpType::DrawIRect:
      case PaintOpType::DrawLine:
      case PaintOpType::DrawOval:
      case PaintOpType::DrawPath:
        return base::nullopt;
      // TODO(vmpstr): Add more tests on exceeding max_ops_to_analyze.
      case PaintOpType::DrawRRect: {
        if (++num_draw_ops > max_ops_to_analyze)
          return base::nullopt;
        const DrawRRectOp* rrect_op = static_cast<const DrawRRectOp*>(op);
        CheckIfSolidShape(canvas, rrect_op->rrect, rrect_op->flags, &is_solid,
                          &is_transparent, &color);
        break;
      }
      case PaintOpType::DrawSkottie:
      case PaintOpType::DrawTextBlob:
      // Anything that has to do a save layer is probably not solid. As it will
      // likely need more than one draw op.
      // TODO(vmpstr): We could investigate handling these.
      case PaintOpType::SaveLayer:
      case PaintOpType::SaveLayerAlpha:
      // Complex clips will probably result in non solid color as it might not
      // cover the canvas.
      // TODO(vmpstr): We could investigate handling these.
      case PaintOpType::ClipPath:
        return base::nullopt;
      case PaintOpType::ClipRRect: {
        const ClipRRectOp* rrect_op = static_cast<const ClipRRectOp*>(op);
        bool does_cover_canvas =
            CheckIfRRectClipCoversCanvas(canvas, rrect_op->rrect);
        // If the clip covers the full canvas, we can treat it as if there's no
        // clip at all and continue, otherwise this is no longer a solid color.
        if (!does_cover_canvas)
          return base::nullopt;
        break;
      }
      case PaintOpType::DrawRect: {
        if (++num_draw_ops > max_ops_to_analyze)
          return base::nullopt;
        const DrawRectOp* rect_op = static_cast<const DrawRectOp*>(op);
        CheckIfSolidShape(canvas, rect_op->rect, rect_op->flags, &is_solid,
                          &is_transparent, &color);
        break;
      }
      case PaintOpType::DrawColor: {
        if (++num_draw_ops > max_ops_to_analyze)
          return base::nullopt;
        const DrawColorOp* color_op = static_cast<const DrawColorOp*>(op);
        CheckIfSolidColor(canvas, color_op->color, color_op->mode, &is_solid,
                          &is_transparent, &color);
        break;
      }
      case PaintOpType::ClipRect: {
        // SolidColorAnalyzer uses an SkNoDrawCanvas which uses an
        // SkNoPixelsDevice which says (without looking) that the canvas's
        // clip is always a rect.  So, if this clip could result in not
        // a rect, this is no longer solid color.
        const ClipRectOp* clip_op = static_cast<const ClipRectOp*>(op);
        if (clip_op->op == SkClipOp::kDifference)
          return base::nullopt;
        op->Raster(&canvas, params);
        break;
      }

      // Don't affect the canvas, so ignore.
      case PaintOpType::Annotate:
      case PaintOpType::CustomData:
      case PaintOpType::Noop:
        break;

      // The rest of the ops should only affect our state canvas.
      case PaintOpType::Concat:
      case PaintOpType::Scale:
      case PaintOpType::SetMatrix:
      case PaintOpType::Restore:
      case PaintOpType::Rotate:
      case PaintOpType::Save:
      case PaintOpType::Translate:
        op->Raster(&canvas, params);
        break;
    }
    ++frame.iter;
  }

  if (is_transparent)
    return SK_ColorTRANSPARENT;
  if (is_solid)
    return color;
  return base::nullopt;
}

}  // namespace cc
