// 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/input/single_scrollbar_animation_controller_thinning.h"

#include <algorithm>

#include "base/memory/ptr_util.h"
#include "base/time/time.h"
#include "cc/base/math_util.h"
#include "cc/input/scrollbar_animation_controller.h"
#include "cc/layers/layer_impl.h"
#include "cc/layers/scrollbar_layer_impl_base.h"
#include "cc/trees/layer_tree_impl.h"

namespace cc {

namespace {

float DistanceToRect(const gfx::PointF& device_viewport_point,
                     const ScrollbarLayerImplBase& scrollbar,
                     const gfx::Rect& rect) {
  gfx::RectF device_viewport_rect = MathUtil::MapClippedRect(
      scrollbar.ScreenSpaceTransform(), gfx::RectF(rect));

  return device_viewport_rect.ManhattanDistanceToPoint(device_viewport_point) /
         scrollbar.layer_tree_impl()->device_scale_factor();
}

float DistanceToScrollbar(const gfx::PointF& device_viewport_point,
                          const ScrollbarLayerImplBase& scrollbar) {
  return DistanceToRect(device_viewport_point, scrollbar,
                        gfx::Rect(scrollbar.bounds()));
}

float DistanceToScrollbarThumb(const gfx::PointF& device_viewport_point,
                               const ScrollbarLayerImplBase& scrollbar) {
  return DistanceToRect(device_viewport_point, scrollbar,
                        scrollbar.ComputeHitTestableExpandedThumbQuadRect());
}

}  // namespace

std::unique_ptr<SingleScrollbarAnimationControllerThinning>
SingleScrollbarAnimationControllerThinning::Create(
    ElementId scroll_element_id,
    ScrollbarOrientation orientation,
    ScrollbarAnimationControllerClient* client,
    base::TimeDelta thinning_duration,
    float idle_thickness_scale) {
  return base::WrapUnique(new SingleScrollbarAnimationControllerThinning(
      scroll_element_id, orientation, client, thinning_duration,
      idle_thickness_scale));
}

SingleScrollbarAnimationControllerThinning::
    SingleScrollbarAnimationControllerThinning(
        ElementId scroll_element_id,
        ScrollbarOrientation orientation,
        ScrollbarAnimationControllerClient* client,
        base::TimeDelta thinning_duration,
        float idle_thickness_scale)
    : client_(client),
      scroll_element_id_(scroll_element_id),
      orientation_(orientation),
      thinning_duration_(thinning_duration),
      idle_thickness_scale_(idle_thickness_scale) {
  ApplyThumbThicknessScale(idle_thickness_scale_);
}

ScrollbarLayerImplBase*
SingleScrollbarAnimationControllerThinning::GetScrollbar() const {
  for (ScrollbarLayerImplBase* scrollbar :
       client_->ScrollbarsFor(scroll_element_id_)) {
    DCHECK(scrollbar->is_overlay_scrollbar());

    if (scrollbar->orientation() == orientation_)
      return scrollbar;
  }

  return nullptr;
}

bool SingleScrollbarAnimationControllerThinning::Animate(base::TimeTicks now) {
  if (!is_animating_)
    return false;

  if (last_awaken_time_.is_null())
    last_awaken_time_ = now;

  float progress = AnimationProgressAtTime(now);
  RunAnimationFrame(progress);

  return true;
}

float SingleScrollbarAnimationControllerThinning::AnimationProgressAtTime(
    base::TimeTicks now) {
  // In tests, there may be no duration; snap to the end in such a case.
  if (thinning_duration_.is_zero())
    return 1.0f;

  const base::TimeDelta delta = now - last_awaken_time_;
  return std::clamp(static_cast<float>(delta / thinning_duration_), 0.0f, 1.0f);
}

void SingleScrollbarAnimationControllerThinning::RunAnimationFrame(
    float progress) {
  if (captured_)
    return;

  ApplyThumbThicknessScale(ThumbThicknessScaleAt(progress));

  client_->SetNeedsRedrawForScrollbarAnimation();
  if (progress == 1.f) {
    StopAnimation();
    thickness_change_ = AnimationChange::kNone;
  }
}

void SingleScrollbarAnimationControllerThinning::StartAnimation() {
  is_animating_ = true;
  last_awaken_time_ = base::TimeTicks();
  client_->SetNeedsAnimateForScrollbarAnimation();
}

void SingleScrollbarAnimationControllerThinning::StopAnimation() {
  is_animating_ = false;
}

void SingleScrollbarAnimationControllerThinning::DidScrollUpdate() {
  if (captured_ || !mouse_is_near_scrollbar_) {
    return;
  }

  CalculateThicknessShouldChange(device_viewport_last_pointer_location_);
  // If scrolling with the pointer on top of the scrollbar, force the scrollbar
  // to expand.
  if (thickness_change_ == AnimationChange::kNone) {
    UpdateThumbThicknessScale();
  }
}

void SingleScrollbarAnimationControllerThinning::DidMouseDown() {
  // When invisible, Fluent scrollbars are disabled and their thumb has no
  // dimensions, which causes mouse_is_over_scrollbar_thumb_ to always be false.
  // This check updates the thumb variable to cover the cases where you mouse
  // over the invisible thumb, make it appear by some mechanism (tickmarks,
  // scrolling, etc.) and press mouse down without moving your pointer.
  if (client_->IsFluentOverlayScrollbar() && !mouse_is_over_scrollbar_thumb_) {
    ScrollbarLayerImplBase* scrollbar = GetScrollbar();
    if (scrollbar) {
      const float distance_to_scrollbar_thumb = DistanceToScrollbarThumb(
          device_viewport_last_pointer_location_, *scrollbar);
      mouse_is_over_scrollbar_thumb_ = distance_to_scrollbar_thumb == 0.0f;
    }
  }

  if (!mouse_is_over_scrollbar_thumb_)
    return;

  captured_ = true;
  UpdateThumbThicknessScale();
}

void SingleScrollbarAnimationControllerThinning::DidMouseUp() {
  if (!captured_)
    return;

  captured_ = false;
  StopAnimation();

  // On mouse up, Fluent scrollbars go straight to the scrollbar disappearance
  // animation (via ScrollbarAnimationController) without queueing a thinning
  // animation.
  const bool thickness_should_decrease =
      !client_->IsFluentOverlayScrollbar() && !mouse_is_near_scrollbar_thumb_;

  if (thickness_should_decrease) {
    thickness_change_ = AnimationChange::kDecrease;
    StartAnimation();
  } else {
    thickness_change_ = AnimationChange::kNone;
  }
}

void SingleScrollbarAnimationControllerThinning::DidMouseLeave() {
  mouse_is_over_scrollbar_thumb_ = false;
  mouse_is_near_scrollbar_thumb_ = false;
  mouse_is_near_scrollbar_ = false;

  if (captured_) {
    return;
  }

  // If fully expanded, Fluent scrollbars don't queue a thinning animation and
  // let the ScrollbarAnimationController make the scrollbars disappear.
  if (client_->IsFluentOverlayScrollbar() &&
      thickness_change_ == AnimationChange::kNone) {
    return;
  }

  thickness_change_ = AnimationChange::kDecrease;
  StartAnimation();
}

void SingleScrollbarAnimationControllerThinning::DidMouseMove(
    const gfx::PointF& device_viewport_point) {
  CalculateThicknessShouldChange(device_viewport_point);
  device_viewport_last_pointer_location_ = device_viewport_point;
}

void SingleScrollbarAnimationControllerThinning::CalculateThicknessShouldChange(
    const gfx::PointF& device_viewport_point) {
  ScrollbarLayerImplBase* scrollbar = GetScrollbar();

  if (!scrollbar)
    return;

  const float distance_to_scrollbar =
      DistanceToScrollbar(device_viewport_point, *scrollbar);
  const float distance_to_scrollbar_thumb =
      DistanceToScrollbarThumb(device_viewport_point, *scrollbar);

  const bool mouse_is_near_scrollbar =
      distance_to_scrollbar <= MouseMoveDistanceToTriggerFadeIn();

  const bool mouse_is_over_scrollbar_thumb =
      distance_to_scrollbar_thumb == 0.0f;
  const bool mouse_is_near_scrollbar_thumb =
      distance_to_scrollbar_thumb <= MouseMoveDistanceToTriggerExpand();
  bool thickness_should_change;
  if (client_->IsFluentOverlayScrollbar()) {
    const bool is_visible = scrollbar->OverlayScrollbarOpacity() > 0.f;
    const bool moved_over_scrollbar =
        mouse_is_near_scrollbar_ != mouse_is_near_scrollbar;
    const bool mouse_far_from_scrollbar =
        (!mouse_is_near_scrollbar &&
         thickness_change_ == AnimationChange::kNone);
    // On mouse move Fluent scrollbars will queue a thinning animation iff the
    // scrollbar is visible and either the mouse has moved over the scrollbar
    // (increase thickness) or the mouse has moved far away from the scrollbar
    // and there is no previously queued animation (decreasse thickness).
    // If tickmarks are shown, the scrollbars should be and should remain in
    // Full mode.
    thickness_should_change =
        !tickmarks_showing_ && is_visible &&
        (moved_over_scrollbar || mouse_far_from_scrollbar);
  } else {
    thickness_should_change =
        (mouse_is_near_scrollbar_thumb_ != mouse_is_near_scrollbar_thumb);
  }

  if (!captured_ && thickness_should_change) {
    const bool thickness_should_increase = client_->IsFluentOverlayScrollbar()
                                               ? mouse_is_near_scrollbar
                                               : mouse_is_near_scrollbar_thumb;
    thickness_change_ = thickness_should_increase ? AnimationChange::kIncrease
                                                  : AnimationChange::kDecrease;
    StartAnimation();
  }

  mouse_is_near_scrollbar_ = mouse_is_near_scrollbar;
  mouse_is_near_scrollbar_thumb_ = mouse_is_near_scrollbar_thumb;
  mouse_is_over_scrollbar_thumb_ = mouse_is_over_scrollbar_thumb;
}

float SingleScrollbarAnimationControllerThinning::ThumbThicknessScaleAt(
    float progress) const {
  CHECK_NE(thickness_change_, AnimationChange::kNone);
  float factor = thickness_change_ == AnimationChange::kIncrease
                     ? progress
                     : (1.f - progress);
  return ((1.f - idle_thickness_scale_) * factor) + idle_thickness_scale_;
}

float SingleScrollbarAnimationControllerThinning::AdjustScale(
    float new_value,
    float current_value,
    AnimationChange animation_change,
    float min_value,
    float max_value) {
  float result;
  if (animation_change == AnimationChange::kIncrease &&
      current_value > new_value) {
    result = current_value;
  } else if (animation_change == AnimationChange::kDecrease &&
             current_value < new_value) {
    result = current_value;
  } else {
    result = new_value;
  }
  if (result > max_value)
    return max_value;
  if (result < min_value)
    return min_value;
  return result;
}

float SingleScrollbarAnimationControllerThinning::
    CurrentForcedThumbThicknessScale() const {
  bool thumb_should_be_expanded;
  if (client_->IsFluentOverlayScrollbar()) {
    thumb_should_be_expanded = mouse_is_near_scrollbar_ || tickmarks_showing_;
  } else {
    thumb_should_be_expanded = mouse_is_near_scrollbar_thumb_;
  }
  thumb_should_be_expanded |= captured_;
  return thumb_should_be_expanded ? 1.f : idle_thickness_scale_;
}

void SingleScrollbarAnimationControllerThinning::UpdateThumbThicknessScale() {
  StopAnimation();
  ApplyThumbThicknessScale(CurrentForcedThumbThicknessScale());
}

#if BUILDFLAG(ARKWEB_SCROLLBAR)
void SingleScrollbarAnimationControllerThinning::DidRequestShow() {
  if (thickness_change_ == AnimationChange::kNone) {
    UpdateThumbThicknessScale();
  }
}
#endif

void SingleScrollbarAnimationControllerThinning::ApplyThumbThicknessScale(
    float thumb_thickness_scale) {
  for (auto* scrollbar : client_->ScrollbarsFor(scroll_element_id_)) {
    if (scrollbar->orientation() != orientation_)
      continue;
    DCHECK(scrollbar->is_overlay_scrollbar());

    float scale = AdjustScale(thumb_thickness_scale,
                              scrollbar->thumb_thickness_scale_factor(),
                              thickness_change_, idle_thickness_scale_, 1);

    scrollbar->SetThumbThicknessScaleFactor(scale);
  }
}

void SingleScrollbarAnimationControllerThinning::UpdateTickmarksVisibility(
    bool show) {
  tickmarks_showing_ = show;
  if (show) {
    UpdateThumbThicknessScale();
  }
}

float SingleScrollbarAnimationControllerThinning::
    MouseMoveDistanceToTriggerExpand() {
  return client_->IsFluentOverlayScrollbar() ? 0.0f : 25.0f;
}

float SingleScrollbarAnimationControllerThinning::
    MouseMoveDistanceToTriggerFadeIn() {
  return client_->IsFluentOverlayScrollbar() ? 0.0f : 30.0f;
}

}  // namespace cc