// 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 "components/input/touchpad_pinch_event_queue.h"

#include "base/functional/bind.h"
#include "base/trace_event/trace_event.h"
#include "third_party/blink/public/common/input/web_mouse_wheel_event.h"
#include "third_party/perfetto/include/perfetto/tracing/track.h"
#include "ui/events/base_event_utils.h"
#include "ui/events/blink/blink_event_util.h"
#include "ui/latency/latency_info.h"

namespace input {

namespace {

blink::WebMouseWheelEvent CreateSyntheticWheelFromTouchpadPinchEvent(
    const blink::WebGestureEvent& pinch_event,
    blink::WebMouseWheelEvent::Phase phase,
    bool cancelable) {
  DCHECK(
      pinch_event.GetType() ==
          blink::WebInputEvent::Type::kGesturePinchUpdate ||
      pinch_event.GetType() == blink::WebInputEvent::Type::kGesturePinchEnd ||
      pinch_event.GetType() == blink::WebInputEvent::Type::kGestureDoubleTap);
  float delta_y = 0.0f;
  float wheel_ticks_y = 0.0f;

  // The function to convert scales to deltaY values is designed to be
  // compatible with websites existing use of wheel events, and with existing
  // Windows trackpad behavior.  In particular, we want:
  //  - deltas should accumulate via addition: f(s1*s2)==f(s1)+f(s2)
  //  - deltas should invert via negation: f(1/s) == -f(s)
  //  - zoom in should be positive: f(s) > 0 iff s > 1
  //  - magnitude roughly matches wheels: f(2) > 25 && f(2) < 100
  //  - a formula that's relatively easy to use from JavaScript
  // Note that 'wheel' event deltaY values have their sign inverted.  So to
  // convert a wheel deltaY back to a scale use Math.exp(-deltaY/100).
  if (pinch_event.GetType() ==
      blink::WebInputEvent::Type::kGesturePinchUpdate) {
    DCHECK_GT(pinch_event.data.pinch_update.scale, 0);
    delta_y = 100.0f * log(pinch_event.data.pinch_update.scale);
    wheel_ticks_y = pinch_event.data.pinch_update.scale > 1 ? 1 : -1;
  }

  // For pinch gesture events, similar to ctrl+wheel zooming, allow content to
  // prevent the browser from zooming by sending fake wheel events with the ctrl
  // modifier set when we see trackpad pinch gestures.  Ideally we'd someday get
  // a platform 'pinch' event and send that instead.
  blink::WebMouseWheelEvent wheel_event(
      blink::WebInputEvent::Type::kMouseWheel,
      pinch_event.GetModifiers() | blink::WebInputEvent::kControlKey,
      pinch_event.TimeStamp());
  wheel_event.SetPositionInWidget(pinch_event.PositionInWidget());
  wheel_event.SetPositionInScreen(pinch_event.PositionInScreen());
  wheel_event.delta_units = ui::ScrollGranularity::kScrollByPrecisePixel;
  wheel_event.delta_x = 0;
  wheel_event.delta_y = delta_y;

  wheel_event.phase = phase;
  wheel_event.wheel_ticks_x = 0;
  wheel_event.wheel_ticks_y = wheel_ticks_y;

  if (cancelable)
    wheel_event.dispatch_type = blink::WebInputEvent::DispatchType::kBlocking;
  else
    wheel_event.dispatch_type =
        blink::WebInputEvent::DispatchType::kEventNonBlocking;

  return wheel_event;
}

}  // namespace

// This is a single queued pinch event to which we add trace events.
class QueuedTouchpadPinchEvent : public GestureEventWithLatencyInfo {
 public:
  QueuedTouchpadPinchEvent(const GestureEventWithLatencyInfo& original_event,
                           DispatchToRendererCallback callback)
      : GestureEventWithLatencyInfo(original_event),
        dispatch_callback(std::move(callback)) {
    TRACE_EVENT_BEGIN(
        "input", "TouchpadPinchEventQueue::QueueEvent",
        perfetto::NamedTrack::FromPointer("TouchpadPinchEventQueue", this));
  }

  QueuedTouchpadPinchEvent(const QueuedTouchpadPinchEvent&) = delete;
  QueuedTouchpadPinchEvent& operator=(const QueuedTouchpadPinchEvent&) = delete;

  ~QueuedTouchpadPinchEvent() {
    TRACE_EVENT_END("input", perfetto::NamedTrack::FromPointer(
                                 "TouchpadPinchEventQueue", this));
  }

  DispatchToRendererCallback dispatch_callback;
};

TouchpadPinchEventQueue::TouchpadPinchEventQueue(
    TouchpadPinchEventQueueClient* client)
    : client_(client) {
  DCHECK(client_);
}

TouchpadPinchEventQueue::~TouchpadPinchEventQueue() = default;

void TouchpadPinchEventQueue::QueueEvent(
    const GestureEventWithLatencyInfo& event,
    DispatchToRendererCallback& dispatch_callback) {
  TRACE_EVENT0("input", "TouchpadPinchEventQueue::QueueEvent");

  if (!pinch_queue_.empty()) {
    QueuedTouchpadPinchEvent* last_event = pinch_queue_.back().get();
    if (last_event->CanCoalesceWith(event)) {
      // Terminate the LatencyInfo of the event before it gets coalesced away.
      event.latency.Terminate();

      std::move(dispatch_callback)
          .Run(event.event, DispatchToRendererResult::kNotDispatched);
      last_event->CoalesceWith(event);
      DCHECK_EQ(blink::WebInputEvent::Type::kGesturePinchUpdate,
                last_event->event.GetType());
      TRACE_EVENT_INSTANT("input",
                          "TouchpadPinchEventQueue::CoalescedPinchEvent",
                          "scale", last_event->event.data.pinch_update.scale);
      return;
    }
  }

  pinch_queue_.push_back(std::make_unique<QueuedTouchpadPinchEvent>(
      event, std::move(dispatch_callback)));
  TryForwardNextEventToRenderer();
}

void TouchpadPinchEventQueue::ProcessMouseWheelAck(
    const MouseWheelEventWithLatencyInfo& ack_event,
    blink::mojom::InputEventResultSource ack_source,
    blink::mojom::InputEventResultState ack_result) {
  TRACE_EVENT0("input", "TouchpadPinchEventQueue::ProcessMouseWheelAck");
  if (!pinch_event_awaiting_ack_)
    return;

  if (pinch_event_awaiting_ack_->event.GetType() ==
          blink::WebInputEvent::Type::kGesturePinchUpdate &&
      !first_event_prevented_.has_value())
    first_event_prevented_ =
        (ack_result == blink::mojom::InputEventResultState::kConsumed);

  pinch_event_awaiting_ack_->latency.AddNewLatencyFrom(ack_event.latency);
  client_->OnGestureEventForPinchAck(*pinch_event_awaiting_ack_, ack_source,
                                     ack_result);

  pinch_event_awaiting_ack_.reset();
  TryForwardNextEventToRenderer();
}

void TouchpadPinchEventQueue::TryForwardNextEventToRenderer() {
  TRACE_EVENT0("input",
               "TouchpadPinchEventQueue::TryForwardNextEventToRenderer");

  if (pinch_queue_.empty() || pinch_event_awaiting_ack_)
    return;

  pinch_event_awaiting_ack_ = std::move(pinch_queue_.front());
  pinch_queue_.pop_front();

  if (pinch_event_awaiting_ack_->event.GetType() ==
      blink::WebInputEvent::Type::kGesturePinchBegin) {
    client_->OnGestureEventForPinchAck(
        *pinch_event_awaiting_ack_,
        blink::mojom::InputEventResultSource::kBrowser,
        blink::mojom::InputEventResultState::kIgnored);
    std::move(pinch_event_awaiting_ack_->dispatch_callback)
        .Run(pinch_event_awaiting_ack_->event,
             DispatchToRendererResult::kNotDispatched);
    pinch_event_awaiting_ack_.reset();
    TryForwardNextEventToRenderer();
    return;
  }

  blink::WebMouseWheelEvent::Phase phase =
      blink::WebMouseWheelEvent::kPhaseNone;
  bool cancelable = true;

  if (pinch_event_awaiting_ack_->event.GetType() ==
      blink::WebInputEvent::Type::kGesturePinchEnd) {
    first_event_prevented_.reset();
    phase = blink::WebMouseWheelEvent::kPhaseEnded;
    cancelable = false;
  } else if (pinch_event_awaiting_ack_->event.GetType() ==
             blink::WebInputEvent::Type::kGestureDoubleTap) {
    phase = blink::WebMouseWheelEvent::kPhaseNone;
    cancelable = true;
  } else {
    DCHECK_EQ(pinch_event_awaiting_ack_->event.GetType(),
              blink::WebInputEvent::Type::kGesturePinchUpdate);
    // The first pinch update event should send a synthetic wheel with phase
    // began.
    if (!first_event_prevented_.has_value()) {
      phase = blink::WebMouseWheelEvent::kPhaseBegan;
      cancelable = true;
    } else {
      phase = blink::WebMouseWheelEvent::kPhaseChanged;
      cancelable = first_event_prevented_.value();
    }
  }

  blink::WebMouseWheelEvent wheel_event_awaiting_ack =
      CreateSyntheticWheelFromTouchpadPinchEvent(
          pinch_event_awaiting_ack_->event, phase, cancelable);
  const MouseWheelEventWithLatencyInfo synthetic_wheel(
      wheel_event_awaiting_ack, pinch_event_awaiting_ack_->latency);

  client_->SendMouseWheelEventForPinchImmediately(
      pinch_event_awaiting_ack_->event, synthetic_wheel,
      base::BindOnce(&TouchpadPinchEventQueue::ProcessMouseWheelAck,
                     base::Unretained(this)),
      pinch_event_awaiting_ack_->dispatch_callback);
}

bool TouchpadPinchEventQueue::has_pending() const {
  return !pinch_queue_.empty() || pinch_event_awaiting_ack_;
}

}  // namespace input
