// 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/animation/scroll_offset_animation_curve.h"

#include <algorithm>
#include <cmath>
#include <utility>

#include "base/check_op.h"
#include "base/memory/ptr_util.h"
#include "cc/base/features.h"
#include "ui/gfx/animation/keyframe/timing_function.h"
#include "ui/gfx/animation/tween.h"

const double kConstantDuration = 9.0;
const double kDurationDivisor = 60.0;

struct CubicBezierPoints {
  double x1;
  double y1;
  double x2;
  double y2;
};

// See `ui/gfx/animation/keyframe/timing_function.cc`
static constexpr CubicBezierPoints kEaseInOutControlPoints{
    .x1 = 0.42,
    .y1 = 0,
    .x2 = 0.58,
    .y2 = 1,
};

CubicBezierPoints GetCubicBezierPointsForProgrammaticScroll() {
  return {
      .x1 = features::kCubicBezierX1.Get(),
      .y1 = features::kCubicBezierY1.Get(),
      .x2 = features::kCubicBezierX2.Get(),
      .y2 = features::kCubicBezierY2.Get(),
  };
}

const double kInverseDeltaRampStartPx = 120.0;
const double kInverseDeltaRampEndPx = 480.0;
const double kInverseDeltaMinDuration = 6.0;
const double kInverseDeltaMaxDuration = 12.0;

const double kInverseDeltaSlope =
    (kInverseDeltaMinDuration - kInverseDeltaMaxDuration) /
    (kInverseDeltaRampEndPx - kInverseDeltaRampStartPx);

const double kInverseDeltaOffset =
    kInverseDeltaMaxDuration - kInverseDeltaRampStartPx * kInverseDeltaSlope;

using gfx::CubicBezierTimingFunction;
using gfx::LinearTimingFunction;
using gfx::TimingFunction;

namespace cc {

namespace {

const double kEpsilon = 0.01f;

static float MaximumDimension(const gfx::Vector2dF& delta) {
  return std::abs(delta.x()) > std::abs(delta.y()) ? delta.x() : delta.y();
}

std::unique_ptr<TimingFunction> EaseInOutWithInitialSlope(
    const CubicBezierPoints& control_points,
    double slope) {
  // Clamp slope to a sane value.
  slope = std::clamp(slope, -1000.0, 1000.0);
  // Scale the first control point with `slope`.
  return CubicBezierTimingFunction::Create(
      control_points.x1, control_points.x1 * slope, control_points.x2,
      control_points.y2);
}

base::TimeDelta VelocityBasedDurationBound(gfx::Vector2dF old_delta,
                                           double velocity,
                                           gfx::Vector2dF new_delta) {
  double new_delta_max_dimension = MaximumDimension(new_delta);

  // If we are already at the target, stop animating.
  if (std::abs(new_delta_max_dimension) < kEpsilon)
    return base::TimeDelta();

  // Guard against division by zero.
  if (std::abs(velocity) < kEpsilon) {
    return base::TimeDelta::Max();
  }

  // Estimate how long it will take to reach the new target at our present
  // velocity, with some fudge factor to account for the "ease out".
  double bound = (new_delta_max_dimension / velocity) * 2.5f;

  // If bound < 0 we are moving in the opposite direction.
  return bound < 0 ? base::TimeDelta::Max() : base::Seconds(bound);
}

}  // namespace

std::optional<double>
    ScrollOffsetAnimationCurve::animation_duration_for_testing_;

ScrollOffsetAnimationCurve::ScrollOffsetAnimationCurve(
    const gfx::PointF& target_value,
    AnimationType animation_type,
    ScrollType scroll_type,
    std::optional<DurationBehavior> duration_behavior)
    : target_value_(target_value),
      animation_type_(animation_type),
      scroll_type_(scroll_type),
      duration_behavior_(duration_behavior),
      has_set_initial_value_(false) {
  DCHECK_EQ(animation_type == AnimationType::kEaseInOut,
            duration_behavior.has_value());
  switch (animation_type) {
    case AnimationType::kEaseInOut:
      timing_function_ = GetEasingFunction(/*slope=*/std::nullopt);
      break;
    case AnimationType::kLinear:
      timing_function_ = LinearTimingFunction::Create();
      break;
  }
}

ScrollOffsetAnimationCurve::ScrollOffsetAnimationCurve(
    const gfx::PointF& target_value,
    std::unique_ptr<TimingFunction> timing_function,
    AnimationType animation_type,
    ScrollType scroll_type,
    std::optional<DurationBehavior> duration_behavior)
    : target_value_(target_value),
      timing_function_(std::move(timing_function)),
      animation_type_(animation_type),
      scroll_type_(scroll_type),
      duration_behavior_(duration_behavior),
      has_set_initial_value_(false) {
  DCHECK_EQ(animation_type == AnimationType::kEaseInOut,
            duration_behavior.has_value());
}

ScrollOffsetAnimationCurve::~ScrollOffsetAnimationCurve() = default;

base::TimeDelta ScrollOffsetAnimationCurve::EaseInOutSegmentDuration(
    const gfx::Vector2dF& delta,
    DurationBehavior duration_behavior,
    base::TimeDelta delayed_by) {
  double duration = kConstantDuration;
  if (!animation_duration_for_testing_) {
    switch (duration_behavior) {
      case DurationBehavior::kConstant:
        duration = kConstantDuration;
        break;
      case DurationBehavior::kDeltaBased: {
        CHECK_EQ(scroll_type_, ScrollType::kProgrammatic);
        duration = std::min<double>(
            std::sqrt(std::abs(MaximumDimension(delta))),
            features::kMaxAnimationDuration.Get().InSecondsF() *
                kDurationDivisor);
        break;
      }
      case DurationBehavior::kInverseDelta:
        duration = kInverseDeltaOffset +
                   std::abs(MaximumDimension(delta)) * kInverseDeltaSlope;
        duration = std::clamp(duration, kInverseDeltaMinDuration,
                              kInverseDeltaMaxDuration);
        break;
    }
    duration /= kDurationDivisor;
  } else {
    duration = animation_duration_for_testing_.value();
  }

  base::TimeDelta delay_adjusted_duration =
      base::Seconds(duration) - delayed_by;
  return (delay_adjusted_duration >= base::TimeDelta())
             ? delay_adjusted_duration
             : base::TimeDelta();
}

base::TimeDelta ScrollOffsetAnimationCurve::EaseInOutBoundedSegmentDuration(
    const gfx::Vector2dF& new_delta,
    base::TimeDelta t,
    base::TimeDelta delayed_by) {
  gfx::Vector2dF old_delta = target_value_ - initial_value_;
  double velocity = CalculateVelocity(t);

  // Use the velocity-based duration bound when it is less than the constant
  // segment duration. This minimizes the "rubber-band" bouncing effect when
  // |velocity| is large and |new_delta| is small.
  return std::min(EaseInOutSegmentDuration(
                      new_delta, duration_behavior_.value(), delayed_by),
                  VelocityBasedDurationBound(old_delta, velocity, new_delta));
}

base::TimeDelta ScrollOffsetAnimationCurve::SegmentDuration(
    const gfx::Vector2dF& delta,
    base::TimeDelta delayed_by,
    std::optional<double> velocity) {
  switch (animation_type_) {
    case AnimationType::kEaseInOut:
      DCHECK(duration_behavior_.has_value());
      return EaseInOutSegmentDuration(delta, duration_behavior_.value(),
                                      delayed_by);
    case AnimationType::kLinear:
      DCHECK(velocity.has_value());
      return LinearSegmentDuration(delta, delayed_by, velocity.value());
  }
}

// static
base::TimeDelta ScrollOffsetAnimationCurve::LinearSegmentDuration(
    const gfx::Vector2dF& delta,
    base::TimeDelta delayed_by,
    float velocity) {
  double duration_in_seconds =
      (animation_duration_for_testing_.has_value())
          ? animation_duration_for_testing_.value()
          : std::abs(MaximumDimension(delta) / velocity);
  base::TimeDelta delay_adjusted_duration =
      base::Seconds(duration_in_seconds) - delayed_by;
  return (delay_adjusted_duration >= base::TimeDelta())
             ? delay_adjusted_duration
             : base::TimeDelta();
}

void ScrollOffsetAnimationCurve::SetInitialValue(
    const gfx::PointF& initial_value,
    base::TimeDelta delayed_by,
    float velocity) {
  initial_value_ = initial_value;
  has_set_initial_value_ = true;

  gfx::Vector2dF delta = target_value_ - initial_value;
  total_animation_duration_ = SegmentDuration(delta, delayed_by, velocity);
}

bool ScrollOffsetAnimationCurve::HasSetInitialValue() const {
  return has_set_initial_value_;
}

void ScrollOffsetAnimationCurve::ApplyAdjustment(
    const gfx::Vector2dF& adjustment) {
  initial_value_ = initial_value_ + adjustment;
  target_value_ = target_value_ + adjustment;
}

gfx::PointF ScrollOffsetAnimationCurve::GetValue(base::TimeDelta t) const {
  const base::TimeDelta duration = total_animation_duration_ - last_retarget_;
  t -= last_retarget_;

  if (duration.is_zero() || (t >= duration))
    return target_value_;
  if (t <= base::TimeDelta())
    return initial_value_;

  const double progress = timing_function_->GetValue(
      t / duration, TimingFunction::LimitDirection::RIGHT);
  return gfx::PointF(gfx::Tween::FloatValueBetween(progress, initial_value_.x(),
                                                   target_value_.x()),
                     gfx::Tween::FloatValueBetween(progress, initial_value_.y(),
                                                   target_value_.y()));
}

base::TimeDelta ScrollOffsetAnimationCurve::Duration() const {
  return total_animation_duration_;
}

int ScrollOffsetAnimationCurve::Type() const {
  return AnimationCurve::SCROLL_OFFSET;
}

const char* ScrollOffsetAnimationCurve::TypeName() const {
  return "ScrollOffset";
}

std::unique_ptr<gfx::AnimationCurve> ScrollOffsetAnimationCurve::Clone() const {
  return CloneToScrollOffsetAnimationCurve();
}

void ScrollOffsetAnimationCurve::Tick(
    base::TimeDelta t,
    int property_id,
    gfx::KeyframeModel* keyframe_model,
    gfx::TimingFunction::LimitDirection unused) const {
  if (target_) {
    target_->OnScrollOffsetAnimated(GetValue(t), property_id, keyframe_model);
  }
}

std::unique_ptr<ScrollOffsetAnimationCurve>
ScrollOffsetAnimationCurve::CloneToScrollOffsetAnimationCurve() const {
  std::unique_ptr<TimingFunction> timing_function(
      static_cast<TimingFunction*>(timing_function_->Clone().release()));
  std::unique_ptr<ScrollOffsetAnimationCurve> curve_clone =
      base::WrapUnique(new ScrollOffsetAnimationCurve(
          target_value_, std::move(timing_function), animation_type_,
          scroll_type_, duration_behavior_));
  curve_clone->initial_value_ = initial_value_;
  curve_clone->total_animation_duration_ = total_animation_duration_;
  curve_clone->last_retarget_ = last_retarget_;
  curve_clone->has_set_initial_value_ = has_set_initial_value_;
  return curve_clone;
}

void ScrollOffsetAnimationCurve::SetAnimationDurationForTesting(
    base::TimeDelta duration) {
  animation_duration_for_testing_ = duration.InSecondsF();
}

double ScrollOffsetAnimationCurve::CalculateVelocity(base::TimeDelta t) {
  base::TimeDelta duration = total_animation_duration_ - last_retarget_;
  const double slope =
      timing_function_->Velocity((t - last_retarget_) / duration);

  gfx::Vector2dF delta = target_value_ - initial_value_;

  // TimingFunction::Velocity just gives the slope of the curve. Convert it to
  // units of pixels per second.
  return slope * (MaximumDimension(delta) / duration.InSecondsF());
}

std::unique_ptr<TimingFunction> ScrollOffsetAnimationCurve::GetEasingFunction(
    std::optional<double> slope) {
  CubicBezierPoints control_points = kEaseInOutControlPoints;
  if (scroll_type_ == ScrollType::kProgrammatic) {
    control_points = GetCubicBezierPointsForProgrammaticScroll();
  }
  if (slope) {
    return EaseInOutWithInitialSlope(control_points, *slope);
  }
  return CubicBezierTimingFunction::Create(control_points.x1, control_points.y1,
                                           control_points.x2,
                                           control_points.y2);
}

void ScrollOffsetAnimationCurve::UpdateTarget(base::TimeDelta t,
                                              const gfx::PointF& new_target) {
  DCHECK_NE(animation_type_, AnimationType::kLinear)
      << "UpdateTarget is not supported on linear scroll animations.";

  // UpdateTarget is still called for linear animations occasionally. This is
  // tracked via crbug.com/1164008.
  if (animation_type_ == AnimationType::kLinear)
    return;

  // If the new UpdateTarget actually happened before the previous one, keep
  // |t| as the most recent, but reduce the duration of any generated
  // animation.
  base::TimeDelta delayed_by = std::max(base::TimeDelta(), last_retarget_ - t);
  t = std::max(t, last_retarget_);

  if (animation_type_ == AnimationType::kEaseInOut &&
      std::abs(MaximumDimension(target_value_ - new_target)) < kEpsilon) {
    // Don't update the animation if the new target is the same as the old one.
    // This is done for EaseInOut-style animation curves, since the duration is
    // inversely proportional to the distance, and it may cause an animation
    // that is longer than the one currently running.
    // Specifically avoid doing this for Impulse-style animation curves since
    // its duration is directly proportional to the distance, and we don't want
    // to drop user input.
    target_value_ = new_target;
    return;
  }

  gfx::PointF current_position = GetValue(t);
  gfx::Vector2dF new_delta = new_target - current_position;

  // We are already at or very close to the new target. Stop animating.
  if (std::abs(MaximumDimension(new_delta)) < kEpsilon) {
    last_retarget_ = t;
    total_animation_duration_ = t;
    target_value_ = new_target;
    return;
  }

  // The last segment was of zero duration.
  base::TimeDelta old_duration = total_animation_duration_ - last_retarget_;
  if (old_duration.is_zero()) {
    DCHECK_EQ(t, last_retarget_);
    total_animation_duration_ = SegmentDuration(new_delta, delayed_by);
    target_value_ = new_target;
    return;
  }

  const base::TimeDelta new_duration =
      EaseInOutBoundedSegmentDuration(new_delta, t, delayed_by);
  if (new_duration.InSecondsF() < kEpsilon) {
    // The duration is (close to) 0, so stop the animation.
    target_value_ = new_target;
    total_animation_duration_ = t;
    return;
  }

  // Adjust the slope of the new animation in order to preserve the velocity of
  // the old animation.
  double velocity = CalculateVelocity(t);
  double new_slope =
      velocity * (new_duration.InSecondsF() / MaximumDimension(new_delta));

  timing_function_ = GetEasingFunction(new_slope);
  initial_value_ = current_position;
  target_value_ = new_target;
  total_animation_duration_ = t + new_duration;
  last_retarget_ = t;
}

const ScrollOffsetAnimationCurve*
ScrollOffsetAnimationCurve::ToScrollOffsetAnimationCurve(
    const AnimationCurve* c) {
  DCHECK_EQ(ScrollOffsetAnimationCurve::SCROLL_OFFSET, c->Type());
  return static_cast<const ScrollOffsetAnimationCurve*>(c);
}

ScrollOffsetAnimationCurve*
ScrollOffsetAnimationCurve::ToScrollOffsetAnimationCurve(AnimationCurve* c) {
  DCHECK_EQ(ScrollOffsetAnimationCurve::SCROLL_OFFSET, c->Type());
  return static_cast<ScrollOffsetAnimationCurve*>(c);
}

}  // namespace cc
