// Copyright (c) 2012 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 "ui/views/widget/desktop_aura/x11_whole_screen_move_loop.h"

#include <stddef.h>
#include <utility>

#include "base/bind.h"
#include "base/logging.h"
#include "base/macros.h"
#include "base/message_loop/message_loop.h"
#include "base/run_loop.h"
#include "base/single_thread_task_runner.h"
#include "base/threading/thread_task_runner_handle.h"
#include "ui/aura/client/capture_client.h"
#include "ui/aura/env.h"
#include "ui/aura/window.h"
#include "ui/aura/window_event_dispatcher.h"
#include "ui/aura/window_tree_host.h"
#include "ui/base/x/x11_pointer_grab.h"
#include "ui/base/x/x11_util.h"
#include "ui/base/x/x11_window_event_manager.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/platform/x11/x11_event_source.h"
#include "ui/gfx/x/x11.h"

namespace views {

// XGrabKey requires the modifier mask to explicitly be specified.
const unsigned int kModifiersMasks[] = {
  0,                                // No additional modifier.
  Mod2Mask,                         // Num lock
  LockMask,                         // Caps lock
  Mod5Mask,                         // Scroll lock
  Mod2Mask | LockMask,
  Mod2Mask | Mod5Mask,
  LockMask | Mod5Mask,
  Mod2Mask | LockMask | Mod5Mask
};

X11WholeScreenMoveLoop::X11WholeScreenMoveLoop(X11MoveLoopDelegate* delegate)
    : delegate_(delegate),
      in_move_loop_(false),
      initial_cursor_(ui::CursorType::kNull),
      should_reset_mouse_flags_(false),
      grab_input_window_(x11::None),
      grabbed_pointer_(false),
      canceled_(false),
      weak_factory_(this) {}

X11WholeScreenMoveLoop::~X11WholeScreenMoveLoop() {}

void X11WholeScreenMoveLoop::DispatchMouseMovement() {
  if (!last_motion_in_screen_)
    return;
  delegate_->OnMouseMovement(last_motion_in_screen_->location(),
                             last_motion_in_screen_->flags(),
                             last_motion_in_screen_->time_stamp());
  last_motion_in_screen_.reset();
}

////////////////////////////////////////////////////////////////////////////////
// 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::MessageLoopForUI::IsCurrent());

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

  XEvent* xev = event;
  ui::EventType type = ui::EventTypeFromNative(xev);
  switch (type) {
    case ui::ET_MOUSE_MOVED:
    case ui::ET_MOUSE_DRAGGED: {
      bool dispatch_mouse_event = !last_motion_in_screen_.get();
      last_motion_in_screen_.reset(
          ui::EventFromNative(xev).release()->AsMouseEvent());
      last_motion_in_screen_->set_location(
          ui::EventSystemLocationFromNative(xev));
      if (dispatch_mouse_event) {
        // Post a task to dispatch mouse movement event when control returns to
        // the message loop. This allows smoother dragging since the events are
        // dispatched without waiting for the drag widget updates.
        base::ThreadTaskRunnerHandle::Get()->PostTask(
            FROM_HERE,
            base::Bind(&X11WholeScreenMoveLoop::DispatchMouseMovement,
                       weak_factory_.GetWeakPtr()));
      }
      return ui::POST_DISPATCH_NONE;
    }
    case ui::ET_MOUSE_RELEASED: {
      int button = (xev->type == ButtonRelease)
          ? xev->xbutton.button
          : ui::EventButtonFromNative(xev);
      if (button == Button1) {
        // Assume that drags are being done with the left mouse button. Only
        // break the drag if the left mouse button was released.
        DispatchMouseMovement();
        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::ET_KEY_PRESSED:
      if (ui::KeyboardCodeFromXKeyEvent(xev) == ui::VKEY_ESCAPE) {
        canceled_ = true;
        EndMoveLoop();
        return ui::POST_DISPATCH_NONE;
      }
      break;
    default:
      break;
  }
  return ui::POST_DISPATCH_PERFORM_DEFAULT;
}

bool X11WholeScreenMoveLoop::RunMoveLoop(aura::Window* source,
                                         gfx::NativeCursor cursor) {
  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_ = source->GetHost()->last_cursor();

  CreateDragInputWindow(gfx::GetXDisplay());

  // Only grab mouse capture of |grab_input_window_| if |source| does not have
  // capture.
  // - The caller may intend to transfer capture to a different aura::Window
  //   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 (!source->HasCapture()) {
    aura::client::CaptureClient* capture_client =
        aura::client::GetCaptureClient(source->GetRootWindow());
    CHECK(capture_client->GetGlobalCaptureWindow() == NULL);
    grabbed_pointer_ = GrabPointer(cursor);
    if (!grabbed_pointer_) {
      XDestroyWindow(gfx::GetXDisplay(), grab_input_window_);
      return false;
    }
  }

  GrabEscKey();

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

  // We are handling a mouse drag outside of the aura::Window system. We must
  // manually make aura think that the mouse button is pressed so that we don't
  // draw extraneous tooltips.
  aura::Env* env = aura::Env::GetInstance();
  if (!env->IsMouseButtonDown()) {
    env->set_mouse_button_flags(ui::EF_LEFT_MOUSE_BUTTON);
    should_reset_mouse_flags_ = true;
  }

  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(gfx::NativeCursor cursor) {
  if (in_move_loop_)
    ui::ChangeActivePointerGrabCursor(cursor.platform());
}

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

  // Prevent DispatchMouseMovement from dispatching any posted motion event.
  last_motion_in_screen_.reset();

  // We undo our emulated mouse click from RunMoveLoop();
  if (should_reset_mouse_flags_) {
    aura::Env::GetInstance()->set_mouse_button_flags(0);
    should_reset_mouse_flags_ = false;
  }

  // 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_);

  XDisplay* display = gfx::GetXDisplay();
  unsigned int esc_keycode = XKeysymToKeycode(display, XK_Escape);
  for (size_t i = 0; i < arraysize(kModifiersMasks); ++i) {
    XUngrabKey(display, esc_keycode, kModifiersMasks[i], grab_input_window_);
  }

  // Restore the previous dispatcher.
  nested_dispatcher_.reset();
  delegate_->OnMoveLoopEnded();
  grab_input_window_events_.reset();
  XDestroyWindow(display, grab_input_window_);
  grab_input_window_ = x11::None;

  in_move_loop_ = false;
  quit_closure_.Run();
}

bool X11WholeScreenMoveLoop::GrabPointer(gfx::NativeCursor cursor) {
  XDisplay* display = gfx::GetXDisplay();

  // Pass "owner_events" as false so that X sends all mouse events to
  // |grab_input_window_|.
  int ret = ui::GrabPointer(grab_input_window_, false, cursor.platform());
  if (ret != GrabSuccess) {
    DLOG(ERROR) << "Grabbing pointer for dragging failed: "
                << ui::GetX11ErrorString(display, ret);
  }
  XFlush(display);
  return ret == GrabSuccess;
}

void X11WholeScreenMoveLoop::GrabEscKey() {
  XDisplay* display = gfx::GetXDisplay();
  unsigned int esc_keycode = XKeysymToKeycode(display, XK_Escape);
  for (size_t i = 0; i < arraysize(kModifiersMasks); ++i) {
    XGrabKey(display, esc_keycode, kModifiersMasks[i], grab_input_window_,
             x11::False, GrabModeAsync, GrabModeAsync);
  }
}

void X11WholeScreenMoveLoop::CreateDragInputWindow(XDisplay* display) {
  unsigned long attribute_mask = CWEventMask | CWOverrideRedirect;
  XSetWindowAttributes swa;
  memset(&swa, 0, sizeof(swa));
  swa.override_redirect = x11::True;
  grab_input_window_ = XCreateWindow(display, DefaultRootWindow(display), -100,
                                     -100, 10, 10, 0, CopyFromParent, InputOnly,
                                     CopyFromParent, attribute_mask, &swa);
  uint32_t event_mask = ButtonPressMask | ButtonReleaseMask |
                        PointerMotionMask | KeyPressMask | KeyReleaseMask |
                        StructureNotifyMask;
  grab_input_window_events_.reset(
      new ui::XScopedEventSelector(grab_input_window_, event_mask));

  XMapRaised(display, grab_input_window_);
}

}  // namespace views
