/**
 * 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 "core.h"
#include "main.h"
#include "app.h"
#include "util.h"
#include "kb.h"
#include "message.h"
#include "input.h"
#include "evdev.h"

#include "common/time.h"
#include "common/debug.h"
#include "common/array.h"
#include "common/proctitle.h"
#include "common/stringutils.h"
#include "common/version.h"

#include <math.h>

#define RESIZE_TIMEOUT (10 * 1000) // 10ms

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

static bool isInView(void)
{
  return
    g_cursor.pos.x >= g_state.dstRect.x                     &&
    g_cursor.pos.x <  g_state.dstRect.x + g_state.dstRect.w &&
    g_cursor.pos.y >= g_state.dstRect.y                     &&
    g_cursor.pos.y <  g_state.dstRect.y + g_state.dstRect.h;
}

static int exitPos(double pos, int edge)
{
  return pos < edge ? floor(pos) : ceil(pos);
}

static void cancelExit(const char * why)
{
  if (!g_cursor.exit)
    return;

  MTRACE("exit cancel=%s target=%.3f,%.3f", why,
      g_cursor.exitPos.x, g_cursor.exitPos.y);
  g_cursor.exit = false;
}

static void cancelSurfaceExit(const char * why)
{
  if (!g_cursor.surfaceExit)
    return;

  MTRACE("surface exit cancel=%s", why);
  g_cursor.surfaceExit = false;
  g_cursor.warpState   = WARP_STATE_ON;
}

static void finishExit(void)
{
  if (!g_cursor.exit || g_cursor.viewReq)
    return;

  const int x = exitPos(g_cursor.exitPos.x, g_state.dstRect.x);
  const int y = exitPos(g_cursor.exitPos.y, g_state.dstRect.y);
  g_cursor.exit = false;

  MTRACE("exit warp target=%d,%d projected=%.3f,%.3f inWin=%d",
      x, y, g_cursor.exitPos.x, g_cursor.exitPos.y, g_cursor.inWindow);

  if (x < 0 || x >= g_state.windowW ||
      y < 0 || y >= g_state.windowH)
    g_cursor.inWindow = false;

  core_warpPointer(x, y, true);
}

static void startExit(double x, double y)
{
  g_cursor.exit    = true;
  g_cursor.exitPos = (struct DoublePoint) { x, y };
  core_setCursorInView(false);
}

static bool moveExit(double ex, double ey)
{
  if (g_cursor.useScale && g_params.scaleMouseInput)
  {
    ex *= g_cursor.scale.x;
    ey *= g_cursor.scale.y;
  }

  struct DoublePoint move = {.x = ex, .y = ey};
  util_rotatePoint(&move);
  g_cursor.exitPos.x += move.x;
  g_cursor.exitPos.y += move.y;

  const bool inside =
    g_cursor.exitPos.x >= g_state.dstRect.x &&
    g_cursor.exitPos.x <  g_state.dstRect.x + g_state.dstRect.w &&
    g_cursor.exitPos.y >= g_state.dstRect.y &&
    g_cursor.exitPos.y <  g_state.dstRect.y + g_state.dstRect.h;

  MTRACE("exit move=%.3f,%.3f target=%.3f,%.3f inside=%d",
      move.x, move.y, g_cursor.exitPos.x, g_cursor.exitPos.y, inside);
  return inside;
}

static bool inputPolicyEnabled(void)
{
  return !g_state.ignoreInput &&
    ((g_cursor.grab && g_params.captureInputOnly) ||
      !g_params.captureInputOnly);
}

bool core_inputEnabled(void)
{
  return lgInput_available() && inputPolicyEnabled();
}

void core_updateKeyboardLEDsSync(void)
{
  lgInput_setKeyboardLEDsSync(
      inputPolicyEnabled() && !evdev_isExclusive());
}

void core_updateKeyboardGrab(void)
{
  const bool inputEnabled = core_inputEnabled();
  const bool localActive  = g_state.focused && !app_isOverlayMode() &&
    !g_state.ignoreInput;
  /* Explicit capture is local window state and must survive an input provider
   * becoming unavailable or changing during startup. */
  const bool capture      = g_cursor.grab && g_params.grabKeyboard &&
    !g_state.ignoreInput;
  const bool view         = g_cursor.inView && g_params.grabKeyboardOnFocus;
  const bool automatic    = !g_cursor.grab &&
    g_cursor.autoCaptureActive && !g_params.captureInputOnly;
  const bool active       = g_state.focused && !app_isOverlayMode() &&
    (capture || (inputEnabled && (view || automatic)));

  evdev_setGrab(active, localActive && g_cursor.grab);
  core_updateKeyboardLEDsSync();
  if (active)
    g_state.ds->grabKeyboard();
  else
    g_state.ds->ungrabKeyboard();
}

static void setAutoCapture(bool active)
{
  active = active && g_params.autoCapture && g_cursor.inView &&
    !g_cursor.grab && !g_params.captureInputOnly;
  if (g_cursor.autoCaptureActive == active)
    return;

  MTRACE("auto capture active=%d old=%d", active,
      g_cursor.autoCaptureActive);
  g_cursor.autoCaptureActive = active;
  core_updateKeyboardGrab();
}

static void applyView(bool active, bool force)
{
  if (active && !g_cursor.viewReq)
  {
    MTRACE("view active skip=req");
    return;
  }

  if (!force && g_cursor.inView == active)
  {
    core_updateKeyboardGrab();
    return;
  }

  MTRACE("view active=%d old=%d req=%d force=%d", active,
      g_cursor.inView, g_cursor.viewReq, force);

  g_cursor.inView            = active;
  g_cursor.draw              =
    (g_params.alwaysShowCursor || g_params.captureInputOnly)
    ? true : g_cursor.inView;
  g_cursor.redraw            = true;
  app_invalidateWindow(false);
  g_cursor.motionValid       = false;
  g_cursor.autoCaptureActive = active && g_params.autoCapture &&
    !g_cursor.grab && !g_params.captureInputOnly;

  g_cursor.warpState = g_cursor.inView ? WARP_STATE_ON : WARP_STATE_OFF;
  if (g_cursor.inView)
  {
    if (g_params.hideMouse)
      g_state.ds->setPointer(LG_POINTER_NONE);
  }
  else
  {
    if (g_params.hideMouse)
      g_state.ds->setPointer(LG_POINTER_SQUARE);
  }

  core_updateKeyboardGrab();
  g_cursor.warpState = WARP_STATE_ON;
}

void core_invalidatePointer(bool detectInView)
{
  MTRACE("invalidate detect=%d inWin=%d req=%d active=%d grab=%d "
      "warp=%d", detectInView, g_cursor.inWindow, g_cursor.viewReq,
      g_cursor.inView, g_cursor.grab, g_cursor.warpState);

  if (!detectInView)
  {
    applyView(g_cursor.inView, true);
    return;
  }

  const bool inView = isInView();

  /* do not allow the view to become active if any mouse buttons are being
   * held, this fixes issues with meta window resizing. */
  if (inView && g_cursor.buttons)
    return;

  core_setCursorInView(inView);
}

void core_setCursorInView(bool enable)
{
  MTRACE("view req=%d oldReq=%d active=%d focus=%d inWin=%d grab=%d",
      enable, g_cursor.viewReq, g_cursor.inView, g_state.focused,
      g_cursor.inWindow, g_cursor.grab);

  if (!enable)
    cancelSurfaceExit("view");

  if (g_cursor.exit &&
      (enable || !g_state.focused || !g_cursor.inWindow ||
       g_state.ignoreInput || !g_state.posInfoValid || app_isOverlayMode()))
    cancelExit(enable ? "view" : "state");

  if (enable && !g_state.focused)
  {
    MTRACE("view skip=focus");
    return;
  }

  g_cursor.viewReq = enable;
  applyView(enable, false);
}

void core_handleGrabEvent(bool active)
{
  MTRACE("conf active=%d req=%d old=%d grab=%d", active,
      g_cursor.viewReq, g_cursor.inView, g_cursor.grab);

  enum LG_DSWarpSupport warpSupport = LG_DS_WARP_NONE;
  app_getProp(LG_DS_WARP_SUPPORT, &warpSupport);

  if (g_cursor.grab || g_params.captureInputOnly ||
      warpSupport == LG_DS_WARP_NONE)
    return;

  if (g_cursor.surfaceExit)
  {
    if (!active && g_cursor.viewReq && g_cursor.inWindow)
    {
      MTRACE("surface exit release pending");
      return;
    }

    cancelSurfaceExit(active ? "confined" : "state");
  }

  if (!active)
    finishExit();

  applyView(active, false);
  if (active)
    core_handleMouseAbsolute();
}

void core_setGrab(bool enable)
{
  core_setGrabQuiet(enable);

  app_alert(
    g_cursor.grab ? LG_ALERT_SUCCESS  : LG_ALERT_WARNING,
    g_cursor.grab ? "Capture Enabled" : "Capture Disabled"
  );
}

void core_setGrabQuiet(bool enable)
{
  MTRACE("grab req=%d old=%d inWin=%d inView=%d focus=%d",
      enable, g_cursor.grab, g_cursor.inWindow, g_cursor.inView,
      g_state.focused);

  /* we always do this so that at init the cursor is in the right state */
  if (g_params.captureInputOnly && g_params.hideMouse)
    g_state.ds->setPointer(enable ? LG_POINTER_NONE : LG_POINTER_SQUARE);

  if (g_cursor.grab == enable)
    return;

  cancelSurfaceExit("capture");
  g_cursor.acc.x       = 0.0;
  g_cursor.acc.y       = 0.0;
  g_cursor.motionValid = false;

  /* if the display server does not support warp we need to ungrab the pointer
   * here instead of in the move handler */
  enum LG_DSWarpSupport warpSupport = LG_DS_WARP_NONE;
  app_getProp(LG_DS_WARP_SUPPORT, &warpSupport);

  if (enable)
  {
    g_state.ignoreInput          = false;
    g_cursor.grab                = true;
    g_cursor.autoCaptureActive   = false;
    core_updateKeyboardGrab();
    core_setCursorInView(true);

    /* ensure the local mouse is inside the window before we capture, this fixes
     * odd UI behaviour if the user is using focus follows mouse and the window
     * was focused without the cursor being in window already */
    if (warpSupport != LG_DS_WARP_NONE)
    {
      struct DoublePoint local =
      {
        .x = g_state.windowCX,
        .y = g_state.windowCY,
      };
      const bool valid = util_guestCurToLocal(&local);
      MTRACE("grab align valid=%d", valid);
      core_warpPointer(local.x, local.y, true);
    }

    g_state.ds->capturePointer();
  }
  else
  {
    g_state.ds->uncapturePointer();
    g_cursor.grab              = false;
    g_cursor.autoCaptureActive = g_params.autoCapture &&
      g_cursor.inView && !g_params.captureInputOnly;
    core_updateKeyboardGrab();

    if (warpSupport == LG_DS_WARP_NONE)
      core_handleMouseAbsolute();
    else
    {
      g_cursor.warpState = WARP_STATE_ON;
      core_alignToGuest();
    }
  }
}

bool core_warpPointer(int x, int y, bool exiting)
{
  if (!g_cursor.inWindow && !exiting)
  {
    MTRACE("warp drop=window target=%d,%d exit=%d", x, y, exiting);
    return false;
  }

  if (app_isOverlayMode())
  {
    MTRACE("warp drop=overlay target=%d,%d exit=%d", x, y, exiting);
    return false;
  }

  enum LG_DSWarpSupport warpSupport = LG_DS_WARP_NONE;
  app_getProp(LG_DS_WARP_SUPPORT, &warpSupport);
  if (warpSupport == LG_DS_WARP_NONE)
  {
    MTRACE("warp drop=support target=%d,%d exit=%d", x, y, exiting);
    return false;
  }

  if (!exiting && g_cursor.warpState == WARP_STATE_OFF)
  {
    MTRACE("warp drop=state target=%d,%d exit=%d", x, y, exiting);
    return false;
  }

  if (exiting)
    g_cursor.warpState = WARP_STATE_OFF;

  if (!exiting && g_cursor.pos.x == x && g_cursor.pos.y == y)
  {
    MTRACE("warp same target=%d,%d exit=%d", x, y, exiting);
    return true;
  }

  MTRACE("warp send target=%d,%d exit=%d pos=%.3f,%.3f",
      x, y, exiting, g_cursor.pos.x, g_cursor.pos.y);
  g_cursor.motionValid = false;
  g_state.ds->warpPointer(x, y, exiting);
  return true;
}

void core_onWindowSizeChanged(unsigned width, unsigned height)
{
  if (!g_state.transport.handle ||
      !(g_state.transportFeatures & LG_TRANSPORT_FEATURE_WINDOW_SIZE))
    return;

  if (g_state.srcSize.x == width && g_state.srcSize.y == height)
    return;

  const LG_TransportControl control =
  {
    .type       = LG_TRANSPORT_CONTROL_WINDOW_SIZE,
    .windowSize = { .width = width, .height = height },
  };

  LG_TransportControlToken token;
  const LG_TransportStatus status = g_state.transport.ops->sendControl(
      g_state.transport.handle, &control, &token);
  if (status != LG_TRANSPORT_OK && status != LG_TRANSPORT_UNAVAILABLE)
    DEBUG_WARN("Window-size control failed with status %d", status);
}

void core_updatePositionInfo(void)
{
  g_cursor.motionValid = false;
  cancelExit("geometry");

  if (g_params.setGuestRes &&
      g_state.transportFeatures & LG_TRANSPORT_FEATURE_WINDOW_SIZE)
  {
    LGMsg msg =
    {
      .type = LG_MSG_WINDOWSIZE,
      .windowSize =
      {
        .width  = round(g_state.windowW * g_state.windowScale),
        .height = round(g_state.windowH * g_state.windowScale)
      }
    };
    lgMessage_post(&msg);
  }

  if (!g_state.haveSrcSize)
    goto done;

  float srcW;
  float srcH;

  switch(g_params.winRotate)
  {
    case LG_ROTATE_0:
    case LG_ROTATE_180:
      srcW = g_state.srcSize.x;
      srcH = g_state.srcSize.y;
      break;

    case LG_ROTATE_90:
    case LG_ROTATE_270:
      srcW = g_state.srcSize.y;
      srcH = g_state.srcSize.x;
      break;

    default:
      DEBUG_UNREACHABLE();
  }

  if (g_params.keepAspect)
  {
    const float srcAspect = srcH / srcW;
    const float wndAspect = (float)g_state.windowH / (float)g_state.windowW;
    bool force = true;

    if (g_params.dontUpscale &&
        srcW <= g_state.windowW &&
        srcH <= g_state.windowH)
    {
      force = false;
      g_state.dstRect.w = srcW;
      g_state.dstRect.h = srcH;
      g_state.dstRect.x = g_state.windowCX - srcW / 2;
      g_state.dstRect.y = g_state.windowCY - srcH / 2;
    }
    else
    if (g_params.intUpscale &&
        srcW <= g_state.windowW &&
        srcH <= g_state.windowH)
    {
      force = false;
      const int scale = min(
          floor(g_state.windowW / srcW),
          floor(g_state.windowH / srcH));
      g_state.dstRect.w = srcW * scale;
      g_state.dstRect.h = srcH * scale;
      g_state.dstRect.x = g_state.windowCX - g_state.dstRect.w / 2;
      g_state.dstRect.y = g_state.windowCY - g_state.dstRect.h / 2;
    }
    else
    if ((int)(wndAspect * 1000) == (int)(srcAspect * 1000))
    {
      force           = false;
      g_state.dstRect.w = g_state.windowW;
      g_state.dstRect.h = g_state.windowH;
      g_state.dstRect.x = 0;
      g_state.dstRect.y = 0;
    }
    else
    if (wndAspect < srcAspect)
    {
      g_state.dstRect.w = (float)g_state.windowH / srcAspect;
      g_state.dstRect.h = g_state.windowH;
      g_state.dstRect.x = (g_state.windowW >> 1) - (g_state.dstRect.w >> 1);
      g_state.dstRect.y = 0;
    }
    else
    {
      g_state.dstRect.w = g_state.windowW;
      g_state.dstRect.h = (float)g_state.windowW * srcAspect;
      g_state.dstRect.x = 0;
      g_state.dstRect.y = (g_state.windowH >> 1) - (g_state.dstRect.h >> 1);
    }

    if (g_params.dontUpscale && g_params.shrinkOnUpscale)
    {
      if (g_state.windowW > srcW)
      {
        force = true;
        g_state.dstRect.w = (int) (srcW + 0.5);
      }
      if (g_state.windowH > srcH)
      {
        force = true;
        g_state.dstRect.h = (int) (srcH + 0.5);
      }
    }

    if (force && g_params.forceAspect)
    {
      g_state.resizeTimeout = microtime() + RESIZE_TIMEOUT;
      g_state.resizeDone    = false;
    }
  }
  else
  {
    g_state.dstRect.x = 0;
    g_state.dstRect.y = 0;
    g_state.dstRect.w = g_state.windowW;
    g_state.dstRect.h = g_state.windowH;
  }
  g_state.dstRect.valid = true;

  g_cursor.useScale = (
      srcH       != g_state.dstRect.h ||
      srcW       != g_state.dstRect.w);

  g_cursor.scale.x  = (float)srcW / (float)g_state.dstRect.w;
  g_cursor.scale.y  = (float)srcH / (float)g_state.dstRect.h;

  if (!g_state.posInfoValid)
  {
    g_state.posInfoValid = true;
    g_state.ds->realignPointer();

    // g_cursor.guest.valid could have become true in the meantime.
    if (g_cursor.guest.valid)
    {
      // Since posInfoValid was false, core_handleGuestMouseUpdate becomes a
      // noop when called on the cursor thread, which means we need to call it
      // again in order for the cursor to show up.
      core_handleGuestMouseUpdate();

      // Similarly, the position needs to be valid before the initial mouse
      // move, otherwise we wouldn't know if the cursor is in the viewport.
      app_handleMouseRelative(0.0, 0.0, 0.0, 0.0);
    }
  }

done:
  if (g_cursor.surfaceExit)
  {
    if (g_cursor.viewReq && g_cursor.inWindow && g_state.focused &&
        !g_state.ignoreInput && g_state.posInfoValid &&
        !app_isOverlayMode())
    {
      MTRACE("surface exit regrab=geometry");
      g_cursor.warpState = WARP_STATE_ON;
      g_state.ds->grabPointer();
    }
    else
      core_setCursorInView(false);
  }

  MTRACE("geometry src=%dx%d dst=%d,%d,%d,%d win=%dx%d "
      "scale=%.4f,%.4f uiScale=%.4f rot=%d/%d border=%d,%d,%d,%d "
      "valid=%d", g_state.srcSize.x, g_state.srcSize.y,
      g_state.dstRect.x, g_state.dstRect.y, g_state.dstRect.w,
      g_state.dstRect.h, g_state.windowW, g_state.windowH,
      g_cursor.scale.x, g_cursor.scale.y, g_state.windowScale,
      g_state.rotate, g_params.winRotate, g_state.border.left,
      g_state.border.top, g_state.border.right, g_state.border.bottom,
      g_state.posInfoValid);
  app_updateMouseState();
  atomic_fetch_add(&g_state.lgrResize, 1);
}

void core_alignToGuest(void)
{
  if (!g_cursor.guest.valid || !g_state.focused)
    return;

  struct DoublePoint local;
  if (util_guestCurToLocal(&local))
    if (core_warpPointer(round(local.x), round(local.y), false))
      core_setCursorInView(true);
}

bool core_isValidPointerPos(int x, int y)
{
  return g_state.ds->isValidPointerPos(x, y);
}

static void cancelVideoWaits(void)
{
  if (!g_state.videoOps || g_state.videoOps->type != LG_VIDEO_TYPE_FRAME)
    return;

  const LG_FrameOps * ops = g_state.videoOps->frame;
  if (g_state.frameThread)
    ops->cancelFrameWait(g_state.transport.handle);
  if (g_state.cursorThread)
    ops->cancelPointerWait(g_state.transport.handle);
}

void core_stopVideoThreads(void)
{
  atomic_store_explicit(
      &g_state.stopVideoThreads, true, memory_order_release);
  cancelVideoWaits();

  if (g_state.frameThread)
    lgJoinThread(g_state.frameThread, NULL);
  if (g_state.cursorThread)
    lgJoinThread(g_state.cursorThread, NULL);

  g_state.frameThread  = NULL;
  g_state.cursorThread = NULL;
}

bool core_startVideoThreads(void)
{
  const LG_FrameOps * ops = g_state.videoOps->frame;
  const bool pointer = ops->nextPointer && ops->releasePointer;
  if (g_state.frameThread && (!pointer || g_state.cursorThread))
    return true;
  if (g_state.frameThread || g_state.cursorThread)
    core_stopVideoThreads();

  atomic_store_explicit(
      &g_state.stopVideoThreads, false, memory_order_release);
  if (!lgCreateThread("frameThread", main_frameThread, NULL,
        &g_state.frameThread))
  {
    DEBUG_ERROR("frame create thread failed");
    return false;
  }

  if (pointer && !lgCreateThread("cursorThread", main_cursorThread, NULL,
        &g_state.cursorThread))
  {
    DEBUG_ERROR("cursor create thread failed");
    core_stopVideoThreads();
    return false;
  }
  return true;
}

void core_handleGuestMouseUpdate(void)
{
  /* The local cursor is authoritative with absolute input. Sending delayed
   * guest positions back to the display server creates a warp feedback loop. */
  if (lgInput_supports(LG_INPUT_SUPPORT_MOUSE_ABSOLUTE))
    return;

  struct DoublePoint localPos;
  if (!util_guestCurToLocal(&localPos))
  {
    MTRACE("guest drop=geometry guest=%d,%d hot=%d,%d valid=%d",
        g_cursor.guest.x, g_cursor.guest.y, g_cursor.guest.hx,
        g_cursor.guest.hy, g_cursor.guest.valid);
    return;
  }

  const bool overlay = app_isOverlayMode();
  if (overlay || !g_cursor.inView || !g_cursor.viewReq)
  {
    MTRACE("guest drop=%s guest=%d,%d local=%.3f,%.3f inWin=%d "
        "inView=%d req=%d", overlay ? "overlay" : "view",
        g_cursor.guest.x,
        g_cursor.guest.y, localPos.x, localPos.y, g_cursor.inWindow,
        g_cursor.inView, g_cursor.viewReq);
    return;
  }

  MTRACE("guest send guest=%d,%d local=%.3f,%.3f inWin=%d inView=%d",
      g_cursor.guest.x, g_cursor.guest.y, localPos.x, localPos.y,
      g_cursor.inWindow, g_cursor.inView);
  g_state.ds->guestPointerUpdated(
    g_cursor.guest.x, g_cursor.guest.y,
    util_clamp(localPos.x, g_state.dstRect.x,
      g_state.dstRect.x + g_state.dstRect.w),
    util_clamp(localPos.y, g_state.dstRect.y,
      g_state.dstRect.y + g_state.dstRect.h)
  );
}

static void handleMouseAbsolute(bool inputEnabled, bool absolute)
{
  if (g_cursor.grab || !g_cursor.inWindow || !g_cursor.valid ||
      !g_state.haveSrcSize || !g_state.posInfoValid ||
      !g_state.focused || g_cursor.realigning ||
      app_isOverlayMode() || !inputEnabled || !absolute)
    return;

  const bool inView = isInView();
  if (g_cursor.viewReq != inView || g_cursor.inView != inView)
    core_setCursorInView(inView);
  if (!inView || !g_cursor.inView || !g_cursor.viewReq)
    return;

  struct DoublePoint guest;
  util_localCurToGuest(&guest);

  const uint32_t width  = g_state.srcSize.x;
  const uint32_t height = g_state.srcSize.y;
  const uint32_t x      = round(util_clamp(guest.x, 0, width  - 1));
  const uint32_t y      = round(util_clamp(guest.y, 0, height - 1));

  MTRACE("absolute local=%.3f,%.3f guest=%u,%u size=%ux%u",
      g_cursor.pos.x, g_cursor.pos.y, x, y, width, height);

  if (!lgInput_mousePosition(x, y, width, height))
    DEBUG_ERROR("failed to send absolute mouse position message");
  else
    g_cursor.realign = false;
}

void core_handleMouseAbsolute(void)
{
  const bool inputEnabled = core_inputEnabled();
  const bool absolute     = inputEnabled &&
    lgInput_supports(LG_INPUT_SUPPORT_MOUSE_ABSOLUTE);
  handleMouseAbsolute(inputEnabled, absolute);
}

void core_handleMousePosition(double x, double y)
{
  bool allowPrediction = g_cursor.motionValid && !g_cursor.realign;
  const double ex = allowPrediction ? x - g_cursor.pos.x : 0.0;
  const double ey = allowPrediction ? y - g_cursor.pos.y : 0.0;

  g_cursor.pos.x       = x;
  g_cursor.pos.y       = y;
  g_cursor.valid       = true;
  g_cursor.motionValid = true;

  const bool inputEnabled = core_inputEnabled();
  const bool absolute     = inputEnabled &&
    lgInput_supports(LG_INPUT_SUPPORT_MOUSE_ABSOLUTE);
  const bool canTrack     = !g_cursor.grab && g_cursor.inWindow &&
    g_state.haveSrcSize && g_state.posInfoValid &&
    g_state.dstRect.valid && g_state.focused &&
    !app_isOverlayMode() && inputEnabled;
  if (canTrack && !g_cursor.realigning)
  {
    const bool inView = isInView();
    if (g_cursor.viewReq != inView || g_cursor.inView != inView)
    {
      core_setCursorInView(inView);
      allowPrediction = false;
      g_cursor.motionValid = true;
    }
  }

  handleMouseAbsolute(inputEnabled, absolute);

  if (!canTrack)
  {
    g_cursor.motionValid = false;
    setAutoCapture(false);
    return;
  }

  if (g_cursor.realigning)
  {
    g_cursor.motionValid = false;
    return;
  }

  bool active = g_cursor.inView && g_cursor.viewReq;
  if (!allowPrediction || !g_params.autoCapture ||
      !active || g_cursor.buttons)
  {
    setAutoCapture(active);
    return;
  }

  const double nextX = x + ex;
  const double nextY = y + ey;
  active =
    nextX >= g_state.dstRect.x &&
    nextX <  g_state.dstRect.x + g_state.dstRect.w &&
    nextY >= g_state.dstRect.y &&
    nextY <  g_state.dstRect.y + g_state.dstRect.h;

  MTRACE("auto capture move=%.3f,%.3f next=%.3f,%.3f inView=%d",
      ex, ey, nextX, nextY, active);
  setAutoCapture(active);
}

void core_handleMouseGrabbed(double ex, double ey)
{
  const bool active  = g_cursor.grab && g_state.ds->isPointerCaptured();
  const bool enabled = active && core_inputEnabled();
  MTRACE("captured delta=%.3f,%.3f active=%d enabled=%d", ex, ey,
      active, enabled);

  if (!enabled)
    return;

  int x, y;
  if (g_params.rawMouse && !g_cursor.sens)
  {
    /* raw unscaled input are always round numbers */
    x = floor(ex);
    y = floor(ey);
  }
  else
  {
    /* apply sensitivity */
    ex = (ex / 10.0) * (g_cursor.sens + 10);
    ey = (ey / 10.0) * (g_cursor.sens + 10);
    util_cursorToInt(ex, ey, &x, &y);
  }

  if (x == 0 && y == 0)
    return;

  if (!lgInput_mouseMotion(x, y))
    DEBUG_ERROR("failed to send mouse motion message");
}

void core_handleMouseNormal(double ex, double ey)
{
  MTRACE("normal delta=%.3f,%.3f guest=%d,%d valid=%d inWin=%d "
      "inView=%d", ex, ey, g_cursor.guest.x, g_cursor.guest.y,
      g_cursor.guest.valid, g_cursor.inWindow, g_cursor.inView);

  const bool absolute =
    lgInput_supports(LG_INPUT_SUPPORT_MOUSE_ABSOLUTE);

  // prevent cursor handling outside of capture if the position is not known
  if (!g_cursor.guest.valid)
  {
    if (absolute)
      core_handleMouseAbsolute();
    else if (app_guestIsWindows())
    {
      // wiggle the mouse when the guest has not provided any information, we need
      // to do this because windows doesn't enable a cursor at all until it has
      // been moved for the first time.
      if (!lgInput_mouseMotion(1, 1))
        DEBUG_ERROR("failed to send mouse motion message");
      if (!lgInput_mouseMotion(-1, -1))
        DEBUG_ERROR("failed to send mouse motion message");
    }
    return;
  }

  if (g_cursor.realigning)
    return;

  if (!core_inputEnabled())
    return;

  if (g_cursor.viewReq != g_cursor.inView)
  {
    if (g_cursor.viewReq)
      core_setCursorInView(true);
    else if (g_cursor.exit && moveExit(ex, ey))
      core_setCursorInView(true);
    return;
  }

  /* scale the movement to the guest */
  if (g_cursor.useScale && g_params.scaleMouseInput)
  {
    ex *= g_cursor.scale.x;
    ey *= g_cursor.scale.y;
  }

  bool testExit = true;
  bool didExit = false;
  const bool inView = isInView();
  if (!g_cursor.inView)
  {
    if (g_cursor.viewReq)
      return;
    else if (inView)
      g_cursor.realign = true;
    else /* nothing to do if we are outside the viewport */
      return;
  }

  /*
   * do not pass mouse events to the guest if we do not have focus, this must be
   * done after the inView test has been performed so that when focus is gained
   * we know if we should be drawing the cursor.
   */
  if (!g_state.focused)
  {
    core_setCursorInView(inView);
    return;
  }

  /* if we have been instructed to realign */
  if (g_cursor.realign && absolute)
  {
    g_cursor.realign = false;
    core_handleMouseAbsolute();
    if (!g_cursor.inView)
      return;

    testExit = false;
  }
  else if (g_cursor.realign)
  {
    struct DoublePoint guest;
    util_localCurToGuest(&guest);

    if (!atomic_load_explicit(&g_state.stopVideo, memory_order_acquire) &&
      g_state.transportFeatures & LG_TRANSPORT_FEATURE_SET_CURSOR_POS)
    {
      const LG_TransportControl control = {
        .type      = LG_TRANSPORT_CONTROL_SET_CURSOR_POS,
        .cursorPos = { .x = round(guest.x), .y = round(guest.y) },
      };

      LG_TransportControlToken token;
      LG_TransportStatus status = g_state.transport.ops->sendControl(
          g_state.transport.handle, &control, &token);
      if (status != LG_TRANSPORT_OK)
      {
        DEBUG_WARN("Cursor-position control failed with status %d", status);
        goto fallback;
      }
      else
      {
        /* wait for the move request to be processed */
        g_cursor.realigning = true;
        unsigned timeout = 200;
        do
        {
          status = g_state.transport.ops->controlStatus(
              g_state.transport.handle, token);
          if (status == LG_TRANSPORT_DISCONNECTED)
          {
            g_cursor.realigning = false;
            break;
          }
          if (status == LG_TRANSPORT_OK)
            break;
          if (status != LG_TRANSPORT_UNAVAILABLE)
          {
            g_cursor.realigning = false;
            return;
          }

          if (--timeout == 0)
          {
            DEBUG_ERROR("Cursor-position control was not acknowledged");
            break;
          }

          g_state.ds->wait(1);
        }
        while(app_isRunning());

        g_cursor.guest.x    = control.cursorPos.x - g_cursor.guest.hx;
        g_cursor.guest.y    = control.cursorPos.y - g_cursor.guest.hy;
        g_cursor.realign    = false;
        g_cursor.realigning = false;
        g_cursor.redraw     = true;
        app_invalidateWindow(false);

        if (!g_cursor.inWindow)
          return;

        core_setCursorInView(true);
        return;
      }
    }
    else
    {
fallback:
      /* add the difference to the offset */
      ex += guest.x - (g_cursor.guest.x + g_cursor.guest.hx);
      ey += guest.y - (g_cursor.guest.y + g_cursor.guest.hy);
      core_setCursorInView(true);

      if (!g_cursor.inView)
        return;
    }

    g_cursor.realign = false;

    /* don't test for an exit as we just entered, we can get into a enter/exit
     * loop otherwise */
    testExit = false;
  }

  /* if any buttons are held we should not allow exit to happen */
  if (g_cursor.buttons)
    testExit = false;

  if (testExit)
  {
    enum LG_DSWarpSupport warpSupport = LG_DS_WARP_NONE;
    app_getProp(LG_DS_WARP_SUPPORT, &warpSupport);

    /* translate the move to the guests orientation */
    struct DoublePoint move = {.x = ex, .y = ey};
    util_rotatePoint(&move);

    /* translate the guests position to our coordinate space */
    struct DoublePoint local;
    util_guestCurToLocal(&local);

    /* check if the move would push the cursor outside the guest's viewport */
    if (
        local.x + move.x <  g_state.dstRect.x ||
        local.y + move.y <  g_state.dstRect.y ||
        local.x + move.x >= g_state.dstRect.x + g_state.dstRect.w ||
        local.y + move.y >= g_state.dstRect.y + g_state.dstRect.h)
    {
      didExit = true;
      local.x += move.x;
      local.y += move.y;
      const int tx = exitPos(local.x, g_state.dstRect.x);
      const int ty = exitPos(local.y, g_state.dstRect.y);

      MTRACE("exit local=%.3f,%.3f move=%.3f,%.3f target=%d,%d "
          "dst=%d,%d,%d,%d support=%d", local.x - move.x,
          local.y - move.y, move.x, move.y, tx, ty, g_state.dstRect.x,
          g_state.dstRect.y, g_state.dstRect.w, g_state.dstRect.h,
          warpSupport);

      switch (warpSupport)
      {
        case LG_DS_WARP_NONE:
          break;

        case LG_DS_WARP_SURFACE:
          if (tx < 0 || tx >= g_state.windowW ||
              ty < 0 || ty >= g_state.windowH)
          {
            g_cursor.surfaceExit = true;
            g_state.ds->ungrabPointer();
            core_warpPointer(tx, ty, true);
            break;
          }

          startExit(local.x, local.y);
          return;

        case LG_DS_WARP_SCREEN:
          if (core_isValidPointerPos(
                g_state.windowPos.x + g_state.border.left + tx,
                g_state.windowPos.y + g_state.border.top  + ty))
          {
            startExit(local.x, local.y);
            return;
          }
      }
    }
    else if (warpSupport == LG_DS_WARP_SURFACE && isInView())
    {
      /* regrab the pointer in case the user did not move off the surface */
      MTRACE("regrab pos=%.3f,%.3f", g_cursor.pos.x, g_cursor.pos.y);
      g_state.ds->grabPointer();
      g_cursor.warpState = WARP_STATE_ON;
    }
  }

  if (g_params.autoCapture && testExit)
    setAutoCapture(!didExit);

  if (absolute)
    return;

  int x, y;
  util_cursorToInt(ex, ey, &x, &y);

  if (x == 0 && y == 0)
    return;

  /* assume the mouse will move to the location we attempt to move it to so we
   * avoid warp out of window issues. The cursorThread will correct this if
   * wrong after the movement has ocurred on the guest */
  g_cursor.guest.x += x;
  g_cursor.guest.y += y;

  MTRACE("motion delta=%d,%d guest=%d,%d exit=%d test=%d warp=%d",
      x, y, g_cursor.guest.x, g_cursor.guest.y, didExit, testExit,
      g_cursor.warpState);

  if (!lgInput_mouseMotion(x, y))
    DEBUG_ERROR("failed to send mouse motion message");
}

void core_resetOverlayInputState(void)
{
  g_state.io->MouseDown[ImGuiMouseButton_Left  ] = false;
  g_state.io->MouseDown[ImGuiMouseButton_Right ] = false;
  g_state.io->MouseDown[ImGuiMouseButton_Middle] = false;
  for(int key = 0; key < KEY_MAX; key++)
    if (linux_to_imgui[key])
      ImGuiIO_AddKeyEvent(g_state.io, linux_to_imgui[key], false);
}

void core_updateOverlayState(void)
{
  g_state.cursorLast = -2;

  static bool wasGrabbed = false;
  if (app_isOverlayMode())
  {
    wasGrabbed = g_cursor.grab;

    g_state.io->ConfigFlags &= ~ImGuiConfigFlags_NoMouse;
    g_state.io->MousePos = (ImVec2) { g_cursor.pos.x, g_cursor.pos.y };

    core_setGrabQuiet(false);
    core_setCursorInView(false);
  }
  else
  {
    g_state.io->ConfigFlags |= ImGuiConfigFlags_NoMouse;
    core_resetOverlayInputState();
    core_setGrabQuiet(wasGrabbed);
    core_invalidatePointer(true);
    app_invalidateWindow(false);

    if (!g_cursor.grab)
    {
      g_cursor.realign = true;
      core_handleMouseNormal(0, 0);
    }
  }
}

void core_setTitle(const char * title)
{
  if (!lgCanSetProcessTitle())
    return;

  char * string;
  if (alloc_sprintf(&string, "Looking Glass Client %s%s%s", BUILD_VERSION,
                    title && *title ? ": " : "", title ? title : "") >= 0)
  {
    lgSetProcessTitle(string);
    free(string);
  }
}
