// 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/passthrough_touch_event_queue.h"

#include <utility>

#include "base/auto_reset.h"
#include "base/feature_list.h"
#include "base/macros.h"
#include "base/metrics/histogram_macros.h"
#include "base/trace_event/trace_event.h"
#include "content/browser/renderer_host/input/touch_timeout_handler.h"
#include "content/common/input/web_touch_event_traits.h"
#include "content/public/common/content_features.h"
#include "ui/events/base_event_utils.h"
#include "ui/gfx/geometry/point_f.h"

using blink::WebInputEvent;
using blink::WebTouchEvent;
using blink::WebTouchPoint;
using ui::LatencyInfo;

namespace content {
namespace {

// Compare all properties of touch points to determine the state.
bool HasPointChanged(const WebTouchPoint& point_1,
                     const WebTouchPoint& point_2) {
  DCHECK_EQ(point_1.id, point_2.id);
  if (point_1.PositionInScreen() != point_2.PositionInScreen() ||
      point_1.PositionInWidget() != point_2.PositionInWidget() ||
      point_1.radius_x != point_2.radius_x ||
      point_1.radius_y != point_2.radius_y ||
      point_1.rotation_angle != point_2.rotation_angle ||
      point_1.force != point_2.force || point_1.tilt_x != point_2.tilt_x ||
      point_1.tilt_y != point_2.tilt_y) {
    return true;
  }
  return false;
}

}  // namespace

PassthroughTouchEventQueue::TouchEventWithLatencyInfoAndAckState::
    TouchEventWithLatencyInfoAndAckState(const TouchEventWithLatencyInfo& event)
    : TouchEventWithLatencyInfo(event),
      ack_state_(INPUT_EVENT_ACK_STATE_UNKNOWN) {}

bool PassthroughTouchEventQueue::TouchEventWithLatencyInfoAndAckState::
operator<(const TouchEventWithLatencyInfoAndAckState& other) const {
  return event.unique_touch_event_id < other.event.unique_touch_event_id;
}

PassthroughTouchEventQueue::PassthroughTouchEventQueue(
    PassthroughTouchEventQueueClient* client,
    const Config& config)
    : client_(client),
      has_handlers_(true),
      maybe_has_handler_for_current_sequence_(false),
      drop_remaining_touches_in_sequence_(false),
      send_touch_events_async_(false),
      processing_acks_(false) {
  if (config.touch_ack_timeout_supported) {
    timeout_handler_.reset(
        new TouchTimeoutHandler(this, config.desktop_touch_ack_timeout_delay,
                                config.mobile_touch_ack_timeout_delay));
  }
}

PassthroughTouchEventQueue::~PassthroughTouchEventQueue() {}

void PassthroughTouchEventQueue::SendTouchCancelEventForTouchEvent(
    const TouchEventWithLatencyInfo& event_to_cancel) {
  TouchEventWithLatencyInfo event = event_to_cancel;
  WebTouchEventTraits::ResetTypeAndTouchStates(
      WebInputEvent::kTouchCancel,
      // TODO(rbyers): Shouldn't we use a fresh timestamp?
      event.event.TimeStamp(), &event.event);
  SendTouchEventImmediately(&event, false);
}

void PassthroughTouchEventQueue::QueueEvent(
    const TouchEventWithLatencyInfo& event) {
  TRACE_EVENT0("input", "PassthroughTouchEventQueue::QueueEvent");
  PreFilterResult filter_result = FilterBeforeForwarding(event.event);
  bool should_forward_touch_event =
      filter_result == PreFilterResult::kUnfiltered;
  UMA_HISTOGRAM_ENUMERATION("Event.Touch.FilteredAtPassthroughQueue",
                            filter_result);

  if (!should_forward_touch_event) {
    client_->OnFilteringTouchEvent(event.event);

    TouchEventWithLatencyInfoAndAckState event_with_ack_state = event;
    event_with_ack_state.set_ack_info(InputEventAckSource::BROWSER,
                                      INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS);
    outstanding_touches_.insert(event_with_ack_state);
    AckCompletedEvents();
    return;
  }
  TouchEventWithLatencyInfo cloned_event(event);
  SendTouchEventImmediately(&cloned_event, true);
}

void PassthroughTouchEventQueue::PrependTouchScrollNotification() {
  TRACE_EVENT0("input",
               "PassthroughTouchEventQueue::PrependTouchScrollNotification");

  TouchEventWithLatencyInfo touch(WebInputEvent::kTouchScrollStarted,
                                  WebInputEvent::kNoModifiers,
                                  ui::EventTimeForNow(), LatencyInfo());
  touch.event.dispatch_type = WebInputEvent::kEventNonBlocking;
  SendTouchEventImmediately(&touch, true);
}

void PassthroughTouchEventQueue::ProcessTouchAck(
    InputEventAckSource ack_source,
    InputEventAckState ack_result,
    const LatencyInfo& latency_info,
    const uint32_t unique_touch_event_id,
    bool should_stop_timeout_monitor) {
  TRACE_EVENT0("input", "PassthroughTouchEventQueue::ProcessTouchAck");
  if (timeout_handler_ &&
      timeout_handler_->ConfirmTouchEvent(unique_touch_event_id, ack_result,
                                          should_stop_timeout_monitor))
    return;

  auto touch_event_iter = outstanding_touches_.begin();
  while (touch_event_iter != outstanding_touches_.end()) {
    if (unique_touch_event_id == touch_event_iter->event.unique_touch_event_id)
      break;
    ++touch_event_iter;
  }

  if (touch_event_iter == outstanding_touches_.end())
    return;

  TouchEventWithLatencyInfoAndAckState& event =
      const_cast<TouchEventWithLatencyInfoAndAckState&>(*touch_event_iter);
  event.latency.AddNewLatencyFrom(latency_info);
  event.set_ack_info(ack_source, ack_result);

  AckCompletedEvents();
}

void PassthroughTouchEventQueue::OnGestureScrollEvent(
    const GestureEventWithLatencyInfo& gesture_event) {
  if (gesture_event.event.GetType() ==
          blink::WebInputEvent::kGestureScrollUpdate &&
      gesture_event.event.resending_plugin_id == -1) {
    send_touch_events_async_ = true;
  }
}

void PassthroughTouchEventQueue::OnGestureEventAck(
    const GestureEventWithLatencyInfo& event,
    InputEventAckState ack_result) {
  // Turn events sent during gesture scrolls to be async.
  if (event.event.GetType() == blink::WebInputEvent::kGestureScrollUpdate &&
      event.event.resending_plugin_id == -1) {
    send_touch_events_async_ = (ack_result == INPUT_EVENT_ACK_STATE_CONSUMED);
  }
}

void PassthroughTouchEventQueue::OnHasTouchEventHandlers(bool has_handlers) {
  has_handlers_ = has_handlers;
}

bool PassthroughTouchEventQueue::IsPendingAckTouchStart() const {
  if (outstanding_touches_.empty())
    return false;

  for (auto& iter : outstanding_touches_) {
    if (iter.event.GetType() == WebInputEvent::kTouchStart)
      return true;
  }
  return false;
}

void PassthroughTouchEventQueue::SetAckTimeoutEnabled(bool enabled) {
  if (timeout_handler_)
    timeout_handler_->SetEnabled(enabled);
}

void PassthroughTouchEventQueue::SetIsMobileOptimizedSite(
    bool mobile_optimized_site) {
  if (timeout_handler_)
    timeout_handler_->SetUseMobileTimeout(mobile_optimized_site);
}

bool PassthroughTouchEventQueue::IsAckTimeoutEnabled() const {
  return timeout_handler_ && timeout_handler_->IsEnabled();
}

bool PassthroughTouchEventQueue::Empty() const {
  return outstanding_touches_.empty();
}

void PassthroughTouchEventQueue::FlushQueue() {
  drop_remaining_touches_in_sequence_ = true;
  client_->FlushDeferredGestureQueue();
  while (!outstanding_touches_.empty()) {
    auto iter = outstanding_touches_.begin();
    TouchEventWithLatencyInfoAndAckState event = *iter;
    outstanding_touches_.erase(iter);
    if (event.ack_state() == INPUT_EVENT_ACK_STATE_UNKNOWN)
      event.set_ack_info(InputEventAckSource::BROWSER,
                         INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS);
    AckTouchEventToClient(event, event.ack_source(), event.ack_state());
  }
}

void PassthroughTouchEventQueue::StopTimeoutMonitor() {
  if (timeout_handler_)
    timeout_handler_->StopTimeoutMonitor();
}

void PassthroughTouchEventQueue::AckCompletedEvents() {
  // Don't allow re-entrancy into this method otherwise
  // the ordering of acks won't be preserved.
  if (processing_acks_)
    return;
  base::AutoReset<bool> process_acks(&processing_acks_, true);
  while (!outstanding_touches_.empty()) {
    auto iter = outstanding_touches_.begin();
    if (iter->ack_state() == INPUT_EVENT_ACK_STATE_UNKNOWN)
      break;
    TouchEventWithLatencyInfoAndAckState event = *iter;
    outstanding_touches_.erase(iter);
    AckTouchEventToClient(event, event.ack_source(), event.ack_state());
  }
}

void PassthroughTouchEventQueue::AckTouchEventToClient(
    const TouchEventWithLatencyInfo& acked_event,
    InputEventAckSource ack_source,
    InputEventAckState ack_result) {
  UpdateTouchConsumerStates(acked_event.event, ack_result);

  // Skip ack for TouchScrollStarted since it was synthesized within the queue.
  if (acked_event.event.GetType() != WebInputEvent::kTouchScrollStarted) {
    client_->OnTouchEventAck(acked_event, ack_source, ack_result);
  }
}

void PassthroughTouchEventQueue::SendTouchEventImmediately(
    TouchEventWithLatencyInfo* touch,
    bool wait_for_ack) {
  // Note: Touchstart events are marked cancelable to allow transitions between
  // platform scrolling and JS pinching. Touchend events, however, remain
  // uncancelable, mitigating the risk of jank when transitioning to a fling.
  if (send_touch_events_async_ &&
      touch->event.GetType() != WebInputEvent::kTouchStart)
    touch->event.dispatch_type = WebInputEvent::kEventNonBlocking;

  if (touch->event.GetType() == WebInputEvent::kTouchStart)
    touch->event.touch_start_or_first_touch_move = true;

  // For touchmove events, compare touch points position from current event
  // to last sent event and update touch points state.
  if (touch->event.GetType() == WebInputEvent::kTouchMove) {
    CHECK(last_sent_touchevent_);
    if (last_sent_touchevent_->GetType() == WebInputEvent::kTouchStart)
      touch->event.touch_start_or_first_touch_move = true;
    for (unsigned int i = 0; i < last_sent_touchevent_->touches_length; ++i) {
      const WebTouchPoint& last_touch_point = last_sent_touchevent_->touches[i];
      // Touches with same id may not have same index in Touches array.
      for (unsigned int j = 0; j < touch->event.touches_length; ++j) {
        const WebTouchPoint& current_touchmove_point = touch->event.touches[j];
        if (current_touchmove_point.id != last_touch_point.id)
          continue;

        if (!HasPointChanged(last_touch_point, current_touchmove_point))
          touch->event.touches[j].state = WebTouchPoint::kStateStationary;

        break;
      }
    }
  }

  if (touch->event.GetType() != WebInputEvent::kTouchScrollStarted) {
    if (last_sent_touchevent_)
      *last_sent_touchevent_ = touch->event;
    else
      last_sent_touchevent_.reset(new WebTouchEvent(touch->event));
  }

  if (timeout_handler_)
    timeout_handler_->StartIfNecessary(*touch);
  if (wait_for_ack)
    outstanding_touches_.insert(*touch);
  client_->SendTouchEventImmediately(*touch);
}

PassthroughTouchEventQueue::PreFilterResult
PassthroughTouchEventQueue::FilterBeforeForwarding(const WebTouchEvent& event) {
  // Unconditionally apply the timeout filter to avoid exacerbating
  // any responsiveness problems on the page.
  if (timeout_handler_ && timeout_handler_->FilterEvent(event))
    return PreFilterResult::kFilteredTimeout;

  if (base::FeatureList::IsEnabled(
          features::kSkipPassthroughTouchEventQueueFilter)) {
    return PreFilterResult::kUnfiltered;
  }

  if (event.GetType() == WebInputEvent::kTouchScrollStarted)
    return PreFilterResult::kUnfiltered;

  if (WebTouchEventTraits::IsTouchSequenceStart(event)) {
    // We don't know if we have a handler until we get the ACK back so
    // assume it is true.
    maybe_has_handler_for_current_sequence_ = true;
    send_touch_events_async_ = false;
    last_sent_touchevent_.reset();

    drop_remaining_touches_in_sequence_ = false;
    if (!has_handlers_) {
      drop_remaining_touches_in_sequence_ = true;
      return PreFilterResult::kFilteredNoPageHandlers;
    }
  }

  if (drop_remaining_touches_in_sequence_ &&
      event.GetType() != WebInputEvent::kTouchCancel)
    return PreFilterResult::kFilteredNoPageHandlers;

  if (event.GetType() == WebInputEvent::kTouchStart) {
    return (has_handlers_ || maybe_has_handler_for_current_sequence_)
               ? PreFilterResult::kUnfiltered
               : PreFilterResult::kFilteredNoPageHandlers;
  }

  // If none of the touch points active in the current sequence have handlers,
  // don't forward the touch event.
  if (!maybe_has_handler_for_current_sequence_)
    return PreFilterResult::kFilteredNoHandlerForSequence;

  // Only forward a touch if it has a non-stationary pointer that is active
  // in the current touch sequence.
  for (size_t i = 0; i < event.touches_length; ++i) {
    const WebTouchPoint& point = event.touches[i];
    if (point.state == WebTouchPoint::kStateStationary)
      continue;

    // |last_sent_touchevent_| will be non-null as long as there is an
    // active touch sequence being forwarded to the renderer.
    if (!last_sent_touchevent_)
      continue;

    for (size_t j = 0; j < last_sent_touchevent_->touches_length; ++j) {
      if (point.id != last_sent_touchevent_->touches[j].id)
        continue;

      if (event.GetType() != WebInputEvent::kTouchMove)
        return PreFilterResult::kUnfiltered;

      // All pointers in TouchMove events may have state as StateMoved,
      // even though none of the pointers have not changed in real.
      // Forward these events when at least one pointer has changed.
      if (HasPointChanged(last_sent_touchevent_->touches[j], point))
        return PreFilterResult::kUnfiltered;

      // This is a TouchMove event for which we have yet to find a
      // non-stationary pointer. Continue checking the next pointers
      // in the |event|.
      break;
    }
  }

  return PreFilterResult::kFilteredNoNonstationaryPointers;
}

void PassthroughTouchEventQueue::UpdateTouchConsumerStates(
    const WebTouchEvent& event,
    InputEventAckState ack_result) {
  if (event.GetType() == WebInputEvent::kTouchStart) {
    if (ack_result == INPUT_EVENT_ACK_STATE_CONSUMED)
      send_touch_events_async_ = false;

    // Once we have the ack back for the sequence we know if there
    // is a handler or not. Other touch-starts sent can upgrade
    // whether we have a handler or not as well.
    if (WebTouchEventTraits::IsTouchSequenceStart(event)) {
      maybe_has_handler_for_current_sequence_ =
          ack_result != INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS;
    } else {
      maybe_has_handler_for_current_sequence_ |=
          ack_result != INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS;
    }
  } else if (WebTouchEventTraits::IsTouchSequenceEnd(event)) {
    maybe_has_handler_for_current_sequence_ = false;
  }
}

size_t PassthroughTouchEventQueue::SizeForTesting() const {
  return outstanding_touches_.size();
}

bool PassthroughTouchEventQueue::IsTimeoutRunningForTesting() const {
  return timeout_handler_ && timeout_handler_->IsTimeoutTimerRunning();
}

const TouchEventWithLatencyInfo&
PassthroughTouchEventQueue::GetLatestEventForTesting() const {
  return *outstanding_touches_.rbegin();
}

}  // namespace content
