// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/ozone/platform/wayland/host/wayland_event_source.h"

#include <algorithm>
#include <cstdint>
#include <functional>
#include <memory>
#include <optional>
#include <tuple>

#include "base/check.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/time/time.h"
#include "base/trace_event/trace_event.h"
#include "ui/base/dragdrop/mojom/drag_drop_types.mojom.h"
#include "ui/events/base_event_utils.h"
#include "ui/events/event_utils.h"
#include "ui/events/keycodes/dom/dom_code.h"
#include "ui/events/keycodes/dom/dom_key.h"
#include "ui/events/keycodes/dom/keycode_converter.h"
#include "ui/events/keycodes/keyboard_code_conversion.h"
#include "ui/events/keycodes/keyboard_codes.h"
#include "ui/events/ozone/events_ozone.h"
#include "ui/events/ozone/layout/keyboard_layout_engine.h"
#include "ui/events/ozone/layout/keyboard_layout_engine_manager.h"
#include "ui/events/platform/wayland/wayland_event_watcher.h"
#include "ui/events/pointer_details.h"
#include "ui/events/types/event_type.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/point_f.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/vector2d_conversions.h"
#include "ui/gfx/geometry/vector2d_f.h"
#include "ui/ozone/common/features.h"
#include "ui/ozone/platform/wayland/host/dump_util.h"
#include "ui/ozone/platform/wayland/host/wayland_connection.h"
#include "ui/ozone/platform/wayland/host/wayland_cursor_position.h"
#include "ui/ozone/platform/wayland/host/wayland_keyboard.h"
#include "ui/ozone/platform/wayland/host/wayland_tablet_tool.h"
#include "ui/ozone/platform/wayland/host/wayland_window.h"
#include "ui/ozone/platform/wayland/host/wayland_window_drag_controller.h"
#include "ui/ozone/platform/wayland/host/wayland_window_manager.h"

namespace ui {

namespace {

constexpr auto kMouseButtonToStringMap =
    base::MakeFixedFlatMap<int, const char*>({
        {EF_LEFT_MOUSE_BUTTON, "Left"},
        {EF_MIDDLE_MOUSE_BUTTON, "Middle"},
        {EF_RIGHT_MOUSE_BUTTON, "Right"},
        {EF_BACK_MOUSE_BUTTON, "Back"},
        {EF_FORWARD_MOUSE_BUTTON, "Forward"},
    });

constexpr auto kModifierToStringMap = base::MakeFixedFlatMap<int, const char*>({
    {ui::EF_SHIFT_DOWN, "Shift"},
    {ui::EF_CONTROL_DOWN, "Control"},
    {ui::EF_ALT_DOWN, "Alt"},
    {ui::EF_COMMAND_DOWN, "Command"},
    {ui::EF_ALTGR_DOWN, "AltGr"},
    {ui::EF_MOD3_DOWN, "Mod3"},
    {ui::EF_CAPS_LOCK_ON, "CapsLock"},
    {ui::EF_NUM_LOCK_ON, "NumLock"},
});

std::string ToPointerFlagsString(int flags) {
  return ToMatchingKeyMaskString(flags, kMouseButtonToStringMap);
}

std::string ToKeyboardModifierStrings(int modifiers) {
  return ToMatchingKeyMaskString(modifiers, kModifierToStringMap);
}

bool HasAnyPointerButtonFlag(int flags) {
  return (flags & (EF_LEFT_MOUSE_BUTTON | EF_MIDDLE_MOUSE_BUTTON |
                   EF_RIGHT_MOUSE_BUTTON | EF_BACK_MOUSE_BUTTON |
                   EF_FORWARD_MOUSE_BUTTON)) != 0;
}

std::vector<uint8_t> ToLittleEndianByteVector(uint32_t value) {
  return {static_cast<uint8_t>(value), static_cast<uint8_t>(value >> 8),
          static_cast<uint8_t>(value >> 16), static_cast<uint8_t>(value >> 24)};
}

EventTarget* GetRootTarget(EventTarget* target) {
  EventTarget* parent = target->GetParentTarget();
  return parent ? GetRootTarget(parent) : target;
}

gfx::Point GetOriginInScreen(WaylandWindow* target) {
  gfx::Point origin = target->GetBoundsInDIP().origin();
  auto* parent = static_cast<WaylandWindow*>(target->GetParentTarget());
  while (parent) {
    origin += parent->GetBoundsInDIP().origin().OffsetFromOrigin();
    parent = static_cast<WaylandWindow*>(parent->GetParentTarget());
  }
  return origin;
}

void SetRootLocation(LocatedEvent* event) {
  gfx::PointF location = event->location_f();
  auto* target = static_cast<WaylandWindow*>(event->target());

  while (target->GetParentTarget()) {
    location += target->GetBoundsInDIP().origin().OffsetFromOrigin();
    target = static_cast<WaylandWindow*>(target->GetParentTarget());
  }
  event->set_root_location_f(location);
}

// Number of fingers for scroll gestures.
constexpr int kGestureScrollFingerCount = 2;

// Maximum size of the latest pointer scroll data set to be stored.
constexpr int kPointerScrollDataSetMaxSize = 3;

// Maximum time delta between last scroll event and lifting of fingers.
constexpr int kFlingStartTimeoutMs = 200;

}  // namespace

struct WaylandEventSource::TouchPoint {
  TouchPoint(gfx::PointF location, WaylandWindow* current_window);
  ~TouchPoint() = default;

  raw_ptr<WaylandWindow> window;
  gfx::PointF last_known_location;
};

WaylandEventSource::TouchPoint::TouchPoint(gfx::PointF location,
                                           WaylandWindow* current_window)
    : window(current_window), last_known_location(location) {
  DCHECK(window);
}

// WaylandEventSource::PointerScrollData implementation
WaylandEventSource::PointerScrollData::PointerScrollData() = default;
WaylandEventSource::PointerScrollData::PointerScrollData(
    const PointerScrollData&) = default;
WaylandEventSource::PointerScrollData::PointerScrollData(PointerScrollData&&) =
    default;
WaylandEventSource::PointerScrollData::~PointerScrollData() = default;

WaylandEventSource::PointerScrollData&
WaylandEventSource::PointerScrollData::operator=(const PointerScrollData&) =
    default;
WaylandEventSource::PointerScrollData&
WaylandEventSource::PointerScrollData::operator=(PointerScrollData&&) = default;

void WaylandEventSource::PointerScrollData::DumpState(std::ostream& out) const {
  if (axis_source) {
    out << "axis_source=" << *axis_source;
  }
  if (timestamp) {
    out << ", timestamp=" << *timestamp;
  } else {
    out << ", no timestamp";
  }
  out << ", d=(" << dx << ", " << dy << "), dt=" << dt
      << ", is_axis_stop=" << ToBoolString(is_axis_stop);
}

// WaylandEventSource::FrameData implementation
WaylandEventSource::FrameData::FrameData(const Event& e,
                                         base::OnceCallback<void()> cb)
    : event(e.Clone()), completion_cb(std::move(cb)) {}

WaylandEventSource::FrameData::~FrameData() = default;

void WaylandEventSource::FrameData::DumpState(std::ostream& out) const {
  out << "event=" << (event ? event->ToString() : "none")
      << ", callback=" << !!completion_cb;
}

// WaylandEventSource implementation

// static
void WaylandEventSource::ConvertEventToTarget(EventTarget* new_target,
                                              LocatedEvent* event) {
  auto* current_target = static_cast<WaylandWindow*>(event->target());
  auto* new_target_window = static_cast<WaylandWindow*>(new_target);
  DCHECK(current_target);
  DCHECK(new_target_window);
  // GetOriginInScreen returns a location in UI coordinates space, ie:
  // ui_scale'd. OTOH `event` location is assumed to be in Wayland
  // coordinates, thus `diff` must be converted before used below.
  auto diff = gfx::ScaleVector2d(
      GetOriginInScreen(current_target) - GetOriginInScreen(new_target_window),
      new_target_window->applied_state().ui_scale);
  event->set_location_f(event->location_f() + diff);
}

WaylandEventSource::WaylandEventSource(wl_display* display,
                                       wl_event_queue* event_queue,
                                       WaylandWindowManager* window_manager,
                                       WaylandConnection* connection,
                                       bool use_threaded_polling)
    : window_manager_(window_manager),
      connection_(connection),
      event_watcher_(WaylandEventWatcher::CreateWaylandEventWatcher(
          display,
          event_queue,
          use_threaded_polling)) {
  DCHECK(window_manager_);

  // Observes remove changes to know when touch points can be removed.
  window_manager_->AddObserver(this);
}

WaylandEventSource::~WaylandEventSource() = default;

void WaylandEventSource::SetShutdownCb(base::OnceCallback<void()> shutdown_cb) {
  event_watcher_->SetShutdownCb(std::move(shutdown_cb));
}

void WaylandEventSource::StartProcessingEvents() {
  event_watcher_->StartProcessingEvents();
}

void WaylandEventSource::OnKeyboardFocusChanged(WaylandWindow* window,
                                                bool focused) {
  DCHECK(window);
#if DCHECK_IS_ON()
  if (!focused) {
    DCHECK_EQ(window, window_manager_->GetCurrentKeyboardFocusedWindow());
  }
#endif
  window_manager_->SetKeyboardFocusedWindow(focused ? window : nullptr);
}

void WaylandEventSource::OnKeyboardModifiersChanged(int modifiers) {
  keyboard_modifiers_ = modifiers;
}

uint32_t WaylandEventSource::OnKeyboardKeyEvent(
    EventType type,
    DomCode dom_code,
    bool repeat,
    std::optional<uint32_t> serial,
    base::TimeTicks timestamp,
    int device_id,
    WaylandKeyboard::KeyEventKind kind) {
  DCHECK(type == EventType::kKeyPressed || type == EventType::kKeyReleased);

  DomKey dom_key;
  KeyboardCode key_code;
  auto* layout_engine = KeyboardLayoutEngineManager::GetKeyboardLayoutEngine();
  if (!layout_engine || !layout_engine->Lookup(dom_code, keyboard_modifiers_,
                                               &dom_key, &key_code)) {
    LOG(ERROR) << "Failed to decode key event.";
    return POST_DISPATCH_NONE;
  }

#if BUILDFLAG(USE_GTK)
  // GTK expects the state of a key event to be the mask of modifier keys
  // _prior_ to this event. Some IMEs rely on this behavior. See
  // https://crbug.com/1086946#c11.
  int state_before_event = keyboard_modifiers_;
#endif

  KeyEvent event(type, key_code, dom_code,
                 keyboard_modifiers_ | (repeat ? EF_IS_REPEAT : 0), dom_key,
                 timestamp);
  event.set_source_device_id(device_id);

  auto* focus = window_manager_->GetCurrentKeyboardFocusedWindow();
  if (!focus) {
    VLOG(1) << "Failed to dispatch key event. No focus surface.";
    return POST_DISPATCH_STOP_PROPAGATION;
  }

  Event::DispatcherApi(&event).set_target(focus);

  Event::Properties properties;
#if BUILDFLAG(USE_GTK)
  // GTK uses XKB keycodes.
  uint32_t converted_key_code =
      ui::KeycodeConverter::DomCodeToXkbKeycode(dom_code);
  properties.emplace(
      kPropertyKeyboardHwKeyCode,
      std::vector<uint8_t>{static_cast<unsigned char>(converted_key_code)});
  // Save state before event. The flags have different values than what GTK
  // expects, but GtkUiPlatformWayland::GetGdkKeyEventState() takes care of the
  // conversion.
  properties.emplace(kPropertyKeyboardState,
                     ToLittleEndianByteVector(state_before_event));
#endif

  if (serial.has_value()) {
    properties.emplace(WaylandKeyboard::kPropertyWaylandSerial,
                       ToLittleEndianByteVector(serial.value()));
  }

  if (kind == WaylandKeyboard::KeyEventKind::kKey) {
    // Mark that this is the key event which IME did not consume.
    SetKeyboardImeFlagProperty(&properties, kPropertyKeyboardImeIgnoredFlag);
  }
  event.SetProperties(properties);
  return DispatchEvent(&event);
}

void WaylandEventSource::OnSynthesizedKeyPressEvent(WaylandWindow* window,
                                                    DomCode dom_code,
                                                    base::TimeTicks timestamp) {
  base::WeakPtr<WaylandWindow> prev_focused_window;
  if (window) {
    if (auto* prev = window_manager_->GetCurrentKeyboardFocusedWindow()) {
      prev_focused_window = prev->AsWeakPtr();
    }
    window_manager_->SetKeyboardFocusedWindow(window);
  }

  std::ignore =
      OnKeyboardKeyEvent(EventType::kKeyPressed, dom_code, /*repeat=*/false,
                         /*serial=*/std::nullopt, timestamp,
                         /*device_id=*/0, WaylandKeyboard::KeyEventKind::kKey);

  // The previously focused window may get destroyed as a side-effect of the
  // above dispatched event, thus only restore focus here if it's still alive.
  if (prev_focused_window) {
    window_manager_->SetKeyboardFocusedWindow(prev_focused_window.get());
  }
}

void WaylandEventSource::OnPointerFocusChanged(
    WaylandWindow* window,
    const gfx::PointF& location,
    base::TimeTicks timestamp,
    wl::EventDispatchPolicy dispatch_policy) {
  bool focused = !!window;
  // Only wl_pointer.enter/leave reach here, so this tracks where the mouse
  // really is even while a tablet tool has borrowed pointer focus.
  wl_pointer_focused_window_ = focused ? window->AsWeakPtr() : nullptr;
  if (focused) {
    // Save new pointer location.
    pointer_location_ = location;
    window_manager_->SetPointerFocusedWindow(window);
    pending_focus_loss_release_ = false;
  } else {
    if (!connection_->IsDragInProgress() && pointer_flags_) {
      pending_focus_loss_release_ = true;
    }
  }

  auto closure = focused ? base::NullCallback()
                         : base::BindOnce(
                               [](WaylandWindowManager* wwm) {
                                 wwm->SetPointerFocusedWindow(nullptr);
                               },
                               window_manager_);

  auto* target = window_manager_->GetCurrentPointerFocusedWindow();
  if (target) {
    EventType type =
        focused ? EventType::kMouseEntered : EventType::kMouseExited;
    MouseEvent event(type, pointer_location_, pointer_location_, timestamp,
                     pointer_flags_, 0);
    if (dispatch_policy == wl::EventDispatchPolicy::kImmediate) {
      SetTargetAndDispatchEvent(&event, target);
    } else {
      pointer_frames_.push_back(
          std::make_unique<FrameData>(event, std::move(closure)));
      return;
    }
  }

  if (!closure.is_null()) {
    std::move(closure).Run();
  }
}

void WaylandEventSource::OnPointerButtonEvent(
    EventType type,
    int changed_button,
    base::TimeTicks timestamp,
    WaylandWindow* window,
    wl::EventDispatchPolicy dispatch_policy,
    bool allow_release_of_unpressed_button,
    bool is_synthesized) {
  DCHECK(type == EventType::kMousePressed || type == EventType::kMouseReleased);
  DCHECK(HasAnyPointerButtonFlag(changed_button));

  // Ignore release events for buttons that aren't currently pressed. Such
  // events should never happen, but there have been compositor bugs before
  // (e.g. crbug.com/1376393).
  if (!allow_release_of_unpressed_button && type == EventType::kMouseReleased &&
      (pointer_flags_ & changed_button) == 0) {
    return;
  }

  // Dispatching the event may delete the previously focused window.
  base::WeakPtr<WaylandWindow> prev_focused_window =
      window_manager_->GetCurrentPointerFocusedWindow()
          ? window_manager_->GetCurrentPointerFocusedWindow()->AsWeakPtr()
          : nullptr;
  if (window) {
    window_manager_->SetPointerFocusedWindow(window);
  }

  auto closure = base::BindOnce(
      &WaylandEventSource::OnPointerButtonEventInternal, base::Unretained(this),
      (window ? prev_focused_window : nullptr), type);

  pointer_flags_ = type == EventType::kMousePressed
                       ? (pointer_flags_ | changed_button)
                       : (pointer_flags_ & ~changed_button);
  last_pointer_button_pressed_ = changed_button;

  auto* target = window_manager_->GetCurrentPointerFocusedWindow();
  // A window may be deleted when the event arrived from the server.
  if (target) {
    // MouseEvent's flags should contain the button that was released too.
    int flags = pointer_flags_ | keyboard_modifiers_ | changed_button;
    if (is_synthesized) {
      flags |= EF_IS_SYNTHESIZED;
    }
    MouseEvent event(type, pointer_location_, pointer_location_, timestamp,
                     flags, changed_button);
    if (dispatch_policy == wl::EventDispatchPolicy::kImmediate) {
      SetTargetAndDispatchEvent(&event, target);
    } else {
      pointer_frames_.push_back(
          std::make_unique<FrameData>(event, std::move(closure)));
      return;
    }
  }

  if (!closure.is_null()) {
    std::move(closure).Run();
  }
}

void WaylandEventSource::OnPointerButtonEventInternal(
    base::WeakPtr<WaylandWindow> window,
    EventType type) {
  if (window) {
    window_manager_->SetPointerFocusedWindow(window.get());
  }
}

void WaylandEventSource::OnPointerMotionEvent(
    const gfx::PointF& location,
    base::TimeTicks timestamp,
    wl::EventDispatchPolicy dispatch_policy,
    bool is_synthesized) {
  pointer_location_ = location;

  // Deliberately excludes `tablet_tool_buttons_`. Some compositors warp the
  // seat cursor to follow a tablet tool, emitting a wl_pointer.motion alongside
  // the tool's own events; folding the tool's buttons in here would report a
  // mouse move with a button held that the mouse does not have held. The tool's
  // own button state reaches Aura through OnTabletToolMotion().
  int flags = pointer_flags_ | keyboard_modifiers_;
  if (is_synthesized) {
    flags |= EF_IS_SYNTHESIZED;
  }
  MouseEvent event(EventType::kMouseMoved, pointer_location_, pointer_location_,
                   timestamp, flags, 0);
  auto* target = window_manager_->GetCurrentPointerFocusedWindow();

  // A window may be deleted when the event arrived from the server.
  if (!target) {
    return;
  }

  if (dispatch_policy == wl::EventDispatchPolicy::kImmediate) {
    SetTargetAndDispatchEvent(&event, target);
  } else {
    pointer_frames_.push_back(
        std::make_unique<FrameData>(event, base::NullCallback()));
  }
}

void WaylandEventSource::OnPointerAxisEvent(
    const gfx::Vector2dF& offset,
    std::optional<base::TimeTicks> timestamp,
    bool is_high_resolution) {
  EnsurePointerScrollData(timestamp);
  if (is_high_resolution == pointer_scroll_data_->is_high_resolution) {
    pointer_scroll_data_->dx += offset.x();
    pointer_scroll_data_->dy += offset.y();
  } else if (!is_high_resolution) {
    return;
  } else {
    pointer_scroll_data_->dx = offset.x();
    pointer_scroll_data_->dy = offset.y();
  }
  pointer_scroll_data_->is_high_resolution = is_high_resolution;
}

void WaylandEventSource::RoundTripQueue() {
  event_watcher_->RoundTripQueue();
}

void WaylandEventSource::DumpState(std::ostream& out) const {
  out << "WaylandEventSource: " << std::endl;
  out << "  pointer_location=" << pointer_location_.ToString()
      << ", flags=" << ToPointerFlagsString(pointer_flags_)
      << ", last button pressed=" << last_pointer_button_pressed_
      << ", keyboard modifiers="
      << ToKeyboardModifierStrings(keyboard_modifiers_) << std::endl;
  if (relative_pointer_location_) {
    out << "  relative_poniter_location="
        << relative_pointer_location_->ToString() << std::endl;
  }

  size_t i = 0;
  for (const auto& frame_data : pointer_frames_) {
    out << "  pointer_frame[" << i++ << "]=";
    frame_data->DumpState(out);
    out << std::endl;
  }
  i = 0;
  for (const auto& frame_data : touch_frames_) {
    out << "  touch_frame[" << i++ << "]=";
    frame_data->DumpState(out);
    out << std::endl;
  }
  i = 0;
  for (const auto& scroll_data : pointer_scroll_data_set_) {
    out << "  point_scroll_data[" << i++ << "]=";
    scroll_data.DumpState(out);
    out << std::endl;
  }
}

void WaylandEventSource::ResetStateForTesting() {
  event_watcher_->Flush();
  event_watcher_->RoundTripQueue();
  event_watcher_->StopProcessingEvents();
}

const gfx::PointF& WaylandEventSource::GetPointerLocation() const {
  return pointer_location_;
}

void WaylandEventSource::OnPointerFrameEvent() {
  base::TimeTicks now = EventTimeForNow();

  // Some compositors don't send a release when a window loses pointer focus
  // (e.g. right-clicking the titlebar buttons on GNOME to open the window
  // menu) Synthesize one if none of our windows is capturing the pointer.
  if (pending_focus_loss_release_) {
    pending_focus_loss_release_ = false;
    if (!window_manager_->located_events_grabber()) {
      ReleasePressedPointerButtons(nullptr, now);
    }
  }

  if (pointer_scroll_data_) {
    pointer_scroll_data_->dt = now - last_pointer_frame_time_;
    ProcessPointerScrollData();
  }

  last_pointer_frame_time_ = now;

  auto* target_window = window_manager_->GetCurrentPointerFocusedWindow();
  if (!target_window) {
    return;
  }
  // Dispatching an event may synchronously destroy the focused window (e.g. a
  // popup closing on click), so hold a WeakPtr and re-check on each iteration.
  base::WeakPtr<WaylandWindow> target = target_window->AsWeakPtr();

  while (!pointer_frames_.empty()) {
    // It is safe to pop the first queued event for processing.
    auto pointer_frame = std::move(pointer_frames_.front());
    pointer_frames_.pop_front();

    if (target) {
      SetTargetAndDispatchEvent(pointer_frame->event.get(), target.get());
    }
    if (!pointer_frame->completion_cb.is_null()) {
      std::move(pointer_frame->completion_cb).Run();
    }
  }
}

void WaylandEventSource::OnPointerAxisSourceEvent(uint32_t axis_source) {
  EnsurePointerScrollData(/*timestamp*/ std::nullopt);
  pointer_scroll_data_->axis_source = axis_source;
}

void WaylandEventSource::OnPointerAxisStopEvent(uint32_t axis,
                                                base::TimeTicks timestamp) {
  EnsurePointerScrollData(timestamp);
  if (axis == WL_POINTER_AXIS_VERTICAL_SCROLL) {
    pointer_scroll_data_->dy = 0;
  } else if (axis == WL_POINTER_AXIS_HORIZONTAL_SCROLL) {
    pointer_scroll_data_->dx = 0;
  }
  pointer_scroll_data_->is_axis_stop = true;
}

void WaylandEventSource::OnTabletToolProximityIn(WaylandWindow* window,
                                                 const gfx::PointF& location,
                                                 const PointerDetails& details,
                                                 base::TimeTicks time) {
  WaylandWindow* old_focus = tablet_tool_focused_window_.get();
  base::WeakPtr<WaylandWindow> window_weak = window->AsWeakPtr();
  if (old_focus && old_focus != window) {
    OnTabletToolProximityOut(details, time);
  }

  if (!window_weak) {
    return;
  }

  tablet_tool_focused_window_ = window->AsWeakPtr();
  tablet_tool_location_ = location;

  // Stylus tab dragging resolves its drag origin through the pointer focused
  // window (WaylandWindowDragController::GetSerial), so the tool has to hold
  // pointer focus while it is in proximity. OnTabletToolProximityOut() hands it
  // back to `wl_pointer_focused_window_`.
  window_manager_->SetPointerFocusedWindow(window);

  MouseEvent event(EventType::kMouseEntered, tablet_tool_location_,
                   tablet_tool_location_, time, keyboard_modifiers_, 0,
                   details);
  SetTargetAndDispatchEvent(&event, window);
  if (tablet_tool_buttons_ && !connection_->IsDragInProgress()) {
    // Release any buttons that were pressed during a DnD session.
    OnTabletToolButton(tablet_tool_buttons_, /*pressed=*/false, details, time);
  }
}

void WaylandEventSource::OnTabletToolProximityOut(const PointerDetails& details,
                                                  base::TimeTicks time) {
  if (!tablet_tool_focused_window_) {
    return;
  }

  // `details` carries the tool's pointer type. Without it the event defaults to
  // a mouse, which surfaces in Blink as a `pointerType:"mouse"` sample with the
  // spec-default `pressure:0.5` injected into the tail of a pen stroke.
  MouseEvent event(EventType::kMouseExited, tablet_tool_location_,
                   tablet_tool_location_, time, keyboard_modifiers_, 0,
                   details);
  SetTargetAndDispatchEvent(&event, tablet_tool_focused_window_.get());
  tablet_tool_focused_window_ = nullptr;

  // Give pointer focus back to the mouse. Clearing it instead would strand it:
  // the mouse never left the surface, so no wl_pointer.enter follows to restore
  // focus and every subsequent mouse event is dropped for lack of a target.
  window_manager_->SetPointerFocusedWindow(wl_pointer_focused_window_.get());

  // Intentionally not resetting `tablet_tool_buttons_` since the button state
  // should still be treated as pressed during a DnD.
}

void WaylandEventSource::OnTabletToolMotion(const gfx::PointF& location,
                                            const PointerDetails& details,
                                            base::TimeTicks time) {
  if (!tablet_tool_focused_window_) {
    return;
  }

  tablet_tool_location_ = location;
  EventType type = tablet_tool_buttons_ != 0 ? EventType::kMouseDragged
                                             : EventType::kMouseMoved;
  MouseEvent event(type, tablet_tool_location_, tablet_tool_location_, time,
                   keyboard_modifiers_ | tablet_tool_buttons_, 0, details);
  SetTargetAndDispatchEvent(&event, tablet_tool_focused_window_.get());
}

void WaylandEventSource::OnTabletToolButton(int32_t button,
                                            bool pressed,
                                            const PointerDetails& details,
                                            base::TimeTicks time) {
  if (!tablet_tool_focused_window_) {
    return;
  }

  EventType type =
      pressed ? EventType::kMousePressed : EventType::kMouseReleased;
  if (pressed) {
    tablet_tool_buttons_ |= button;
  } else {
    tablet_tool_buttons_ &= ~button;
  }

  // `button` should be included in `flags` even for button release events.
  int flags = keyboard_modifiers_ | tablet_tool_buttons_ | button;
  MouseEvent event(type, tablet_tool_location_, tablet_tool_location_, time,
                   flags, button, details);
  SetTargetAndDispatchEvent(&event, tablet_tool_focused_window_.get());
}

void WaylandEventSource::OnTouchPressEvent(
    WaylandWindow* window,
    const gfx::PointF& location,
    base::TimeTicks timestamp,
    PointerId id,
    wl::EventDispatchPolicy dispatch_policy) {
  DCHECK(window);
  HandleTouchFocusChange(window, true);

  // Make sure this touch point wasn't present before.
  auto success = touch_points_.try_emplace(
      id, std::make_unique<TouchPoint>(location, window));
  if (!success.second) {
    LOG(WARNING) << "Touch down fired with wrong id";
    return;
  }

  PointerDetails details(EventPointerType::kTouch, id);
  TouchEvent event(EventType::kTouchPressed, location, location, timestamp,
                   details, keyboard_modifiers_);
  touch_frames_.push_back(
      std::make_unique<FrameData>(event, base::NullCallback()));
}

void WaylandEventSource::OnTouchReleaseEvent(
    base::TimeTicks timestamp,
    PointerId id,
    wl::EventDispatchPolicy dispatch_policy,
    bool is_synthesized) {
  // Make sure this touch point was present before.
  const auto it = touch_points_.find(id);
  if (it == touch_points_.end()) {
    LOG(WARNING) << "Touch up fired with no matching touch down";
    return;
  }

  TouchPoint* touch_point = it->second.get();
  gfx::PointF location = touch_point->last_known_location;
  PointerDetails details(EventPointerType::kTouch, id);
  int flags = keyboard_modifiers_;
  if (is_synthesized) {
    flags |= EF_IS_SYNTHESIZED;
  }

  TouchEvent event(EventType::kTouchReleased, location, location, timestamp,
                   details, flags);
  if (dispatch_policy == wl::EventDispatchPolicy::kImmediate) {
    SetTouchTargetAndDispatchTouchEvent(&event);
    OnTouchReleaseInternal(id);
  } else {
    touch_frames_.push_back(std::make_unique<FrameData>(
        event, base::BindOnce(&WaylandEventSource::OnTouchReleaseInternal,
                              base::Unretained(this), id)));
  }
}

void WaylandEventSource::OnTouchReleaseInternal(PointerId id) {
  // It is possible that an user interaction triggers nested loops
  // in higher levels of the application stack in order to process a
  // given touch down/up action.
  // For instance, a modal dialog might block this execution point,
  // and trigger thread to continue to process events.
  // The auxiliary flow might clear entries in touch_points_.
  //
  // Hence, we check whether the TouchId is still being held.
  const auto it = touch_points_.find(id);
  if (it == touch_points_.end()) {
    LOG(WARNING) << "Touch has been released during processing.";
    return;
  }

  TouchPoint* touch_point = it->second.get();
  HandleTouchFocusChange(touch_point->window, false, id);
  touch_points_.erase(it);
}

void WaylandEventSource::SetTargetAndDispatchEvent(Event* event,
                                                   EventTarget* target) {
  CHECK(target);
  Event::DispatcherApi(event).set_target(target);
  if (event->IsLocatedEvent()) {
    auto* located_event = event->AsLocatedEvent();
    SetRootLocation(located_event);
    auto* cursor_position = connection_->wayland_cursor_position();
    // TODO(crbug.com/40934709): Touch event should not update the cursor
    // position.
    if (cursor_position) {
      auto* root_window = static_cast<WaylandWindow*>(GetRootTarget(target));
      // GetBoundsInDIP's origin is in UI DIP coordinates, cursor position
      // tracker stores Wayland DIP coordinates, so it must be ui-scaled.
      auto root_origin_wl_dip =
          gfx::ScaleToRoundedPoint(root_window->GetBoundsInDIP().origin(),
                                   root_window->applied_state().ui_scale);
      cursor_position->OnCursorPositionChanged(
          located_event->root_location() +
          root_origin_wl_dip.OffsetFromOrigin());
    }
  }
  DispatchEvent(event);
}

void WaylandEventSource::SetTouchTargetAndDispatchTouchEvent(
    TouchEvent* event) {
  auto iter = touch_points_.find(event->pointer_details().id);
  auto target = iter != touch_points_.end() ? iter->second->window : nullptr;
  // Skip if the touch target has alrady been removed.
  if (!target.get()) {
    return;
  }
  SetTargetAndDispatchEvent(event, target.get());
}

void WaylandEventSource::OnTouchMotionEvent(
    const gfx::PointF& location,
    base::TimeTicks timestamp,
    PointerId id,
    wl::EventDispatchPolicy dispatch_policy,
    bool is_synthesized) {
  const auto it = touch_points_.find(id);
  // Make sure this touch point was present before.
  if (it == touch_points_.end()) {
    LOG(WARNING) << "Touch event fired with wrong id";
    return;
  }

  it->second->last_known_location = location;
  PointerDetails details(EventPointerType::kTouch, id);
  int flags = keyboard_modifiers_;
  if (is_synthesized) {
    flags |= EF_IS_SYNTHESIZED;
  }

  TouchEvent event(EventType::kTouchMoved, location, location, timestamp,
                   details, flags);
  if (dispatch_policy == wl::EventDispatchPolicy::kImmediate) {
    SetTouchTargetAndDispatchTouchEvent(&event);
  } else {
    touch_frames_.push_back(
        std::make_unique<FrameData>(event, base::NullCallback()));
  }
}

void WaylandEventSource::OnTouchCancelEvent() {
  // Some compositors emit a TouchCancel event when a drag'n drop
  // session is started on the server, eg Exo.
  // On Chrome, this event would actually abort the whole drag'n drop
  // session on the client side.
  if (connection_->IsDragInProgress()) {
    return;
  }

  gfx::PointF location;
  base::TimeTicks timestamp = base::TimeTicks::Now();
  std::vector<PointerId> ids;
  ids.reserve(touch_points_.size());
  for (const auto& touch_point : touch_points_) {
    ids.push_back(touch_point.first);
  }
  for (PointerId id : ids) {
    auto it = touch_points_.find(id);
    if (it == touch_points_.end()) {
      continue;
    }
    base::WeakPtr<WaylandWindow> window = it->second->window->AsWeakPtr();
    TouchEvent event(EventType::kTouchCancelled, location, location, timestamp,
                     PointerDetails(EventPointerType::kTouch, id));
    SetTouchTargetAndDispatchTouchEvent(&event);
    if (window) {
      HandleTouchFocusChange(window.get(), false);
    }
  }
  touch_points_.clear();
}

void WaylandEventSource::OnTouchFrame() {
  while (!touch_frames_.empty()) {
    // It is safe to pop the first queued event for processing.
    auto touch_frame = std::move(touch_frames_.front());
    touch_frames_.pop_front();

    SetTouchTargetAndDispatchTouchEvent(touch_frame->event->AsTouchEvent());
    if (!touch_frame->completion_cb.is_null()) {
      std::move(touch_frame->completion_cb).Run();
    }
  }
}

void WaylandEventSource::OnTouchFocusChanged(WaylandWindow* window) {
  // If a window dragging session is active (and touch-based), transfer the
  // touch points to it.
  auto drag_source = connection_->window_drag_controller()->drag_source();
  if (drag_source && window) {
    DCHECK_EQ(*drag_source, mojom::DragEventSource::kTouch);
    for (auto& touch_point : touch_points_) {
      touch_point.second->window = window;
    }
  }

  window_manager_->SetTouchFocusedWindow(window);
}

std::vector<PointerId> WaylandEventSource::GetActiveTouchPointIds() {
  std::vector<PointerId> pointer_ids;
  for (auto& touch_point : touch_points_) {
    pointer_ids.push_back(touch_point.first);
  }
  return pointer_ids;
}

const WaylandWindow* WaylandEventSource::GetTouchTarget(PointerId id) const {
  const auto it = touch_points_.find(id);
  return it == touch_points_.end() ? nullptr : it->second->window.get();
}

void WaylandEventSource::OnPinchEvent(EventType event_type,
                                      const gfx::Vector2dF& delta,
                                      base::TimeTicks timestamp,
                                      int device_id,
                                      std::optional<float> scale_delta) {
  GestureEventDetails details(event_type);
  details.set_device_type(GestureDeviceType::DEVICE_TOUCHPAD);
  if (scale_delta) {
    details.set_scale(*scale_delta);
  }

  auto location = pointer_location_ + delta;
  GestureEvent event(location.x(), location.y(), 0 /* flags */, timestamp,
                     details);
  event.set_source_device_id(device_id);

  auto* target = window_manager_->GetCurrentPointerFocusedWindow();
  // A window may be deleted when the event arrived from the server.
  if (!target) {
    return;
  }

  SetTargetAndDispatchEvent(&event, target);
}

void WaylandEventSource::OnHoldEvent(EventType event_type,
                                     uint32_t finger_count,
                                     base::TimeTicks timestamp,
                                     int device_id,
                                     wl::EventDispatchPolicy dispatch_policy) {
  // Lifting the finger from the touchpad will be ignored.
  if (event_type != EventType::kTouchPressed) {
    return;
  }

  // Prevent generating any scroll events if pointer has just been moved.
  if (!is_fling_active_) {
    return;
  }
  is_fling_active_ = false;

  // Prevent fling start if axis stop arrives after hold gesture.
  if (pointer_scroll_data_) {
    pointer_scroll_data_->dx = 0;
    pointer_scroll_data_->dy = 0;
  }

  pointer_scroll_data_set_.clear();

  ScrollEvent event(EventType::kScrollFlingCancel, pointer_location_,
                    pointer_location_, timestamp, pointer_flags_, 0, 0, 0, 0,
                    finger_count);

  auto* target = window_manager_->GetCurrentPointerFocusedWindow();
  if (!target) {
    return;
  }

  if (dispatch_policy == wl::EventDispatchPolicy::kImmediate) {
    SetTargetAndDispatchEvent(&event, target);
  } else {
    pointer_frames_.push_back(
        std::make_unique<FrameData>(event, base::NullCallback()));
  }
}

void WaylandEventSource::SetRelativePointerMotionEnabled(bool enabled) {
  if (enabled) {
    relative_pointer_location_ = pointer_location_;
  } else {
    relative_pointer_location_.reset();
  }
}

void WaylandEventSource::OnRelativePointerMotion(const gfx::Vector2dF& delta,
                                                 base::TimeTicks timestamp) {
  DCHECK(relative_pointer_location_.has_value());
  relative_pointer_location_ = *relative_pointer_location_ + delta;
  OnPointerMotionEvent(*relative_pointer_location_, timestamp,
                       wl::EventDispatchPolicy::kImmediate,
                       /*is_sythesized=*/false);
}

bool WaylandEventSource::IsPointerButtonPressed(EventFlags button) const {
  DCHECK(HasAnyPointerButtonFlag(button));
  return pointer_flags_ & button;
}

void WaylandEventSource::ReleasePressedPointerButtons(
    WaylandWindow* window,
    base::TimeTicks timestamp) {
  // This may be called through the pointer delegate to cleanup pointer state.
  // Clients may call this proactively regardless of whether the any pointer
  // buttons are registered as pressed.
  if (!pointer_flags_) {
    return;
  }

  // Dispatching the event may delete the window.
  base::WeakPtr<WaylandWindow> window_weak =
      window ? window->AsWeakPtr() : nullptr;
  for (const auto& [button, name] : kMouseButtonToStringMap) {
    if (button & pointer_flags_) {
      VLOG(1) << "Synthesizing pointer release for: " << name;
      TRACE_EVENT_INSTANT("wayland.debug", "SynthesizePointerRelease", "button",
                          name);
      OnPointerButtonEvent(EventType::kMouseReleased, button, timestamp,
                           window_weak.get(),
                           wl::EventDispatchPolicy::kImmediate,
                           /*allow_release_of_unpressed_button=*/false,
                           /*is_synthesized=*/true);
      pointer_flags_ &= ~button;
    }
    if (!pointer_flags_) {
      break;
    }
  }
  CHECK(!pointer_flags_);
}

void WaylandEventSource::OnDispatcherListChanged() {
  StartProcessingEvents();
}

void WaylandEventSource::OnWindowRemoved(WaylandWindow* window) {
  // A window can be `swallowed` by another window during tab-dragging, which
  // results in OnWindowRemoved() being called.
  //
  // If a window dragging session is active and is touch-based, verify if there
  // is a valid target window to transfer the touch points to.
  if (auto* target_window = window_manager_->GetCurrentTouchFocusedWindow()) {
    auto drag_source = connection_->window_drag_controller()->drag_source();
    if (drag_source && *drag_source == mojom::DragEventSource::kTouch) {
      for (auto& touch_point : touch_points_) {
        touch_point.second->window = target_window;
      }
      return;
    }
  }

  // Clear touch-related data.
  base::EraseIf(touch_points_, [window](const auto& point) {
    return point.second->window == window;
  });
}

void WaylandEventSource::HandleTouchFocusChange(WaylandWindow* window,
                                                bool focused,
                                                std::optional<PointerId> id) {
  DCHECK(window);
  bool actual_focus = id ? !ShouldUnsetTouchFocus(window, id.value()) : focused;
  window->set_touch_focus(actual_focus);
}

// Focus must not be unset if there is another touch point within |window|.
bool WaylandEventSource::ShouldUnsetTouchFocus(WaylandWindow* win,
                                               PointerId id) {
  return std::ranges::none_of(touch_points_, [win, id](auto& p) {
    return p.second->window == win && p.first != id;
  });
}

gfx::Vector2dF WaylandEventSource::ComputeFlingVelocity() {
  struct RegressionSums {
    float tt_;  // Cumulative sum of t^2.
    float t_;   // Cumulative sum of t.
    float tx_;  // Cumulative sum of t * x.
    float ty_;  // Cumulative sum of t * y.
    float x_;   // Cumulative sum of x.
    float y_;   // Cumulative sum of y.
  };

  const size_t count = pointer_scroll_data_set_.size();

  if (count == 0) {
    return gfx::Vector2dF();
  }

  // Prevents small jumps if someone scrolls fast, immediately stops scrolling
  // and then waits a little before lifting fingers from touchpad.
  if (pointer_scroll_data_->dt > base::Milliseconds(kFlingStartTimeoutMs)) {
    return gfx::Vector2dF();
  }

  if (count == 1) {
    const auto& pointer_frame = pointer_scroll_data_set_.front();
    const float dt =
        pointer_frame.dt.InSecondsF() + pointer_scroll_data_->dt.InSecondsF();
    return gfx::Vector2dF(pointer_frame.dx * dt, pointer_frame.dy * dt);
  }

  RegressionSums sums = {0, 0, 0, 0, 0, 0};

  float time = pointer_scroll_data_->dt.InSecondsF();
  float x_coord = 0;
  float y_coord = 0;

  // Formula matches libgestures's RegressScrollVelocity()
  // from src/platform/gestures/src/immediate_interpreter.cc
  for (const auto& frame : pointer_scroll_data_set_) {
    if (frame.axis_source &&
        *frame.axis_source != WL_POINTER_AXIS_SOURCE_FINGER) {
      break;
    }
    time += frame.dt.InSecondsF();
    x_coord += frame.dx;
    y_coord += frame.dy;

    sums.tt_ += time * time;
    sums.t_ += time;
    sums.tx_ += time * x_coord;
    sums.ty_ += time * y_coord;
    sums.x_ += x_coord;
    sums.y_ += y_coord;
  }
  pointer_scroll_data_set_.clear();

  // Note the regression determinant only depends on the values of t, and should
  // never be zero so long as (1) count > 1, and (2) dt[0] != d[1]. The
  // condition of (1) was already caught at the beginning of the method.
  const float det = count * sums.tt_ - sums.t_ * sums.t_;
  if (!det) {
    // This will return the average scroll value if dt values are
    // non-zero.
    if (sums.t_) {
      return gfx::Vector2dF(x_coord / sums.t_, y_coord / sums.t_);
    }
    return gfx::Vector2dF();
  }

  const float det_inv = 1.0 / det;
  return gfx::Vector2dF((count * sums.tx_ - sums.t_ * sums.x_) * det_inv,
                        (count * sums.ty_ - sums.t_ * sums.y_) * det_inv);
}

void WaylandEventSource::EnsurePointerScrollData(
    const std::optional<base::TimeTicks>& timestamp) {
  if (!pointer_scroll_data_) {
    pointer_scroll_data_ = PointerScrollData();
  }
  if (!pointer_scroll_data_->timestamp && timestamp) {
    pointer_scroll_data_->timestamp = *timestamp;
  }
}

// This method behaves differently in Exo than in other window managers.
// If you place a finger on the touchpad, Exo dispatches axis events with an
// offset of 0 to indicate that a fling should be aborted. This event does not
// exist in Linux window managers. Instead, some window managers implement
// zwp_pointer_gesture_hold_v1 for this. However, for those who don't implement
// that, it needs to be ensured that flings are aborted when new axis events
// arrive.
void WaylandEventSource::ProcessPointerScrollData() {
  DCHECK(pointer_scroll_data_);
  // While it does not make sense for a server to send axis source only,
  // the protocol does not explicitly specify it's illegal. Just skip if
  // that happens.
  if (!pointer_scroll_data_->timestamp) {
    pointer_scroll_data_.reset();
    return;
  }
  base::TimeTicks& timestamp = *pointer_scroll_data_->timestamp;

  int flags = pointer_flags_ | keyboard_modifiers_;
  // Dispatch Fling event if pointer.axis_stop is notified and the recent
  // pointer.axis events meets the criteria to start fling scroll.
  if (pointer_scroll_data_->dx == 0 && pointer_scroll_data_->dy == 0 &&
      pointer_scroll_data_->is_axis_stop) {
    gfx::Vector2dF initial_velocity = ComputeFlingVelocity();
    float vx = initial_velocity.x();
    float vy = initial_velocity.y();
    // In Linux there is no axis event with 0 delta when start scrolling.
    // A fling is therefore always started at this point.
    ScrollEvent event(EventType::kScrollFlingStart, pointer_location_,
                      pointer_location_, timestamp, flags, vx, vy, vx, vy,
                      kGestureScrollFingerCount);
    is_fling_active_ = true;
    pointer_frames_.push_back(
        std::make_unique<FrameData>(event, base::NullCallback()));
  } else if (pointer_scroll_data_->axis_source) {
    if (*pointer_scroll_data_->axis_source == WL_POINTER_AXIS_SOURCE_WHEEL ||
        *pointer_scroll_data_->axis_source ==
            WL_POINTER_AXIS_SOURCE_WHEEL_TILT) {
      float dx = pointer_scroll_data_->dx;
      float dy = pointer_scroll_data_->dy;
      if (IsWaylandUnscaledTouchpadScrollingEnabled() &&
          !pointer_scroll_data_->is_high_resolution) {
        // Wayland compositors send axis events with values in the surface
        // coordinate
        // space. They send a value of 10 per mouse wheel click by convention,
        // so clients (e.g. GTK+) typically scale down by this amount to convert
        // to discrete step coordinates. wl_pointer version 5 improves the
        // situation by adding axis sources and discrete axis events.
        constexpr double kAxisValueScale = 10.0;
        dx = dx / kAxisValueScale * MouseWheelEvent::kWheelDelta;
        dy = dy / kAxisValueScale * MouseWheelEvent::kWheelDelta;
      }
      MouseWheelEvent event(
          gfx::ToRoundedVector2d(gfx::Vector2dF(dx, dy)),
          pointer_location_, pointer_location_, timestamp, flags, 0);
      pointer_frames_.push_back(
          std::make_unique<FrameData>(event, base::NullCallback()));
    } else if (*pointer_scroll_data_->axis_source ==
                   WL_POINTER_AXIS_SOURCE_FINGER ||
               *pointer_scroll_data_->axis_source ==
                   WL_POINTER_AXIS_SOURCE_CONTINUOUS) {
      // Fling has to be stopped if a new scroll event is received.
      // From Wayland 1.23 this will be done through hold event.
      if (is_fling_active_) {
        is_fling_active_ = false;
        ScrollEvent stop_fling_event(
            EventType::kScrollFlingCancel, pointer_location_, pointer_location_,
            timestamp, flags, 0, 0, 0, 0, kGestureScrollFingerCount);
        pointer_frames_.push_back(std::make_unique<FrameData>(
            stop_fling_event, base::NullCallback()));
      }
      ScrollEvent event(EventType::kScroll, pointer_location_,
                        pointer_location_, timestamp, flags,
                        pointer_scroll_data_->dx, pointer_scroll_data_->dy,
                        pointer_scroll_data_->dx, pointer_scroll_data_->dy,
                        kGestureScrollFingerCount);
      pointer_frames_.push_back(
          std::make_unique<FrameData>(event, base::NullCallback()));
    }

    if (pointer_scroll_data_set_.size() + 1 > kPointerScrollDataSetMaxSize) {
      pointer_scroll_data_set_.pop_back();
    }
    pointer_scroll_data_set_.push_front(*pointer_scroll_data_);
  }

  pointer_scroll_data_.reset();
}

}  // namespace ui
