// 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 "ui/events/blink/fling_booster.h"

using blink::WebGestureEvent;
using blink::WebInputEvent;

namespace {
// Minimum fling velocity required for the active fling and new fling for the
// two to accumulate.
const double kMinBoostFlingSpeedSquare = 350. * 350.;

// Minimum velocity for the active touch scroll to preserve (boost) an active
// fling for which cancellation has been deferred.
const double kMinBoostTouchScrollSpeedSquare = 150 * 150.;

// Timeout window after which the active fling will be cancelled if no animation
// ticks, scrolls or flings of sufficient velocity relative to the current fling
// are received. The default value on Android native views is 40ms, but we use a
// slightly increased value to accomodate small IPC message delays.
const double kFlingBoostTimeoutDelaySeconds = 0.05;
}  // namespace

namespace ui {

FlingBooster::FlingBooster(const gfx::Vector2dF& fling_velocity,
                           blink::WebGestureDevice source_device,
                           int modifiers)
    : current_fling_velocity_(fling_velocity),
      source_device_(source_device),
      modifiers_(modifiers),
      deferred_fling_cancel_time_seconds_(0),
      last_fling_animate_time_seconds_(0),
      fling_boosted_(false) {}

bool FlingBooster::FilterGestureEventForFlingBoosting(
    const WebGestureEvent& gesture_event,
    bool* out_cancel_current_fling) {
  DCHECK(out_cancel_current_fling);
  *out_cancel_current_fling = false;

  if (gesture_event.GetType() == WebInputEvent::kGestureFlingCancel) {
    if (gesture_event.data.fling_cancel.prevent_boosting)
      return false;

    if (current_fling_velocity_.LengthSquared() < kMinBoostFlingSpeedSquare)
      return false;

    deferred_fling_cancel_time_seconds_ =
        gesture_event.TimeStampSeconds() + kFlingBoostTimeoutDelaySeconds;
    return true;
  }

  // A fling is either inactive or is "free spinning", i.e., has yet to be
  // interrupted by a touch gesture, in which case there is nothing to filter.
  if (!deferred_fling_cancel_time_seconds_)
    return false;

  // Gestures from a different source should immediately interrupt the fling.
  if (gesture_event.source_device != source_device_) {
    *out_cancel_current_fling = true;
    return false;
  }

  switch (gesture_event.GetType()) {
    case WebInputEvent::kGestureTapCancel:
    case WebInputEvent::kGestureTapDown:
      return false;

    case WebInputEvent::kGestureScrollBegin:
      // TODO(jdduke): Use |gesture_event.data.scrollBegin.delta{X,Y}Hint| to
      // determine if the ScrollBegin should immediately cancel the fling.
      ExtendBoostedFlingTimeout(gesture_event);
      return true;

    case WebInputEvent::kGestureScrollUpdate: {
      if (gesture_event.data.scroll_update.inertial_phase ==
          WebGestureEvent::kMomentumPhase) {
        // GSU events in momentum phase are generated by FlingController to
        // progress fling and should not interfere with fling boosting.
        return false;
      }

      if (ShouldSuppressScrollForFlingBoosting(gesture_event)) {
        ExtendBoostedFlingTimeout(gesture_event);
        return true;
      }

      *out_cancel_current_fling = true;
      return false;
    }

    case WebInputEvent::kGestureScrollEnd:
      // Clear the last fling boost event *prior* to fling cancellation,
      // preventing insertion of a synthetic GestureScrollBegin.
      last_fling_boost_event_ = WebGestureEvent();
      *out_cancel_current_fling = true;
      return true;

    case WebInputEvent::kGestureFlingStart: {
      DCHECK_EQ(source_device_, gesture_event.source_device);
      gfx::Vector2dF new_fling_velocity(
          gesture_event.data.fling_start.velocity_x,
          gesture_event.data.fling_start.velocity_y);
      DCHECK(!new_fling_velocity.IsZero());

      fling_boosted_ = ShouldBoostFling(gesture_event);
      if (fling_boosted_)
        current_fling_velocity_ += new_fling_velocity;
      else
        current_fling_velocity_ = new_fling_velocity;

      deferred_fling_cancel_time_seconds_ = 0;
      last_fling_boost_event_ = WebGestureEvent();
      return true;
    }

    default:
      // All other types of gestures (taps, presses, etc...) will complete the
      // deferred fling cancellation.
      *out_cancel_current_fling = true;
      return false;
  }
}

bool FlingBooster::MustCancelDeferredFling() const {
  return deferred_fling_cancel_time_seconds_ &&
         last_fling_animate_time_seconds_ > deferred_fling_cancel_time_seconds_;
}

bool FlingBooster::ShouldBoostFling(const WebGestureEvent& fling_start_event) {
  DCHECK_EQ(WebInputEvent::kGestureFlingStart, fling_start_event.GetType());

  gfx::Vector2dF new_fling_velocity(
      fling_start_event.data.fling_start.velocity_x,
      fling_start_event.data.fling_start.velocity_y);

  if (gfx::DotProduct(current_fling_velocity_, new_fling_velocity) <= 0)
    return false;

  if (current_fling_velocity_.LengthSquared() < kMinBoostFlingSpeedSquare)
    return false;

  if (new_fling_velocity.LengthSquared() < kMinBoostFlingSpeedSquare)
    return false;

  if (modifiers_ != fling_start_event.GetModifiers())
    return false;

  return true;
}

bool FlingBooster::ShouldSuppressScrollForFlingBoosting(
    const WebGestureEvent& scroll_update_event) {
  DCHECK_EQ(WebInputEvent::kGestureScrollUpdate, scroll_update_event.GetType());

  gfx::Vector2dF dx(scroll_update_event.data.scroll_update.delta_x,
                    scroll_update_event.data.scroll_update.delta_y);
  if (gfx::DotProduct(current_fling_velocity_, dx) <= 0)
    return false;

  const double time_since_last_fling_animate =
      std::max(0.0, scroll_update_event.TimeStampSeconds() -
                        last_fling_animate_time_seconds_);
  if (time_since_last_fling_animate > kFlingBoostTimeoutDelaySeconds)
    return false;

  const double time_since_last_boost_event =
      scroll_update_event.TimeStampSeconds() -
      last_fling_boost_event_.TimeStampSeconds();
  if (time_since_last_boost_event < 0.001)
    return true;

  // TODO(jdduke): Use |scroll_update_event.data.scrollUpdate.velocity{X,Y}|.
  // The scroll must be of sufficient velocity to maintain the active fling.
  const gfx::Vector2dF scroll_velocity =
      gfx::ScaleVector2d(dx, 1. / time_since_last_boost_event);
  if (scroll_velocity.LengthSquared() < kMinBoostTouchScrollSpeedSquare)
    return false;

  return true;
}

void FlingBooster::ExtendBoostedFlingTimeout(
    const blink::WebGestureEvent& event) {
  deferred_fling_cancel_time_seconds_ =
      event.TimeStampSeconds() + kFlingBoostTimeoutDelaySeconds;
  last_fling_boost_event_ = event;
}

}  // namespace ui
