import { layout } from '../stores/layout.svelte.js';
import { isResizable, isCurvedType, isSpurType, isEpcType, isInductionType, SYMBOLS, EPC_DEFAULT_WAYPOINTS, EPC_LEFT_BOX, EPC_RIGHT_BOX, INDUCTION_STRIP_TOP_FRAC, INDUCTION_STRIP_BOTTOM_FRAC } from '../symbols.js';
import type { EpcWaypoint } from '../types.js';
import { getAABB, snapToGrid, clamp, findValidPosition } from './collision.js';
import { orientedBoxCorners, pastDragThreshold } from './geometry.js';

const ZOOM_MIN = 0.1;
const ZOOM_MAX = 5;
const ZOOM_IN_FACTOR = 1.1;
const ZOOM_OUT_FACTOR = 0.9;
const ROTATION_STEP = 15;
const CONVEYOR_MIN_W = 40;
const DRAG_THRESHOLD = 4; // pixels before drag actually starts

interface DragState {
  type: 'palette' | 'move' | 'multi-move' | 'resize-left' | 'resize-right' | 'resize-spur-top' | 'resize-spur-bottom' | 'epc-waypoint' | 'pdf';
  placedId?: number;
  offsetX?: number;
  offsetY?: number;
  startX?: number;
  startY?: number;
  origW?: number;
  origW2?: number;
  origX?: number;
  origY?: number;
  origPdfOffsetX?: number;
  origPdfOffsetY?: number;
  symbolDef?: (typeof SYMBOLS)[number];
  ghost?: HTMLDivElement;
  multiOffsets?: { id: number; offsetX: number; offsetY: number }[];
  dragActivated?: boolean; // true once mouse moves past threshold
  waypointIndex?: number; // which EPC waypoint is being dragged
  deviceLabel?: string; // label to assign when dropping from device dock
}

let dragState: DragState | null = null;
let isPanning = false;
let panStartX = 0;
let panStartY = 0;
let canvasEl: HTMLCanvasElement | null = null;
let wrapperEl: HTMLDivElement | null = null;

// Context menu callback
let contextMenuCallback: ((x: number, y: number, symId: number) => void) | null = null;

export function setContextMenuCallback(cb: (x: number, y: number, symId: number) => void) {
  contextMenuCallback = cb;
}

export function initInteractions(canvas: HTMLCanvasElement, wrapper: HTMLDivElement) {
  canvasEl = canvas;
  wrapperEl = wrapper;

  canvas.addEventListener('mousedown', onCanvasMousedown);
  canvas.addEventListener('contextmenu', onContextMenu);
  wrapper.addEventListener('wheel', onWheel, { passive: false });
  wrapper.addEventListener('mousedown', onWrapperMousedown);
  document.addEventListener('mousemove', onMousemove);
  document.addEventListener('mouseup', onMouseup);
  document.addEventListener('keydown', onKeydown);
}

export function destroyInteractions() {
  if (canvasEl) {
    canvasEl.removeEventListener('mousedown', onCanvasMousedown);
    canvasEl.removeEventListener('contextmenu', onContextMenu);
  }
  if (wrapperEl) {
    wrapperEl.removeEventListener('wheel', onWheel);
    wrapperEl.removeEventListener('mousedown', onWrapperMousedown);
  }
  document.removeEventListener('mousemove', onMousemove);
  document.removeEventListener('mouseup', onMouseup);
  document.removeEventListener('keydown', onKeydown);
}

function screenToCanvas(clientX: number, clientY: number): { x: number; y: number } {
  if (!wrapperEl) return { x: 0, y: 0 };
  const rect = wrapperEl.getBoundingClientRect();
  return {
    x: (clientX - rect.left - layout.panX) / layout.zoomLevel,
    y: (clientY - rect.top - layout.panY) / layout.zoomLevel,
  };
}

/** Transform a point into a symbol's unrotated local space */
function toSymbolLocal(px: number, py: number, sym: typeof layout.symbols[0]): { x: number; y: number } {
  if (!sym.rotation) return { x: px, y: py };
  const scx = sym.x + sym.w / 2;
  const scy = sym.y + sym.h / 2;
  const rad = (-sym.rotation * Math.PI) / 180;
  const cos = Math.cos(rad), sin = Math.sin(rad);
  const dx = px - scx, dy = py - scy;
  return { x: dx * cos - dy * sin + scx, y: dx * sin + dy * cos + scy };
}

function pointInRect(px: number, py: number, rx: number, ry: number, rw: number, rh: number): boolean {
  return px >= rx && px <= rx + rw && py >= ry && py <= ry + rh;
}

function pointInTrapezoid(px: number, py: number, sym: typeof layout.symbols[0]): boolean {
  const w2 = sym.w2 ?? sym.w;
  // The right edge of the trapezoid is a slanted line from (x+w2, y) to (x+w, y+h)
  // For a given py, the max x is interpolated between w2 (top) and w (bottom)
  const t = (py - sym.y) / sym.h;
  const maxX = sym.x + w2 + t * (sym.w - w2);
  return px >= sym.x && px <= maxX && py >= sym.y && py <= sym.y + sym.h;
}

function hitTest(cx: number, cy: number): number | null {
  for (let i = layout.symbols.length - 1; i >= 0; i--) {
    const sym = layout.symbols[i];
    const local = toSymbolLocal(cx, cy, sym);
    if (isSpurType(sym.symbolId)) {
      if (pointInTrapezoid(local.x, local.y, sym)) return sym.id;
    } else if (pointInRect(local.x, local.y, sym.x, sym.y, sym.w, sym.h)) {
      return sym.id;
    }
  }
  return null;
}

/** Hit test EPC waypoint handles. Returns waypoint index or -1 */
function hitEpcWaypoint(cx: number, cy: number, sym: typeof layout.symbols[0]): number {
  if (!isEpcType(sym.symbolId)) return -1;
  const waypoints = sym.epcWaypoints || EPC_DEFAULT_WAYPOINTS;
  const { x: lx, y: ly } = toSymbolLocal(cx, cy, sym);
  const hs = 6;
  for (let i = 0; i < waypoints.length; i++) {
    const wx = sym.x + waypoints[i].x;
    const wy = sym.y + waypoints[i].y;
    const dx = lx - wx, dy = ly - wy;
    if (dx * dx + dy * dy <= hs * hs) return i;
  }
  return -1;
}

/** Hit test EPC line segment midpoints for adding waypoints. Returns insert index or -1 */
function hitEpcSegmentMidpoint(cx: number, cy: number, sym: typeof layout.symbols[0]): number {
  if (!isEpcType(sym.symbolId)) return -1;
  const waypoints = sym.epcWaypoints || EPC_DEFAULT_WAYPOINTS;
  const { x: lx, y: ly } = toSymbolLocal(cx, cy, sym);
  const hs = 8;
  for (let i = 0; i < waypoints.length - 1; i++) {
    const mx = sym.x + (waypoints[i].x + waypoints[i + 1].x) / 2;
    const my = sym.y + (waypoints[i].y + waypoints[i + 1].y) / 2;
    const dx = lx - mx, dy = ly - (my - 4); // offset for the "+" indicator position
    if (dx * dx + dy * dy <= hs * hs) return i + 1;
  }
  return -1;
}

function hitResizeHandle(cx: number, cy: number, sym: typeof layout.symbols[0]): 'left' | 'right' | 'spur-top' | 'spur-bottom' | null {
  if (!isResizable(sym.symbolId) || layout.selectedIds.size !== 1 || !layout.selectedIds.has(sym.id)) return null;

  const hs = 10;
  const { x: lx, y: ly } = toSymbolLocal(cx, cy, sym);

  if (isCurvedType(sym.symbolId)) {
    const arcAngle = sym.curveAngle || 90;
    const arcRad = (arcAngle * Math.PI) / 180;
    const outerR = sym.w;
    const arcCx = sym.x;
    const arcCy = sym.y + sym.h;
    const h1x = arcCx + outerR, h1y = arcCy;
    if (pointInRect(lx, ly, h1x - hs / 2, h1y - hs / 2, hs, hs)) return 'right';
    const h2x = arcCx + outerR * Math.cos(arcRad);
    const h2y = arcCy - outerR * Math.sin(arcRad);
    if (pointInRect(lx, ly, h2x - hs / 2, h2y - hs / 2, hs, hs)) return 'left';
    return null;
  }

  if (isSpurType(sym.symbolId)) {
    const w2 = sym.w2 ?? sym.w;
    // Top-right handle (controls w2)
    if (pointInRect(lx, ly, sym.x + w2 - hs / 2, sym.y - hs / 2, hs, hs)) return 'spur-top';
    // Bottom-right handle (controls w)
    if (pointInRect(lx, ly, sym.x + sym.w - hs / 2, sym.y + sym.h - hs / 2, hs, hs)) return 'spur-bottom';
    return null;
  }

  if (isInductionType(sym.symbolId)) {
    // Only right handle — arrow head is fixed width
    const stripTopY = sym.y + sym.h * INDUCTION_STRIP_TOP_FRAC;
    const stripBottomY = sym.y + sym.h * INDUCTION_STRIP_BOTTOM_FRAC;
    const stripMidY = (stripTopY + stripBottomY) / 2 - hs / 2;
    if (pointInRect(lx, ly, sym.x + sym.w - hs / 2, stripMidY, hs, hs)) return 'right';
    return null;
  }

  const midY = sym.y + sym.h / 2 - hs / 2;
  if (pointInRect(lx, ly, sym.x - hs / 2, midY, hs, hs)) return 'left';
  if (pointInRect(lx, ly, sym.x + sym.w - hs / 2, midY, hs, hs)) return 'right';
  return null;
}

function onCanvasMousedown(e: MouseEvent) {
  if (e.button !== 0) return;
  e.preventDefault();

  const pos = screenToCanvas(e.clientX, e.clientY);

  // PDF editing mode: drag the PDF background
  if (layout.editingBackground) {
    dragState = {
      type: 'pdf',
      startX: pos.x,
      startY: pos.y,
      origPdfOffsetX: layout.pdfOffsetX,
      origPdfOffsetY: layout.pdfOffsetY,
    };
    return;
  }

  // Check EPC waypoint handles and resize handles (only for single selection)
  if (layout.selectedIds.size === 1) {
    const selId = [...layout.selectedIds][0];
    const sel = layout.symbols.find(s => s.id === selId);
    if (sel && isEpcType(sel.symbolId)) {
      // Double-click on existing waypoint to remove it (if >2 waypoints)
      if (e.detail === 2) {
        const wpIdx = hitEpcWaypoint(pos.x, pos.y, sel);
        const wps = sel.epcWaypoints || EPC_DEFAULT_WAYPOINTS;
        if (wpIdx >= 0 && wps.length > 2) {
          layout.pushUndo();
          if (!sel.epcWaypoints) {
            sel.epcWaypoints = EPC_DEFAULT_WAYPOINTS.map(wp => ({ ...wp }));
          }
          sel.epcWaypoints.splice(wpIdx, 1);
          recalcEpcBounds(sel);
          layout.markDirty();
          layout.saveMcmState();
          return;
        }
        // Double-click on "+" midpoint to add waypoint
        const insertIdx = hitEpcSegmentMidpoint(pos.x, pos.y, sel);
        if (insertIdx >= 0) {
          layout.pushUndo();
          if (!sel.epcWaypoints) {
            sel.epcWaypoints = EPC_DEFAULT_WAYPOINTS.map(wp => ({ ...wp }));
          }
          const prev = sel.epcWaypoints[insertIdx - 1];
          const next = sel.epcWaypoints[insertIdx];
          sel.epcWaypoints.splice(insertIdx, 0, {
            x: (prev.x + next.x) / 2,
            y: (prev.y + next.y) / 2,
          });
          layout.markDirty();
          layout.saveMcmState();
          return;
        }
      }
      // Check waypoint handles for drag
      const wpIdx = hitEpcWaypoint(pos.x, pos.y, sel);
      if (wpIdx >= 0) {
        layout.pushUndo();
        if (!sel.epcWaypoints) {
          sel.epcWaypoints = EPC_DEFAULT_WAYPOINTS.map(wp => ({ ...wp }));
        }
        dragState = {
          type: 'epc-waypoint',
          placedId: sel.id,
          waypointIndex: wpIdx,
          startX: pos.x,
          startY: pos.y,
          dragActivated: false,
        };
        return;
      }
    }
    if (sel) {
      const handle = hitResizeHandle(pos.x, pos.y, sel);
      if (handle) {
        layout.pushUndo();
        if (handle === 'spur-top' || handle === 'spur-bottom') {
          dragState = {
            type: handle === 'spur-top' ? 'resize-spur-top' : 'resize-spur-bottom',
            placedId: sel.id,
            startX: pos.x,
            startY: pos.y,
            origW: sel.w,
            origW2: sel.w2 ?? sel.w,
            origX: sel.x,
            origY: sel.y,
          };
        } else {
          dragState = {
            type: handle === 'left' ? 'resize-left' : 'resize-right',
            placedId: sel.id,
            startX: pos.x,
            startY: pos.y,
            origW: sel.w,
            origX: sel.x,
            origY: sel.y,
          };
        }
        return;
      }
    }
  }

  const hitId = hitTest(pos.x, pos.y);

  if (hitId !== null) {
    if (e.shiftKey) {
      // Shift+Click: toggle symbol in/out of selection
      const newSet = new Set(layout.selectedIds);
      if (newSet.has(hitId)) {
        newSet.delete(hitId);
      } else {
        newSet.add(hitId);
      }
      layout.selectedIds = newSet;
      layout.markDirty();
      return; // Don't start drag on shift+click
    }

    // No shift: if symbol not already selected, make it the only selection
    if (!layout.selectedIds.has(hitId)) {
      layout.selectedIds = new Set([hitId]);
    }

    layout.pushUndo();

    // Start drag for selected symbols (with threshold — won't move until mouse travels DRAG_THRESHOLD px)
    if (layout.selectedIds.size > 1) {
      const multiOffsets = [...layout.selectedIds].map(id => {
        const sym = layout.symbols.find(s => s.id === id)!;
        return { id, offsetX: pos.x - sym.x, offsetY: pos.y - sym.y };
      });
      dragState = { type: 'multi-move', multiOffsets, startX: pos.x, startY: pos.y, dragActivated: false };
    } else {
      const sym = layout.symbols.find(s => s.id === hitId)!;
      dragState = {
        type: 'move',
        placedId: hitId,
        offsetX: pos.x - sym.x,
        offsetY: pos.y - sym.y,
        startX: pos.x,
        startY: pos.y,
        dragActivated: false,
      };
    }
    layout.markDirty();
  } else {
    // Click on empty space
    if (!e.shiftKey) {
      layout.selectedIds = new Set();
      layout.markDirty();
    }
  }
}

function onContextMenu(e: MouseEvent) {
  e.preventDefault();
  const pos = screenToCanvas(e.clientX, e.clientY);
  const hitId = hitTest(pos.x, pos.y);
  if (hitId !== null && contextMenuCallback) {
    contextMenuCallback(e.clientX, e.clientY, hitId);
  }
}

function onWheel(e: WheelEvent) {
  e.preventDefault();
  if (!wrapperEl) return;

  const rect = wrapperEl.getBoundingClientRect();
  const mx = e.clientX - rect.left;
  const my = e.clientY - rect.top;
  const cx = (mx - layout.panX) / layout.zoomLevel;
  const cy = (my - layout.panY) / layout.zoomLevel;

  const factor = e.deltaY > 0 ? ZOOM_OUT_FACTOR : ZOOM_IN_FACTOR;
  layout.zoomLevel = clamp(layout.zoomLevel * factor, ZOOM_MIN, ZOOM_MAX);

  layout.panX = mx - cx * layout.zoomLevel;
  layout.panY = my - cy * layout.zoomLevel;
}

function onWrapperMousedown(e: MouseEvent) {
  if (e.button === 1) {
    e.preventDefault();
    isPanning = true;
    panStartX = e.clientX - layout.panX;
    panStartY = e.clientY - layout.panY;
    document.body.style.cursor = 'grabbing';
  }
}

const SPUR_MIN_W = 10;

// orientedBoxCorners imported from geometry.ts

/** Recalculate EPC symbol w/h to encompass all waypoints + oriented boxes.
 *  Symbol origin stays fixed — only w/h expand. Negative local coords are allowed. */
function recalcEpcBounds(sym: typeof layout.symbols[0]) {
  const wps = sym.epcWaypoints;
  if (!wps || wps.length < 2) return;

  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;

  // Include all waypoints
  for (const wp of wps) {
    minX = Math.min(minX, wp.x);
    minY = Math.min(minY, wp.y);
    maxX = Math.max(maxX, wp.x);
    maxY = Math.max(maxY, wp.y);
  }

  // Left box corners (oriented along first segment)
  const lbDir = { x: wps[1].x - wps[0].x, y: wps[1].y - wps[0].y };
  const lbCorners = orientedBoxCorners(wps[0].x, wps[0].y, lbDir.x, lbDir.y, EPC_LEFT_BOX.w, EPC_LEFT_BOX.h, 'right');
  for (const [bx, by] of lbCorners) {
    minX = Math.min(minX, bx); minY = Math.min(minY, by);
    maxX = Math.max(maxX, bx); maxY = Math.max(maxY, by);
  }

  // Right box corners (oriented along last segment)
  const last = wps[wps.length - 1];
  const prev = wps[wps.length - 2];
  const rbDir = { x: last.x - prev.x, y: last.y - prev.y };
  const rbCorners = orientedBoxCorners(last.x, last.y, rbDir.x, rbDir.y, EPC_RIGHT_BOX.w, EPC_RIGHT_BOX.h, 'left');
  for (const [bx, by] of rbCorners) {
    minX = Math.min(minX, bx); minY = Math.min(minY, by);
    maxX = Math.max(maxX, bx); maxY = Math.max(maxY, by);
  }

  // Add small padding
  minX -= 1; minY -= 1; maxX += 1; maxY += 1;

  // Set w/h to cover full extent from origin (allow negative local coords)
  sym.w = Math.max(maxX, 1);
  sym.h = Math.max(maxY, 1);
}

function snapWidth(w: number, ctrlKey: boolean, minW: number = CONVEYOR_MIN_W): number {
  if (layout.snapEnabled && !ctrlKey) {
    w = Math.round(w / layout.gridSize) * layout.gridSize;
  }
  return Math.max(minW, Math.round(w));
}

function resizeCurved(sym: typeof layout.symbols[0], dx: number, dy: number, isRight: boolean, ctrlKey: boolean) {
  const arcAngle = sym.curveAngle || 90;
  let delta: number;
  if (isRight) {
    delta = dx;
  } else {
    const arcRad = (arcAngle * Math.PI) / 180;
    delta = dx * Math.cos(arcRad) - dy * Math.sin(arcRad);
  }
  const newR = snapWidth(dragState!.origW! + delta, ctrlKey);
  const arcCenterY = dragState!.origY! + dragState!.origW!;
  sym.w = newR;
  sym.h = newR;
  sym.x = dragState!.origX!;
  sym.y = arcCenterY - newR;
}

function resizeSpur(sym: typeof layout.symbols[0], dx: number, dy: number, isTop: boolean, ctrlKey: boolean) {
  const rad = (sym.rotation || 0) * Math.PI / 180;
  const projectedDelta = dx * Math.cos(rad) + dy * Math.sin(rad);
  if (isTop) {
    sym.w2 = snapWidth(dragState!.origW2! + projectedDelta, ctrlKey, SPUR_MIN_W);
  } else {
    sym.w = snapWidth(dragState!.origW! + projectedDelta, ctrlKey, SPUR_MIN_W);
  }
}

function resizeStraight(sym: typeof layout.symbols[0], dx: number, dy: number, isRight: boolean, ctrlKey: boolean) {
  const rad = (sym.rotation || 0) * Math.PI / 180;
  const projectedDelta = dx * Math.cos(rad) + dy * Math.sin(rad);
  const newW = snapWidth(dragState!.origW! + (isRight ? projectedDelta : -projectedDelta), ctrlKey);

  const cosR = Math.cos(rad), sinR = Math.sin(rad);
  const origCx = dragState!.origX! + dragState!.origW! / 2;
  const origCy = dragState!.origY! + sym.h / 2;
  // Anchor the opposite edge: left edge for resize-right, right edge for resize-left
  const anchorDir = isRight ? -1 : 1;
  const anchorX = origCx + (anchorDir * dragState!.origW! / 2) * cosR;
  const anchorY = origCy + (anchorDir * dragState!.origW! / 2) * sinR;
  const newCx = anchorX + (-anchorDir * newW / 2) * cosR;
  const newCy = anchorY + (-anchorDir * newW / 2) * sinR;
  sym.w = newW;
  sym.x = newCx - newW / 2;
  sym.y = newCy - sym.h / 2;
}

function onMousemove(e: MouseEvent) {
  if (isPanning) {
    layout.panX = e.clientX - panStartX;
    layout.panY = e.clientY - panStartY;
    return;
  }

  if (!dragState) return;

  if (dragState.type === 'pdf') {
    const pos = screenToCanvas(e.clientX, e.clientY);
    layout.pdfOffsetX = dragState.origPdfOffsetX! + (pos.x - dragState.startX!);
    layout.pdfOffsetY = dragState.origPdfOffsetY! + (pos.y - dragState.startY!);
    layout.markDirty();
    return;
  }

  if (dragState.type === 'epc-waypoint' && dragState.placedId != null && dragState.waypointIndex != null) {
    const pos = screenToCanvas(e.clientX, e.clientY);
    if (!dragState.dragActivated) {
      if (!pastDragThreshold(pos.x, pos.y, dragState.startX!, dragState.startY!, DRAG_THRESHOLD)) return;
      dragState.dragActivated = true;
    }
    const sym = layout.symbols.find(s => s.id === dragState!.placedId);
    if (!sym || !sym.epcWaypoints) return;
    const wps = sym.epcWaypoints;
    const wpIdx = dragState.waypointIndex;

    // Transform mouse position to local (unrotated) coords
    const local = toSymbolLocal(pos.x, pos.y, sym);
    let localX = local.x - sym.x;
    let localY = local.y - sym.y;

    // Snap to grid (in local coords, grid aligns with symbol origin in world)
    if (layout.snapEnabled && !e.ctrlKey) {
      // Snap the world position, then convert back to local
      const worldSnappedX = Math.round((sym.x + localX) / layout.gridSize) * layout.gridSize;
      const worldSnappedY = Math.round((sym.y + localY) / layout.gridSize) * layout.gridSize;
      localX = worldSnappedX - sym.x;
      localY = worldSnappedY - sym.y;
    }

    // Constrain to 0/45/90 degree angles relative to adjacent waypoint
    const anchor = wpIdx > 0 ? wps[wpIdx - 1] : (wpIdx < wps.length - 1 ? wps[wpIdx + 1] : null);
    if (anchor) {
      const dx = localX - anchor.x;
      const dy = localY - anchor.y;
      const dist = Math.sqrt(dx * dx + dy * dy);
      if (dist > 0.1) {
        // Snap angle to nearest 15-degree increment
        const rawAngle = Math.atan2(dy, dx);
        const snappedAngle = Math.round(rawAngle / (Math.PI / 12)) * (Math.PI / 12);
        localX = anchor.x + dist * Math.cos(snappedAngle);
        localY = anchor.y + dist * Math.sin(snappedAngle);

        // Re-snap to grid after angle constraint
        if (layout.snapEnabled && !e.ctrlKey) {
          const worldSnappedX = Math.round((sym.x + localX) / layout.gridSize) * layout.gridSize;
          const worldSnappedY = Math.round((sym.y + localY) / layout.gridSize) * layout.gridSize;
          localX = worldSnappedX - sym.x;
          localY = worldSnappedY - sym.y;
        }
      }
    }

    wps[wpIdx] = { x: localX, y: localY };
    // Recalculate bounding box
    recalcEpcBounds(sym);
    layout.markDirty();
  }

  if (dragState.type === 'palette' && dragState.ghost && dragState.symbolDef) {
    const sym = dragState.symbolDef;
    dragState.ghost.style.left = (e.clientX - sym.w / 2) + 'px';
    dragState.ghost.style.top = (e.clientY - sym.h / 2) + 'px';
  }

  if (dragState.type === 'move' && dragState.placedId != null) {
    const pos = screenToCanvas(e.clientX, e.clientY);
    if (!dragState.dragActivated) {
      if (!pastDragThreshold(pos.x, pos.y, dragState.startX!, dragState.startY!, DRAG_THRESHOLD)) return;
      dragState.dragActivated = true;
    }
    const sym = layout.symbols.find(s => s.id === dragState!.placedId);
    if (!sym) return;
    const bb = getAABB(0, 0, sym.w, sym.h, sym.rotation);
    const newX = clamp(pos.x - dragState.offsetX!, -bb.x, layout.canvasW - bb.w - bb.x);
    const newY = clamp(pos.y - dragState.offsetY!, -bb.y, layout.canvasH - bb.h - bb.y);
    // Hold Ctrl for pixel-precise positioning (bypass grid snap)
    const snapped = e.ctrlKey ? { x: Math.round(newX), y: Math.round(newY) } : snapToGrid(newX, newY, sym.w, sym.h, sym.rotation);
    sym.x = snapped.x;
    sym.y = snapped.y;
    layout.markDirty();
  }

  if (dragState.type === 'multi-move' && dragState.multiOffsets) {
    const pos = screenToCanvas(e.clientX, e.clientY);
    if (!dragState.dragActivated) {
      if (!pastDragThreshold(pos.x, pos.y, dragState.startX!, dragState.startY!, DRAG_THRESHOLD)) return;
      dragState.dragActivated = true;
    }
    for (const { id, offsetX, offsetY } of dragState.multiOffsets) {
      const sym = layout.symbols.find(s => s.id === id);
      if (!sym) continue;
      const bb = getAABB(0, 0, sym.w, sym.h, sym.rotation);
      const rawX = clamp(pos.x - offsetX, -bb.x, layout.canvasW - bb.w - bb.x);
      const rawY = clamp(pos.y - offsetY, -bb.y, layout.canvasH - bb.h - bb.y);
      const snapped = e.ctrlKey
        ? { x: Math.round(rawX), y: Math.round(rawY) }
        : snapToGrid(rawX, rawY, sym.w, sym.h, sym.rotation);
      sym.x = snapped.x;
      sym.y = snapped.y;
    }
    layout.markDirty();
  }

  if ((dragState.type === 'resize-spur-top' || dragState.type === 'resize-spur-bottom') && dragState.placedId != null) {
    const sym = layout.symbols.find(s => s.id === dragState!.placedId);
    if (!sym) return;
    const pos = screenToCanvas(e.clientX, e.clientY);
    const dx = pos.x - dragState.startX!;
    const dy = pos.y - dragState.startY!;
    resizeSpur(sym, dx, dy, dragState.type === 'resize-spur-top', e.ctrlKey);
    layout.markDirty();
  }

  if ((dragState.type === 'resize-left' || dragState.type === 'resize-right') && dragState.placedId != null) {
    const sym = layout.symbols.find(s => s.id === dragState!.placedId);
    if (!sym) return;
    const pos = screenToCanvas(e.clientX, e.clientY);
    const dx = pos.x - dragState.startX!;
    const dy = pos.y - dragState.startY!;
    const isRight = dragState.type === 'resize-right';

    if (isCurvedType(sym.symbolId)) {
      resizeCurved(sym, dx, dy, isRight, e.ctrlKey);
    } else {
      resizeStraight(sym, dx, dy, isRight, e.ctrlKey);
    }
    layout.markDirty();
  }
}

function onMouseup(e: MouseEvent) {
  if (e.button === 1 && isPanning) {
    isPanning = false;
    document.body.style.cursor = '';
    return;
  }

  if (!dragState) return;

  if (dragState.type === 'pdf') {
    layout.saveMcmState();
    dragState = null;
    return;
  }

  if (dragState.type === 'palette' && dragState.ghost && dragState.symbolDef) {
    dragState.ghost.remove();
    const sym = dragState.symbolDef;
    const rot = sym.defaultRotation || 0;
    const pos = screenToCanvas(e.clientX, e.clientY);
    let dropX = pos.x - sym.w / 2;
    let dropY = pos.y - sym.h / 2;

    if (dropX >= -sym.w && dropX <= layout.canvasW && dropY >= -sym.h && dropY <= layout.canvasH) {
      dropX = clamp(dropX, 0, layout.canvasW - sym.w);
      dropY = clamp(dropY, 0, layout.canvasH - sym.h);
      const valid = findValidPosition(-1, dropX, dropY, sym.w, sym.h, sym.id, rot, sym.curveAngle, sym.w2);
      layout.addSymbol({
        symbolId: sym.id,
        file: sym.file,
        name: sym.name,
        label: dragState.deviceLabel || '',
        x: valid.x,
        y: valid.y,
        w: sym.w,
        h: sym.h,
        w2: sym.w2,
        rotation: rot,
        curveAngle: sym.curveAngle,
        epcWaypoints: isEpcType(sym.id) ? EPC_DEFAULT_WAYPOINTS.map(wp => ({ ...wp })) : undefined,
        pdpCBs: undefined,
      });
    }
  }

  if (dragState.type === 'move' && dragState.placedId != null) {
    if (dragState.dragActivated) {
      const sym = layout.symbols.find(s => s.id === dragState!.placedId);
      if (sym) {
        if (!e.ctrlKey) {
          const valid = findValidPosition(sym.id, sym.x, sym.y, sym.w, sym.h, sym.symbolId, sym.rotation, sym.curveAngle, sym.w2);
          sym.x = valid.x;
          sym.y = valid.y;
        }
      }
      layout.markDirty();
      layout.saveMcmState();
    }
  }

  if (dragState.type === 'multi-move' && dragState.dragActivated) {
    layout.markDirty();
    layout.saveMcmState();
  }

  if (dragState.type === 'resize-left' || dragState.type === 'resize-right' || dragState.type === 'resize-spur-top' || dragState.type === 'resize-spur-bottom') {
    layout.markDirty();
    layout.saveMcmState();
  }

  if (dragState.type === 'epc-waypoint' && dragState.dragActivated) {
    layout.markDirty();
    layout.saveMcmState();
  }

  dragState = null;
}

function rotateSelected(delta: number) {
  if (layout.selectedIds.size === 0) return;
  layout.pushUndo();
  for (const id of layout.selectedIds) {
    const sym = layout.symbols.find(s => s.id === id);
    if (!sym) continue;
    sym.rotation = ((sym.rotation || 0) + delta + 360) % 360;
  }
  layout.markDirty();
  layout.saveMcmState();
}

function onKeydown(e: KeyboardEvent) {
  // Ctrl+Z: undo
  if (e.ctrlKey && e.key === 'z') {
    e.preventDefault();
    layout.undo();
    return;
  }

  // Ctrl+D: duplicate selected
  if (e.ctrlKey && (e.key === 'd' || e.key === 'D')) {
    e.preventDefault();
    layout.duplicateSelected();
    return;
  }

  // Delete: remove all selected
  if (e.key === 'Delete' && layout.selectedIds.size > 0) {
    layout.removeSelected();
    return;
  }

  // E/Q: rotate all selected
  if (layout.selectedIds.size > 0 && (e.key === 'e' || e.key === 'E')) {
    rotateSelected(ROTATION_STEP);
  }
  if (layout.selectedIds.size > 0 && (e.key === 'q' || e.key === 'Q')) {
    rotateSelected(-ROTATION_STEP);
  }
}

// Called from Palette component to initiate a palette drag
export function startPaletteDrag(e: MouseEvent, symbolDef: (typeof SYMBOLS)[number], deviceLabel?: string) {
  const ghost = document.createElement('div');
  ghost.style.cssText = `
    position: fixed;
    pointer-events: none;
    opacity: 0.6;
    z-index: 9999;
    width: ${symbolDef.w}px;
    height: ${symbolDef.h}px;
  `;
  if (symbolDef.defaultRotation) {
    ghost.style.transform = `rotate(${symbolDef.defaultRotation}deg)`;
  }
  const img = document.createElement('img');
  img.src = symbolDef.file;
  img.style.width = '100%';
  img.style.height = '100%';
  ghost.appendChild(img);
  ghost.style.left = (e.clientX - symbolDef.w / 2) + 'px';
  ghost.style.top = (e.clientY - symbolDef.h / 2) + 'px';
  document.body.appendChild(ghost);

  dragState = { type: 'palette', symbolDef, ghost, deviceLabel };
}
