// Copyright (c) 2012 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 "content/browser/renderer_host/overscroll_controller.h"

#include <algorithm>

#include "base/command_line.h"
#include "base/logging.h"
#include "content/browser/renderer_host/overscroll_controller_delegate.h"
#include "content/public/browser/overscroll_configuration.h"
#include "content/public/common/content_features.h"
#include "content/public/common/content_switches.h"

namespace content {

namespace {

// Minimum amount of time after an actual scroll after which a pull-to-refresh
// can start.
constexpr base::TimeDelta kPullToRefreshCoolOffDelay =
    base::TimeDelta::FromMilliseconds(600);

bool IsGestureEventFromTouchpad(const blink::WebInputEvent& event) {
  DCHECK(blink::WebInputEvent::IsGestureEventType(event.GetType()));
  const blink::WebGestureEvent& gesture =
      static_cast<const blink::WebGestureEvent&>(event);
  return gesture.SourceDevice() == blink::kWebGestureDeviceTouchpad;
}

bool IsGestureScrollUpdateInertialEvent(const blink::WebInputEvent& event) {
  if (event.GetType() != blink::WebInputEvent::kGestureScrollUpdate)
    return false;

  const blink::WebGestureEvent& gesture =
      static_cast<const blink::WebGestureEvent&>(event);
  return gesture.data.scroll_update.inertial_phase ==
         blink::WebGestureEvent::kMomentumPhase;
}

float ClampAbsoluteValue(float value, float max_abs) {
  DCHECK_LT(0.f, max_abs);
  return std::max(-max_abs, std::min(value, max_abs));
}

}  // namespace

OverscrollController::OverscrollController() {}

OverscrollController::~OverscrollController() {}

bool OverscrollController::ShouldProcessEvent(
    const blink::WebInputEvent& event) {
  switch (event.GetType()) {
    case blink::WebInputEvent::kGestureScrollBegin:
    case blink::WebInputEvent::kGestureScrollUpdate:
    case blink::WebInputEvent::kGestureScrollEnd: {
      const blink::WebGestureEvent& gesture =
          static_cast<const blink::WebGestureEvent&>(event);

      // GestureScrollBegin and GestureScrollEnd events are created to wrap
      // individual resent GestureScrollUpdates from a plugin. Hence these
      // should not be used to indicate the beginning/end of the overscroll.
      // TODO(mcnee): When we remove BrowserPlugin, delete this code.
      // See crbug.com/533069
      if (gesture.resending_plugin_id != -1 &&
          event.GetType() != blink::WebInputEvent::kGestureScrollUpdate)
        return false;

      blink::WebGestureEvent::ScrollUnits scrollUnits;
      switch (event.GetType()) {
        case blink::WebInputEvent::kGestureScrollBegin:
          scrollUnits = gesture.data.scroll_begin.delta_hint_units;
          break;
        case blink::WebInputEvent::kGestureScrollUpdate:
          scrollUnits = gesture.data.scroll_update.delta_units;
          break;
        case blink::WebInputEvent::kGestureScrollEnd:
          scrollUnits = gesture.data.scroll_end.delta_units;
          break;
        default:
          scrollUnits = blink::WebGestureEvent::kPixels;
          break;
      }

      return scrollUnits == blink::WebGestureEvent::kPrecisePixels;
    }
    default:
      break;
  }
  return true;
}

bool OverscrollController::ShouldIgnoreInertialEvent(
    const blink::WebInputEvent& event) const {
  return ignore_following_inertial_events_ &&
         IsGestureScrollUpdateInertialEvent(event);
  }

bool OverscrollController::WillHandleEvent(const blink::WebInputEvent& event) {
  if (!ShouldProcessEvent(event))
    return false;

  // TODO(mohsen): Consider filtering mouse-wheel events during overscroll. See
  // https://crbug.com/772106.
  if (event.GetType() == blink::WebInputEvent::kMouseWheel)
    return false;

  if (event.GetType() == blink::WebInputEvent::kGestureScrollBegin) {
    ignore_following_inertial_events_ = false;
    first_inertial_event_time_.reset();
    time_since_last_ignored_scroll_ =
        event.TimeStamp() - last_ignored_scroll_time_;
    // Will handle events when processing ACKs to ensure the correct order.
    return false;
  }

  if (event.GetType() == blink::WebInputEvent::kGestureScrollEnd) {
    if (scroll_state_ == ScrollState::CONTENT_CONSUMING ||
        overscroll_ignored_) {
      last_ignored_scroll_time_ = event.TimeStamp();
    }
    // Will handle events when processing ACKs to ensure the correct order.
    return false;
  }

  // Consume the scroll-update events if they are from a inertial scroll (fling)
  // event that completed an overscroll gesture.
  if (ShouldIgnoreInertialEvent(event))
    return true;

  bool reset_scroll_state = false;
  if (scroll_state_ != ScrollState::NONE || overscroll_delta_x_ ||
      overscroll_delta_y_) {
    switch (event.GetType()) {
      case blink::WebInputEvent::kGestureFlingStart:
        reset_scroll_state = true;
        break;

      default:
        if (blink::WebInputEvent::IsMouseEventType(event.GetType()) ||
            blink::WebInputEvent::IsKeyboardEventType(event.GetType())) {
          reset_scroll_state = true;
        }
        break;
    }
  }

  if (reset_scroll_state)
    ResetScrollState();

  if (DispatchEventCompletesAction(event)) {
    CompleteAction();

    // Let the event be dispatched to the renderer.
    return false;
  }

  if (overscroll_mode_ != OVERSCROLL_NONE && DispatchEventResetsState(event)) {
    SetOverscrollMode(OVERSCROLL_NONE, OverscrollSource::NONE);

    // Let the event be dispatched to the renderer.
    return false;
  }

  if (overscroll_mode_ != OVERSCROLL_NONE) {
    // Consume the event only if it updates the overscroll state.
    if (ProcessEventForOverscroll(event))
      return true;
  } else if (reset_scroll_state) {
    overscroll_delta_x_ = overscroll_delta_y_ = 0.f;
  }

  // In overscrolling state, consume scroll-update and fling-start events when
  // they do not contribute to overscroll in order to prevent content scroll.
  return scroll_state_ == ScrollState::OVERSCROLLING &&
         (event.GetType() == blink::WebInputEvent::kGestureScrollUpdate ||
          event.GetType() == blink::WebInputEvent::kGestureFlingStart);
}

void OverscrollController::OnDidOverscroll(
    const ui::DidOverscrollParams& params) {
  // TODO(sunyunjia): We should also decide whether to trigger overscroll,
  // update scroll_state_ here. See https://crbug.com/799467.
  behavior_ = params.overscroll_behavior;
}

void OverscrollController::ReceivedEventACK(const blink::WebInputEvent& event,
                                            bool processed) {
  if (!ShouldProcessEvent(event))
    return;

  // An inertial scroll (fling) event from a completed overscroll gesture
  // should not modify states below.
  if (ShouldIgnoreInertialEvent(event))
    return;

  if (processed) {
    // If a scroll event is consumed by the page, i.e. some content on the page
    // has been scrolled, then there is not going to be an overscroll gesture,
    // until the current scroll ends, and a new scroll gesture starts.
    // Similarly, if a mouse-wheel event is consumed, probably the page has
    // implemented its own scroll-like behavior and no overscroll should happen.
    if (scroll_state_ == ScrollState::NONE &&
        (event.GetType() == blink::WebInputEvent::kGestureScrollUpdate ||
         event.GetType() == blink::WebInputEvent::kMouseWheel)) {
      scroll_state_ = ScrollState::CONTENT_CONSUMING;
    }
    // In overscrolling state, only return if we are in an overscroll mode;
    // otherwise, we would want to ProcessEventForOverscroll to let it start a
    // new overscroll mode.
    if (scroll_state_ != ScrollState::OVERSCROLLING ||
        overscroll_mode_ != OVERSCROLL_NONE) {
      return;
    }
  }

  if (event.GetType() == blink::WebInputEvent::kMouseWheel)
    return;

  ProcessEventForOverscroll(event);
}

void OverscrollController::Reset() {
  overscroll_mode_ = OVERSCROLL_NONE;
  overscroll_source_ = OverscrollSource::NONE;
  overscroll_delta_x_ = overscroll_delta_y_ = 0.f;
  ResetScrollState();
}

void OverscrollController::Cancel() {
  SetOverscrollMode(OVERSCROLL_NONE, OverscrollSource::NONE);
  overscroll_delta_x_ = overscroll_delta_y_ = 0.f;
  ResetScrollState();
}

bool OverscrollController::DispatchEventCompletesAction(
    const blink::WebInputEvent& event) const {
  if (overscroll_mode_ == OVERSCROLL_NONE)
    return false;
  DCHECK_NE(OverscrollSource::NONE, overscroll_source_);

  // Complete the overscroll gesture if there was a mouse move or a scroll-end
  // after the threshold.
  if (event.GetType() != blink::WebInputEvent::kMouseMove &&
      event.GetType() != blink::WebInputEvent::kGestureScrollEnd &&
      event.GetType() != blink::WebInputEvent::kGestureFlingStart &&
      event.GetType() != blink::WebInputEvent::kGestureScrollUpdate)
    return false;

  // Complete the overscroll gesture for inertial scroll (fling) event from
  // touchpad.
  if (event.GetType() == blink::WebInputEvent::kGestureScrollUpdate) {
    if (overscroll_source_ != OverscrollSource::TOUCHPAD)
      return false;
    DCHECK(IsGestureEventFromTouchpad(event));
    const blink::WebGestureEvent gesture_event =
        static_cast<const blink::WebGestureEvent&>(event);
    if (gesture_event.data.scroll_update.inertial_phase !=
        blink::WebGestureEvent::kMomentumPhase)
      return false;
  }

  if (event.GetType() == blink::WebInputEvent::kGestureScrollEnd &&
      overscroll_source_ == OverscrollSource::TOUCHPAD) {
    DCHECK(IsGestureEventFromTouchpad(event));
    // Complete the action for a GSE with touchpad source only when it is in
    // momentumPhase.
    const blink::WebGestureEvent gesture_event =
        static_cast<const blink::WebGestureEvent&>(event);
    if (gesture_event.data.scroll_end.inertial_phase !=
        blink::WebGestureEvent::kMomentumPhase)
      return false;
  }

  if (!delegate_)
    return false;

  if (event.GetType() == blink::WebInputEvent::kGestureFlingStart) {
    // Check to see if the fling is in the same direction of the overscroll.
    const blink::WebGestureEvent gesture =
        static_cast<const blink::WebGestureEvent&>(event);
    switch (overscroll_mode_) {
      case OVERSCROLL_EAST:
        if (gesture.data.fling_start.velocity_x < 0)
          return false;
        break;
      case OVERSCROLL_WEST:
        if (gesture.data.fling_start.velocity_x > 0)
          return false;
        break;
      case OVERSCROLL_NORTH:
        if (gesture.data.fling_start.velocity_y > 0)
          return false;
        break;
      case OVERSCROLL_SOUTH:
        if (gesture.data.fling_start.velocity_y < 0)
          return false;
        break;
      case OVERSCROLL_NONE:
        NOTREACHED();
    }
  }

  const gfx::Size size = delegate_->GetDisplaySize();
  if (size.IsEmpty())
    return false;

  const float delta =
      overscroll_mode_ == OVERSCROLL_WEST || overscroll_mode_ == OVERSCROLL_EAST
          ? overscroll_delta_x_
          : overscroll_delta_y_;
  const float ratio = fabs(delta) / std::max(size.width(), size.height());
  const float threshold = OverscrollConfig::GetThreshold(
      overscroll_source_ == OverscrollSource::TOUCHPAD
          ? OverscrollConfig::Threshold::kCompleteTouchpad
          : OverscrollConfig::Threshold::kCompleteTouchscreen);
  return ratio >= threshold;
}

bool OverscrollController::DispatchEventResetsState(
    const blink::WebInputEvent& event) const {
  switch (event.GetType()) {
    // GestureScrollBegin/End ACK will reset overscroll state when necessary.
    case blink::WebInputEvent::kGestureScrollBegin:
    case blink::WebInputEvent::kGestureScrollEnd:
    case blink::WebInputEvent::kGestureScrollUpdate:
    case blink::WebInputEvent::kGestureFlingCancel:
      return false;

    default:
      // Touch events can arrive during an overscroll gesture initiated by
      // touch-scrolling. These events should not reset the overscroll state.
      return !blink::WebInputEvent::IsTouchEventType(event.GetType());
  }
}

bool OverscrollController::ProcessEventForOverscroll(
    const blink::WebInputEvent& event) {
  bool event_processed = false;
  switch (event.GetType()) {
    case blink::WebInputEvent::kGestureScrollBegin: {
      if (overscroll_mode_ != OVERSCROLL_NONE)
        SetOverscrollMode(OVERSCROLL_NONE, OverscrollSource::NONE);
      break;
    }
    case blink::WebInputEvent::kGestureScrollEnd: {
      // Only reset the state on  GestureScrollEnd generated from the touchpad
      // when the scrolling is in inertial state.
      const blink::WebGestureEvent gesture_event =
          static_cast<const blink::WebGestureEvent&>(event);
      bool reset_scroll_state = !IsGestureEventFromTouchpad(event) ||
                                (gesture_event.data.scroll_end.inertial_phase ==
                                 blink::WebGestureEvent::kMomentumPhase);

      if (reset_scroll_state)
        ResetScrollState();

      if (DispatchEventCompletesAction(event)) {
        CompleteAction();
        break;
      }

      if (!reset_scroll_state)
        break;

      if (overscroll_mode_ != OVERSCROLL_NONE) {
        SetOverscrollMode(OVERSCROLL_NONE, OverscrollSource::NONE);
      } else {
        overscroll_delta_x_ = overscroll_delta_y_ = 0.f;
      }
      break;
    }
    case blink::WebInputEvent::kGestureScrollUpdate: {
      const blink::WebGestureEvent& gesture =
          static_cast<const blink::WebGestureEvent&>(event);
      bool is_gesture_scroll_update_inertial_event =
          IsGestureScrollUpdateInertialEvent(event);
      event_processed = ProcessOverscroll(
          gesture.data.scroll_update.delta_x,
          gesture.data.scroll_update.delta_y,
          gesture.SourceDevice() == blink::kWebGestureDeviceTouchpad,
          is_gesture_scroll_update_inertial_event);
      if (is_gesture_scroll_update_inertial_event) {
        // Record the timestamp of first inertial event.
        if (!first_inertial_event_time_) {
          first_inertial_event_time_ = event.TimeStamp();
          break;
        }
        base::TimeDelta inertial_event_interval =
            event.TimeStamp() - first_inertial_event_time_.value();
        if (inertial_event_interval >=
            OverscrollConfig::MaxInertialEventsBeforeOverscrollCancellation()) {
          ignore_following_inertial_events_ = true;
          // Reset overscroll state if fling didn't complete the overscroll
          // gesture within the first 20 inertial events.
          Cancel();
        }
      }
      break;
    }
    case blink::WebInputEvent::kGestureFlingStart: {
      const float kFlingVelocityThreshold = 1100.f;
      const blink::WebGestureEvent& gesture =
          static_cast<const blink::WebGestureEvent&>(event);
      float velocity_x = gesture.data.fling_start.velocity_x;
      float velocity_y = gesture.data.fling_start.velocity_y;
      if (fabs(velocity_x) > kFlingVelocityThreshold) {
        if ((overscroll_mode_ == OVERSCROLL_WEST && velocity_x < 0) ||
            (overscroll_mode_ == OVERSCROLL_EAST && velocity_x > 0)) {
          CompleteAction();
          event_processed = true;
          break;
        }
      } else if (fabs(velocity_y) > kFlingVelocityThreshold) {
        if ((overscroll_mode_ == OVERSCROLL_NORTH && velocity_y < 0) ||
            (overscroll_mode_ == OVERSCROLL_SOUTH && velocity_y > 0)) {
          CompleteAction();
          event_processed = true;
          break;
        }
      }

      // Reset overscroll state if fling didn't complete the overscroll gesture.
      SetOverscrollMode(OVERSCROLL_NONE, OverscrollSource::NONE);
      break;
    }

    default:
      DCHECK(blink::WebInputEvent::IsGestureEventType(event.GetType()) ||
             blink::WebInputEvent::IsTouchEventType(event.GetType()))
          << "Received unexpected event: " << event.GetType();
  }
  return event_processed;
}

bool OverscrollController::ProcessOverscroll(float delta_x,
                                             float delta_y,
                                             bool is_touchpad,
                                             bool is_inertial) {
  if (scroll_state_ == ScrollState::CONTENT_CONSUMING)
    return false;

  // Do not start overscroll for inertial events.
  if (overscroll_mode_ == OVERSCROLL_NONE && is_inertial)
    return false;

  overscroll_delta_x_ += delta_x;
  overscroll_delta_y_ += delta_y;

  const float start_threshold = OverscrollConfig::GetThreshold(
      is_touchpad ? OverscrollConfig::Threshold::kStartTouchpad
                  : OverscrollConfig::Threshold::kStartTouchscreen);
  if (fabs(overscroll_delta_x_) <= start_threshold &&
      fabs(overscroll_delta_y_) <= start_threshold) {
    SetOverscrollMode(OVERSCROLL_NONE, OverscrollSource::NONE);
    return true;
  }

  if (delegate_) {
    base::Optional<float> cap = delegate_->GetMaxOverscrollDelta();
    if (cap) {
      DCHECK_LE(0.f, cap.value());
      switch (overscroll_mode_) {
        case OVERSCROLL_WEST:
        case OVERSCROLL_EAST:
          overscroll_delta_x_ = ClampAbsoluteValue(
              overscroll_delta_x_, cap.value() + start_threshold);
          break;
        case OVERSCROLL_NORTH:
        case OVERSCROLL_SOUTH:
          overscroll_delta_y_ = ClampAbsoluteValue(
              overscroll_delta_y_, cap.value() + start_threshold);
          break;
        case OVERSCROLL_NONE:
          break;
      }
    }
  }

  // Compute the current overscroll direction. If the direction is different
  // from the current direction, then always switch to no-overscroll mode first
  // to make sure that subsequent scroll events go through to the page first.
  OverscrollMode new_mode = OVERSCROLL_NONE;
  const float kMinRatio = 2.5;
  if (fabs(overscroll_delta_x_) > start_threshold &&
      fabs(overscroll_delta_x_) > fabs(overscroll_delta_y_) * kMinRatio)
    new_mode = overscroll_delta_x_ > 0.f ? OVERSCROLL_EAST : OVERSCROLL_WEST;
  else if (fabs(overscroll_delta_y_) > start_threshold &&
           fabs(overscroll_delta_y_) > fabs(overscroll_delta_x_) * kMinRatio)
    new_mode = overscroll_delta_y_ > 0.f ? OVERSCROLL_SOUTH : OVERSCROLL_NORTH;

  // The horizontal overscroll is used for history navigation. Enable it for
  // touchpad only if TouchpadOverscrollHistoryNavigation is enabled.
  if ((new_mode == OVERSCROLL_EAST || new_mode == OVERSCROLL_WEST) &&
      is_touchpad &&
      !OverscrollConfig::TouchpadOverscrollHistoryNavigationEnabled()) {
    new_mode = OVERSCROLL_NONE;
  }

  // The vertical overscroll is used for pull-to-refresh. Enable it only if
  // pull-to-refresh is enabled.
  if (new_mode == OVERSCROLL_SOUTH || new_mode == OVERSCROLL_NORTH) {
    auto ptr_mode = OverscrollConfig::GetPullToRefreshMode();
    if (ptr_mode == OverscrollConfig::PullToRefreshMode::kDisabled ||
        (ptr_mode ==
             OverscrollConfig::PullToRefreshMode::kEnabledTouchschreen &&
         is_touchpad) ||
        time_since_last_ignored_scroll_ < kPullToRefreshCoolOffDelay) {
      overscroll_ignored_ = true;
      new_mode = OVERSCROLL_NONE;
    }
  }

  if (overscroll_mode_ == OVERSCROLL_NONE) {
    SetOverscrollMode(new_mode, is_touchpad ? OverscrollSource::TOUCHPAD
                                            : OverscrollSource::TOUCHSCREEN);
  } else if (new_mode != overscroll_mode_) {
    SetOverscrollMode(OVERSCROLL_NONE, OverscrollSource::NONE);
  }

  if (overscroll_mode_ == OVERSCROLL_NONE)
    return false;

  // Tell the delegate about the overscroll update so that it can update
  // the display accordingly (e.g. show history preview etc.).
  if (delegate_) {
    // Do not include the threshold amount when sending the deltas to the
    // delegate.
    float delegate_delta_x = overscroll_delta_x_;
    if (fabs(delegate_delta_x) > start_threshold) {
      if (delegate_delta_x < 0)
        delegate_delta_x += start_threshold;
      else
        delegate_delta_x -= start_threshold;
    } else {
      delegate_delta_x = 0.f;
    }

    float delegate_delta_y = overscroll_delta_y_;
    if (fabs(delegate_delta_y) > start_threshold) {
      if (delegate_delta_y < 0)
        delegate_delta_y += start_threshold;
      else
        delegate_delta_y -= start_threshold;
    } else {
      delegate_delta_y = 0.f;
    }
    return delegate_->OnOverscrollUpdate(delegate_delta_x, delegate_delta_y);
  }
  return false;
}

void OverscrollController::CompleteAction() {
  ignore_following_inertial_events_ = true;
  if (delegate_)
    delegate_->OnOverscrollComplete(overscroll_mode_);
  Reset();
}

void OverscrollController::SetOverscrollMode(OverscrollMode mode,
                                             OverscrollSource source) {
  if (overscroll_mode_ == mode)
    return;

  // If the mode changes to NONE, source is also NONE.
  DCHECK(mode != OVERSCROLL_NONE || source == OverscrollSource::NONE);

  // When setting to a non-NONE mode and there is a locked mode, don't set the
  // mode if the new mode is not the same as the locked mode.
  if (mode != OVERSCROLL_NONE && locked_mode_ != OVERSCROLL_NONE &&
      mode != locked_mode_) {
    return;
  }

  OverscrollMode old_mode = overscroll_mode_;
  overscroll_mode_ = mode;
  overscroll_source_ = source;
  if (overscroll_mode_ == OVERSCROLL_NONE) {
    overscroll_delta_x_ = overscroll_delta_y_ = 0.f;
  } else {
    scroll_state_ = ScrollState::OVERSCROLLING;
    locked_mode_ = overscroll_mode_;
  }
  if (delegate_) {
    delegate_->OnOverscrollModeChange(old_mode, overscroll_mode_, source,
                                      behavior_);
  }
}

void OverscrollController::ResetScrollState() {
  scroll_state_ = ScrollState::NONE;
  locked_mode_ = OVERSCROLL_NONE;
}

}  // namespace content
