// Copyright 2018 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/snap_fling_controller.h"

#include <utility>

#include "cc/base/features.h"
#include "cc/input/snap_fling_curve.h"

namespace cc {
namespace {

// If the inertial scroll doesn't decay below the threshold for this many
// updates we predict the scroll using the current decay. This ensures that even
// if our minimum decay is not low enough we still apply a prediction.
constexpr int kMaxFramesToWaitForDecay = 3;

}  // namespace

SnapFlingController::SnapFlingController(SnapFlingClient* client)
    : client_(client), state_(State::kIdle) {}

SnapFlingController::~SnapFlingController() = default;

bool SnapFlingController::FilterEventForSnap(
    SnapFlingController::GestureScrollType gesture_scroll_type) {
  switch (gesture_scroll_type) {
    case GestureScrollType::kBegin: {
      ClearSnapFling();
      return false;
    }
    // TODO(sunyunjia): Need to update the existing snap curve if the GSU is
    // from a fling boosting event.
    case GestureScrollType::kUpdate:
    case GestureScrollType::kEnd: {
      return state_ == State::kActive || state_ == State::kFinished;
    }
  }
}

void SnapFlingController::ClearSnapFling() {
  if (state_ == State::kActive)
    client_->ScrollEndForSnapFling(false /* did_finish */);

  curve_.reset();
  last_inertial_delta_.reset();
  consecutive_decay_frames_ = 0;
  state_ = State::kIdle;
}

void SnapFlingController::Finish() {
  if (state_ != State::kActive) {
    return;
  }

  client_->ScrollEndForSnapFling(true /* did_finish */);
  state_ = State::kFinished;
}

bool SnapFlingController::HandleGestureScrollUpdate(
    const SnapFlingController::GestureScrollUpdateInfo& info) {
  DCHECK(state_ == State::kIdle || state_ == State::kIgnored);
  if (!info.is_in_inertial_phase || info.is_overscroll) {
    last_inertial_delta_.reset();
    consecutive_decay_frames_ = 0;
    return false;
  }

  if (last_inertial_delta_.has_value() &&
      info.delta.LengthSquared() < last_inertial_delta_->LengthSquared()) {
    consecutive_decay_frames_++;
  } else {
    consecutive_decay_frames_ = 0;
  }
  bool allow_slow_decay = consecutive_decay_frames_ >= kMaxFramesToWaitForDecay;

  std::optional<gfx::Vector2dF> ending_displacement =
      SnapFlingCurve::EstimateDisplacement(info.delta, last_inertial_delta_,
                                           allow_slow_decay);
  last_inertial_delta_ = info.delta;

  if (!ending_displacement.has_value()) {
    return false;
  }

  gfx::PointF target_offset, start_offset;
  if (!client_->GetSnapFlingInfoAndSetAnimatingSnapTarget(
          info.delta, *ending_displacement, &start_offset, &target_offset)) {
    state_ = State::kIgnored;
    return false;
  }

  if (start_offset == target_offset) {
    client_->ScrollEndForSnapFling(true /* did_finish */);
    state_ = State::kFinished;
    return true;
  }

  curve_ = std::make_unique<SnapFlingCurve>(start_offset, target_offset,
                                            info.event_time);
  state_ = State::kActive;
  Animate(info.event_time);
  return true;
}

void SnapFlingController::Animate(base::TimeTicks time) {
  if (state_ != State::kActive)
    return;

  if (curve_->IsFinished()) {
    client_->ScrollEndForSnapFling(true /* did_finish */);
    state_ = State::kFinished;
    return;
  }
  gfx::Vector2dF snapped_delta = curve_->GetScrollDelta(time);
  gfx::PointF current_offset = client_->ScrollByForSnapFling(snapped_delta);
  // The fling may be canceled if we hit the snap constraint.
  if (state_ != State::kActive) {
    return;
  }
  curve_->UpdateCurrentOffset(current_offset);
  client_->RequestAnimationForSnapFling();
}

void SnapFlingController::SetCurveForTest(
    std::unique_ptr<SnapFlingCurve> curve) {
  curve_ = std::move(curve);
  state_ = State::kActive;
}

}  // namespace cc
