import { layout } from '../stores/layout.svelte.js';
import { isCurvedType, isEpcType, getSymbolGroup, SYMBOLS, EPC_CONFIG } from '../symbols.js';
import type { EpcWaypoint } from '../types.js';
import { getAABB, snapToGrid, clamp, findValidPosition } from './collision.js';
import { orientedBoxCorners, pastDragThreshold } from './geometry.js';
import { toSymbolLocal, pointInRect, pointInTrapezoid, hitEpcWaypoint, hitEpcSegmentMidpoint, hitEpcSegmentNearest, hitResizeHandle, hitTestSymbols } from './hit-testing.js';

/** Ensure an EPC symbol has its waypoints array initialized */
function ensureEpcWaypoints(sym: { epcWaypoints?: import('../types.js').EpcWaypoint[] }) {
  if (!sym.epcWaypoints) {
    sym.epcWaypoints = EPC_CONFIG.defaultWaypoints.map(wp => ({ ...wp }));
  }
  return sym.epcWaypoints;
}

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' | 'assign-label' | 'marquee';
  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;

// Marquee selection rectangle (canvas coords), accessible by renderer
export let marqueeRect: { x: number; y: number; w: number; h: number } | null = null;
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,
  };
}

function hitTest(cx: number, cy: number): number | null {
  return hitTestSymbols(cx, cy, layout.symbols);
}

/** Check if a device type (symbolDef) is compatible with a placed symbol for label assignment.
 *  Matches by SVG file so base and _v variants are treated as the same type. */
function isTypeCompatible(symbolDef: (typeof SYMBOLS)[number], placedSymbolId: string): boolean {
  const placedDef = SYMBOLS.find(s => s.id === placedSymbolId);
  if (!placedDef) return false;
  return symbolDef.file === placedDef.file;
}

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_CONFIG.defaultWaypoints;
        if (wpIdx >= 0 && wps.length > 2) {
          layout.pushUndo();
          if (!sel.epcWaypoints) {
            sel.epcWaypoints = EPC_CONFIG.defaultWaypoints.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_CONFIG.defaultWaypoints.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();
        ensureEpcWaypoints(sel);
        dragState = {
          type: 'epc-waypoint',
          placedId: sel.id,
          waypointIndex: wpIdx,
          startX: pos.x,
          startY: pos.y,
          dragActivated: false,
        };
        return;
      }
    }
    if (sel && layout.selectedIds.size === 1) {
      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 {
    // Empty space: start marquee selection
    if (!e.shiftKey) {
      layout.selectedIds = new Set();
    }
    dragState = {
      type: 'marquee',
      startX: pos.x,
      startY: pos.y,
      dragActivated: false,
    };
    layout.markDirty();
  }
}

function onContextMenu(e: MouseEvent) {
  e.preventDefault();
  const pos = screenToCanvas(e.clientX, e.clientY);

  // Right-click on selected EPC: add waypoint at nearest segment
  if (layout.selectedIds.size === 1) {
    const selId = [...layout.selectedIds][0];
    const sel = layout.symbols.find(s => s.id === selId);
    if (sel && isEpcType(sel.symbolId)) {
      const insertIdx = hitEpcSegmentNearest(pos.x, pos.y, sel);
      if (insertIdx >= 0) {
        layout.pushUndo();
        ensureEpcWaypoints(sel);
        const prev = sel.epcWaypoints![insertIdx - 1];
        const next = sel.epcWaypoints![insertIdx];
        const local = toSymbolLocal(pos.x, pos.y, sel);
        sel.epcWaypoints!.splice(insertIdx, 0, {
          x: local.x - sel.x,
          y: local.y - sel.y,
        });
        recalcEpcBounds(sel);
        layout.markDirty();
        layout.saveMcmState();
        return;
      }
    }
  }

  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_CONFIG.leftBox.w, EPC_CONFIG.leftBox.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 rbDx = last.x - prev.x, rbDy = last.y - prev.y;
  const rbCorners = orientedBoxCorners(last.x, last.y, rbDy, -rbDx, EPC_CONFIG.rightBox.w, EPC_CONFIG.rightBox.h, 'right');
  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;

  // Don't shift origin — just expand w/h to cover full extent from (0,0)
  sym.w = Math.max(maxX - Math.min(minX, 0), 1);
  sym.h = Math.max(maxY - Math.min(minY, 0), 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
    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 };
    // Defer recalcEpcBounds to mouseup — updating w/h mid-drag shifts
    // the rotation center in toSymbolLocal, causing waypoints to jump.
    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';
    // Show drop-target highlight when dragging a device over a compatible symbol
    if (dragState.deviceLabel) {
      const pos = screenToCanvas(e.clientX, e.clientY);
      const hoverId = hitTest(pos.x, pos.y);
      const hoverSym = hoverId !== null ? layout.symbols.find(s => s.id === hoverId) : null;
      const compatible = hoverSym && isTypeCompatible(sym, hoverSym.symbolId) ? hoverId : null;
      if (layout.labelDropTarget !== compatible) {
        layout.labelDropTarget = compatible;
        layout.markDirty();
      }
    }
  }

  if (dragState.type === 'assign-label' && dragState.ghost) {
    dragState.ghost.style.left = (e.clientX + 8) + 'px';
    dragState.ghost.style.top = (e.clientY - 10) + 'px';
    const pos = screenToCanvas(e.clientX, e.clientY);
    const hoverId = hitTest(pos.x, pos.y);
    if (layout.labelDropTarget !== hoverId) {
      layout.labelDropTarget = hoverId;
      layout.markDirty();
    }
  }

  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;
    }
    // Shift: constrain to orthogonal axis (horizontal or vertical)
    if (e.shiftKey) {
      if (Math.abs(pos.x - dragState.startX!) >= Math.abs(pos.y - dragState.startY!)) {
        pos.y = dragState.startY!;
      } else {
        pos.x = dragState.startX!;
      }
    }
    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;
    }
    // Shift: constrain to orthogonal axis
    if (e.shiftKey) {
      if (Math.abs(pos.x - dragState.startX!) >= Math.abs(pos.y - dragState.startY!)) {
        pos.y = dragState.startY!;
      } else {
        pos.x = dragState.startX!;
      }
    }
    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);
    let dx = pos.x - dragState.startX!;
    const dy = pos.y - dragState.startY!;
    if (sym.mirrored) dx = -dx;
    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);
    let dx = pos.x - dragState.startX!;
    const dy = pos.y - dragState.startY!;
    let isRight = dragState.type === 'resize-right';
    // When mirrored, swap handle direction and invert horizontal delta
    if (sym.mirrored) {
      dx = -dx;
      isRight = !isRight;
    }

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

  if (dragState.type === 'marquee') {
    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 x1 = Math.min(dragState.startX!, pos.x);
    const y1 = Math.min(dragState.startY!, pos.y);
    const x2 = Math.max(dragState.startX!, pos.x);
    const y2 = Math.max(dragState.startY!, pos.y);
    marqueeRect = { x: x1, y: y1, w: x2 - x1, h: y2 - y1 };

    // Select all visible symbols whose AABB intersects the marquee
    const selected = new Set<number>();
    for (const sym of layout.symbols) {
      if (sym.hidden || layout.hiddenGroups.has(getSymbolGroup(sym.symbolId))) continue;
      const bb = getAABB(sym.x, sym.y, sym.w, sym.h, sym.rotation);
      if (bb.x + bb.w >= x1 && bb.x <= x2 && bb.y + bb.h >= y1 && bb.y <= y2) {
        selected.add(sym.id);
      }
    }
    layout.selectedIds = selected;
    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 === 'assign-label' && dragState.ghost && dragState.deviceLabel) {
    dragState.ghost.remove();
    layout.labelDropTarget = null;
    const pos = screenToCanvas(e.clientX, e.clientY);
    const hitId = hitTest(pos.x, pos.y);
    if (hitId !== null) {
      const sym = layout.symbols.find(s => s.id === hitId);
      if (sym) {
        layout.pushUndo();
        sym.label = dragState.deviceLabel;
        layout.markDirty();
        layout.saveMcmState();
      }
    }
    dragState = null;
    return;
  }

  if (dragState.type === 'palette' && dragState.ghost && dragState.symbolDef) {
    dragState.ghost.remove();
    layout.labelDropTarget = null;
    const sym = dragState.symbolDef;
    const rot = sym.defaultRotation || 0;
    const pos = screenToCanvas(e.clientX, e.clientY);

    // If dropped onto a compatible existing symbol and we have a device label, assign the label
    const hitId = dragState.deviceLabel ? hitTest(pos.x, pos.y) : null;
    const target = hitId !== null ? layout.symbols.find(s => s.id === hitId) : null;
    if (target && dragState.deviceLabel && isTypeCompatible(sym, target.symbolId)) {
      layout.pushUndo();
      target.label = dragState.deviceLabel;
      layout.markDirty();
      layout.saveMcmState();
    } else {
      // Drop onto empty space: place a new symbol
      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_CONFIG.defaultWaypoints.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.mirrored);
          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) {
    const sym = layout.symbols.find(s => s.id === dragState!.placedId);
    if (sym) recalcEpcBounds(sym);
    layout.markDirty();
    layout.saveMcmState();
  }

  if (dragState.type === 'marquee') {
    marqueeRect = null;
    layout.markDirty();
  }

  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);
  }

  // M: mirror selected
  if (layout.selectedIds.size > 0 && (e.key === 'm' || e.key === 'M')) {
    layout.mirrorSelected();
  }
}

// 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 };
}

// Called from DeviceDock IDs tab to drag a label onto a placed symbol
export function startLabelDrag(e: MouseEvent, label: string) {
  const ghost = document.createElement('div');
  ghost.style.cssText = `
    position: fixed;
    pointer-events: none;
    z-index: 9999;
    background: #e94560;
    color: #fff;
    padding: 2px 8px;
    border-radius: 3px;
    font-size: 11px;
    font-weight: 600;
    white-space: nowrap;
    opacity: 0.85;
  `;
  ghost.textContent = label;
  ghost.style.left = (e.clientX + 8) + 'px';
  ghost.style.top = (e.clientY - 10) + 'px';
  document.body.appendChild(ghost);

  dragState = { type: 'assign-label', ghost, deviceLabel: label };
}
