import { useSimulatorStore, getEsp32Bridge } from '../../store/useSimulatorStore';
import React, { useEffect, useState, useRef, useCallback } from 'react';
import { ESP32_ADC_PIN_MAP } from '../components-wokwi/Esp32Element';
import { ComponentPickerModal } from '../ComponentPickerModal';
import { ComponentPropertyDialog } from './ComponentPropertyDialog';
import { SensorControlPanel } from './SensorControlPanel';
import { SENSOR_CONTROLS } from '../../simulation/sensorControlConfig';
import { DynamicComponent, createComponentFromMetadata } from '../DynamicComponent';
import { ComponentRegistry } from '../../services/ComponentRegistry';
import { PinSelector } from './PinSelector';
import { WireLayer } from './WireLayer';
import type { SegmentHandle } from './WireLayer';
import { BoardOnCanvas } from './BoardOnCanvas';
import { PartSimulationRegistry } from '../../simulation/parts';
import { PinOverlay } from './PinOverlay';
import { isBoardComponent, boardPinToNumber } from '../../utils/boardPinMapping';
import { autoWireColor, WIRE_KEY_COLORS } from '../../utils/wireUtils';
import {
  findWireNearPoint,
  getRenderedPoints,
  getRenderedSegments,
  moveSegment,
  renderedToWaypoints,
  renderedPointsToPath,
} from '../../utils/wireHitDetection';

/** Detect touch-capable device once */
const isTouchDevice = typeof window !== 'undefined' && ('ontouchstart' in window || navigator.maxTouchPoints > 0);
import type { ComponentMetadata } from '../../types/component-metadata';
import type { BoardKind } from '../../types/board';
import { BOARD_KIND_LABELS } from '../../types/board';
import { useOscilloscopeStore } from '../../store/useOscilloscopeStore';
import { trackSelectBoard, trackAddComponent, trackCreateWire, trackToggleSerialMonitor } from '../../utils/analytics';
import './SimulatorCanvas.css';

/** Check if a board kind is an ESP32-family board. */
function isEsp32Kind(kind: BoardKind): boolean {
  return kind.startsWith('esp32') || kind === 'xiao-esp32-s3' || kind === 'xiao-esp32-c3'
    || kind === 'arduino-nano-esp32' || kind === 'aitewinrobot-esp32c3-supermini'
    || kind === 'esp32-cam' || kind === 'wemos-lolin32-lite' || kind === 'esp32-devkit-c-v4';
}

export const SimulatorCanvas = () => {
  const {
    boards,
    activeBoardId,
    setBoardPosition,
    addBoard,
    components,
    running,
    pinManager,
    initSimulator,
    updateComponentState,
    addComponent,
    removeComponent,
    removeBoard,
    updateComponent,
    serialMonitorOpen,
    toggleSerialMonitor,
  } = useSimulatorStore();

  // Active board (for WiFi/BLE status display)
  const activeBoard = boards.find((b) => b.id === activeBoardId) ?? null;

  // Legacy derived values for components that still use them
  const boardType = useSimulatorStore((s) => s.boardType);
  const boardPosition = useSimulatorStore((s) => s.boardPosition);

  // Wire management from store
  const startWireCreation = useSimulatorStore((s) => s.startWireCreation);
  const updateWireInProgress = useSimulatorStore((s) => s.updateWireInProgress);
  const addWireWaypoint = useSimulatorStore((s) => s.addWireWaypoint);
  const setWireInProgressColor = useSimulatorStore((s) => s.setWireInProgressColor);
  const finishWireCreation = useSimulatorStore((s) => s.finishWireCreation);
  const cancelWireCreation = useSimulatorStore((s) => s.cancelWireCreation);
  const wireInProgress = useSimulatorStore((s) => s.wireInProgress);
  const recalculateAllWirePositions = useSimulatorStore((s) => s.recalculateAllWirePositions);
  const selectedWireId = useSimulatorStore((s) => s.selectedWireId);
  const setSelectedWire = useSimulatorStore((s) => s.setSelectedWire);
  const removeWire = useSimulatorStore((s) => s.removeWire);
  const updateWire = useSimulatorStore((s) => s.updateWire);
  const wires = useSimulatorStore((s) => s.wires);

  // Oscilloscope
  const oscilloscopeOpen = useOscilloscopeStore((s) => s.open);
  const toggleOscilloscope = useOscilloscopeStore((s) => s.toggleOscilloscope);

  // ESP32 crash notification
  const esp32CrashBoardId = useSimulatorStore((s) => s.esp32CrashBoardId);
  const dismissEsp32Crash = useSimulatorStore((s) => s.dismissEsp32Crash);

  // Component picker modal
  const [showComponentPicker, setShowComponentPicker] = useState(false);
  const [registry] = useState(() => ComponentRegistry.getInstance());
  const [registryLoaded, setRegistryLoaded] = useState(registry.isLoaded);

  // Wait for registry to finish loading before rendering components
  useEffect(() => {
    if (!registryLoaded) {
      registry.loadPromise.then(() => setRegistryLoaded(true));
    }
  }, [registry, registryLoaded]);

  // Component selection
  const [selectedComponentId, setSelectedComponentId] = useState<string | null>(null);
  const [showPinSelector, setShowPinSelector] = useState(false);
  const [pinSelectorPos, setPinSelectorPos] = useState({ x: 0, y: 0 });

  // Component property dialog
  const [showPropertyDialog, setShowPropertyDialog] = useState(false);
  const [propertyDialogComponentId, setPropertyDialogComponentId] = useState<string | null>(null);
  const [propertyDialogPosition, setPropertyDialogPosition] = useState({ x: 0, y: 0 });

  // Sensor control panel (shown instead of property dialog for sensor components during simulation)
  const [sensorControlComponentId, setSensorControlComponentId] = useState<string | null>(null);
  const [sensorControlMetadataId, setSensorControlMetadataId] = useState<string | null>(null);

  // Board context menu (right-click)
  const [boardContextMenu, setBoardContextMenu] = useState<{ boardId: string; x: number; y: number } | null>(null);
  // Board removal confirmation dialog
  const [boardToRemove, setBoardToRemove] = useState<string | null>(null);

  // Click vs drag detection
  const [clickStartTime, setClickStartTime] = useState<number>(0);
  const [clickStartPos, setClickStartPos] = useState({ x: 0, y: 0 });

  // Component dragging state
  const [draggedComponentId, setDraggedComponentId] = useState<string | null>(null);
  const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });

  // Canvas ref for coordinate calculations
  const canvasRef = useRef<HTMLDivElement>(null);

  // Pan & zoom state
  const [pan, setPan] = useState({ x: 0, y: 0 });
  const [zoom, setZoom] = useState(1);
  // Use refs during active pan to avoid setState lag
  const isPanningRef = useRef(false);
  const panStartRef = useRef({ mouseX: 0, mouseY: 0, panX: 0, panY: 0 });
  const panRef = useRef({ x: 0, y: 0 });
  const zoomRef = useRef(1);

  // Refs that mirror state/props for use inside touch event closures
  // (touch listeners are added imperatively and can't access current React state)
  const runningRef = useRef(running);
  runningRef.current = running;
  const componentsRef = useRef(components);
  componentsRef.current = components;
  const boardPositionRef = useRef(boardPosition);
  boardPositionRef.current = boardPosition;

  // Wire interaction state (canvas-level hit detection — bypasses SVG pointer-events issues)
  const [hoveredWireId, setHoveredWireId] = useState<string | null>(null);
  const [segmentDragPreview, setSegmentDragPreview] = useState<{
    wireId: string;
    overridePath: string;
  } | null>(null);
  const segmentDragRef = useRef<{
    wireId: string;
    segIndex: number;
    axis: 'horizontal' | 'vertical';
    renderedPts: { x: number; y: number }[];
    isDragging: boolean;
  } | null>(null);
  /** Set to true during mouseup if a segment drag committed, so onClick can skip selection. */
  const segmentDragJustCommittedRef = useRef(false);
  const wiresRef = useRef(wires);
  wiresRef.current = wires;

  // Compute midpoint handles for the selected wire's segments
  const segmentHandles = React.useMemo<SegmentHandle[]>(() => {
    if (!selectedWireId) return [];
    const wire = wires.find((w) => w.id === selectedWireId);
    if (!wire) return [];
    return getRenderedSegments(wire).map((seg, i) => ({
      segIndex: i,
      axis: seg.axis,
      mx: (seg.x1 + seg.x2) / 2,
      my: (seg.y1 + seg.y2) / 2,
    }));
  }, [selectedWireId, wires]);

  // Touch-specific state refs (for single-finger drag and pinch-to-zoom)
  const touchDraggedComponentIdRef = useRef<string | null>(null);
  const touchDragOffsetRef = useRef({ x: 0, y: 0 });
  const touchClickStartTimeRef = useRef(0);
  const touchClickStartPosRef = useRef({ x: 0, y: 0 });
  const pinchStartDistRef = useRef(0);
  const pinchStartZoomRef = useRef(1);
  const pinchStartMidRef = useRef({ x: 0, y: 0 });
  const pinchStartPanRef = useRef({ x: 0, y: 0 });

  // Refs for touch-based wire creation, selection, and interactive passthrough
  const wireInProgressRef = useRef(wireInProgress);
  wireInProgressRef.current = wireInProgress;
  const selectedWireIdRef = useRef(selectedWireId);
  selectedWireIdRef.current = selectedWireId;
  const touchPassthroughRef = useRef(false);
  const touchOnPinRef = useRef(false);
  const lastTapTimeRef = useRef(0);

  // Convert viewport coords to world (canvas) coords
  const toWorld = useCallback((screenX: number, screenY: number) => {
    const rect = canvasRef.current?.getBoundingClientRect();
    if (!rect) return { x: screenX, y: screenY };
    return {
      x: (screenX - rect.left - panRef.current.x) / zoomRef.current,
      y: (screenY - rect.top  - panRef.current.y) / zoomRef.current,
    };
  }, []);

  // Initialize simulator on mount
  useEffect(() => {
    initSimulator();
  }, [initSimulator]);

  // Auto-start/stop Pi bridges when simulation state changes
  const startBoard = useSimulatorStore((s) => s.startBoard);
  const stopBoard = useSimulatorStore((s) => s.stopBoard);
  useEffect(() => {
    const remoteBoards = boards.filter(
      (b) => b.boardKind === 'raspberry-pi-3' ||
             b.boardKind === 'esp32' || b.boardKind === 'esp32-s3' || b.boardKind === 'esp32-c3'
    );
    remoteBoards.forEach((b) => {
      if (running && !b.running) startBoard(b.id);
      else if (!running && b.running) stopBoard(b.id);
    });
  }, [running, boards, startBoard, stopBoard]);

  // Attach wheel listener as non-passive so preventDefault() works
  useEffect(() => {
    const el = canvasRef.current;
    if (!el) return;
    const onWheel = (e: WheelEvent) => {
      e.preventDefault();
      const rect = el.getBoundingClientRect();
      const factor = e.deltaY < 0 ? 1.1 : 0.9;
      const newZoom = Math.min(5, Math.max(0.1, zoomRef.current * factor));
      const mx = e.clientX - rect.left;
      const my = e.clientY - rect.top;
      const worldX = (mx - panRef.current.x) / zoomRef.current;
      const worldY = (my - panRef.current.y) / zoomRef.current;
      const newPan = { x: mx - worldX * newZoom, y: my - worldY * newZoom };
      zoomRef.current = newZoom;
      panRef.current = newPan;
      setZoom(newZoom);
      setPan(newPan);
    };
    el.addEventListener('wheel', onWheel, { passive: false });
    return () => el.removeEventListener('wheel', onWheel);
  }, []);

  // Attach touch listeners as non-passive so preventDefault() works, enabling
  // single-finger pan, component drag, wire creation/selection, and two-finger pinch-to-zoom.
  useEffect(() => {
    const el = canvasRef.current;
    if (!el) return;

    const onTouchStart = (e: TouchEvent) => {
      // Reset per-gesture flags
      touchOnPinRef.current = false;
      touchPassthroughRef.current = false;
      pinchStartDistRef.current = 0;

      if (e.touches.length === 2) {
        e.preventDefault();
        // Cancel wire in progress on two-finger gesture
        if (wireInProgressRef.current) {
          useSimulatorStore.getState().cancelWireCreation();
        }
        // Cancel any active drag/pan and prepare zoom
        isPanningRef.current = false;
        touchDraggedComponentIdRef.current = null;

        const dx = e.touches[0].clientX - e.touches[1].clientX;
        const dy = e.touches[0].clientY - e.touches[1].clientY;
        pinchStartDistRef.current = Math.sqrt(dx * dx + dy * dy);
        pinchStartZoomRef.current = zoomRef.current;
        pinchStartPanRef.current = { ...panRef.current };

        const rect = el.getBoundingClientRect();
        pinchStartMidRef.current = {
          x: (e.touches[0].clientX + e.touches[1].clientX) / 2 - rect.left,
          y: (e.touches[0].clientY + e.touches[1].clientY) / 2 - rect.top,
        };
        return;
      }

      if (e.touches.length !== 1) return;
      const touch = e.touches[0];

      // Identify what element was touched
      const target = document.elementFromPoint(touch.clientX, touch.clientY);

      // ── 1. Pin overlay → let pin's onTouchEnd React handler call handlePinClick ──
      if (target?.closest('[data-pin-overlay]')) {
        e.preventDefault();
        touchOnPinRef.current = true;
        return;
      }

      // ── 2. Interactive web component during simulation → let browser synthesize mouse events ──
      //    (potentiometer knobs, button presses, etc. need mousedown/mouseup synthesis)
      //    touch-action:none on .canvas-content already prevents browser scroll/zoom.
      if (runningRef.current) {
        const webComp = target?.closest('.web-component-container');
        if (webComp) {
          touchPassthroughRef.current = true;
          // Don't preventDefault → browser synthesizes mouse events for the component
          return;
        }
      }

      e.preventDefault();

      touchClickStartTimeRef.current = Date.now();
      touchClickStartPosRef.current = { x: touch.clientX, y: touch.clientY };

      // ── 3. Wire in progress → track for waypoint, update preview ──
      if (wireInProgressRef.current) {
        const world = toWorld(touch.clientX, touch.clientY);
        useSimulatorStore.getState().updateWireInProgress(world.x, world.y);
        // Don't start pan/drag — let touchmove update wire preview, touchend add waypoint
        return;
      }

      // ── 4. Component detection ──
      const componentWrapper = target?.closest('[data-component-id]') as HTMLElement | null;
      const boardOverlay = target?.closest('[data-board-overlay]') as HTMLElement | null;

      if (componentWrapper) {
        const componentId = componentWrapper.getAttribute('data-component-id');
        if (componentId) {
          const component = componentsRef.current.find((c) => c.id === componentId);
          if (component) {
            const world = toWorld(touch.clientX, touch.clientY);
            touchDraggedComponentIdRef.current = componentId;
            touchDragOffsetRef.current = {
              x: world.x - component.x,
              y: world.y - component.y,
            };
            setSelectedComponentId(componentId);
          }
        }
      } else if (boardOverlay && !runningRef.current) {
        // ── 5. Board overlay: use multi-board path ──
        const boardId = boardOverlay.getAttribute('data-board-id');
        const storeBoards = useSimulatorStore.getState().boards;
        const boardInstance = boardId ? storeBoards.find(b => b.id === boardId) : null;
        if (boardInstance) {
          const world = toWorld(touch.clientX, touch.clientY);
          touchDraggedComponentIdRef.current = `__board__:${boardId}`;
          touchDragOffsetRef.current = {
            x: world.x - boardInstance.x,
            y: world.y - boardInstance.y,
          };
        } else {
          // Fallback to legacy single board
          const board = boardPositionRef.current;
          const world = toWorld(touch.clientX, touch.clientY);
          touchDraggedComponentIdRef.current = '__board__';
          touchDragOffsetRef.current = {
            x: world.x - board.x,
            y: world.y - board.y,
          };
        }
      } else {
        // ── 6. Empty canvas → start pan ──
        isPanningRef.current = true;
        panStartRef.current = {
          mouseX: touch.clientX,
          mouseY: touch.clientY,
          panX: panRef.current.x,
          panY: panRef.current.y,
        };
      }
    };

    const onTouchMove = (e: TouchEvent) => {
      // Let interactive components handle their own touch (potentiometer drag, etc.)
      if (touchPassthroughRef.current) return;
      // Pin touch: no move processing needed
      if (touchOnPinRef.current) { e.preventDefault(); return; }

      e.preventDefault();

      if (e.touches.length === 2 && pinchStartDistRef.current > 0) {
        // ── Two-finger pinch: update zoom ──
        const dx = e.touches[0].clientX - e.touches[1].clientX;
        const dy = e.touches[0].clientY - e.touches[1].clientY;
        const dist = Math.sqrt(dx * dx + dy * dy);
        const scale = dist / pinchStartDistRef.current;
        const newZoom = Math.min(5, Math.max(0.1, pinchStartZoomRef.current * scale));

        const mid = pinchStartMidRef.current;
        const startPan = pinchStartPanRef.current;
        const startZoom = pinchStartZoomRef.current;
        const worldX = (mid.x - startPan.x) / startZoom;
        const worldY = (mid.y - startPan.y) / startZoom;
        const newPan = {
          x: mid.x - worldX * newZoom,
          y: mid.y - worldY * newZoom,
        };

        zoomRef.current = newZoom;
        panRef.current = newPan;
        const worldEl = el.querySelector('.canvas-world') as HTMLElement | null;
        if (worldEl) {
          worldEl.style.transform = `translate(${newPan.x}px, ${newPan.y}px) scale(${newZoom})`;
        }
        return;
      }

      if (e.touches.length !== 1) return;
      const touch = e.touches[0];

      // ── Segment drag (wire editing) via touch ──
      if (segmentDragRef.current) {
        const world = toWorld(touch.clientX, touch.clientY);
        const sd = segmentDragRef.current;
        sd.isDragging = true;
        const newValue = sd.axis === 'horizontal' ? world.y : world.x;
        const newPts = moveSegment(sd.renderedPts, sd.segIndex, sd.axis, newValue);
        const overridePath = renderedPointsToPath(newPts);
        setSegmentDragPreview({ wireId: sd.wireId, overridePath });
        return;
      }

      // ── Wire preview: update position as finger moves ──
      if (wireInProgressRef.current && !isPanningRef.current && !touchDraggedComponentIdRef.current) {
        const world = toWorld(touch.clientX, touch.clientY);
        useSimulatorStore.getState().updateWireInProgress(world.x, world.y);
        return;
      }

      if (isPanningRef.current) {
        // ── Single finger pan ──
        const dx = touch.clientX - panStartRef.current.mouseX;
        const dy = touch.clientY - panStartRef.current.mouseY;
        const newPan = {
          x: panStartRef.current.panX + dx,
          y: panStartRef.current.panY + dy,
        };
        panRef.current = newPan;
        const worldEl = el.querySelector('.canvas-world') as HTMLElement | null;
        if (worldEl) {
          worldEl.style.transform = `translate(${newPan.x}px, ${newPan.y}px) scale(${zoomRef.current})`;
        }
      } else if (touchDraggedComponentIdRef.current) {
        // ── Single finger component/board drag ──
        const world = toWorld(touch.clientX, touch.clientY);
        const touchId = touchDraggedComponentIdRef.current;
        if (touchId && touchId.startsWith('__board__:')) {
          const boardId = touchId.slice('__board__:'.length);
          setBoardPosition({
            x: world.x - touchDragOffsetRef.current.x,
            y: world.y - touchDragOffsetRef.current.y,
          }, boardId);
        } else if (touchId === '__board__') {
          setBoardPosition({
            x: world.x - touchDragOffsetRef.current.x,
            y: world.y - touchDragOffsetRef.current.y,
          });
        } else {
          updateComponent(touchDraggedComponentIdRef.current!, {
            x: world.x - touchDragOffsetRef.current.x,
            y: world.y - touchDragOffsetRef.current.y,
          } as any);
        }
      }
    };

    const onTouchEnd = (e: TouchEvent) => {
      // Let interactive components handle their own touch
      if (touchPassthroughRef.current) {
        touchPassthroughRef.current = false;
        return;
      }
      // Pin touch: let pin's onTouchEnd React handler deal with it
      if (touchOnPinRef.current) {
        touchOnPinRef.current = false;
        e.preventDefault();
        return;
      }

      e.preventDefault();

      // ── Finish pinch zoom: commit values to React state ──
      if (pinchStartDistRef.current > 0 && e.touches.length < 2) {
        setZoom(zoomRef.current);
        setPan({ ...panRef.current });
        pinchStartDistRef.current = 0;
      }

      if (e.touches.length > 0) return; // Still fingers on screen

      // ── Finish segment drag (wire editing) via touch ──
      if (segmentDragRef.current) {
        const sd = segmentDragRef.current;
        if (sd.isDragging) {
          segmentDragJustCommittedRef.current = true;
          const changed = e.changedTouches[0];
          if (changed) {
            const world = toWorld(changed.clientX, changed.clientY);
            const newValue = sd.axis === 'horizontal' ? world.y : world.x;
            const newPts = moveSegment(sd.renderedPts, sd.segIndex, sd.axis, newValue);
            updateWire(sd.wireId, { waypoints: renderedToWaypoints(newPts) });
          }
        }
        segmentDragRef.current = null;
        setSegmentDragPreview(null);
        return;
      }

      // ── Finish panning ──
      let wasPanning = false;
      if (isPanningRef.current) {
        isPanningRef.current = false;
        setPan({ ...panRef.current });
        wasPanning = true;
        // Don't return — fall through so short taps can select wires
      }

      const changed = e.changedTouches[0];
      if (!changed) return;

      const elapsed = Date.now() - touchClickStartTimeRef.current;
      const dx = changed.clientX - touchClickStartPosRef.current.x;
      const dy = changed.clientY - touchClickStartPosRef.current.y;
      const dist = Math.sqrt(dx * dx + dy * dy);
      const isShortTap = dist < 20 && elapsed < 400;

      // If we actually panned (moved significantly), don't process as tap
      if (wasPanning && !isShortTap) return;

      // ── Finish component/board drag ──
      if (touchDraggedComponentIdRef.current) {
        const touchId = touchDraggedComponentIdRef.current;

        if (isShortTap) {
          if (touchId.startsWith('__board__:')) {
            // Short tap on board → make it the active board
            const boardId = touchId.slice('__board__:'.length);
            useSimulatorStore.getState().setActiveBoardId(boardId);
          } else if (touchId !== '__board__') {
            // Short tap on component → open property dialog or sensor panel
            const component = componentsRef.current.find(
              (c) => c.id === touchId
            );
            if (component) {
              if (runningRef.current && SENSOR_CONTROLS[component.metadataId] !== undefined) {
                setSensorControlComponentId(touchId);
                setSensorControlMetadataId(component.metadataId);
              } else {
                setPropertyDialogComponentId(touchId);
                setPropertyDialogPosition({ x: component.x, y: component.y });
                setShowPropertyDialog(true);
              }
            }
          }
        }

        recalculateAllWirePositions();
        touchDraggedComponentIdRef.current = null;
        return;
      }

      // ── Wire in progress: short tap adds waypoint ──
      if (wireInProgressRef.current) {
        if (isShortTap) {
          const world = toWorld(changed.clientX, changed.clientY);
          useSimulatorStore.getState().addWireWaypoint(world.x, world.y);
        }
        return;
      }

      // ── Short tap on empty canvas: wire selection + double-tap wire deletion ──
      if (isShortTap) {
        const now = Date.now();
        const world = toWorld(changed.clientX, changed.clientY);
        const baseThreshold = isTouchDevice ? 20 : 8;
        const threshold = baseThreshold / zoomRef.current;
        const wire = findWireNearPoint(wiresRef.current, world.x, world.y, threshold);

        // Double-tap → delete wire
        const timeSinceLastTap = now - lastTapTimeRef.current;
        if (timeSinceLastTap < 350 && wire) {
          useSimulatorStore.getState().removeWire(wire.id);
          lastTapTimeRef.current = 0;
          return;
        }
        lastTapTimeRef.current = now;

        if (wire) {
          const curr = selectedWireIdRef.current;
          useSimulatorStore.getState().setSelectedWire(curr === wire.id ? null : wire.id);
        } else {
          useSimulatorStore.getState().setSelectedWire(null);
          setSelectedComponentId(null);
        }
      }
    };

    el.addEventListener('touchstart', onTouchStart, { passive: false });
    el.addEventListener('touchmove', onTouchMove, { passive: false });
    el.addEventListener('touchend', onTouchEnd, { passive: false });

    return () => {
      el.removeEventListener('touchstart', onTouchStart);
      el.removeEventListener('touchmove', onTouchMove);
      el.removeEventListener('touchend', onTouchEnd);
    };
  }, [toWorld, setBoardPosition, updateComponent, recalculateAllWirePositions]);

  // Recalculate wire positions after web components initialize their pinInfo
  useEffect(() => {
    const timer = setTimeout(() => {
      recalculateAllWirePositions();
    }, 500);
    return () => clearTimeout(timer);
  }, [recalculateAllWirePositions]);

  // Connect components to pin manager
  useEffect(() => {
    const unsubscribers: (() => void)[] = [];

    // Helper to add subscription
    const subscribeComponentToPin = (component: any, pin: number, componentPinName?: string) => {
      // Components with attachEvents in PartSimulationRegistry manage their own
      // visual state (e.g. servo, buzzer). Skip generic digital/PWM updates for them
      // to avoid flickering from raw PWM pulses being misinterpreted as on/off state.
      const logic = PartSimulationRegistry.get(component.metadataId);
      const hasSelfManagedVisuals = !!(logic && logic.attachEvents);

      const unsubscribe = pinManager.onPinChange(
        pin,
        (_pin, state) => {
          if (!hasSelfManagedVisuals) {
            // 1. Update React state for standard properties (LEDs, buttons, etc.)
            updateComponentState(component.id, state);
          }

          // 2. Delegate to PartSimulationRegistry for custom visual updates
          if (logic && logic.onPinStateChange) {
            const el = document.getElementById(component.id);
            if (el) {
              logic.onPinStateChange(componentPinName || 'A', state, el);
            }
          }
        }
      );
      unsubscribers.push(unsubscribe);

      // PWM subscription: update LED opacity when the pin receives a PWM duty cycle.
      // Skip for self-managed components (servo, buzzer) — their duty cycle is a
      // control signal, not a brightness value, so setting opacity would cause flicker.
      if (!hasSelfManagedVisuals) {
        const pwmUnsub = pinManager.onPwmChange(pin, (_p, duty) => {
          const el = document.getElementById(component.id);
          if (el) el.style.opacity = duty > 0 ? String(duty) : '';
        });
        unsubscribers.push(pwmUnsub);
      }
    };

    components.forEach((component) => {
      // 1. Subscribe by explicit pin property
      if (component.properties.pin !== undefined) {
        subscribeComponentToPin(component, component.properties.pin as number, 'A');
      } else {
        // 2. Subscribe by finding wires connected to arduino
        const connectedWires = wires.filter(
          w => w.start.componentId === component.id || w.end.componentId === component.id
        );

        connectedWires.forEach(wire => {
          const isStartSelf = wire.start.componentId === component.id;
          const selfEndpoint = isStartSelf ? wire.start : wire.end;
          const otherEndpoint = isStartSelf ? wire.end : wire.start;

          if (isBoardComponent(otherEndpoint.componentId)) {
            // Use the board's actual boardKind (not just its instance ID) so that
            // a board whose ID is 'arduino-uno' but whose kind is 'esp32' gets the
            // correct GPIO mapping ('GPIO4' → 4, not null).
            const boardInstance = boards.find(b => b.id === otherEndpoint.componentId);
            const lookupKey = boardInstance ? boardInstance.boardKind : otherEndpoint.componentId;
            const pin = boardPinToNumber(lookupKey, otherEndpoint.pinName);
            console.log(
              `[WirePin] component=${component.id} board=${otherEndpoint.componentId}` +
              ` kind=${lookupKey} pinName=${otherEndpoint.pinName} → gpioPin=${pin}`
            );
            if (pin !== null && pin >= 0) {
              subscribeComponentToPin(component, pin, selfEndpoint.pinName);
            } else if (pin === null) {
              console.warn(`[WirePin] Could not resolve pin "${otherEndpoint.pinName}" on ${lookupKey}`);
            }
            // pin === -1 → power/GND pin, skip silently
          }
        });
      }
    });

    return () => {
      unsubscribers.forEach(unsub => unsub());
    };
  }, [components, wires, boards, pinManager, updateComponentState]);

  // ESP32 input components: forward button presses and potentiometer values to QEMU
  useEffect(() => {
    const cleanups: (() => void)[] = [];

    components.forEach((component) => {
      const connectedWires = wires.filter(
        w => w.start.componentId === component.id || w.end.componentId === component.id
      );

      connectedWires.forEach(wire => {
        const isStartSelf = wire.start.componentId === component.id;
        const selfEndpoint  = isStartSelf ? wire.start : wire.end;
        const otherEndpoint = isStartSelf ? wire.end   : wire.start;

        if (!isBoardComponent(otherEndpoint.componentId)) return;

        const boardId  = otherEndpoint.componentId;
        const bridge   = getEsp32Bridge(boardId);
        if (!bridge) return;  // not an ESP32 board

        const boardInstance = boards.find(b => b.id === boardId);
        const lookupKey = boardInstance ? boardInstance.boardKind : boardId;
        const gpioPin = boardPinToNumber(lookupKey, otherEndpoint.pinName);
        if (gpioPin === null) return;

        // Delay lookup so the web component has time to render
        const timeout = setTimeout(() => {
          const el = document.getElementById(component.id);
          if (!el) return;
          const tag = el.tagName.toLowerCase();

          // Push-button: forward press/release as GPIO level changes
          if (tag === 'wokwi-pushbutton') {
            const onPress   = () => bridge.sendPinEvent(gpioPin, true);
            const onRelease = () => bridge.sendPinEvent(gpioPin, false);
            el.addEventListener('button-press',   onPress);
            el.addEventListener('button-release', onRelease);
            cleanups.push(() => {
              el.removeEventListener('button-press',   onPress);
              el.removeEventListener('button-release', onRelease);
            });
          }

          // Potentiometer: forward analog value as ADC millivolts
          if (tag === 'wokwi-potentiometer' && selfEndpoint.pinName === 'SIG') {
            const adcInfo = ESP32_ADC_PIN_MAP[gpioPin];
            if (adcInfo) {
              const onInput = (e: Event) => {
                const pct = parseFloat((e.target as any).value ?? '0');  // 0–100
                bridge.setAdc(adcInfo.chn, Math.round(pct / 100 * 3300));
              };
              el.addEventListener('input', onInput);
              cleanups.push(() => el.removeEventListener('input', onInput));
            }
          }
        }, 300);

        cleanups.push(() => clearTimeout(timeout));
      });
    });

    return () => cleanups.forEach(fn => fn());
  }, [components, wires, boards]);

  // Handle keyboard delete for components and boards
  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      if (e.key === 'Delete' || e.key === 'Backspace') {
        if (selectedComponentId) {
          removeComponent(selectedComponentId);
          setSelectedComponentId(null);
        } else if (activeBoardId && boards.length > 1) {
          // Only allow deleting boards if more than one exists
          setBoardToRemove(activeBoardId);
        }
      }
    };

    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, [selectedComponentId, removeComponent, activeBoardId, boards.length]);

  // Handle component selection from modal
  const handleSelectComponent = (metadata: ComponentMetadata) => {
    // Calculate grid position to avoid overlapping
    // Use existing components count to determine position
    const componentsCount = components.length;
    const gridSize = 250; // Space between components
    const cols = 3; // Components per row

    const col = componentsCount % cols;
    const row = Math.floor(componentsCount / cols);

    const x = 400 + (col * gridSize);
    const y = 100 + (row * gridSize);

    const component = createComponentFromMetadata(metadata, x, y);
    trackAddComponent(metadata.id);
    addComponent(component as any);
    setShowComponentPicker(false);
  };

  // Component selection (double click to open pin selector)
  const handleComponentDoubleClick = (componentId: string, event: React.MouseEvent) => {
    event.stopPropagation();
    setSelectedComponentId(componentId);
    setPinSelectorPos({ x: event.clientX, y: event.clientY });
    setShowPinSelector(true);
  };

  // Pin assignment
  const handlePinSelect = (componentId: string, pin: number) => {
    updateComponent(componentId, {
      properties: {
        ...components.find((c) => c.id === componentId)?.properties,
        pin,
      },
    } as any);
  };

  // Component rotation
  const handleRotateComponent = (componentId: string) => {
    const component = components.find((c) => c.id === componentId);
    if (!component) return;

    const currentRotation = (component.properties.rotation as number) || 0;
    updateComponent(componentId, {
      properties: {
        ...component.properties,
        rotation: (currentRotation + 90) % 360,
      },
    } as any);
  };

  // Component dragging handlers
  const handleComponentMouseDown = (componentId: string, e: React.MouseEvent) => {
    if (showPinSelector || showPropertyDialog) return;

    e.stopPropagation();
    const component = components.find((c) => c.id === componentId);
    if (!component) return;

    setClickStartTime(Date.now());
    setClickStartPos({ x: e.clientX, y: e.clientY });

    const world = toWorld(e.clientX, e.clientY);
    setDraggedComponentId(componentId);
    setDragOffset({
      x: world.x - component.x,
      y: world.y - component.y,
    });
    setSelectedComponentId(componentId);
  };

  const handleCanvasMouseMove = (e: React.MouseEvent) => {
    // Handle active panning (ref-based, no setState lag)
    if (isPanningRef.current) {
      const dx = e.clientX - panStartRef.current.mouseX;
      const dy = e.clientY - panStartRef.current.mouseY;
      const newPan = {
        x: panStartRef.current.panX + dx,
        y: panStartRef.current.panY + dy,
      };
      panRef.current = newPan;
      // Update the transform directly for zero-lag panning
      const world = canvasRef.current?.querySelector('.canvas-world') as HTMLElement | null;
      if (world) {
        world.style.transform = `translate(${newPan.x}px, ${newPan.y}px) scale(${zoomRef.current})`;
      }
      return;
    }

    // Handle component/board dragging
    if (draggedComponentId) {
      const world = toWorld(e.clientX, e.clientY);
      if (draggedComponentId.startsWith('__board__:')) {
        const boardId = draggedComponentId.slice('__board__:'.length);
        setBoardPosition({ x: world.x - dragOffset.x, y: world.y - dragOffset.y }, boardId);
      } else if (draggedComponentId === '__board__') {
        // legacy fallback
        setBoardPosition({ x: world.x - dragOffset.x, y: world.y - dragOffset.y });
      } else {
        updateComponent(draggedComponentId, {
          x: world.x - dragOffset.x,
          y: world.y - dragOffset.y,
        } as any);
      }
    }

    // Handle wire creation preview
    if (wireInProgress) {
      const world = toWorld(e.clientX, e.clientY);
      updateWireInProgress(world.x, world.y);
      return;
    }

    // Handle segment handle dragging
    if (segmentDragRef.current) {
      const world = toWorld(e.clientX, e.clientY);
      const sd = segmentDragRef.current;
      sd.isDragging = true;
      const newValue = sd.axis === 'horizontal' ? world.y : world.x;
      const newPts = moveSegment(sd.renderedPts, sd.segIndex, sd.axis, newValue);
      const overridePath = renderedPointsToPath(newPts);
      setSegmentDragPreview({ wireId: sd.wireId, overridePath });
      return;
    }

    // Wire hover detection (when not dragging anything)
    if (!draggedComponentId) {
      const world = toWorld(e.clientX, e.clientY);
      const threshold = 8 / zoomRef.current;
      const wire = findWireNearPoint(wiresRef.current, world.x, world.y, threshold);
      setHoveredWireId(wire ? wire.id : null);
    }
  };

  const handleCanvasMouseUp = (e: React.MouseEvent) => {
    // Finish panning — commit ref value to state so React knows the final pan
    if (isPanningRef.current) {
      isPanningRef.current = false;
      setPan({ ...panRef.current });
      return;
    }

    // Commit segment handle drag
    if (segmentDragRef.current) {
      const sd = segmentDragRef.current;
      if (sd.isDragging) {
        segmentDragJustCommittedRef.current = true;
        const world = toWorld(e.clientX, e.clientY);
        const newValue = sd.axis === 'horizontal' ? world.y : world.x;
        const newPts = moveSegment(sd.renderedPts, sd.segIndex, sd.axis, newValue);
        updateWire(sd.wireId, { waypoints: renderedToWaypoints(newPts) });
      }
      segmentDragRef.current = null;
      setSegmentDragPreview(null);
      return;
    }

    if (draggedComponentId) {
      const timeDiff = Date.now() - clickStartTime;
      const posDiff = Math.sqrt(
        Math.pow(e.clientX - clickStartPos.x, 2) +
        Math.pow(e.clientY - clickStartPos.y, 2)
      );

      if (posDiff < 5 && timeDiff < 300) {
        if (draggedComponentId.startsWith('__board__:')) {
          // Click on a board — make it the active board (editor switches to its code)
          const boardId = draggedComponentId.slice('__board__:'.length);
          useSimulatorStore.getState().setActiveBoardId(boardId);
        } else if (draggedComponentId !== '__board__') {
          const component = components.find((c) => c.id === draggedComponentId);
          if (component) {
            // During simulation: sensor components show the SensorControlPanel
            if (running && SENSOR_CONTROLS[component.metadataId] !== undefined) {
              setSensorControlComponentId(draggedComponentId);
              setSensorControlMetadataId(component.metadataId);
            } else {
              setPropertyDialogComponentId(draggedComponentId);
              setPropertyDialogPosition({ x: component.x, y: component.y });
              setShowPropertyDialog(true);
            }
          }
        }
      }

      recalculateAllWirePositions();
      setDraggedComponentId(null);
    }
  };

  // Start panning on middle-click or right-click
  const handleCanvasMouseDown = (e: React.MouseEvent) => {
    if (e.button === 1 || e.button === 2) {
      e.preventDefault();
      isPanningRef.current = true;
      panStartRef.current = {
        mouseX: e.clientX,
        mouseY: e.clientY,
        panX: panRef.current.x,
        panY: panRef.current.y,
      };
    }
  };

  // Handle mousedown on a segment handle circle (called from WireLayer)
  const handleHandleMouseDown = useCallback(
    (e: React.MouseEvent, segIndex: number) => {
      e.stopPropagation();
      e.preventDefault();
      if (!selectedWireId) return;
      const wire = wiresRef.current.find((w) => w.id === selectedWireId);
      if (!wire) return;
      const segments = getRenderedSegments(wire);
      const seg = segments[segIndex];
      if (!seg) return;
      const expandedPts = getRenderedPoints(wire);
      segmentDragRef.current = {
        wireId: wire.id,
        segIndex,
        axis: seg.axis,
        renderedPts: expandedPts,
        isDragging: false,
      };
    },
    [selectedWireId],
  );

  // Handle touchstart on a segment handle circle (mobile wire editing)
  const handleHandleTouchStart = useCallback(
    (e: React.TouchEvent, segIndex: number) => {
      e.stopPropagation();
      if (!selectedWireId) return;
      const wire = wiresRef.current.find((w) => w.id === selectedWireId);
      if (!wire) return;
      const segments = getRenderedSegments(wire);
      const seg = segments[segIndex];
      if (!seg) return;
      const expandedPts = getRenderedPoints(wire);
      segmentDragRef.current = {
        wireId: wire.id,
        segIndex,
        axis: seg.axis,
        renderedPts: expandedPts,
        isDragging: false,
      };
    },
    [selectedWireId],
  );

  // Zoom centered on cursor
  const handleWheel = (e: React.WheelEvent) => {
    e.preventDefault();
    const rect = canvasRef.current?.getBoundingClientRect();
    if (!rect) return;

    const factor = e.deltaY < 0 ? 1.1 : 0.9;
    const newZoom = Math.min(5, Math.max(0.1, zoomRef.current * factor));

    const mx = e.clientX - rect.left;
    const my = e.clientY - rect.top;
    // Keep the world point under the cursor fixed
    const worldX = (mx - panRef.current.x) / zoomRef.current;
    const worldY = (my - panRef.current.y) / zoomRef.current;
    const newPan = {
      x: mx - worldX * newZoom,
      y: my - worldY * newZoom,
    };

    zoomRef.current = newZoom;
    panRef.current = newPan;
    setZoom(newZoom);
    setPan(newPan);
  };

  const handleResetView = () => {
    zoomRef.current = 1;
    panRef.current = { x: 0, y: 0 };
    setZoom(1);
    setPan({ x: 0, y: 0 });
  };

  // Wire creation via pin clicks
  const handlePinClick = (componentId: string, pinName: string, x: number, y: number) => {
    // Close property dialog when starting wire creation
    if (showPropertyDialog) {
      setShowPropertyDialog(false);
    }

    if (wireInProgress) {
      // Finish wire: connect to this pin
      finishWireCreation({ componentId, pinName, x, y });
      trackCreateWire();
    } else {
      // Start wire: auto-detect color from pin name
      startWireCreation({ componentId, pinName, x, y }, autoWireColor(pinName));
    }
  };

  // Keyboard handlers for wires
  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      // Escape → cancel in-progress wire
      if (e.key === 'Escape' && wireInProgress) {
        cancelWireCreation();
        return;
      }
      // Delete / Backspace → remove selected wire
      if ((e.key === 'Delete' || e.key === 'Backspace') && selectedWireId) {
        removeWire(selectedWireId);
        return;
      }
      // Color shortcuts (0-9, c, l, m, p, y) — Wokwi style
      const key = e.key.toLowerCase();
      if (key in WIRE_KEY_COLORS) {
        if (wireInProgress) {
          setWireInProgressColor(WIRE_KEY_COLORS[key]);
        } else if (selectedWireId) {
          updateWire(selectedWireId, { color: WIRE_KEY_COLORS[key] });
        }
      }
    };

    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, [wireInProgress, cancelWireCreation, selectedWireId, removeWire, setWireInProgressColor, updateWire]);

  // Recalculate wire positions when components change (e.g., when loading an example)
  useEffect(() => {
    // Wait for components to render and pinInfo to be available
    // Use multiple retries to ensure pinInfo is ready
    const timers: ReturnType<typeof setTimeout>[] = [];

    // Try at 100ms, 300ms, and 500ms to ensure all components have rendered
    timers.push(setTimeout(() => recalculateAllWirePositions(), 100));
    timers.push(setTimeout(() => recalculateAllWirePositions(), 300));
    timers.push(setTimeout(() => recalculateAllWirePositions(), 500));

    return () => timers.forEach(t => clearTimeout(t));
  }, [components, recalculateAllWirePositions]);

  // Auto-pan to keep the board and all components visible after a project import/load.
  // We track the previous component count and only re-center when the count
  // jumps (indicating the user loaded a new circuit, not just added one part).
  const prevComponentCountRef = useRef(-1);
  useEffect(() => {
    const prev = prevComponentCountRef.current;
    const curr = components.length;
    prevComponentCountRef.current = curr;

    // Only re-center when the component list transitions from empty/different
    // project to a populated one (i.e., a load/import event).
    const isLoad = curr > 0 && (prev <= 0 || Math.abs(curr - prev) > 2);
    if (!isLoad) return;

    const timer = setTimeout(() => {
      const canvas = canvasRef.current;
      if (!canvas) return;
      const rect = canvas.getBoundingClientRect();
      const currentZoom = zoomRef.current;

      // Compute the centroid of all world-space elements (board + extra components)
      // so that the auto-pan keeps everything visible, not just the board.
      const allX = [boardPositionRef.current.x, ...componentsRef.current.map((c) => c.x)];
      const allY = [boardPositionRef.current.y, ...componentsRef.current.map((c) => c.y)];
      const minX = Math.min(...allX);
      const maxX = Math.max(...allX);
      const minY = Math.min(...allY);
      const maxY = Math.max(...allY);
      const centerX = (minX + maxX) / 2;
      const centerY = (minY + maxY) / 2;

      const newPan = {
        x: rect.width  / 2 - centerX * currentZoom,
        y: rect.height / 2 - centerY * currentZoom,
      };
      panRef.current = newPan;
      setPan(newPan);
    }, 150);

    return () => clearTimeout(timer);
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [components.length]);

  // Render component using dynamic renderer
  const renderComponent = (component: any) => {
    const metadata = registry.getById(component.metadataId);
    if (!metadata) {
      console.warn(`Metadata not found for component: ${component.metadataId}`);
      return null;
    }

    const isSelected = selectedComponentId === component.id;
    // Always show pins for better UX when creating wires
    const showPinsForComponent = true;

    return (
      <React.Fragment key={component.id}>
        <DynamicComponent
          id={component.id}
          metadata={metadata}
          properties={component.properties}
          x={component.x}
          y={component.y}
          isSelected={isSelected}
          onMouseDown={(e) => {
            handleComponentMouseDown(component.id, e);
          }}
          onDoubleClick={(e) => {
            // Only handle UI events when simulation is NOT running
            if (!running) {
              handleComponentDoubleClick(component.id, e);
            }
          }}
        />

        {/* Pin overlay for wire creation - hide when running */}
        {!running && (
          <PinOverlay
            componentId={component.id}
            componentX={component.x}
            componentY={component.y}
            onPinClick={handlePinClick}
            showPins={showPinsForComponent}
            zoom={zoom}
          />
        )}
      </React.Fragment>
    );
  };

  return (
    <div className="simulator-canvas-container">
      {/* ESP32 crash notification */}
      {esp32CrashBoardId && (
        <div style={{
          position: 'absolute', top: 8, left: '50%', transform: 'translateX(-50%)',
          zIndex: 1000, background: '#c0392b', color: '#fff',
          padding: '8px 16px', borderRadius: 6, display: 'flex', alignItems: 'center',
          gap: 12, fontSize: 13, boxShadow: '0 2px 8px rgba(0,0,0,0.4)',
        }}>
          <span>ESP32 crash detected on board <strong>{esp32CrashBoardId}</strong> — cache error (IDF incompatibility)</span>
          <button
            onClick={dismissEsp32Crash}
            style={{
              background: 'transparent', border: '1px solid rgba(255,255,255,0.6)',
              color: '#fff', borderRadius: 4, padding: '2px 8px', cursor: 'pointer',
            }}
          >
            Dismiss
          </button>
        </div>
      )}

      {/* Main Canvas */}
      <div className="simulator-canvas">
        <div className="canvas-header">
          <div className="canvas-header-left">
            {/* Status LED */}
            <span className={`status-dot ${running ? 'running' : 'stopped'}`} title={running ? 'Running' : 'Stopped'} />

            {/* Active board selector (multi-board) */}
            <select
              className="board-selector"
              value={activeBoardId ?? ''}
              onChange={(e) => useSimulatorStore.getState().setActiveBoardId(e.target.value)}
              disabled={running}
              title="Active board"
            >
              {boards.map((b) => (
                <option key={b.id} value={b.id}>{BOARD_KIND_LABELS[b.boardKind] ?? b.id}</option>
              ))}
            </select>

            {/* Serial Monitor toggle */}
            <button
              onClick={() => { toggleSerialMonitor(); trackToggleSerialMonitor(!serialMonitorOpen); }}
              className={`canvas-serial-btn${serialMonitorOpen ? ' canvas-serial-btn-active' : ''}`}
              title="Toggle Serial Monitor"
            >
              <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                <rect x="2" y="3" width="20" height="14" rx="2" />
                <path d="M8 21h8M12 17v4" />
              </svg>
              Serial
            </button>

            {/* WiFi status indicator (ESP32 boards only) */}
            {activeBoard && isEsp32Kind(activeBoard.boardKind) && activeBoard.wifiStatus && (
              <span
                className={`canvas-wifi-badge canvas-wifi-${activeBoard.wifiStatus.status}`}
                title={
                  activeBoard.wifiStatus.status === 'got_ip'
                    ? `WiFi: ${activeBoard.wifiStatus.ssid ?? 'Velxio-GUEST'} — IP: ${activeBoard.wifiStatus.ip}`
                    : activeBoard.wifiStatus.status === 'connected'
                    ? `WiFi: ${activeBoard.wifiStatus.ssid ?? 'Velxio-GUEST'} — Connecting...`
                    : activeBoard.wifiStatus.status === 'initializing'
                    ? 'WiFi: Initializing...'
                    : 'WiFi: Disconnected'
                }
              >
                <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                  <path d="M5 12.55a11 11 0 0 1 14.08 0" />
                  <path d="M1.42 9a16 16 0 0 1 21.16 0" />
                  <path d="M8.53 16.11a6 6 0 0 1 6.95 0" />
                  <circle cx="12" cy="20" r="1" />
                </svg>
              </span>
            )}

            {/* BLE status indicator (ESP32 boards only) */}
            {activeBoard && isEsp32Kind(activeBoard.boardKind) && activeBoard.bleStatus && (
              <span
                className={`canvas-ble-badge canvas-ble-${activeBoard.bleStatus.status}`}
                title={
                  activeBoard.bleStatus.status === 'advertising'
                    ? 'BLE: Advertising'
                    : 'BLE: Initialized'
                }
              >
                <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                  <polyline points="6.5 6.5 17.5 17.5 12 23 12 1 17.5 6.5 6.5 17.5" />
                </svg>
              </span>
            )}

            {/* Oscilloscope toggle */}
            <button
              onClick={toggleOscilloscope}
              className={`canvas-serial-btn${oscilloscopeOpen ? ' canvas-serial-btn-active' : ''}`}
              title="Toggle Oscilloscope / Logic Analyzer"
            >
              <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                <polyline points="2 14 6 8 10 14 14 6 18 14 22 10" />
              </svg>
              Scope
            </button>
          </div>

          <div className="canvas-header-right">
            {/* Zoom controls */}
            <div className="zoom-controls">
              <button className="zoom-btn" onClick={() => handleWheel({ deltaY: 100, clientX: 0, clientY: 0, preventDefault: () => {} } as any)} title="Zoom out">
                <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><line x1="5" y1="12" x2="19" y2="12" /></svg>
              </button>
              <button className="zoom-level" onClick={handleResetView} title="Reset view (click to reset)">
                {Math.round(zoom * 100)}%
              </button>
              <button className="zoom-btn" onClick={() => handleWheel({ deltaY: -100, clientX: 0, clientY: 0, preventDefault: () => {} } as any)} title="Zoom in">
                <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" /></svg>
              </button>
            </div>

            {/* Component count */}
            <span className="component-count" title={`${components.length} component${components.length !== 1 ? 's' : ''}`}>
              <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                <rect x="2" y="7" width="20" height="14" rx="2" />
                <path d="M16 7V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2" />
              </svg>
              {components.length}
            </span>

            {/* Add Component */}
            <button
              className="add-component-btn"
              onClick={() => setShowComponentPicker(true)}
              title="Add Component"
              disabled={running}
            >
              <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
                <line x1="12" y1="5" x2="12" y2="19" />
                <line x1="5" y1="12" x2="19" y2="12" />
              </svg>
              Add
            </button>

          </div>
        </div>
        <div
          ref={canvasRef}
          className="canvas-content"
          onMouseDown={handleCanvasMouseDown}
          onMouseMove={handleCanvasMouseMove}
          onMouseUp={handleCanvasMouseUp}
          onMouseLeave={() => { isPanningRef.current = false; setPan({ ...panRef.current }); setDraggedComponentId(null); }}
          onContextMenu={(e) => {
            e.preventDefault();
            if (wireInProgress) cancelWireCreation();
          }}
          onClick={(e) => {
            if (wireInProgress) {
              const world = toWorld(e.clientX, e.clientY);
              addWireWaypoint(world.x, world.y);
              return;
            }
            // If a segment handle drag just finished, don't also select
            if (segmentDragJustCommittedRef.current) {
              segmentDragJustCommittedRef.current = false;
              return;
            }
            // Wire selection via canvas-level hit detection
            const world = toWorld(e.clientX, e.clientY);
            const threshold = 8 / zoomRef.current;
            const wire = findWireNearPoint(wiresRef.current, world.x, world.y, threshold);
            if (wire) {
              setSelectedWire(selectedWireId === wire.id ? null : wire.id);
            } else {
              setSelectedWire(null);
              setSelectedComponentId(null);
            }
          }}
          onDoubleClick={(e) => {
            if (wireInProgress) return;
            const world = toWorld(e.clientX, e.clientY);
            const threshold = 8 / zoomRef.current;
            const wire = findWireNearPoint(wiresRef.current, world.x, world.y, threshold);
            if (wire) {
              removeWire(wire.id);
            }
          }}
          style={{
            cursor: isPanningRef.current ? 'grabbing'
              : wireInProgress ? 'crosshair'
              : hoveredWireId ? 'pointer'
              : 'default',
          }}
        >
          {/* Sensor Control Panel — shown when a sensor component is clicked during simulation */}
          {sensorControlComponentId && sensorControlMetadataId && (() => {
            const meta = registry.getById(sensorControlMetadataId);
            return (
              <SensorControlPanel
                componentId={sensorControlComponentId}
                metadataId={sensorControlMetadataId}
                sensorName={meta?.name ?? sensorControlMetadataId}
                onClose={() => {
                  setSensorControlComponentId(null);
                  setSensorControlMetadataId(null);
                }}
              />
            );
          })()}

          {/* Infinite world — pan+zoom applied here */}
          <div
            className="canvas-world"
            style={{ transform: `translate(${pan.x}px, ${pan.y}px) scale(${zoom})` }}
          >
            {/* Wire Layer - Renders below all components */}
            <WireLayer
              hoveredWireId={hoveredWireId}
              segmentDragPreview={segmentDragPreview}
              segmentHandles={segmentHandles}
              onHandleMouseDown={handleHandleMouseDown}
              onHandleTouchStart={handleHandleTouchStart}
            />

            {/* All boards on canvas */}
            {boards.map((board) => (
              <BoardOnCanvas
                key={board.id}
                board={board}
                running={running}
                isActive={board.id === activeBoardId}
                led13={Boolean(components.find((c) => c.id === 'led-builtin')?.properties.state)}
                onMouseDown={(e) => {
                  setClickStartTime(Date.now());
                  setClickStartPos({ x: e.clientX, y: e.clientY });
                  const world = toWorld(e.clientX, e.clientY);
                  setDraggedComponentId(`__board__:${board.id}`);
                  setDragOffset({ x: world.x - board.x, y: world.y - board.y });
                }}
                onContextMenu={(e) => {
                  e.preventDefault();
                  e.stopPropagation();
                  setBoardContextMenu({ boardId: board.id, x: e.clientX, y: e.clientY });
                }}
                onPinClick={handlePinClick}
                zoom={zoom}
              />
            ))}

            {/* Components using wokwi-elements */}
            <div className="components-area">{registryLoaded && components.map(renderComponent)}</div>
          </div>

          {/* Wire creation mode banner — visible on both desktop and mobile */}
          {wireInProgress && (
            <div className="wire-mode-banner">
              <span>Tap a pin to connect — tap canvas for waypoints</span>
              <button onClick={() => cancelWireCreation()}>Cancel</button>
            </div>
          )}
        </div>
      </div>

      {/* Pin Selector Modal */}
      {showPinSelector && selectedComponentId && (
        <PinSelector
          componentId={selectedComponentId}
          componentType={
            components.find((c) => c.id === selectedComponentId)?.metadataId || 'unknown'
          }
          currentPin={
            components.find((c) => c.id === selectedComponentId)?.properties.pin as number | undefined
          }
          onPinSelect={handlePinSelect}
          onClose={() => setShowPinSelector(false)}
          position={pinSelectorPos}
        />
      )}

      {/* Component Property Dialog */}
      {showPropertyDialog && propertyDialogComponentId && (() => {
        const component = components.find((c) => c.id === propertyDialogComponentId);
        const metadata = component ? registry.getById(component.metadataId) : null;
        if (!component || !metadata) return null;

        const element = document.getElementById(propertyDialogComponentId);
        const pinInfo = element ? (element as any).pinInfo : [];

        return (
          <ComponentPropertyDialog
            componentId={propertyDialogComponentId}
            componentMetadata={metadata}
            componentProperties={component.properties}
            position={propertyDialogPosition}
            pinInfo={pinInfo || []}
            onClose={() => setShowPropertyDialog(false)}
            onRotate={handleRotateComponent}
            onDelete={(id) => {
              removeComponent(id);
              setShowPropertyDialog(false);
            }}
          />
        );
      })()}

      {/* Component Picker Modal */}
      <ComponentPickerModal
        isOpen={showComponentPicker}
        onClose={() => setShowComponentPicker(false)}
        onSelectComponent={handleSelectComponent}
        onSelectBoard={(kind: BoardKind) => {
          trackSelectBoard(kind);
          const sameKind = boards.filter((b) => b.boardKind === kind);
          const newBoardId = sameKind.length === 0 ? kind : `${kind}-${sameKind.length + 1}`;
          const x = boardPosition.x + boards.length * 60 + 420;
          const y = boardPosition.y + boards.length * 30;
          addBoard(kind, x, y);
          // file group is created inside addBoard
          void newBoardId;
        }}
      />

      {/* Board right-click context menu */}
      {boardContextMenu && (() => {
        const board = boards.find((b) => b.id === boardContextMenu.boardId);
        const label = board ? BOARD_KIND_LABELS[board.boardKind] : 'Board';
        const connectedWires = wires.filter(
          (w) => w.start.componentId === boardContextMenu.boardId || w.end.componentId === boardContextMenu.boardId
        ).length;
        return (
          <>
            <div
              style={{ position: 'fixed', inset: 0, zIndex: 9998 }}
              onClick={() => setBoardContextMenu(null)}
              onContextMenu={(e) => { e.preventDefault(); setBoardContextMenu(null); }}
            />
            <div
              style={{
                position: 'fixed',
                left: boardContextMenu.x,
                top: boardContextMenu.y,
                background: '#252526',
                border: '1px solid #3c3c3c',
                borderRadius: 6,
                padding: '4px 0',
                zIndex: 9999,
                minWidth: 180,
                boxShadow: '0 4px 16px rgba(0,0,0,0.4)',
                fontSize: 13,
              }}
            >
              <div style={{ padding: '6px 14px', color: '#888', fontSize: 11, borderBottom: '1px solid #3c3c3c', marginBottom: 2 }}>
                {label}
              </div>
              <button
                style={{
                  display: 'flex', alignItems: 'center', gap: 8,
                  width: '100%', padding: '7px 14px', background: 'none', border: 'none',
                  color: boards.length <= 1 ? '#555' : '#e06c75', cursor: boards.length <= 1 ? 'default' : 'pointer',
                  fontSize: 13, textAlign: 'left',
                }}
                disabled={boards.length <= 1}
                title={boards.length <= 1 ? 'Cannot remove the last board' : undefined}
                onMouseEnter={(e) => { if (boards.length > 1) (e.currentTarget.style.background = '#2a2d2e'); }}
                onMouseLeave={(e) => { e.currentTarget.style.background = 'none'; }}
                onClick={() => {
                  setBoardContextMenu(null);
                  setBoardToRemove(boardContextMenu.boardId);
                }}
              >
                <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                  <polyline points="3 6 5 6 21 6" />
                  <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
                </svg>
                Remove board
                {connectedWires > 0 && <span style={{ color: '#888', fontSize: 11 }}>({connectedWires} wire{connectedWires > 1 ? 's' : ''})</span>}
              </button>
            </div>
          </>
        );
      })()}

      {/* Board removal confirmation dialog */}
      {boardToRemove && (() => {
        const board = boards.find((b) => b.id === boardToRemove);
        const label = board ? BOARD_KIND_LABELS[board.boardKind] : 'Board';
        const connectedWires = wires.filter(
          (w) => w.start.componentId === boardToRemove || w.end.componentId === boardToRemove
        ).length;
        return (
          <div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', zIndex: 10000, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
            <div style={{ background: '#1e1e1e', border: '1px solid #3c3c3c', borderRadius: 8, padding: '20px 24px', maxWidth: 380, boxShadow: '0 8px 32px rgba(0,0,0,0.6)' }}>
              <h3 style={{ margin: '0 0 10px', color: '#e0e0e0', fontSize: 15 }}>Remove {label}?</h3>
              <p style={{ margin: '0 0 16px', color: '#999', fontSize: 13, lineHeight: 1.5 }}>
                This will remove the board from the workspace
                {connectedWires > 0 && <> and <strong style={{ color: '#e06c75' }}>{connectedWires} connected wire{connectedWires > 1 ? 's' : ''}</strong></>}
                . This action cannot be undone.
              </p>
              <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
                <button
                  onClick={() => setBoardToRemove(null)}
                  style={{ padding: '6px 16px', background: '#333', border: '1px solid #555', borderRadius: 4, color: '#ccc', cursor: 'pointer', fontSize: 13 }}
                >
                  Cancel
                </button>
                <button
                  onClick={() => { removeBoard(boardToRemove); setBoardToRemove(null); }}
                  style={{ padding: '6px 16px', background: '#e06c75', border: 'none', borderRadius: 4, color: '#fff', cursor: 'pointer', fontSize: 13 }}
                >
                  Remove
                </button>
              </div>
            </div>
          </div>
        );
      })()}

    </div>
  );
};
