// Copyright 2012 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/base/x/x11_whole_screen_move_loop.h"

#include <stddef.h>

#include <memory>
#include <utility>

#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/run_loop.h"
#include "base/task/current_thread.h"
#include "base/task/single_thread_task_runner.h"
#include "ui/base/x/x11_pointer_grab.h"
#include "ui/base/x/x11_util.h"
#include "ui/events/event.h"
#include "ui/events/event_utils.h"
#include "ui/events/keycodes/keyboard_code_conversion_x.h"
#include "ui/events/platform/platform_event_source.h"
#include "ui/events/platform/scoped_event_dispatcher.h"
#include "ui/events/x/events_x_utils.h"
#include "ui/events/x/x11_event_translation.h"
#include "ui/gfx/x/connection.h"
#include "ui/gfx/x/keysyms/keysyms.h"
#include "ui/gfx/x/window_event_manager.h"
#include "ui/gfx/x/xproto.h"

namespace ui {

namespace {

// XGrabKey requires the modifier mask to explicitly be specified.
constexpr x11::ModMask kModifiersMasks[] = {
    {},                  // No additional modifier.
    x11::ModMask::c_2,   // Num lock
    x11::ModMask::Lock,  // Caps lock
    x11::ModMask::c_5,   // Scroll lock
    x11::ModMask::c_2 | x11::ModMask::Lock,
    x11::ModMask::c_2 | x11::ModMask::c_5,
    x11::ModMask::Lock | x11::ModMask::c_5,
    x11::ModMask::c_2 | x11::ModMask::Lock | x11::ModMask::c_5,
};

const char* GrabStatusToString(x11::GrabStatus grab_status) {
  switch (grab_status) {
    case x11::GrabStatus::Success:
      return "Success";
    case x11::GrabStatus::AlreadyGrabbed:
      return "AlreadyGrabbed";
    case x11::GrabStatus::InvalidTime:
      return "InvalidTime";
    case x11::GrabStatus::NotViewable:
      return "NotViewable";
    case x11::GrabStatus::Frozen:
      return "Frozen";
  }
  NOTREACHED();
}

}  // namespace

X11WholeScreenMoveLoop::X11WholeScreenMoveLoop(X11MoveLoopDelegate* delegate)
    : delegate_(delegate),
      in_move_loop_(false),
      grab_input_window_(x11::Window::None),
      grabbed_pointer_(false),
      canceled_(false) {}

X11WholeScreenMoveLoop::~X11WholeScreenMoveLoop() {
  EndMoveLoop();
}

void X11WholeScreenMoveLoop::PostDispatchIfNeeded(const ui::MouseEvent& event) {
}

////////////////////////////////////////////////////////////////////////////////
// DesktopWindowTreeHostLinux, ui::PlatformEventDispatcher implementation:

bool X11WholeScreenMoveLoop::CanDispatchEvent(const ui::PlatformEvent& event) {
  return in_move_loop_;
}

uint32_t X11WholeScreenMoveLoop::DispatchEvent(const ui::PlatformEvent& event) {
  DCHECK(base::CurrentUIThread::IsSet());

  // This method processes all events while the move loop is active.
  if (!in_move_loop_) {
    return ui::POST_DISPATCH_PERFORM_DEFAULT;
  }

  switch (event->type()) {
    case ui::EventType::kMouseMoved:
    case ui::EventType::kMouseDragged: {
      auto& current_xevent = *x11::Connection::Get()->dispatching_event();
      x11::Event last_xevent;
      std::unique_ptr<ui::Event> last_motion;
      auto* mouse_event = event->AsMouseEvent();
      if ((current_xevent.As<x11::MotionNotifyEvent>() ||
           current_xevent.As<x11::Input::DeviceEvent>()) &&
          ui::CoalescePendingMotionEvents(current_xevent, &last_xevent)) {
        last_motion = ui::BuildEventFromXEvent(last_xevent);
        mouse_event = last_motion->AsMouseEvent();
      }
      delegate_->OnMouseMovement(mouse_event->root_location(),
                                 mouse_event->flags(),
                                 mouse_event->time_stamp());
      return ui::POST_DISPATCH_NONE;
    }
    case ui::EventType::kMouseReleased: {
      if (event->AsMouseEvent()->IsLeftMouseButton()) {
        // Assume that drags are being done with the left mouse button. Only
        // break the drag if the left mouse button was released.
        delegate_->OnMouseReleased();

        if (!grabbed_pointer_) {
          // If the source widget had capture prior to the move loop starting,
          // it may be relying on views::Widget getting the mouse release and
          // releasing capture in Widget::OnMouseEvent().
          return ui::POST_DISPATCH_PERFORM_DEFAULT;
        }
      }
      return ui::POST_DISPATCH_NONE;
    }
    case ui::EventType::kTouchMoved: {
      // XDND is pointer-based; synthesize pointer motion from the active touch
      // point so touch drags continue to send XdndPosition updates.
      const auto* touch_event = event->AsTouchEvent();
      delegate_->OnMouseMovement(touch_event->root_location(),
                                 touch_event->flags(),
                                 touch_event->time_stamp());
      return ui::POST_DISPATCH_NONE;
    }
    case ui::EventType::kTouchReleased:
    case ui::EventType::kTouchCancelled: {
      delegate_->OnMouseReleased();
      return ui::POST_DISPATCH_NONE;
    }
    case ui::EventType::kKeyPressed:
      if (event->AsKeyEvent()->key_code() == ui::VKEY_ESCAPE) {
        canceled_ = true;
        EndMoveLoop();
        return ui::POST_DISPATCH_NONE;
      }
      break;
    default:
      break;
  }
  return ui::POST_DISPATCH_PERFORM_DEFAULT;
}

bool X11WholeScreenMoveLoop::RunMoveLoop(
    bool can_grab_pointer,
    scoped_refptr<ui::X11Cursor> old_cursor,
    scoped_refptr<ui::X11Cursor> new_cursor,
    base::OnceClosure started_callback) {
  DCHECK(!in_move_loop_);  // Can only handle one nested loop at a time.

  // Query the mouse cursor prior to the move loop starting so that it can be
  // restored when the move loop finishes.
  initial_cursor_ = old_cursor;

  auto* connection = x11::Connection::Get();
  CreateDragInputWindow(connection);

  // Only grab mouse capture of |grab_input_window_| if |can_grab_pointer| is
  // true aka the source that initiated the move loop doesn't have explicit
  // grab.
  // - The caller may intend to transfer capture to a different X11Window
  //   when the move loop ends and not release capture.
  // - Releasing capture and X window destruction are both asynchronous. We drop
  //   events targeted at |grab_input_window_| in the time between the move
  //   loop ends and |grab_input_window_| loses capture.
  grabbed_pointer_ = false;
  if (can_grab_pointer) {
    grabbed_pointer_ = GrabPointer(new_cursor);
    if (!grabbed_pointer_) {
      x11::Connection::Get()->DestroyWindow({grab_input_window_});
      return false;
    }
  }

  GrabEscKey();

  std::unique_ptr<ui::ScopedEventDispatcher> old_dispatcher =
      std::move(nested_dispatcher_);
  nested_dispatcher_ =
      ui::PlatformEventSource::GetInstance()->OverrideDispatcher(this);

  std::move(started_callback).Run();

  base::WeakPtr<X11WholeScreenMoveLoop> alive(weak_factory_.GetWeakPtr());

  in_move_loop_ = true;
  canceled_ = false;
  base::RunLoop run_loop(base::RunLoop::Type::kNestableTasksAllowed);
  quit_closure_ = run_loop.QuitClosure();
  run_loop.Run();

  if (!alive) {
    return false;
  }

  nested_dispatcher_ = std::move(old_dispatcher);
  return !canceled_;
}

void X11WholeScreenMoveLoop::UpdateCursor(scoped_refptr<ui::X11Cursor> cursor) {
  if (in_move_loop_) {
    ui::ChangeActivePointerGrabCursor(cursor);
  }
}

void X11WholeScreenMoveLoop::EndMoveLoop() {
  if (!in_move_loop_) {
    return;
  }

  // TODO(erg): Is this ungrab the cause of having to click to give input focus
  // on drawn out windows? Not ungrabbing here screws the X server until I kill
  // the chrome process.

  // Ungrab before we let go of the window.
  if (grabbed_pointer_) {
    ui::UngrabPointer();
  } else {
    UpdateCursor(initial_cursor_);
  }

  auto* connection = x11::Connection::Get();
  auto esc_keycode = connection->KeysymToKeycode(XK_Escape);
  for (auto mask : kModifiersMasks) {
    connection->UngrabKey({esc_keycode, grab_input_window_, mask});
  }

  // Restore the previous dispatcher.
  nested_dispatcher_.reset();
  base::WeakPtr<X11WholeScreenMoveLoop> alive(weak_factory_.GetWeakPtr());
  delegate_->OnMoveLoopEnded();
  if (!alive) {
    return;
  }
  grab_input_window_events_.Reset();
  connection->DestroyWindow({grab_input_window_});
  grab_input_window_ = x11::Window::None;

  in_move_loop_ = false;
  std::move(quit_closure_).Run();
}

bool X11WholeScreenMoveLoop::GrabPointer(scoped_refptr<X11Cursor> cursor) {
  auto* connection = x11::Connection::Get();

  // Pass "owner_events" as false so that X sends all mouse events to
  // |grab_input_window_|.
  auto ret = ui::GrabPointer(grab_input_window_, false, cursor);
  if (ret != x11::GrabStatus::Success) {
    DLOG(ERROR) << "Grabbing pointer for dragging failed: "
                << GrabStatusToString(ret);
  }
  connection->Flush();
  return ret == x11::GrabStatus::Success;
}

void X11WholeScreenMoveLoop::GrabEscKey() {
  auto* connection = x11::Connection::Get();
  auto esc_keycode = connection->KeysymToKeycode(XK_Escape);
  for (auto mask : kModifiersMasks) {
    connection->GrabKey({false, grab_input_window_, mask, esc_keycode,
                         x11::GrabMode::Async, x11::GrabMode::Async});
  }
}

void X11WholeScreenMoveLoop::CreateDragInputWindow(
    x11::Connection* connection) {
  grab_input_window_ = connection->GenerateId<x11::Window>();
  connection->CreateWindow(x11::CreateWindowRequest{
      .wid = grab_input_window_,
      .parent = connection->default_root(),
      .x = -100,
      .y = -100,
      .width = 10,
      .height = 10,
      .c_class = x11::WindowClass::InputOnly,
      .override_redirect = x11::Bool32(true),
  });
  auto event_mask =
      x11::EventMask::ButtonPress | x11::EventMask::ButtonRelease |
      x11::EventMask::PointerMotion | x11::EventMask::KeyPress |
      x11::EventMask::KeyRelease | x11::EventMask::StructureNotify;
  grab_input_window_events_ =
      connection->ScopedSelectEvent(grab_input_window_, event_mask);
  connection->MapWindow(grab_input_window_);
  connection->RaiseWindow(grab_input_window_);
}

}  // namespace ui
