import { useMemo, useState, useSyncExternalStore } from "react";
import type { KeyboardEvent as ReactKeyboardEvent, PointerEvent as ReactPointerEvent } from "react";
import { createSchemetaStore } from "../state/store.js";
import type { SchemetaInstance, SchemetaModel, SchemetaStore } from "../state/store.js";

const MIN_SCALE = 0.4;
const MAX_SCALE = 2.5;
const SCALE_STEP = 0.1;
const AUTO_COLUMNS = 4;
const AUTO_X_START = 60;
const AUTO_Y_START = 60;
const AUTO_X_GAP = 210;
const AUTO_Y_GAP = 150;
const NUDGE_STEP = 10;
const NUDGE_STEP_FAST = 20;

const BOOTSTRAP_MODEL: SchemetaModel = {
  symbols: {
    mcu: {
      body: { width: 180, height: 140 },
      pins: [
        { name: "3V3", number: "1", side: "left", offset: 28 },
        { name: "GND", number: "2", side: "left", offset: 56 },
        { name: "GPIO1", number: "6", side: "right", offset: 40 },
        { name: "GPIO2", number: "7", side: "right", offset: 72 }
      ]
    },
    sensor: {
      body: { width: 150, height: 110 },
      pins: [
        { name: "3V3", number: "1", side: "left", offset: 30 },
        { name: "GND", number: "2", side: "left", offset: 58 },
        { name: "SCL", number: "3", side: "right", offset: 40 },
        { name: "SDA", number: "4", side: "right", offset: 68 }
      ]
    },
    conn: {
      body: { width: 130, height: 90 },
      pins: [
        { name: "TX", number: "1", side: "right", offset: 30 },
        { name: "RX", number: "2", side: "right", offset: 56 }
      ]
    }
  },
  instances: [
    { ref: "U1", symbol: "mcu", properties: { value: "Control MCU" }, placement: { x: 120, y: 120, rotation: 0 } },
    { ref: "U2", symbol: "sensor", properties: { value: "IMU" }, placement: { x: 420, y: 130, rotation: 0 } },
    { ref: "U3", symbol: "sensor", properties: { value: "Temp Sensor" }, placement: { x: null, y: null, rotation: 0 } },
    { ref: "J1", symbol: "conn", properties: { value: "Debug Header" }, placement: { x: 180, y: 320, rotation: 0 } }
  ],
  nets: []
};

type SymbolPin = {
  name?: string;
  number?: string;
  side?: string;
  offset?: number;
};

type NodePlacement = {
  x: number;
  y: number;
  width: number;
  height: number;
  rotation: number;
  locked: boolean;
  leftPins: SymbolPin[];
  rightPins: SymbolPin[];
};

type CanvasPoint = { x: number; y: number };

type CanvasWireSegment = {
  a: CanvasPoint;
  b: CanvasPoint;
};

type CanvasWire = {
  id: string;
  label: string;
  netClass: string | null;
  segments: CanvasWireSegment[];
  labelPoint: CanvasPoint | null;
  highlight: boolean;
  source: "routed" | "fallback";
};

type DragState = {
  refs: string[];
  pointerId: number;
  startClientX: number;
  startClientY: number;
  starts: Map<string, { x: number; y: number }>;
};

type BoxSelectState = {
  pointerId: number;
  start: CanvasPoint;
  current: CanvasPoint;
};

const fallbackStore = createSchemetaStore({
  model: BOOTSTRAP_MODEL,
  viewport: { scale: 1, panX: 30, panY: 30 },
  selection: { selectedRefs: [], selectedNet: null, selectedPin: null }
});

function useStoreState(activeStore: SchemetaStore): ReturnType<SchemetaStore["getState"]> {
  return useSyncExternalStore(
    (listener: () => void) => activeStore.subscribe(() => listener()),
    () => activeStore.getState(),
    () => activeStore.getState()
  );
}

function toFinite(value: unknown, fallback: number) {
  return typeof value === "number" && Number.isFinite(value) ? value : fallback;
}

function toDimension(value: unknown, fallback: number, min = 80, max = 320) {
  const parsed = toFinite(value, fallback);
  return Math.max(min, Math.min(max, Math.round(parsed)));
}

function asRecord(value: unknown): Record<string, unknown> | null {
  if (typeof value !== "object" || value === null) {
    return null;
  }
  return value as Record<string, unknown>;
}

function asPoint(value: unknown): CanvasPoint | null {
  if (Array.isArray(value) && value.length >= 2) {
    const x = Number(value[0]);
    const y = Number(value[1]);
    if (Number.isFinite(x) && Number.isFinite(y)) {
      return { x, y };
    }
  }

  const record = asRecord(value);
  if (!record) {
    return null;
  }

  const x = Number(record.x);
  const y = Number(record.y);
  if (Number.isFinite(x) && Number.isFinite(y)) {
    return { x, y };
  }

  return null;
}

function rotatePoint(point: CanvasPoint, center: CanvasPoint, rotationDeg: number): CanvasPoint {
  if (!Number.isFinite(rotationDeg) || rotationDeg === 0) {
    return point;
  }

  const radians = (rotationDeg * Math.PI) / 180;
  const cos = Math.cos(radians);
  const sin = Math.sin(radians);
  const dx = point.x - center.x;
  const dy = point.y - center.y;
  return {
    x: center.x + dx * cos - dy * sin,
    y: center.y + dx * sin + dy * cos
  };
}

function pointsToSegments(points: CanvasPoint[]): CanvasWireSegment[] {
  const segments: CanvasWireSegment[] = [];
  for (let index = 1; index < points.length; index += 1) {
    const a = points[index - 1];
    const b = points[index];
    if (a.x === b.x && a.y === b.y) {
      continue;
    }
    segments.push({ a, b });
  }
  return segments;
}

function asSegment(value: unknown): CanvasWireSegment | null {
  const record = asRecord(value);
  if (!record) {
    return null;
  }

  const a = asPoint(record.a ?? record.start ?? record.from);
  const b = asPoint(record.b ?? record.end ?? record.to);
  if (a && b) {
    return { a, b };
  }

  const x1 = Number(record.x1);
  const y1 = Number(record.y1);
  const x2 = Number(record.x2);
  const y2 = Number(record.y2);
  if (Number.isFinite(x1) && Number.isFinite(y1) && Number.isFinite(x2) && Number.isFinite(y2)) {
    return { a: { x: x1, y: y1 }, b: { x: x2, y: y2 } };
  }

  return null;
}

function getSegmentsFromUnknown(value: unknown): CanvasWireSegment[] {
  if (!Array.isArray(value)) {
    return [];
  }

  const points = value.map((entry) => asPoint(entry)).filter((entry): entry is CanvasPoint => entry !== null);
  if (points.length >= 2 && points.length === value.length) {
    return pointsToSegments(points);
  }

  const segments: CanvasWireSegment[] = [];
  value.forEach((entry) => {
    const segment = asSegment(entry);
    if (segment) {
      segments.push(segment);
      return;
    }

    const nested = asRecord(entry);
    if (!nested) {
      return;
    }

    const nestedSegments = [
      ...getSegmentsFromUnknown(nested.segments),
      ...getSegmentsFromUnknown(nested.path),
      ...getSegmentsFromUnknown(nested.polyline),
      ...getSegmentsFromUnknown(nested.points)
    ];
    segments.push(...nestedSegments);
  });

  return segments;
}

function segmentLengthSquared(segment: CanvasWireSegment): number {
  const dx = segment.b.x - segment.a.x;
  const dy = segment.b.y - segment.a.y;
  return dx * dx + dy * dy;
}

function segmentMidpoint(segment: CanvasWireSegment): CanvasPoint {
  return {
    x: (segment.a.x + segment.b.x) / 2,
    y: (segment.a.y + segment.b.y) / 2
  };
}

function pickLabelPoint(segments: CanvasWireSegment[], preferred: CanvasPoint | null): CanvasPoint | null {
  if (preferred) {
    return preferred;
  }
  if (segments.length === 0) {
    return null;
  }

  let longest = segments[0];
  let bestLength = segmentLengthSquared(segments[0]);
  for (let index = 1; index < segments.length; index += 1) {
    const candidateLength = segmentLengthSquared(segments[index]);
    if (candidateLength > bestLength) {
      bestLength = candidateLength;
      longest = segments[index];
    }
  }

  return segmentMidpoint(longest);
}

function getSymbolNode(symbols: SchemetaModel["symbols"], instance: SchemetaInstance) {
  if (!instance.symbol || !symbols || typeof symbols !== "object") {
    return null;
  }
  const candidate = symbols[instance.symbol];
  return candidate && typeof candidate === "object" ? candidate : null;
}

function getNodePlacement(instance: SchemetaInstance, index: number, symbols: SchemetaModel["symbols"]): NodePlacement {
  const col = index % AUTO_COLUMNS;
  const row = Math.floor(index / AUTO_COLUMNS);
  const symbol = getSymbolNode(symbols, instance) as
    | { body?: { width?: number; height?: number }; pins?: SymbolPin[] }
    | null;
  const width = toDimension(symbol?.body?.width, 150);
  const height = toDimension(symbol?.body?.height, 100);
  const x = toFinite(instance.placement?.x, AUTO_X_START + col * AUTO_X_GAP);
  const y = toFinite(instance.placement?.y, AUTO_Y_START + row * AUTO_Y_GAP);
  const rotation = toFinite(instance.placement?.rotation, 0);
  const locked = Boolean(instance.placement?.locked);
  const pins = Array.isArray(symbol?.pins) ? symbol.pins : [];
  const leftPins = pins.filter((pin) => pin?.side === "left").slice(0, 6);
  const rightPins = pins.filter((pin) => pin?.side === "right").slice(0, 6);

  return { x, y, width, height, rotation, locked, leftPins, rightPins };
}

function toNetIdentity(netRecord: Record<string, unknown>, index: number) {
  const named = typeof netRecord.name === "string" && netRecord.name.trim().length > 0 ? netRecord.name.trim() : null;
  return {
    id: named ?? `net-${index + 1}`,
    label: named ?? `Net ${index + 1}`
  };
}

function getPinAnchor(
  placement: NodePlacement,
  instance: SchemetaInstance,
  symbols: SchemetaModel["symbols"],
  pinName: string | null
): CanvasPoint {
  const symbol = getSymbolNode(symbols, instance) as { pins?: SymbolPin[] } | null;
  const pins = Array.isArray(symbol?.pins) ? symbol.pins : [];
  const normalizedPin = pinName?.trim() ?? "";
  const pin = pins.find((candidate) => {
    const candidateName = typeof candidate?.name === "string" ? candidate.name : "";
    const candidateNumber = typeof candidate?.number === "string" ? candidate.number : "";
    return normalizedPin.length > 0 && (candidateName === normalizedPin || candidateNumber === normalizedPin);
  });

  const offset = Math.max(0, Math.min(placement.height, toFinite(pin?.offset, placement.height * 0.5)));
  let anchor: CanvasPoint;
  switch (pin?.side) {
    case "left":
      anchor = { x: placement.x, y: placement.y + offset };
      break;
    case "top":
      anchor = { x: placement.x + placement.width * 0.5, y: placement.y };
      break;
    case "bottom":
      anchor = { x: placement.x + placement.width * 0.5, y: placement.y + placement.height };
      break;
    case "right":
    default:
      anchor = { x: placement.x + placement.width, y: placement.y + offset };
      break;
  }

  const center = { x: placement.x + placement.width / 2, y: placement.y + placement.height / 2 };
  return rotatePoint(anchor, center, placement.rotation);
}

function extractRoutedWires(compileResult: unknown, selectedNet: string | null): CanvasWire[] {
  const compile = asRecord(compileResult);
  if (!compile) {
    return [];
  }

  const layout = asRecord(compile.layout);
  const topology = asRecord(compile.topology);
  const candidates: unknown[] = [layout?.routed, layout?.routes, compile.routed, compile.routes, topology?.routed];

  for (const candidate of candidates) {
    if (!Array.isArray(candidate) || candidate.length === 0) {
      continue;
    }

    const parsed: CanvasWire[] = [];
    candidate.forEach((entry, index) => {
      const route = asRecord(entry);
      if (!route) {
        return;
      }

      const netRecord = asRecord(route.net);
      const netNameRaw =
        (typeof netRecord?.name === "string" && netRecord.name.trim().length > 0 ? netRecord.name.trim() : null) ??
        (typeof route.net_name === "string" && route.net_name.trim().length > 0 ? route.net_name.trim() : null) ??
        (typeof route.net === "string" && route.net.trim().length > 0 ? route.net.trim() : null) ??
        (typeof route.name === "string" && route.name.trim().length > 0 ? route.name.trim() : null);

      const id = netNameRaw ?? `net-${index + 1}`;
      const label = netNameRaw ?? `Net ${index + 1}`;
      const netClass =
        (typeof netRecord?.class === "string" && netRecord.class.trim().length > 0 ? netRecord.class.trim() : null) ??
        (typeof route.class === "string" && route.class.trim().length > 0 ? route.class.trim() : null);

      const segments = [
        ...getSegmentsFromUnknown(route.routes),
        ...getSegmentsFromUnknown(route.segments),
        ...getSegmentsFromUnknown(route.path),
        ...getSegmentsFromUnknown(route.polyline),
        ...getSegmentsFromUnknown(route.points)
      ];
      if (segments.length === 0) {
        return;
      }

      const preferredPoint =
        asPoint(Array.isArray(route.labelPoints) ? route.labelPoints[0] : null) ??
        asPoint(Array.isArray(route.tiePoints) ? route.tiePoints[0] : null) ??
        asPoint(Array.isArray(route.junctionPoints) ? route.junctionPoints[0] : null);

      parsed.push({
        id,
        label,
        netClass,
        segments,
        labelPoint: pickLabelPoint(segments, preferredPoint),
        highlight: selectedNet !== null && selectedNet === id,
        source: "routed"
      });
    });

    if (parsed.length > 0) {
      return parsed;
    }
  }

  return [];
}

function buildFallbackWires(
  nets: Record<string, unknown>[],
  placements: Map<string, NodePlacement>,
  instancesByRef: Map<string, SchemetaInstance>,
  symbols: SchemetaModel["symbols"],
  selectedNet: string | null
): CanvasWire[] {
  const wires: CanvasWire[] = [];

  nets.forEach((net, index) => {
    const { id, label } = toNetIdentity(net, index);
    const netClass = typeof net.class === "string" && net.class.trim().length > 0 ? net.class.trim() : null;
    const nodesRaw = Array.isArray(net.nodes) ? net.nodes : [];

    const points = nodesRaw
      .map((node) => {
        const endpoint = asRecord(node);
        if (!endpoint || typeof endpoint.ref !== "string") {
          return null;
        }

        const placement = placements.get(endpoint.ref);
        const instance = instancesByRef.get(endpoint.ref);
        if (!placement || !instance) {
          return null;
        }

        const pinName = typeof endpoint.pin === "string" && endpoint.pin.trim().length > 0 ? endpoint.pin : null;
        return getPinAnchor(placement, instance, symbols, pinName);
      })
      .filter((value): value is CanvasPoint => value !== null);

    if (points.length < 2) {
      return;
    }

    const root = points[0];
    const segments = points.slice(1).map((point) => ({ a: root, b: point }));
    if (segments.length === 0) {
      return;
    }

    wires.push({
      id,
      label,
      netClass,
      segments,
      labelPoint: pickLabelPoint(segments, null),
      highlight: selectedNet !== null && selectedNet === id,
      source: "fallback"
    });
  });

  return wires;
}

function formatZoom(scale: number) {
  return `${Math.round(scale * 100)}%`;
}

type CanvasAreaProps = {
  store?: SchemetaStore;
};

function shouldIgnoreKeyboardTarget(target: EventTarget | null): boolean {
  if (!(target instanceof HTMLElement)) {
    return false;
  }
  const tag = target.tagName.toLowerCase();
  return tag === "input" || tag === "textarea" || target.isContentEditable;
}

function toggleRefSelection(selectedRefs: string[], ref: string): string[] {
  const set = new Set(selectedRefs);
  if (set.has(ref)) {
    set.delete(ref);
  } else {
    set.add(ref);
  }
  return Array.from(set).sort((a, b) => a.localeCompare(b));
}

function normalizeRotation(value: unknown): number {
  const numeric = Number(value);
  if (!Number.isFinite(numeric)) {
    return 0;
  }
  const normalized = ((Math.round(numeric / 90) * 90) % 360 + 360) % 360;
  return normalized;
}

function removeRefsFromModel(model: SchemetaModel, selectedRefs: string[]): SchemetaModel {
  if (!Array.isArray(model.instances) || selectedRefs.length === 0) {
    return model;
  }

  const refSet = new Set(selectedRefs);
  const instances = model.instances.filter((instance) => !refSet.has(instance.ref));

  const nets = Array.isArray(model.nets)
    ? model.nets
        .map((net) => {
          if (!Array.isArray(net.nodes)) {
            return net;
          }
          const nodes = net.nodes.filter((node) => {
            const endpoint = asRecord(node);
            return !(endpoint && typeof endpoint.ref === "string" && refSet.has(endpoint.ref));
          });
          return {
            ...net,
            nodes
          };
        })
        .filter((net) => Array.isArray(net.nodes) && net.nodes.length > 1)
    : model.nets;

  return {
    ...model,
    instances,
    nets
  };
}

export function CanvasArea({ store: providedStore }: CanvasAreaProps) {
  const store = providedStore ?? fallbackStore;
  const state = useStoreState(store);
  const [drag, setDrag] = useState<DragState | null>(null);
  const [boxSelect, setBoxSelect] = useState<BoxSelectState | null>(null);

  const instances = state.model?.instances ?? [];
  const symbols = state.model?.symbols ?? {};
  const selectedRefs = state.selection.selectedRefs;
  const selectedSet = useMemo(() => new Set(selectedRefs), [selectedRefs]);
  const selectedNet = state.selection.selectedNet;

  const placements = useMemo(() => {
    const byRef = new Map<string, NodePlacement>();
    instances.forEach((instance, index) => {
      byRef.set(instance.ref, getNodePlacement(instance, index, symbols));
    });
    return byRef;
  }, [instances, symbols]);

  const instancesByRef = useMemo(() => {
    return new Map(instances.map((instance) => [instance.ref, instance]));
  }, [instances]);

  const modelNets = useMemo(
    () =>
      (state.model?.nets ?? []).filter((item): item is Record<string, unknown> => typeof item === "object" && item !== null),
    [state.model?.nets]
  );

  const routedWires = useMemo(() => extractRoutedWires(state.compileResult, selectedNet), [state.compileResult, selectedNet]);

  const fallbackWires = useMemo(
    () => buildFallbackWires(modelNets, placements, instancesByRef, symbols, selectedNet),
    [instancesByRef, modelNets, placements, selectedNet, symbols]
  );

  const hasRoutedWires = routedWires.length > 0;
  const visibleWires = hasRoutedWires ? routedWires : fallbackWires;
  const hasSelectedNet = selectedNet !== null;

  function clearSelection() {
    store.actions.setSelection({
      selectedRefs: [],
      selectedNet: null,
      selectedPin: null
    });
    store.actions.setUiFlags({ isolateComponent: false, isolateNet: false });
  }

  function updateScale(nextScale: number) {
    const clamped = Math.max(MIN_SCALE, Math.min(MAX_SCALE, nextScale));
    store.actions.setViewport({ scale: clamped });
  }

  function eventToCanvasPoint(event: ReactPointerEvent<HTMLElement>): CanvasPoint {
    const surface = event.currentTarget;
    const rect = surface.getBoundingClientRect();
    const localX = event.clientX - rect.left + surface.scrollLeft;
    const localY = event.clientY - rect.top + surface.scrollTop;

    return {
      x: (localX - state.viewport.panX) / state.viewport.scale,
      y: (localY - state.viewport.panY) / state.viewport.scale
    };
  }

  function beginDrag(refs: string[], event: ReactPointerEvent<HTMLDivElement>) {
    const starts = new Map<string, { x: number; y: number }>();
    refs.forEach((ref) => {
      const placement = placements.get(ref);
      if (!placement || placement.locked) {
        return;
      }
      starts.set(ref, { x: placement.x, y: placement.y });
    });

    if (starts.size === 0) {
      return;
    }

    store.actions.beginTransaction(starts.size > 1 ? "drag components" : "drag component");

    event.currentTarget.setPointerCapture(event.pointerId);
    setDrag({
      refs: Array.from(starts.keys()),
      pointerId: event.pointerId,
      startClientX: event.clientX,
      startClientY: event.clientY,
      starts
    });
  }

  function handleCanvasPointerDown(event: ReactPointerEvent<HTMLElement>) {
    if (event.button !== 0 || event.target !== event.currentTarget) {
      return;
    }

    event.currentTarget.focus();

    const additive = event.shiftKey || event.metaKey || event.ctrlKey;
    if (!additive) {
      clearSelection();
    }

    const point = eventToCanvasPoint(event);
    event.currentTarget.setPointerCapture(event.pointerId);
    setBoxSelect({
      pointerId: event.pointerId,
      start: point,
      current: point
    });
  }

  function handleCanvasPointerMove(event: ReactPointerEvent<HTMLElement>) {
    if (!boxSelect || boxSelect.pointerId !== event.pointerId) {
      return;
    }

    event.preventDefault();
    const point = eventToCanvasPoint(event);
    setBoxSelect((prev) => {
      if (!prev) {
        return null;
      }
      return {
        ...prev,
        current: point
      };
    });
  }

  function handleCanvasPointerUpOrCancel(event: ReactPointerEvent<HTMLElement>) {
    if (!boxSelect || boxSelect.pointerId !== event.pointerId) {
      return;
    }

    if (event.currentTarget.hasPointerCapture(event.pointerId)) {
      event.currentTarget.releasePointerCapture(event.pointerId);
    }

    const additive = event.shiftKey || event.metaKey || event.ctrlKey;
    const minX = Math.min(boxSelect.start.x, boxSelect.current.x);
    const minY = Math.min(boxSelect.start.y, boxSelect.current.y);
    const maxX = Math.max(boxSelect.start.x, boxSelect.current.x);
    const maxY = Math.max(boxSelect.start.y, boxSelect.current.y);
    const width = maxX - minX;
    const height = maxY - minY;

    const nextSelected =
      width < 4 && height < 4
        ? []
        : instances
            .map((instance, index) => ({ ref: instance.ref, placement: placements.get(instance.ref) ?? getNodePlacement(instance, index, symbols) }))
            .filter((entry) => {
              const nodeMinX = entry.placement.x;
              const nodeMinY = entry.placement.y;
              const nodeMaxX = entry.placement.x + entry.placement.width;
              const nodeMaxY = entry.placement.y + entry.placement.height;
              return !(nodeMaxX < minX || nodeMinX > maxX || nodeMaxY < minY || nodeMinY > maxY);
            })
            .map((entry) => entry.ref)
            .sort((a, b) => a.localeCompare(b));

    if (nextSelected.length > 0 || !additive) {
      const merged = additive ? Array.from(new Set([...state.selection.selectedRefs, ...nextSelected])) : nextSelected;
      store.actions.setSelection({
        selectedRefs: merged.sort((a, b) => a.localeCompare(b)),
        selectedNet: null,
        selectedPin: null
      });
      if (merged.length > 0) {
        store.actions.setUiFlags({ isolateComponent: false, isolateNet: false });
      }
    }

    setBoxSelect(null);
  }

  function handleNodePointerDown(event: ReactPointerEvent<HTMLDivElement>, ref: string) {
    if (event.button !== 0) {
      return;
    }

    event.stopPropagation();
    event.currentTarget.focus();

    const additive = event.shiftKey || event.metaKey || event.ctrlKey;
    if (additive) {
      store.actions.setSelection({
        selectedRefs: toggleRefSelection(state.selection.selectedRefs, ref),
        selectedNet: null,
        selectedPin: null
      });
      store.actions.setUiFlags({ isolateComponent: false, isolateNet: false });
      return;
    }

    const isAlreadySelected = selectedSet.has(ref);
    const dragCandidates = isAlreadySelected && selectedRefs.length > 1 ? selectedRefs : [ref];

    if (!isAlreadySelected || selectedRefs.length !== dragCandidates.length) {
      store.actions.setSelection({
        selectedRefs: dragCandidates,
        selectedNet: null,
        selectedPin: null
      });
      store.actions.setUiFlags({ isolateComponent: false, isolateNet: false });
    }

    beginDrag(dragCandidates, event);
  }

  function handleNodePointerMove(event: ReactPointerEvent<HTMLDivElement>) {
    if (!drag || drag.pointerId !== event.pointerId) {
      return;
    }

    event.preventDefault();
    const dx = (event.clientX - drag.startClientX) / state.viewport.scale;
    const dy = (event.clientY - drag.startClientY) / state.viewport.scale;

    drag.refs.forEach((ref) => {
      const origin = drag.starts.get(ref);
      if (!origin) {
        return;
      }
      store.actions.moveComponent(ref, { x: origin.x + dx, y: origin.y + dy });
    });
  }

  function handleNodePointerUpOrCancel(event: ReactPointerEvent<HTMLDivElement>) {
    if (!drag || drag.pointerId !== event.pointerId) {
      return;
    }

    if (event.currentTarget.hasPointerCapture(event.pointerId)) {
      event.currentTarget.releasePointerCapture(event.pointerId);
    }

    setDrag(null);
    store.actions.commitTransaction();
  }

  function rotateSelected() {
    if (selectedRefs.length === 0) {
      return;
    }

    const refs = selectedRefs.filter((ref) => {
      const placement = placements.get(ref);
      return Boolean(placement && !placement.locked);
    });

    if (refs.length === 0) {
      return;
    }

    store.actions.beginTransaction(refs.length > 1 ? "rotate components" : "rotate component");
    refs.forEach((ref) => {
      const instance = instancesByRef.get(ref);
      const current = normalizeRotation(instance?.placement?.rotation ?? 0);
      store.actions.moveComponent(ref, { rotation: normalizeRotation(current + 90) });
    });
    store.actions.commitTransaction();
  }

  function nudgeSelected(dx: number, dy: number) {
    if (selectedRefs.length === 0) {
      return;
    }

    const refs = selectedRefs.filter((ref) => {
      const placement = placements.get(ref);
      return Boolean(placement && !placement.locked);
    });

    if (refs.length === 0) {
      return;
    }

    store.actions.beginTransaction(refs.length > 1 ? "nudge components" : "nudge component");
    refs.forEach((ref) => {
      const placement = placements.get(ref);
      if (!placement) {
        return;
      }
      store.actions.moveComponent(ref, { x: placement.x + dx, y: placement.y + dy });
    });
    store.actions.commitTransaction();
  }

  function deleteSelected() {
    if (!state.model || selectedRefs.length === 0) {
      return;
    }

    const nextModel = removeRefsFromModel(state.model, selectedRefs);
    store.actions.beginTransaction(selectedRefs.length > 1 ? "delete components" : "delete component");
    store.actions.setModel(nextModel);
    clearSelection();
    store.actions.commitTransaction();
  }

  function handleCanvasKeyDown(event: ReactKeyboardEvent<HTMLElement>) {
    if (shouldIgnoreKeyboardTarget(event.target)) {
      return;
    }

    const hasCommand = event.ctrlKey || event.metaKey;
    if (hasCommand && event.key.toLowerCase() === "a") {
      event.preventDefault();
      store.actions.setSelection({
        selectedRefs: instances.map((instance) => instance.ref).sort((a, b) => a.localeCompare(b)),
        selectedNet: null,
        selectedPin: null
      });
      store.actions.setUiFlags({ isolateComponent: false, isolateNet: false });
      return;
    }

    if (event.key === "Escape") {
      event.preventDefault();
      clearSelection();
      return;
    }

    if (event.key === "Delete" || event.key === "Backspace") {
      event.preventDefault();
      deleteSelected();
      return;
    }

    if (event.code === "Space") {
      event.preventDefault();
      rotateSelected();
      return;
    }

    const step = event.shiftKey ? NUDGE_STEP_FAST : NUDGE_STEP;
    switch (event.key) {
      case "ArrowUp":
        event.preventDefault();
        nudgeSelected(0, -step);
        break;
      case "ArrowDown":
        event.preventDefault();
        nudgeSelected(0, step);
        break;
      case "ArrowLeft":
        event.preventDefault();
        nudgeSelected(-step, 0);
        break;
      case "ArrowRight":
        event.preventDefault();
        nudgeSelected(step, 0);
        break;
      default:
        break;
    }
  }

  function selectNet(netId: string | null) {
    store.actions.setSelection({
      selectedRefs: [],
      selectedNet: netId,
      selectedPin: null
    });
    store.actions.setUiFlags({ isolateComponent: false, isolateNet: false });
  }

  const selectionBoxStyle = useMemo(() => {
    if (!boxSelect) {
      return null;
    }

    const left = Math.min(boxSelect.start.x, boxSelect.current.x);
    const top = Math.min(boxSelect.start.y, boxSelect.current.y);
    const width = Math.max(1, Math.abs(boxSelect.current.x - boxSelect.start.x));
    const height = Math.max(1, Math.abs(boxSelect.current.y - boxSelect.start.y));
    return {
      left: `${left}px`,
      top: `${top}px`,
      width: `${width}px`,
      height: `${height}px`
    };
  }, [boxSelect]);

  return (
    <main className="canvas" aria-label="Canvas area">
      <div className="canvas__toolbar" role="toolbar" aria-label="Canvas controls">
        <button
          type="button"
          onClick={() => updateScale(state.viewport.scale - SCALE_STEP)}
          disabled={state.viewport.scale <= MIN_SCALE}
        >
          Zoom Out
        </button>
        <div className="canvas__zoom-readout">Zoom {formatZoom(state.viewport.scale)}</div>
        <button
          type="button"
          onClick={() => updateScale(state.viewport.scale + SCALE_STEP)}
          disabled={state.viewport.scale >= MAX_SCALE}
        >
          Zoom In
        </button>
        <button type="button" className="canvas__placeholder-control" disabled title="Fit viewport is not implemented yet">
          Fit (Soon)
        </button>
        <button
          type="button"
          className="canvas__placeholder-control"
          disabled
          title="Focus selection is not implemented yet"
        >
          Focus (Soon)
        </button>
      </div>

      <section
        className="canvas__surface"
        onPointerDown={handleCanvasPointerDown}
        onPointerMove={handleCanvasPointerMove}
        onPointerUp={handleCanvasPointerUpOrCancel}
        onPointerCancel={handleCanvasPointerUpOrCancel}
        onKeyDown={handleCanvasKeyDown}
        tabIndex={0}
      >
        <div
          className="canvas__viewport"
          style={{
            transform: `translate(${state.viewport.panX}px, ${state.viewport.panY}px) scale(${state.viewport.scale})`
          }}
        >
          <svg className="canvas-wires" aria-hidden="true" viewBox="0 0 2200 1500" preserveAspectRatio="none">
            {visibleWires.map((wire) =>
              wire.segments.map((segment, segmentIndex) => {
                const isDimmed = hasSelectedNet && !wire.highlight;
                return (
                  <line
                    key={`${wire.id}-${segmentIndex}-${segment.a.x}-${segment.a.y}-${segment.b.x}-${segment.b.y}`}
                    x1={segment.a.x}
                    y1={segment.a.y}
                    x2={segment.b.x}
                    y2={segment.b.y}
                    className={`canvas-wire-segment ${wire.highlight ? "is-highlight" : ""} ${isDimmed ? "is-dimmed" : ""}`}
                    data-net-id={wire.id}
                    data-wire-source={wire.source}
                    data-net-class={wire.netClass ?? undefined}
                    onPointerDown={(event) => {
                      event.stopPropagation();
                      selectNet(wire.id);
                    }}
                  />
                );
              })
            )}
          </svg>

          {state.uiFlags.showLabels
            ? visibleWires.map((wire) => {
                if (!wire.labelPoint) {
                  return null;
                }
                const isDimmed = hasSelectedNet && !wire.highlight;
                return (
                  <button
                    type="button"
                    key={`label-${wire.id}`}
                    className={`canvas-wire-label ${wire.highlight ? "is-highlight" : ""} ${isDimmed ? "is-dimmed" : ""}`}
                    style={{ left: `${wire.labelPoint.x}px`, top: `${wire.labelPoint.y}px` }}
                    data-net-id={wire.id}
                    onPointerDown={(event) => {
                      event.stopPropagation();
                      selectNet(wire.id);
                    }}
                  >
                    {wire.label}
                  </button>
                );
              })
            : null}

          {selectionBoxStyle ? <div className="canvas__selection-box" style={selectionBoxStyle} /> : null}

          {instances.map((instance, index) => {
            const placement = placements.get(instance.ref) ?? getNodePlacement(instance, index, symbols);
            const isSelected = selectedSet.has(instance.ref);
            const valueLabel =
              typeof instance.properties?.value === "string" && instance.properties.value.length > 0
                ? instance.properties.value
                : instance.symbol ?? instance.part ?? "Component";

            return (
              <div
                key={instance.ref}
                className={`canvas-node ${isSelected ? "is-selected" : ""} ${drag?.refs.includes(instance.ref) ? "is-dragging" : ""}`}
                style={{ left: `${placement.x}px`, top: `${placement.y}px`, width: `${placement.width}px`, minHeight: `${placement.height}px` }}
                onPointerDown={(event) => handleNodePointerDown(event, instance.ref)}
                onPointerMove={handleNodePointerMove}
                onPointerUp={handleNodePointerUpOrCancel}
                onPointerCancel={handleNodePointerUpOrCancel}
                tabIndex={-1}
              >
                <div className="canvas-node__chrome" style={{ transform: `rotate(${placement.rotation}deg)` }}>
                  <div className="canvas-node__content" style={{ transform: `rotate(${-placement.rotation}deg)` }}>
                    <div className="canvas-node__ref">{instance.ref}</div>
                    <div className="canvas-node__value">{valueLabel}</div>
                    <div className="canvas-node__pins">
                      <div className="canvas-node__pin-col">
                        {placement.leftPins.map((pin) => (
                          <div key={`${instance.ref}-left-${pin.number ?? pin.name}`} className="canvas-node__pin">
                            <span className="canvas-node__pin-number">{pin.number ?? "-"}</span>
                            <span className="canvas-node__pin-name">{pin.name ?? "PIN"}</span>
                          </div>
                        ))}
                      </div>
                      <div className="canvas-node__pin-col canvas-node__pin-col--right">
                        {placement.rightPins.map((pin) => (
                          <div key={`${instance.ref}-right-${pin.number ?? pin.name}`} className="canvas-node__pin">
                            <span className="canvas-node__pin-name">{pin.name ?? "PIN"}</span>
                            <span className="canvas-node__pin-number">{pin.number ?? "-"}</span>
                          </div>
                        ))}
                      </div>
                    </div>
                    {placement.locked ? <div className="canvas-node__lock">Locked</div> : null}
                  </div>
                </div>
              </div>
            );
          })}
        </div>
      </section>
    </main>
  );
}
