// Mouse capture: translate browser MouseEvent / WheelEvent into protobuf
// Message{mouse_event: {mask, x, y}}.
//
// Mask layout (from /Users/sn0/Desktop/rustdesk/src/common.rs:69-92):
//   mask        = (button_bitmask << 3) | event_type
//   event_type  = 0=move, 1=down, 2=up, 3=wheel, 4=trackpad
//   button_bit  = 0x01=left, 0x02=right, 0x04=middle/wheel,
//                 0x08=back, 0x10=forward
//
// Coordinate translation:
//   The host expects coordinates in *virtual desktop space* — i.e.
//   relative to the union of all monitors, not relative to the active
//   display. So we map browser canvas-rect coords → active-display
//   pixel coords (via canvas.size scaling), then add the active
//   display's origin (DisplayInfo.x / .y). Without the offset, clicks
//   on display 2 land on display 1 because both share canvas (0,0)
//   but only display 1's origin is (0,0) in virtual desktop space.

import { hbb } from "../proto/generated.js";
import type { Session } from "../transport/session.js";
import type { CanvasView } from "../ui/canvas.js";

const MOUSE_TYPE_MOVE = 0;
const MOUSE_TYPE_DOWN = 1;
const MOUSE_TYPE_UP = 2;
const MOUSE_TYPE_WHEEL = 3;

const MOUSE_BUTTON_LEFT = 0x01;
const MOUSE_BUTTON_RIGHT = 0x02;
const MOUSE_BUTTON_MIDDLE = 0x04;
const MOUSE_BUTTON_BACK = 0x08;
const MOUSE_BUTTON_FORWARD = 0x10;

function buttonBitForEvent(button: number): number {
  switch (button) {
    case 0: return MOUSE_BUTTON_LEFT;
    case 1: return MOUSE_BUTTON_MIDDLE;
    case 2: return MOUSE_BUTTON_RIGHT;
    case 3: return MOUSE_BUTTON_BACK;
    case 4: return MOUSE_BUTTON_FORWARD;
    default: return 0;
  }
}

export function bindMouse(
  canvas: CanvasView,
  session: Session,
  /** Returns the current display's virtual-desktop origin (DisplayInfo.x, .y).
   *  Polled per-event so display switches take effect immediately. */
  getOrigin: () => { x: number; y: number },
): () => void {
  const el = canvas.el();

  const sendMouse = (mask: number, x: number, y: number, ev: { altKey: boolean; ctrlKey: boolean; shiftKey: boolean; metaKey: boolean }): void => {
    const modifiers: hbb.ControlKey[] = [];
    if (ev.altKey) modifiers.push(hbb.ControlKey.Alt);
    if (ev.ctrlKey) modifiers.push(hbb.ControlKey.Control);
    if (ev.shiftKey) modifiers.push(hbb.ControlKey.Shift);
    if (ev.metaKey) modifiers.push(hbb.ControlKey.Meta);
    session.send(hbb.Message.create({
      mouse_event: hbb.MouseEvent.create({
        mask,
        x: Math.round(x),
        y: Math.round(y),
        modifiers,
      }),
    })).catch(() => { /* relay closed */ });
  };

  // Translate viewport pixel coordinates → virtual-desktop pixels
  // (active display origin + position within display).
  const mapCoords = (clientX: number, clientY: number): { x: number; y: number } | null => {
    const sz = canvas.size();
    if (!sz.width || !sz.height) return null;
    const r = canvas.rect();
    if (r.width <= 0 || r.height <= 0) return null;
    // CanvasView uses object-fit: contain, so the rendered image is
    // letterboxed inside `r`. Compute the actual rendered rectangle.
    const scale = Math.min(r.width / sz.width, r.height / sz.height);
    const renderedW = sz.width * scale;
    const renderedH = sz.height * scale;
    const offsetX = r.left + (r.width - renderedW) / 2;
    const offsetY = r.top + (r.height - renderedH) / 2;
    const px = (clientX - offsetX) / scale;
    const py = (clientY - offsetY) / scale;
    if (px < 0 || py < 0 || px > sz.width || py > sz.height) return null;
    // Offset by the active display's virtual-desktop origin. For a
    // single-display host this is (0,0). For multi-display, display 2
    // typically starts at x = display1.width (or wherever Windows put
    // it in Display Settings).
    const origin = getOrigin();
    return { x: px + origin.x, y: py + origin.y };
  };

  // Track currently-pressed buttons so we can include them in move events.
  // (Some hosts ignore button bits during MOVE; doesn't hurt to include.)
  let pressed = 0;

  const onMove = (e: MouseEvent): void => {
    const c = mapCoords(e.clientX, e.clientY);
    if (!c) return;
    const mask = (pressed << 3) | MOUSE_TYPE_MOVE;
    sendMouse(mask, c.x, c.y, e);
  };

  const onDown = (e: MouseEvent): void => {
    const c = mapCoords(e.clientX, e.clientY);
    if (!c) return;
    const btn = buttonBitForEvent(e.button);
    if (!btn) return;
    pressed |= btn;
    const mask = (btn << 3) | MOUSE_TYPE_DOWN;
    sendMouse(mask, c.x, c.y, e);
    // Make sure the canvas gets focus so keystrokes flow.
    el.focus({ preventScroll: true });
    // Suppress the browser context menu for right-click; do NOT
    // preventDefault for left-click — that would block focusing the
    // canvas in some browsers and silently break keyboard input.
    if (e.button === 2) {
      e.preventDefault();
    }
  };

  const onUp = (e: MouseEvent): void => {
    const c = mapCoords(e.clientX, e.clientY);
    if (!c) return;
    const btn = buttonBitForEvent(e.button);
    if (!btn) return;
    pressed &= ~btn;
    const mask = (btn << 3) | MOUSE_TYPE_UP;
    sendMouse(mask, c.x, c.y, e);
  };

  const onWheel = (e: WheelEvent): void => {
    const c = mapCoords(e.clientX, e.clientY);
    if (!c) return;
    // Browser wheel deltas are in "lines" (deltaMode=DOM_DELTA_LINE) or
    // "pixels" (DOM_DELTA_PIXEL). Quantize to small ints — the host's
    // input_service.rs multiplies by WHEEL_DELTA on Windows. We send
    // ±1..±5 lines; values larger than that scroll absurdly fast.
    const lines = (px: number, mode: number): number => {
      if (mode === WheelEvent.DOM_DELTA_PIXEL) {
        // ~16px per line is the typical default.
        return Math.max(-5, Math.min(5, Math.round(px / 16)));
      }
      return Math.max(-5, Math.min(5, Math.round(px)));
    };
    const dx = lines(e.deltaX, e.deltaMode);
    const dy = lines(-e.deltaY, e.deltaMode); // browsers use +down; host wants +up
    if (dx === 0 && dy === 0) return;
    const mask = MOUSE_TYPE_WHEEL;
    sendMouse(mask, dx, dy, e);
    e.preventDefault();
  };

  const onContextMenu = (e: Event): void => { e.preventDefault(); };

  el.addEventListener("mousemove", onMove);
  el.addEventListener("mousedown", onDown);
  el.addEventListener("mouseup", onUp);
  el.addEventListener("wheel", onWheel, { passive: false });
  el.addEventListener("contextmenu", onContextMenu);

  // Detach handle.
  return (): void => {
    el.removeEventListener("mousemove", onMove);
    el.removeEventListener("mousedown", onDown);
    el.removeEventListener("mouseup", onUp);
    el.removeEventListener("wheel", onWheel);
    el.removeEventListener("contextmenu", onContextMenu);
  };
}
