// Copyright 2013 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/layers/painted_scrollbar_layer.h"

#include <algorithm>
#include <memory>
#include <utility>

#include "cc/layers/painted_scrollbar_layer_impl.h"
#include "cc/paint/skia_paint_canvas.h"
#include "cc/trees/draw_property_utils.h"
#include "cc/trees/layer_tree_host.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/gfx/geometry/transform_util.h"

namespace cc {

std::unique_ptr<LayerImpl> PaintedScrollbarLayer::CreateLayerImpl(
    LayerTreeImpl* tree_impl) const {
  return PaintedScrollbarLayerImpl::Create(tree_impl, id(), orientation(),
                                           is_left_side_vertical_scrollbar(),
                                           is_overlay_);
}

scoped_refptr<PaintedScrollbarLayer> PaintedScrollbarLayer::CreateOrReuse(
    scoped_refptr<Scrollbar> scrollbar,
    PaintedScrollbarLayer* existing_layer) {
  if (existing_layer &&
      existing_layer->scrollbar_.Read(*existing_layer)->IsSame(*scrollbar))
    return existing_layer;
  return Create(std::move(scrollbar));
}

scoped_refptr<PaintedScrollbarLayer> PaintedScrollbarLayer::Create(
    scoped_refptr<Scrollbar> scrollbar) {
  return base::WrapRefCounted(new PaintedScrollbarLayer(std::move(scrollbar)));
}

PaintedScrollbarLayer::PaintedScrollbarLayer(scoped_refptr<Scrollbar> scrollbar)
    : ScrollbarLayerBase(scrollbar->Orientation(),
                         scrollbar->IsLeftSideVerticalScrollbar()),
      scrollbar_(std::move(scrollbar)),
      internal_contents_scale_(1.f),
      painted_opacity_(scrollbar_.Read(*this)->Opacity()),
      has_thumb_(scrollbar_.Read(*this)->HasThumb()),
      jump_on_track_click_(scrollbar_.Read(*this)->JumpOnTrackClick()),
      supports_drag_snap_back_(scrollbar_.Read(*this)->SupportsDragSnapBack()),
      is_overlay_(scrollbar_.Read(*this)->IsOverlay()),
      is_web_test_(scrollbar_.Read(*this)->IsRunningWebTest()),
      uses_nine_patch_track_and_buttons_(
          scrollbar_.Read(*this)->UsesNinePatchTrackAndButtonsResource()),
      uses_solid_color_thumb_(scrollbar_.Read(*this)->UsesSolidColorThumb()) {}

PaintedScrollbarLayer::~PaintedScrollbarLayer() = default;

bool PaintedScrollbarLayer::OpacityCanAnimateOnImplThread() const {
  return is_overlay_;
}

void PaintedScrollbarLayer::PushDirtyPropertiesTo(
    LayerImpl* layer,
    uint8_t dirty_flag,
    const CommitState& commit_state,
    const ThreadUnsafeCommitState& unsafe_state) {
  ScrollbarLayerBase::PushDirtyPropertiesTo(layer, dirty_flag, commit_state,
                                            unsafe_state);

  if (dirty_flag & kChangedGeneralProperty) {
    PaintedScrollbarLayerImpl* scrollbar_layer =
        static_cast<PaintedScrollbarLayerImpl*>(layer);

    scrollbar_layer->set_internal_contents_scale_and_bounds(
        internal_contents_scale_.Read(*this),
        internal_content_bounds_.Read(*this));

    scrollbar_layer->SetJumpOnTrackClick(jump_on_track_click_.Read(*this));
    scrollbar_layer->SetSupportsDragSnapBack(supports_drag_snap_back_);
    scrollbar_layer->SetBackButtonRect(back_button_rect_.Read(*this));
    scrollbar_layer->SetForwardButtonRect(forward_button_rect_.Read(*this));
    scrollbar_layer->SetTrackRect(track_rect_.Read(*this));
    if (orientation() == ScrollbarOrientation::kHorizontal) {
      scrollbar_layer->SetThumbThickness(thumb_size_.Read(*this).height());
      scrollbar_layer->SetThumbLength(thumb_size_.Read(*this).width());
    } else {
      scrollbar_layer->SetThumbThickness(thumb_size_.Read(*this).width());
      scrollbar_layer->SetThumbLength(thumb_size_.Read(*this).height());
    }

    if (track_and_buttons_resource_.Read(*this)) {
      scrollbar_layer->set_track_and_buttons_ui_resource_id(
          track_and_buttons_resource_.Read(*this)->id());
    } else {
      scrollbar_layer->set_track_and_buttons_ui_resource_id(0);
    }
    if (thumb_resource_.Read(*this)) {
      scrollbar_layer->set_thumb_ui_resource_id(
          thumb_resource_.Read(*this)->id());
    } else {
      scrollbar_layer->set_thumb_ui_resource_id(0);
    }

    scrollbar_layer->SetScrollbarPaintedOpacity(painted_opacity_.Read(*this));

    scrollbar_layer->set_is_overlay_scrollbar(is_overlay_);
    scrollbar_layer->set_is_web_test(is_web_test_);

    if (thumb_color_.Read(*this).has_value()) {
      scrollbar_layer->SetThumbColor(thumb_color_.Read(*this).value());
    }
    if (uses_nine_patch_track_and_buttons_ &&
        track_and_buttons_resource_.Read(*this)) {
      const auto iter = commit_state.ui_resource_sizes.find(
          track_and_buttons_resource_.Read(*this)->id());
      const gfx::Size image_bounds =
          (iter == commit_state.ui_resource_sizes.end()) ? gfx::Size()
                                                         : iter->second;
      scrollbar_layer->SetTrackAndButtonsImageBounds(image_bounds);
      scrollbar_layer->SetTrackAndButtonsAperture(
          track_and_buttons_aperture_.Read(*this));
    } else {
      scrollbar_layer->SetTrackAndButtonsImageBounds(gfx::Size());
      scrollbar_layer->SetTrackAndButtonsAperture(gfx::Rect());
    }
    scrollbar_layer->set_uses_nine_patch_track_and_buttons(
        uses_nine_patch_track_and_buttons_);
  }
}

void PaintedScrollbarLayer::SetLayerTreeHost(LayerTreeHost* host) {
  // When the LTH is set to null or has changed, then this layer should remove
  // all of its associated resources.
  if (!host || host != layer_tree_host()) {
    track_and_buttons_resource_.Write(*this) = nullptr;
    thumb_resource_.Write(*this) = nullptr;
  }

  ScrollbarLayerBase::SetLayerTreeHost(host);
}

gfx::Size PaintedScrollbarLayer::LayerSizeToContentSize(
    const gfx::Size& layer_size) const {
  gfx::Size content_size =
      gfx::ScaleToCeiledSize(layer_size, internal_contents_scale_.Read(*this));
  // We should never return a rect bigger than the content bounds.
  content_size.SetToMin(internal_content_bounds_.Read(*this));
  return content_size;
}

bool PaintedScrollbarLayer::UpdateGeometry() {
  // These properties should never change.
  DCHECK_EQ(supports_drag_snap_back_,
            scrollbar_.Read(*this)->SupportsDragSnapBack());
  DCHECK_EQ(is_left_side_vertical_scrollbar(),
            scrollbar_.Read(*this)->IsLeftSideVerticalScrollbar());
  DCHECK_EQ(is_overlay_, scrollbar_.Read(*this)->IsOverlay());
  DCHECK_EQ(orientation(), scrollbar_.Read(*this)->Orientation());

  bool updated = false;
  const auto& scrollbar = scrollbar_.Read(*this);
  updated |= UpdateProperty(scrollbar->JumpOnTrackClick(),
                            &jump_on_track_click_.Write(*this));
  updated |= UpdateProperty(scrollbar->TrackRect(), &track_rect_.Write(*this));
  updated |= UpdateProperty(scrollbar->BackButtonRect(),
                            &back_button_rect_.Write(*this));
  updated |= UpdateProperty(scrollbar->ForwardButtonRect(),
                            &forward_button_rect_.Write(*this));
  updated |= UpdateProperty(scrollbar->HasThumb(), &has_thumb_.Write(*this));
  if (has_thumb_.Read(*this)) {
    gfx::Rect thumb_rect = scrollbar->ThumbRect();
    if (uses_solid_color_thumb_) {
      thumb_rect.Inset(scrollbar->SolidColorThumbInsets());
    }
    // Ignore ThumbRect's location because the PaintedScrollbarLayerImpl will
    // compute it from scroll offset.
    updated |= UpdateProperty(thumb_rect.size(), &thumb_size_.Write(*this));
  } else {
    updated |= UpdateProperty(gfx::Size(), &thumb_size_.Write(*this));
  }
  return updated;
}

bool PaintedScrollbarLayer::UpdateInternalContentScale() {
  gfx::Transform transform;
  transform = draw_property_utils::ScreenSpaceTransform(
      this, layer_tree_host()->property_trees()->transform_tree());

  gfx::Vector2dF transform_scales = gfx::ComputeTransform2dScaleComponents(
      transform, layer_tree_host()->device_scale_factor());
  float scale = std::max(transform_scales.x(), transform_scales.y());
  // Clamp minimum scale to 1 to avoid too low scale during scale animation.
  // TODO(crbug.com/40100995): Move rasterization of scrollbars to the impl side
  // to better handle scale changes.
  scale = std::max(1.0f, scale);

  bool updated = false;
  updated |= UpdateProperty(scale, &internal_contents_scale_.Write(*this));
  updated |= UpdateProperty(
      gfx::ScaleToCeiledSize(bounds(), internal_contents_scale_.Read(*this)),
      &internal_content_bounds_.Write(*this));
  return updated;
}

bool PaintedScrollbarLayer::Update() {
  bool updated = false;

  updated |= ScrollbarLayerBase::Update();
  const bool internal_content_scaled = UpdateInternalContentScale();
  updated |= internal_content_scaled;
  updated |= UpdateGeometry();
  const bool tickmarks_status_changed =
      SetHasFindInPageTickmarks(scrollbar_.Read(*this)->HasTickmarks());
  updated |= tickmarks_status_changed;

  if (internal_content_bounds_.Read(*this).IsEmpty()) {
    if (track_and_buttons_resource_.Read(*this)) {
      track_and_buttons_resource_.Write(*this) = nullptr;
      thumb_resource_.Write(*this) = nullptr;
      SetNeedsPushProperties();
      updated = true;
    }
    return updated;
  }

  if (!has_thumb_.Read(*this) && thumb_resource_.Read(*this)) {
    thumb_resource_.Write(*this) = nullptr;
    SetNeedsPushProperties();
    updated = true;
  }

  // Scaling content requires scrollbars to be repainted to give the arrows
  // appropriate proportions and tickmarks changing status needs a repaint to
  // avoid incorrectly stretching the smaller 9patch bitmap.
  updated |= UpdateTrackAndButtonsIfNeeded(
      uses_nine_patch_track_and_buttons_ &&
      (internal_content_scaled || tickmarks_status_changed));
  updated |= UpdateThumbIfNeeded();

  return updated;
}

bool PaintedScrollbarLayer::UpdateTrackAndButtonsIfNeeded(
    bool force_repaint_for_nine_patch) {
  bool updated = false;
  gfx::Size size = bounds();
  gfx::Size scaled_size = internal_content_bounds_.Read(*this);
  if (!track_and_buttons_resource_.Read(*this) ||
      scrollbar_.Read(*this)->TrackAndButtonsNeedRepaint() ||
      force_repaint_for_nine_patch) {
    if (uses_nine_patch_track_and_buttons_ &&
        // Can't use nine-patch track and buttons if tickmarks are present.
        !scrollbar_.Read(*this)->HasTickmarks()) {
      size = scrollbar_.Read(*this)->NinePatchTrackAndButtonsCanvasSize(
          /*scale=*/1.f);
      const float scale = internal_contents_scale_.Read(*this);
      scaled_size =
          scrollbar_.Read(*this)->NinePatchTrackAndButtonsCanvasSize(scale);
      track_and_buttons_aperture_.Write(*this) =
          scrollbar_.Read(*this)->NinePatchTrackAndButtonsAperture(scale);
    }

    track_and_buttons_resource_.Write(*this) = ScopedUIResource::Create(
        layer_tree_host()->GetUIResourceManager(),
        RasterizeScrollbarPart(size, scaled_size,
                               [this, size](PaintCanvas& canvas) {
                                 scrollbar_.Write(*this)->PaintTrackAndButtons(
                                     canvas, gfx::Rect(size));
                               }));
    SetNeedsPushProperties();
    updated = true;
  }

  return updated;
}

bool PaintedScrollbarLayer::UpdateThumbIfNeeded() {
  bool updated = false;
  // If the scrollbar uses solid color thumb, it sends the correct color for
  // the thumb to the Impl class instead of generating a bitmap.
#if !BUILDFLAG(ARKWEB_SCROLLBAR)
  if (uses_solid_color_thumb_) {
    if (scrollbar_.Read(*this)->ThumbNeedsRepaint() ||
        !thumb_color_.Read(*this).has_value()) {
      const SkColor4f thumb_color = scrollbar_.Read(*this)->ThumbColor();
      if (!thumb_color_.Read(*this).has_value() ||
          thumb_color != thumb_color_.Read(*this).value()) {
        thumb_color_.Write(*this) = thumb_color;
        SetNeedsPushProperties();
        updated = true;
      }
      // Clear thumb needs repaint regardless of if the thumb's color changed.
      scrollbar_.Write(*this)->ClearThumbNeedsRepaint();
    }
    return updated;
  }
#endif

  gfx::Size thumb_size = thumb_size_.Read(*this);
  gfx::Size scaled_thumb_size = LayerSizeToContentSize(thumb_size);
  if (has_thumb_.Read(*this) && !scaled_thumb_size.IsEmpty()) {
    if (!thumb_resource_.Read(*this) ||
        scrollbar_.Read(*this)->ThumbNeedsRepaint() ||
        scaled_thumb_size !=
            thumb_resource_.Write(*this)->GetBitmap(0, false).GetSize()) {
      thumb_resource_.Write(*this) = ScopedUIResource::Create(
          layer_tree_host()->GetUIResourceManager(),
          RasterizeScrollbarPart(thumb_size, scaled_thumb_size,
                                 [this, thumb_size](PaintCanvas& canvas) {
                                   scrollbar_.Write(*this)->PaintThumb(
                                       canvas, gfx::Rect(thumb_size));
                                 }));
      SetNeedsPushProperties();
      updated = true;
    }
    updated |= UpdateProperty(scrollbar_.Read(*this)->Opacity(),
                              &painted_opacity_.Write(*this));
  }

  return updated;
}

UIResourceBitmap PaintedScrollbarLayer::RasterizeScrollbarPart(
    const gfx::Size& size,
    const gfx::Size& requested_content_size,
    base::FunctionRef<void(PaintCanvas&)> paint_function) {
  DCHECK(!requested_content_size.IsEmpty());
  DCHECK(!size.IsEmpty());

  gfx::Size content_size = requested_content_size;

  // Pages can end up requesting arbitrarily large scrollbars.  Prevent this
  // from crashing due to OOM and try something smaller.
  SkBitmap skbitmap;
  bool allocation_succeeded =
      skbitmap.tryAllocN32Pixels(content_size.width(), content_size.height());
  // Assuming 4bpp, caps at 4M.
  constexpr int kMinScrollbarDimension = 1024;
  int dimension = std::max(content_size.width(), content_size.height()) / 2;
  while (!allocation_succeeded && dimension >= kMinScrollbarDimension) {
    content_size.SetToMin(gfx::Size(dimension, dimension));
    allocation_succeeded =
        skbitmap.tryAllocN32Pixels(content_size.width(), content_size.height());
    if (!allocation_succeeded)
      dimension = dimension / 2;
  }
  CHECK(allocation_succeeded)
      << "Failed to allocate memory for scrollbar at dimension : " << dimension;

  SkiaPaintCanvas canvas(skbitmap);
  canvas.clear(SkColors::kTransparent);

  float scale_x = content_size.width() / static_cast<float>(size.width());
  float scale_y = content_size.height() / static_cast<float>(size.height());
  canvas.scale(SkFloatToScalar(scale_x), SkFloatToScalar(scale_y));
  paint_function(canvas);
  // Make sure that the pixels are no longer mutable to unavoid unnecessary
  // allocation and copying.
  skbitmap.setImmutable();

  return UIResourceBitmap(skbitmap);
}

ScrollbarLayerBase::ScrollbarLayerType
PaintedScrollbarLayer::GetScrollbarLayerType() const {
  return kPainted;
}

}  // namespace cc