/**
 * Looking Glass
 * Copyright © 2017-2026 The Looking Glass Authors
 * https://looking-glass.io
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc., 59
 * Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */

#include "wayland.h"

#include <errno.h>
#include <stdbool.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>

#include <wayland-client.h>
#include <xkbcommon/xkbcommon.h>

#include "app.h"
#include "common/debug.h"

const double WL_SCROLL_STEP = 15.0;
const double WL_HALF_SCROLL_STEP = WL_SCROLL_STEP / 2.0;

#define MTRACE(fmt, ...) \
  do \
  { \
    const uint64_t seq = app_mouseSeq(); \
    if (seq) \
      app_mouseTrace(__FILE__, __LINE__, __FUNCTION__, seq, \
          "wl." fmt, ##__VA_ARGS__); \
  } \
  while (0)

#define MLOG(seq, fmt, ...) \
  do \
  { \
    if (seq) \
      app_mouseTrace(__FILE__, __LINE__, __FUNCTION__, seq, \
          "wl." fmt, ##__VA_ARGS__); \
  } \
  while (0)

static uint32_t proxyId(void * proxy)
{
  return proxy ? wl_proxy_get_id(proxy) : 0;
}

// Mouse-handling listeners.

static void pointerMotionHandler(void * data, struct wl_pointer * pointer,
    uint32_t time, wl_fixed_t sxW, wl_fixed_t syW)
{
  wlWm.cursorX = wl_fixed_to_double(sxW);
  wlWm.cursorY = wl_fixed_to_double(syW);
  MTRACE("abs time=%u pos=%.3f,%.3f", time, wlWm.cursorX,
      wlWm.cursorY);
  app_updateCursorPos(wlWm.cursorX, wlWm.cursorY);

  if (!wlWm.warpSupport && !wlWm.relativePointer)
    app_handleMouseBasic();
}

static void pointerEnterHandler(void * data, struct wl_pointer * pointer,
    uint32_t serial, struct wl_surface * surface, wl_fixed_t sxW,
    wl_fixed_t syW)
{
  MTRACE("enter main=%d surface=%p serial=%u pos=%.3f,%.3f",
      surface == wlWm.surface, (void *)surface, serial,
      wl_fixed_to_double(sxW), wl_fixed_to_double(syW));

  if (surface != wlWm.surface)
    return;

  wlWm.pointerInSurface = true;
  app_handleEnterEvent(true);

  wl_pointer_set_cursor(pointer, serial, wlWm.cursor, wlWm.cursorHotX, wlWm.cursorHotY);
  wlWm.pointerEnterSerial = serial;

  wlWm.cursorX = wl_fixed_to_double(sxW);
  wlWm.cursorY = wl_fixed_to_double(syW);
  app_updateCursorPos(wlWm.cursorX, wlWm.cursorY);

  if (wlWm.warpSupport)
  {
    app_handleMouseRelative(0.0, 0.0, 0.0, 0.0);
    return;
  }

  if (wlWm.relativePointer)
    return;

  app_resyncMouseBasic();
  app_handleMouseBasic();
}

static void pointerLeaveHandler(void * data, struct wl_pointer * pointer,
    uint32_t serial, struct wl_surface * surface)
{
  MTRACE("leave main=%d surface=%p serial=%u",
      surface == wlWm.surface, (void *)surface, serial);

  if (surface != wlWm.surface)
    return;

  wlWm.pointerInSurface = false;
  app_handleEnterEvent(false);
}

static void pointerAxisHandler(void * data, struct wl_pointer * pointer,
  uint32_t serial, uint32_t axis, wl_fixed_t value)
{
  if (axis != WL_POINTER_AXIS_VERTICAL_SCROLL)
    return;

  double delta = wl_fixed_to_double(value);

  wlWm.scrollState += delta;

  while (wlWm.scrollState > WL_HALF_SCROLL_STEP)
  {
    app_handleButtonPress(5 /* SPICE_MOUSE_BUTTON_DOWN */);
    app_handleButtonRelease(5 /* SPICE_MOUSE_BUTTON_DOWN */);
    wlWm.scrollState -= WL_SCROLL_STEP;
  }

  while (wlWm.scrollState < -WL_HALF_SCROLL_STEP)
  {
    app_handleButtonPress(4 /* SPICE_MOUSE_BUTTON_UP */);
    app_handleButtonRelease(4 /* SPICE_MOUSE_BUTTON_UP */);
    wlWm.scrollState += WL_SCROLL_STEP;
  }

  app_handleWheelMotion(delta / WL_SCROLL_STEP);
}

static int mapWaylandToSpiceButton(uint32_t button)
{
  switch (button)
  {
    case BTN_LEFT:
      return 1;  // SPICE_MOUSE_BUTTON_LEFT
    case BTN_MIDDLE:
      return 2;  // SPICE_MOUSE_BUTTON_MIDDLE
    case BTN_RIGHT:
      return 3;  // SPICE_MOUSE_BUTTON_RIGHT
    case BTN_SIDE:
      return 6;  // SPICE_MOUSE_BUTTON_SIDE
    case BTN_EXTRA:
      return 7;  // SPICE_MOUSE_BUTTON_EXTRA
  }

  return 0;  // SPICE_MOUSE_BUTTON_INVALID
}

static void pointerButtonHandler(void *data, struct wl_pointer *pointer,
    uint32_t serial, uint32_t time, uint32_t button, uint32_t stateW)
{
  button = mapWaylandToSpiceButton(button);

  if (stateW == WL_POINTER_BUTTON_STATE_PRESSED)
    app_handleButtonPress(button);
  else
    app_handleButtonRelease(button);
}

static const struct wl_pointer_listener pointerListener = {
  .enter = pointerEnterHandler,
  .leave = pointerLeaveHandler,
  .motion = pointerMotionHandler,
  .button = pointerButtonHandler,
  .axis = pointerAxisHandler,
};

static void confinedHandler(void * data,
    struct zwp_confined_pointer_v1 * pointer)
{
  MTRACE("conf active=1 id=%u", proxyId(pointer));
}

static void unconfinedHandler(void * data,
    struct zwp_confined_pointer_v1 * pointer)
{
  MTRACE("conf active=0 id=%u", proxyId(pointer));
}

static const struct zwp_confined_pointer_v1_listener confinedListener = {
  .confined   = confinedHandler,
  .unconfined = unconfinedHandler,
};

static void lockedHandler(void * data,
    struct zwp_locked_pointer_v1 * pointer)
{
  MTRACE("lock active=1 id=%u", proxyId(pointer));
}

static void unlockedHandler(void * data,
    struct zwp_locked_pointer_v1 * pointer)
{
  MTRACE("lock active=0 id=%u", proxyId(pointer));
}

static const struct zwp_locked_pointer_v1_listener lockedListener = {
  .locked   = lockedHandler,
  .unlocked = unlockedHandler,
};

static struct zwp_confined_pointer_v1 * createConfine(
    struct wl_region * region)
{
  struct zwp_confined_pointer_v1 * pointer =
    zwp_pointer_constraints_v1_confine_pointer(
        wlWm.pointerConstraints, wlWm.surface, wlWm.pointer, region,
        ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT);
  zwp_confined_pointer_v1_add_listener(pointer, &confinedListener, NULL);
  return pointer;
}

static struct zwp_locked_pointer_v1 * createLock(void)
{
  struct zwp_locked_pointer_v1 * pointer =
    zwp_pointer_constraints_v1_lock_pointer(
        wlWm.pointerConstraints, wlWm.surface, wlWm.pointer, NULL,
        ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT);
  zwp_locked_pointer_v1_add_listener(pointer, &lockedListener, NULL);
  return pointer;
}

static void relativePointerMotionHandler(void * data,
    struct zwp_relative_pointer_v1 *pointer, uint32_t timeHi, uint32_t timeLo,
    wl_fixed_t dxW, wl_fixed_t dyW, wl_fixed_t dxUnaccelW,
    wl_fixed_t dyUnaccelW)
{
  const double dx = wl_fixed_to_double(dxW);
  const double dy = wl_fixed_to_double(dyW);
  wlWm.cursorX += dx;
  wlWm.cursorY += dy;
  MTRACE("rel time=%u:%u delta=%.3f,%.3f raw=%.3f,%.3f "
      "pos=%.3f,%.3f", timeHi, timeLo, dx, dy,
      wl_fixed_to_double(dxUnaccelW), wl_fixed_to_double(dyUnaccelW),
      wlWm.cursorX, wlWm.cursorY);
  app_updateCursorPos(wlWm.cursorX, wlWm.cursorY);

  app_handleMouseRelative(
      dx,
      dy,
      wl_fixed_to_double(dxUnaccelW),
      wl_fixed_to_double(dyUnaccelW));
}

static const struct zwp_relative_pointer_v1_listener relativePointerListener = {
    .relative_motion = relativePointerMotionHandler,
};

// Keyboard-handling listeners.

static void keyboardKeymapHandler(void * data, struct wl_keyboard * keyboard,
    uint32_t format, int fd, uint32_t size)
{
  if (!wlWm.xkb)
    goto done;

  if (wlWm.keymap)
  {
    xkb_keymap_unref(wlWm.keymap);
    wlWm.keymap = NULL;
  }

  if (wlWm.xkbState)
  {
    xkb_state_unref(wlWm.xkbState);
    wlWm.xkbState = NULL;
  }

  if (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1)
  {
    DEBUG_WARN("Unsupported keymap format, keyboard input will not work: %d", format);
    goto done;
  }

  char * map = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
  if (map == MAP_FAILED)
  {
    DEBUG_ERROR("Failed to mmap keymap: %s", strerror(errno));
    goto done;
  }

  wlWm.keymap = xkb_keymap_new_from_string(wlWm.xkb, map,
    XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS);

  if (!wlWm.keymap)
    DEBUG_WARN("Failed to load keymap, keyboard input will not work");

  munmap(map, size);

  if (wlWm.keymap)
  {
    wlWm.xkbState = xkb_state_new(wlWm.keymap);
    if (!wlWm.xkbState)
      DEBUG_WARN("Failed to create xkb_state");
  }

done:
  close(fd);
}

bool waylandGetKeyLabel(int key, char * label, size_t size)
{
  if (!wlWm.xkbState)
    return false;

  key += 8; // xkb scancode is evdev scancode + 8
  xkb_keysym_t sym = xkb_state_key_get_one_sym(wlWm.xkbState, key);
  sym = xkb_keysym_to_upper(sym);

  const uint32_t codepoint = xkb_keysym_to_utf32(sym);
  return codepoint > 0x20 && codepoint != 0x7F &&
    xkb_keysym_to_utf8(sym, label, size) > 0;
}

static void keyboardEnterHandler(void * data, struct wl_keyboard * keyboard,
    uint32_t serial, struct wl_surface * surface, struct wl_array * keys)
{
  if (surface != wlWm.surface)
    return;

  wlWm.focusedOnSurface = true;
  app_handleFocusEvent(true);
  wlWm.keyboardEnterSerial = serial;

  uint32_t * key;
  wl_array_for_each(key, keys)
    app_handleKeyPress(*key);
}

static void keyboardLeaveHandler(void * data, struct wl_keyboard * keyboard,
    uint32_t serial, struct wl_surface * surface)
{
  if (surface != wlWm.surface)
    return;

  wlWm.focusedOnSurface = false;
  waylandCBInvalidate();
  app_handleFocusEvent(false);
}

static void keyboardKeyHandler(void * data, struct wl_keyboard * keyboard,
    uint32_t serial, uint32_t time, uint32_t key, uint32_t state)
{
  if (!wlWm.focusedOnSurface)
    return;

  if (state == WL_KEYBOARD_KEY_STATE_PRESSED)
    app_handleKeyPress(key);
  else
    app_handleKeyRelease(key);

  if (!wlWm.xkbState || !app_isOverlayMode() || state != WL_KEYBOARD_KEY_STATE_PRESSED)
    return;

  key += 8; // xkb scancode is evdev scancode + 8
  int size = xkb_state_key_get_utf8(wlWm.xkbState, key, NULL, 0);

  if (size <= 0)
    return;

  char buffer[size + 1];
  xkb_state_key_get_utf8(wlWm.xkbState, key, buffer, size + 1);
  app_handleKeyboardTyped(buffer);
}

static void keyboardModifiersHandler(void * data,
    struct wl_keyboard * keyboard, uint32_t serial, uint32_t modsDepressed,
    uint32_t modsLatched, uint32_t modsLocked, uint32_t group)
{
  if (!wlWm.xkbState)
    return;

  xkb_state_update_mask(wlWm.xkbState, modsDepressed, modsLatched, modsLocked, 0, 0, group);
  app_handleKeyboardModifiers(
    xkb_state_mod_name_is_active(wlWm.xkbState, XKB_MOD_NAME_CTRL, XKB_STATE_MODS_EFFECTIVE) > 0,
    xkb_state_mod_name_is_active(wlWm.xkbState, XKB_MOD_NAME_SHIFT, XKB_STATE_MODS_EFFECTIVE) > 0,
    xkb_state_mod_name_is_active(wlWm.xkbState, XKB_MOD_NAME_ALT, XKB_STATE_MODS_EFFECTIVE) > 0,
    xkb_state_mod_name_is_active(wlWm.xkbState, XKB_MOD_NAME_LOGO, XKB_STATE_MODS_EFFECTIVE) > 0
  );

  app_handleKeyboardLEDs(
    xkb_state_led_name_is_active(wlWm.xkbState, XKB_LED_NAME_NUM) > 0,
    xkb_state_led_name_is_active(wlWm.xkbState, XKB_LED_NAME_CAPS) > 0,
    xkb_state_led_name_is_active(wlWm.xkbState, XKB_LED_NAME_SCROLL) > 0
  );
}

static const struct wl_keyboard_listener keyboardListener = {
  .keymap = keyboardKeymapHandler,
  .enter = keyboardEnterHandler,
  .leave = keyboardLeaveHandler,
  .key = keyboardKeyHandler,
  .modifiers = keyboardModifiersHandler,
};

static void waylandCleanUpPointer(void)
{
  uint32_t lockId = 0;
  uint32_t confId = 0;
  uint64_t lockSeq = 0;
  uint64_t confSeq = 0;
  INTERLOCKED_SECTION(wlWm.surfaceLock,
  {
    if (wlWm.lockedPointer)
    {
      lockId = proxyId(wlWm.lockedPointer);
      zwp_locked_pointer_v1_destroy(wlWm.lockedPointer);
      wlWm.lockedPointer = NULL;
      lockSeq = app_mouseSeq();
    }

    if (wlWm.confinedPointer)
    {
      confId = proxyId(wlWm.confinedPointer);
      zwp_confined_pointer_v1_destroy(wlWm.confinedPointer);
      wlWm.confinedPointer = NULL;
      confSeq = app_mouseSeq();
    }
  });

  MLOG(lockSeq, "lock destroy id=%u why=pointer", lockId);
  MLOG(confSeq, "conf destroy id=%u why=pointer", confId);

  if (wlWm.relativePointer)
  {
    zwp_relative_pointer_v1_destroy(wlWm.relativePointer);
    wlWm.relativePointer = NULL;
  }

  wl_pointer_destroy(wlWm.pointer);
  wlWm.pointer = NULL;
}

// Seat-handling listeners.

static void handlePointerCapability(uint32_t capabilities)
{
  bool hasPointer = capabilities & WL_SEAT_CAPABILITY_POINTER;
  if (!hasPointer && wlWm.pointer)
    waylandCleanUpPointer();
  else if (hasPointer && !wlWm.pointer)
  {
    wlWm.pointer = wl_seat_get_pointer(wlWm.seat);
    wl_pointer_add_listener(wlWm.pointer, &pointerListener, NULL);
    waylandSetPointer(wlWm.cursorId);

    if (wlWm.warpSupport)
    {
      wlWm.relativePointer =
        zwp_relative_pointer_manager_v1_get_relative_pointer(
          wlWm.relativePointerManager, wlWm.pointer);
      zwp_relative_pointer_v1_add_listener(wlWm.relativePointer,
        &relativePointerListener, NULL);
    }
  }
}

static void handleKeyboardCapability(uint32_t capabilities)
{
  bool hasKeyboard = capabilities & WL_SEAT_CAPABILITY_KEYBOARD;
  if (!hasKeyboard && wlWm.keyboard)
  {
    wl_keyboard_destroy(wlWm.keyboard);
    wlWm.keyboard = NULL;
  }
  else if (hasKeyboard && !wlWm.keyboard)
  {
    wlWm.keyboard = wl_seat_get_keyboard(wlWm.seat);
    wl_keyboard_add_listener(wlWm.keyboard, &keyboardListener, NULL);
  }
}

static void seatCapabilitiesHandler(void * data, struct wl_seat * seat,
    uint32_t capabilities)
{
  wlWm.capabilities = capabilities;
  handlePointerCapability(capabilities);
  handleKeyboardCapability(capabilities);
}

static void seatNameHandler(void * data, struct wl_seat * seat,
    const char * name)
{
  // Do nothing.
}

static const struct wl_seat_listener seatListener = {
    .capabilities = seatCapabilitiesHandler,
    .name = seatNameHandler,
};

bool waylandInputInit(bool allowNoInput)
{
  if (!wlWm.seat)
  {
    MTRACE("input seat=0 allowNone=%d", allowNoInput);
    if (allowNoInput)
    {
      DEBUG_WARN("Compositor missing wl_seat, input will be disabled");
      wlWm.warpSupport = false;
      return true;
    }

    DEBUG_ERROR("Compositor missing wl_seat, will not proceed");
    return false;
  }

  if (wlWm.warpSupport && (!wlWm.relativePointerManager || !wlWm.pointerConstraints))
  {
    DEBUG_WARN("Cursor warp is requested, but cannot be honoured due to lack "
               "of zwp_relative_pointer_manager_v1 or zwp_pointer_constraints_v1");
    wlWm.warpSupport = false;
  }

  if (!wlWm.relativePointerManager)
    DEBUG_WARN("zwp_relative_pointer_manager_v1 not exported by compositor, "
               "mouse will not be captured");

  if (!wlWm.pointerConstraints)
    DEBUG_WARN("zwp_pointer_constraints_v1 not exported by compositor, mouse "
               "will not be captured");

  if (!wlWm.keyboardInhibitManager)
    DEBUG_WARN("zwp_keyboard_shortcuts_inhibit_manager_v1 not exported by "
               "compositor, keyboard will not be grabbed");

  MTRACE("input seat=1 warp=%d relMgr=%d constraints=%d",
      wlWm.warpSupport, !!wlWm.relativePointerManager,
      !!wlWm.pointerConstraints);

  wlWm.xkb = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
  if (!wlWm.xkb)
    DEBUG_WARN("Failed to initialize xkb, keyboard input will not work");

  wl_seat_add_listener(wlWm.seat, &seatListener, NULL);
  wl_display_roundtrip(wlWm.display);

  return true;
}

void waylandInputFree(void)
{
  if (!wlWm.seat)
    return;

  waylandUngrabPointer();

  if (wlWm.pointer)
    waylandCleanUpPointer();

  // The only legal way the keyboard can be null is if it never existed.
  // When unplugged, the compositor must have an inert object.
  if (wlWm.keyboard)
    wl_keyboard_destroy(wlWm.keyboard);

  wl_seat_destroy(wlWm.seat);

  if (wlWm.xkbState)
    xkb_state_unref(wlWm.xkbState);

  if (wlWm.keymap)
    xkb_keymap_unref(wlWm.keymap);

  if (wlWm.xkb)
    xkb_context_unref(wlWm.xkb);
}

void waylandGrabPointer(void)
{
  if (!wlWm.pointer ||
      !wlWm.relativePointerManager || !wlWm.pointerConstraints)
  {
    MTRACE("grab drop pointer=%d relMgr=%d constraints=%d",
        !!wlWm.pointer, !!wlWm.relativePointerManager,
        !!wlWm.pointerConstraints);
    return;
  }

  if (!wlWm.warpSupport && !wlWm.relativePointer)
  {
    struct zwp_relative_pointer_v1 * relative =
      zwp_relative_pointer_manager_v1_get_relative_pointer(
        wlWm.relativePointerManager, wlWm.pointer);
    zwp_relative_pointer_v1_add_listener(relative,
      &relativePointerListener, NULL);
    const uint32_t relativeId = proxyId(relative);
    wlWm.relativePointer = relative;
    MTRACE("rel req id=%u why=grab", relativeId);
  }

  uint32_t confId = 0;
  uint64_t confSeq = 0;
  bool haveConf = false;
  bool haveLock = false;
  INTERLOCKED_SECTION(wlWm.surfaceLock,
  {
    if (!wlWm.confinedPointer && !wlWm.lockedPointer)
    {
      wlWm.confinedPointer = createConfine(NULL);
      confId = proxyId(wlWm.confinedPointer);
      confSeq = app_mouseSeq();
    }
    haveConf = !!wlWm.confinedPointer;
    haveLock = !!wlWm.lockedPointer;
  });

  if (confId)
    MLOG(confSeq, "conf req id=%u why=grab", confId);
  else
    MTRACE("conf skip conf=%d lock=%d", haveConf, haveLock);
}

inline static uint32_t internalUngrabPointer(bool lock, uint64_t * traceSeq)
{
  *traceSeq = 0;
  if (!wlWm.pointer)
    return 0;

  if (lock)
    LG_LOCK(wlWm.surfaceLock);

  uint32_t confId = 0;
  if (wlWm.confinedPointer)
  {
    confId = proxyId(wlWm.confinedPointer);
    zwp_confined_pointer_v1_destroy(wlWm.confinedPointer);
    wlWm.confinedPointer = NULL;
    *traceSeq = app_mouseSeq();
  }

  if (lock)
    LG_UNLOCK(wlWm.surfaceLock);

  if (!wlWm.warpSupport)
  {
    if (!wlWm.relativePointer)
    {
      wlWm.relativePointer =
        zwp_relative_pointer_manager_v1_get_relative_pointer(
          wlWm.relativePointerManager, wlWm.pointer);
      zwp_relative_pointer_v1_add_listener(wlWm.relativePointer,
        &relativePointerListener, NULL);
    }

    app_resyncMouseBasic();
    app_handleMouseBasic();
  }

  return confId;
}

void waylandUngrabPointer(void)
{
  uint64_t confSeq;
  const uint32_t confId = internalUngrabPointer(true, &confSeq);
  MLOG(confSeq, "conf destroy id=%u why=ungrab", confId);
}

void waylandCapturePointer(void)
{
  if (!wlWm.warpSupport)
  {
    MTRACE("capture fallback=confine");
    waylandGrabPointer();
    return;
  }

  uint32_t confId = 0;
  uint32_t lockId = 0;
  uint64_t confSeq = 0;
  uint64_t lockSeq = 0;
  INTERLOCKED_SECTION(wlWm.surfaceLock,
  {
    if (wlWm.confinedPointer)
    {
      confId = proxyId(wlWm.confinedPointer);
      zwp_confined_pointer_v1_destroy(wlWm.confinedPointer);
      wlWm.confinedPointer = NULL;
      confSeq = app_mouseSeq();
    }

    wlWm.lockedPointer = createLock();
    lockId = proxyId(wlWm.lockedPointer);
    lockSeq = app_mouseSeq();
  });

  MLOG(confSeq, "conf destroy id=%u why=capture", confId);
  MLOG(lockSeq, "lock req id=%u why=capture", lockId);
}

void waylandUncapturePointer(void)
{
  uint32_t lockId = 0;
  uint32_t confDropId = 0;
  uint32_t confReqId = 0;
  uint64_t lockSeq = 0;
  uint64_t confDropSeq = 0;
  uint64_t confReqSeq = 0;
  INTERLOCKED_SECTION(wlWm.surfaceLock,
  {
    if (wlWm.lockedPointer)
    {
      lockId = proxyId(wlWm.lockedPointer);
      zwp_locked_pointer_v1_destroy(wlWm.lockedPointer);
      wlWm.lockedPointer = NULL;
      lockSeq = app_mouseSeq();
    }

    /* we need to ungrab the pointer on the following conditions when exiting capture mode:
     *   - if warp is not supported, exit via window edge detection will never work
     *     as the cursor can not be warped out of the window when we release it.
     *   - if the format is invalid as we do not know where the guest cursor is,
     *     which also breaks edge detection.
     *   - if the user has opted to use captureInputOnly mode.
     */
    if (!wlWm.warpSupport || !app_isFormatValid() || app_isCaptureOnlyMode())
      confDropId = internalUngrabPointer(false, &confDropSeq);
    else if (wlWm.pointer)
    {
      wlWm.confinedPointer = createConfine(NULL);
      confReqId = proxyId(wlWm.confinedPointer);
      confReqSeq = app_mouseSeq();
    }
  });

  MLOG(lockSeq, "lock destroy id=%u why=uncapture", lockId);
  MLOG(confDropSeq, "conf destroy id=%u why=uncapture", confDropId);
  MLOG(confReqSeq, "conf req id=%u why=uncapture", confReqId);
}

void waylandGrabKeyboard(void)
{
  if (wlWm.seat &&
      wlWm.keyboardInhibitManager && !wlWm.keyboardInhibitor)
  {
    wlWm.keyboardInhibitor = zwp_keyboard_shortcuts_inhibit_manager_v1_inhibit_shortcuts(
        wlWm.keyboardInhibitManager, wlWm.surface, wlWm.seat);
  }
}

void waylandUngrabKeyboard(void)
{
  if (wlWm.keyboardInhibitor)
  {
    zwp_keyboard_shortcuts_inhibitor_v1_destroy(wlWm.keyboardInhibitor);
    wlWm.keyboardInhibitor = NULL;
  }
}

void waylandWarpPointer(int x, int y, bool exiting)
{
  const int reqX = x;
  const int reqY = y;
  if (!wlWm.pointerInSurface)
  {
    MTRACE("warp drop=surface target=%d,%d exit=%d", x, y, exiting);
    return;
  }

  if (wlWm.lockedPointer)
  {
    MTRACE("warp drop=lock target=%d,%d exit=%d", x, y, exiting);
    return;
  }

  LG_LOCK(wlWm.surfaceLock);
  if (wlWm.lockedPointer)
  {
    LG_UNLOCK(wlWm.surfaceLock);
    MTRACE("warp drop=lock-race target=%d,%d exit=%d", x, y, exiting);
    return;
  }

  int width, height;
  wlWm.desktop->getSize(&width, &height);

  if (x < 0) x = 0;
  else if (x >= width) x = width - 1;
  if (y < 0) y = 0;
  else if (y >= height) y = height - 1;

  struct wl_region * region = wl_compositor_create_region(wlWm.compositor);
  wl_region_add(region, x, y, 1, 1);

  const uint32_t confId = proxyId(wlWm.confinedPointer);
  uint32_t tempId = 0;
  uint64_t warpSeq = 0;
  if (wlWm.confinedPointer)
  {
    zwp_confined_pointer_v1_set_region(wlWm.confinedPointer, region);
    wl_surface_commit(wlWm.surface);
    zwp_confined_pointer_v1_set_region(wlWm.confinedPointer, NULL);
  }
  else
  {
    struct zwp_confined_pointer_v1 * confine = createConfine(region);
    tempId = proxyId(confine);
    wl_surface_commit(wlWm.surface);
    zwp_confined_pointer_v1_destroy(confine);
  }

  wl_surface_commit(wlWm.surface);
  wl_region_destroy(region);
  warpSeq = app_mouseSeq();
  LG_UNLOCK(wlWm.surfaceLock);

  MLOG(warpSeq, "warp req=%d,%d target=%d,%d exit=%d conf=%u temp=%u",
      reqX, reqY, x, y, exiting, confId, tempId);
}

void waylandRealignPointer(void)
{
  if (!wlWm.warpSupport)
    app_resyncMouseBasic();
}

void waylandGuestPointerUpdated(double x, double y, double localX, double localY)
{
  const char * drop = NULL;
  if (!wlWm.pointer)
    drop = "pointer";
  else if (!wlWm.warpSupport)
    drop = "support";
  else if (!wlWm.pointerInSurface)
    drop = "surface";
  else if (wlWm.lockedPointer)
    drop = "lock";

  if (drop)
  {
    MTRACE("guest drop=%s guest=%.3f,%.3f local=%.3f,%.3f",
        drop, x, y, localX, localY);
    return;
  }

  MTRACE("guest warp guest=%.3f,%.3f local=%.3f,%.3f",
      x, y, localX, localY);
  waylandWarpPointer((int) localX, (int) localY, false);
}
