// 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 "content/browser/renderer_host/input/mouse_wheel_phase_handler.h"

#include "base/trace_event/trace_event.h"
#include "content/browser/renderer_host/render_widget_host_impl.h"
#include "content/browser/renderer_host/render_widget_host_input_event_router.h"
#include "content/browser/renderer_host/render_widget_host_view_base.h"
#include "ui/events/base_event_utils.h"

namespace content {
MouseWheelPhaseHandler::MouseWheelPhaseHandler(
    RenderWidgetHostViewBase* const host_view)
    : host_view_(host_view),
      mouse_wheel_end_dispatch_timeout_(kDefaultMouseWheelLatchingTransaction),
      touchpad_scroll_phase_state_(TOUCHPAD_SCROLL_STATE_UNKNOWN) {}

void MouseWheelPhaseHandler::AddPhaseIfNeededAndScheduleEndEvent(
    blink::WebMouseWheelEvent& mouse_wheel_event,
    bool should_route_event) {

  bool has_phase =
      mouse_wheel_event.phase != blink::WebMouseWheelEvent::kPhaseNone ||
      mouse_wheel_event.momentum_phase != blink::WebMouseWheelEvent::kPhaseNone;
  TRACE_EVENT2("input",
               "MouseWheelPhaseHandler::AddPhaseIfNeededAndScheduleEndEvent",
               "has_phase", has_phase, "touchpad_scroll_phase_state",
               touchpad_scroll_phase_state_);

  if (has_phase) {
    if (mouse_wheel_event.phase == blink::WebMouseWheelEvent::kPhaseEnded) {
      // Don't send the wheel end event immediately, start a timer instead to
      // see whether momentum phase of the scrolling starts or not.
      ScheduleMouseWheelEndDispatching(
          should_route_event,
          kMaximumTimeBetweenPhaseEndedAndMomentumPhaseBegan);
    } else if (mouse_wheel_event.phase ==
               blink::WebMouseWheelEvent::kPhaseBegan) {
      // A new scrolling sequence has started, send the pending wheel end
      // event to end the previous scrolling sequence.
      DispatchPendingWheelEndEvent();
    } else if (mouse_wheel_event.momentum_phase ==
               blink::WebMouseWheelEvent::kPhaseBegan) {
      // Momentum phase has started, drop the pending wheel end event to make
      // sure that no wheel end event will be sent during the momentum phase
      // of scrolling.
      IgnorePendingWheelEndEvent();
    }
  } else {  // !has_phase
    switch (touchpad_scroll_phase_state_) {
      case TOUCHPAD_SCROLL_STATE_UNKNOWN: {
        mouse_wheel_event.has_synthetic_phase = true;
        // Break the latching when the location difference between the current
        // and the initial wheel event positions exceeds the maximum allowed
        // threshold or when the event modifiers have changed.
        if (!IsWithinSlopRegion(mouse_wheel_event) ||
            HasDifferentModifiers(mouse_wheel_event) ||
            ShouldBreakLatchingDueToDirectionChange(mouse_wheel_event)) {
          DispatchPendingWheelEndEvent();
        }

        if (!mouse_wheel_end_dispatch_timer_.IsRunning()) {
          mouse_wheel_event.phase = blink::WebMouseWheelEvent::kPhaseBegan;
          first_wheel_location_ =
              gfx::Vector2dF(mouse_wheel_event.PositionInWidget().x,
                             mouse_wheel_event.PositionInWidget().y);
          initial_wheel_event_ = mouse_wheel_event;
          first_scroll_update_ack_state_ =
              FirstScrollUpdateAckState::kNotArrived;
          ScheduleMouseWheelEndDispatching(should_route_event,
                                           mouse_wheel_end_dispatch_timeout_);
        } else {  // mouse_wheel_end_dispatch_timer_.IsRunning()
          bool non_zero_delta =
              mouse_wheel_event.delta_x || mouse_wheel_event.delta_y;
          mouse_wheel_event.phase =
              non_zero_delta ? blink::WebMouseWheelEvent::kPhaseChanged
                             : blink::WebMouseWheelEvent::kPhaseStationary;
          mouse_wheel_end_dispatch_timer_.Reset();
        }
        break;
      }
      case TOUCHPAD_SCROLL_MAY_BEGIN:
        mouse_wheel_event.phase = blink::WebMouseWheelEvent::kPhaseBegan;
        touchpad_scroll_phase_state_ = TOUCHPAD_SCROLL_IN_PROGRESS;
        break;
      case TOUCHPAD_SCROLL_IN_PROGRESS:
        mouse_wheel_event.phase = blink::WebMouseWheelEvent::kPhaseChanged;
        break;
      default:
        NOTREACHED();
    }
  }

  last_mouse_wheel_event_ = mouse_wheel_event;
}

void MouseWheelPhaseHandler::DispatchPendingWheelEndEvent() {
  if (!mouse_wheel_end_dispatch_timer_.IsRunning())
    return;

  TRACE_EVENT_INSTANT0("input", "MouseWheelPhaseHandler Dispatched",
                       TRACE_EVENT_SCOPE_THREAD);
  mouse_wheel_end_dispatch_timer_.FireNow();
}

void MouseWheelPhaseHandler::IgnorePendingWheelEndEvent() {
  if (!mouse_wheel_end_dispatch_timer_.IsRunning())
    return;

  TRACE_EVENT_INSTANT0("input", "MouseWheelPhaseHandler Ignored",
                       TRACE_EVENT_SCOPE_THREAD);
  mouse_wheel_end_dispatch_timer_.Stop();
}

void MouseWheelPhaseHandler::ResetTouchpadScrollSequence() {
  TRACE_EVENT_INSTANT0("input", "MouseWheelPhaseHandler Reset",
                       TRACE_EVENT_SCOPE_THREAD);
  touchpad_scroll_phase_state_ = TOUCHPAD_SCROLL_STATE_UNKNOWN;
}

void MouseWheelPhaseHandler::SendWheelEndForTouchpadScrollingIfNeeded() {
  if (touchpad_scroll_phase_state_ == TOUCHPAD_SCROLL_IN_PROGRESS) {
    RenderWidgetHostImpl* widget_host = host_view_->host();
    if (!widget_host) {
      ResetTouchpadScrollSequence();
      return;
    }

    bool should_route_event = widget_host->delegate() &&
                              widget_host->delegate()->GetInputEventRouter();
    TRACE_EVENT_INSTANT0("input", "MouseWheelPhaseHandler Sent touchpad end",
                         TRACE_EVENT_SCOPE_THREAD);
    SendSyntheticWheelEventWithPhaseEnded(should_route_event);
  }

  ResetTouchpadScrollSequence();
}

void MouseWheelPhaseHandler::TouchpadScrollingMayBegin() {
  // End the timer-based wheel scroll sequence before starting a touchpad scroll
  // sequence.
  if (mouse_wheel_end_dispatch_timer_.IsRunning()) {
    DCHECK_EQ(TOUCHPAD_SCROLL_STATE_UNKNOWN, touchpad_scroll_phase_state_);
    DispatchPendingWheelEndEvent();
  }

  TRACE_EVENT_INSTANT0("input", "MouseWheelPhaseHandler May Begin",
                       TRACE_EVENT_SCOPE_THREAD);
  touchpad_scroll_phase_state_ = TOUCHPAD_SCROLL_MAY_BEGIN;
}

void MouseWheelPhaseHandler::SendSyntheticWheelEventWithPhaseEnded(
    bool should_route_event) {
  TRACE_EVENT0("input",
               "MouseWheelPhaseHandler::SendSyntheticWheelEventWithPhaseEnded");
  last_mouse_wheel_event_.SetTimeStamp(ui::EventTimeForNow());
  last_mouse_wheel_event_.delta_x = 0;
  last_mouse_wheel_event_.delta_y = 0;
  last_mouse_wheel_event_.wheel_ticks_x = 0;
  last_mouse_wheel_event_.wheel_ticks_y = 0;
  last_mouse_wheel_event_.phase = blink::WebMouseWheelEvent::kPhaseEnded;
  last_mouse_wheel_event_.dispatch_type =
      blink::WebInputEvent::DispatchType::kEventNonBlocking;

  if (should_route_event) {
    RenderWidgetHostImpl* widget_host = host_view_->host();
    if (!widget_host || !widget_host->delegate() ||
        !widget_host->delegate()->GetInputEventRouter())
      return;

    widget_host->delegate()->GetInputEventRouter()->RouteMouseWheelEvent(
        host_view_, &last_mouse_wheel_event_,
        ui::LatencyInfo(ui::SourceEventType::WHEEL));
  } else {
    host_view_->ProcessMouseWheelEvent(
        last_mouse_wheel_event_, ui::LatencyInfo(ui::SourceEventType::WHEEL));
  }
}

void MouseWheelPhaseHandler::ScheduleMouseWheelEndDispatching(
    bool should_route_event,
    const base::TimeDelta timeout) {
  TRACE_EVENT_INSTANT0("input", "MouseWheelPhaseHandler timer started",
                       TRACE_EVENT_SCOPE_THREAD);
  mouse_wheel_end_dispatch_timer_.Start(
      FROM_HERE, timeout,
      base::BindOnce(
          &MouseWheelPhaseHandler::SendSyntheticWheelEventWithPhaseEnded,
          base::Unretained(this), should_route_event));
}

bool MouseWheelPhaseHandler::IsWithinSlopRegion(
    const blink::WebMouseWheelEvent& wheel_event) const {
  // This function is called to check if breaking timer-based wheel scroll
  // latching sequence is needed or not, and timer-based wheel scroll latching
  // happens only when scroll state is unknown.
  DCHECK(touchpad_scroll_phase_state_ == TOUCHPAD_SCROLL_STATE_UNKNOWN);
  gfx::Vector2dF current_wheel_location(wheel_event.PositionInWidget().x,
                                        wheel_event.PositionInWidget().y);
  return (current_wheel_location - first_wheel_location_).LengthSquared() <
         kWheelLatchingSlopRegion * kWheelLatchingSlopRegion;
}

bool MouseWheelPhaseHandler::HasDifferentModifiers(
    const blink::WebMouseWheelEvent& wheel_event) const {
  // This function is called to check if breaking timer-based wheel scroll
  // latching sequence is needed or not, and timer-based wheel scroll latching
  // happens only when scroll state is unknown.
  DCHECK(touchpad_scroll_phase_state_ == TOUCHPAD_SCROLL_STATE_UNKNOWN);
  return wheel_event.GetModifiers() != initial_wheel_event_.GetModifiers();
}

bool MouseWheelPhaseHandler::ShouldBreakLatchingDueToDirectionChange(
    const blink::WebMouseWheelEvent& wheel_event) const {
  // This function is called to check if breaking timer-based wheel scroll
  // latching sequence is needed or not, and timer-based wheel scroll latching
  // happens only when scroll state is unknown.
  DCHECK(touchpad_scroll_phase_state_ == TOUCHPAD_SCROLL_STATE_UNKNOWN);
  if (first_scroll_update_ack_state_ != FirstScrollUpdateAckState::kNotConsumed)
    return false;

  bool consistent_x_direction =
      (wheel_event.delta_x == 0 && initial_wheel_event_.delta_x == 0) ||
      wheel_event.delta_x * initial_wheel_event_.delta_x > 0;
  bool consistent_y_direction =
      (wheel_event.delta_y == 0 && initial_wheel_event_.delta_y == 0) ||
      wheel_event.delta_y * initial_wheel_event_.delta_y > 0;
  return !consistent_x_direction || !consistent_y_direction;
}

void MouseWheelPhaseHandler::GestureEventAck(
    const blink::WebGestureEvent& event,
    InputEventAckState ack_result) {
  if (event.GetType() != blink::WebInputEvent::kGestureScrollUpdate ||
      first_scroll_update_ack_state_ !=
          FirstScrollUpdateAckState::kNotArrived) {
    return;
  }
  if (ack_result == INPUT_EVENT_ACK_STATE_CONSUMED)
    first_scroll_update_ack_state_ = FirstScrollUpdateAckState::kConsumed;
  else
    first_scroll_update_ack_state_ = FirstScrollUpdateAckState::kNotConsumed;
}

}  // namespace content
