import type { AABB, EpcWaypoint } from '../types.js';
import { SPACING_EXEMPT, isCurvedType, isSpurType, isEpcType, getCurveBandWidth, EPC_LEFT_BOX, EPC_RIGHT_BOX, EPC_LINE_WIDTH, EPC_DEFAULT_WAYPOINTS } from '../symbols.js';
import { layout } from '../stores/layout.svelte.js';
import { orientedBoxCorners, createCurveTransforms } from './geometry.js';

export function getAABB(x: number, y: number, w: number, h: number, rotation: number): AABB {
  if (!rotation) return { x, y, w, h };
  const cx = x + w / 2;
  const cy = y + h / 2;
  const rad = (rotation * Math.PI) / 180;
  const cos = Math.abs(Math.cos(rad));
  const sin = Math.abs(Math.sin(rad));
  const bw = w * cos + h * sin;
  const bh = w * sin + h * cos;
  return { x: cx - bw / 2, y: cy - bh / 2, w: bw, h: bh };
}

// ─── OBB collision via Separating Axis Theorem ───

function obbOverlapWithSpacing(
  ax: number, ay: number, aw: number, ah: number, arot: number,
  bx: number, by: number, bw: number, bh: number, brot: number,
  spacing: number
): boolean {
  const acx = ax + aw / 2, acy = ay + ah / 2;
  const bcx = bx + bw / 2, bcy = by + bh / 2;
  const ahw = aw / 2 + spacing, ahh = ah / 2 + spacing;
  const bhw = bw / 2, bhh = bh / 2;

  const arad = (arot || 0) * Math.PI / 180;
  const brad = (brot || 0) * Math.PI / 180;
  const cosA = Math.cos(arad), sinA = Math.sin(arad);
  const cosB = Math.cos(brad), sinB = Math.sin(brad);
  const tx = bcx - acx, ty = bcy - acy;

  const axes: [number, number][] = [
    [cosA, sinA], [-sinA, cosA],
    [cosB, sinB], [-sinB, cosB],
  ];

  for (const [nx, ny] of axes) {
    const projA = ahw * Math.abs(cosA * nx + sinA * ny) + ahh * Math.abs(-sinA * nx + cosA * ny);
    const projB = bhw * Math.abs(cosB * nx + sinB * ny) + bhh * Math.abs(-sinB * nx + cosB * ny);
    const dist = Math.abs(tx * nx + ty * ny);
    if (dist > projA + projB) return false;
  }
  return true;
}

// ─── Convex polygon helpers ───

/** Get the 4 vertices of a spur trapezoid in world space (with rotation) */
function getSpurVertices(
  x: number, y: number, w: number, h: number, w2: number, rotation: number
): [number, number][] {
  const cx = x + w / 2;
  const cy = y + h / 2;
  const local: [number, number][] = [
    [x, y],           // TL
    [x + w2, y],      // TR (top base end)
    [x + w, y + h],   // BR (bottom base end)
    [x, y + h],       // BL
  ];
  if (!rotation) return local;
  const rad = rotation * Math.PI / 180;
  const cos = Math.cos(rad), sin = Math.sin(rad);
  return local.map(([px, py]) => {
    const dx = px - cx, dy = py - cy;
    return [cx + dx * cos - dy * sin, cy + dx * sin + dy * cos] as [number, number];
  });
}

/** Get the 4 vertices of an OBB in world space */
function getOBBVertices(
  x: number, y: number, w: number, h: number, rotation: number
): [number, number][] {
  const cx = x + w / 2, cy = y + h / 2;
  const rad = (rotation || 0) * Math.PI / 180;
  const cos = Math.cos(rad), sin = Math.sin(rad);
  const hw = w / 2, hh = h / 2;
  return [
    [cx - hw * cos + hh * sin, cy - hw * sin - hh * cos], // TL
    [cx + hw * cos + hh * sin, cy + hw * sin - hh * cos], // TR
    [cx + hw * cos - hh * sin, cy + hw * sin + hh * cos], // BR
    [cx - hw * cos - hh * sin, cy - hw * sin + hh * cos], // BL
  ];
}

/** General SAT for two convex polygons with spacing tolerance */
function polygonSATWithSpacing(
  aVerts: [number, number][],
  bVerts: [number, number][],
  spacing: number
): boolean {
  const allVerts = [aVerts, bVerts];
  for (const verts of allVerts) {
    for (let i = 0; i < verts.length; i++) {
      const j = (i + 1) % verts.length;
      const ex = verts[j][0] - verts[i][0];
      const ey = verts[j][1] - verts[i][1];
      const len = Math.sqrt(ex * ex + ey * ey);
      if (len === 0) continue;
      const nx = -ey / len, ny = ex / len;

      let aMin = Infinity, aMax = -Infinity;
      for (const [px, py] of aVerts) {
        const proj = px * nx + py * ny;
        aMin = Math.min(aMin, proj);
        aMax = Math.max(aMax, proj);
      }

      let bMin = Infinity, bMax = -Infinity;
      for (const [px, py] of bVerts) {
        const proj = px * nx + py * ny;
        bMin = Math.min(bMin, proj);
        bMax = Math.max(bMax, proj);
      }

      const gap = Math.max(bMin - aMax, aMin - bMax);
      if (gap > spacing) return false;
    }
  }
  return true;
}

/** Check if point is inside a convex polygon (CCW or CW winding) */
function pointInConvexPolygon(px: number, py: number, verts: [number, number][]): boolean {
  let sign = 0;
  for (let i = 0; i < verts.length; i++) {
    const [x1, y1] = verts[i];
    const [x2, y2] = verts[(i + 1) % verts.length];
    const cross = (x2 - x1) * (py - y1) - (y2 - y1) * (px - x1);
    if (cross !== 0) {
      if (sign === 0) sign = cross > 0 ? 1 : -1;
      else if ((cross > 0 ? 1 : -1) !== sign) return false;
    }
  }
  return true;
}

/** Distance from a point to a convex polygon (0 if inside) */
function pointToConvexPolygonDist(px: number, py: number, verts: [number, number][]): number {
  if (pointInConvexPolygon(px, py, verts)) return 0;
  let minDist = Infinity;
  for (let i = 0; i < verts.length; i++) {
    const [x1, y1] = verts[i];
    const [x2, y2] = verts[(i + 1) % verts.length];
    minDist = Math.min(minDist, distPointToSegment(px, py, x1, y1, x2, y2));
  }
  return minDist;
}

// ─── Geometric helpers for annular sector distance ───

/** Distance from a point to a line segment */
function distPointToSegment(px: number, py: number, x1: number, y1: number, x2: number, y2: number): number {
  const dx = x2 - x1, dy = y2 - y1;
  const len2 = dx * dx + dy * dy;
  if (len2 === 0) return Math.sqrt((px - x1) ** 2 + (py - y1) ** 2);
  const t = Math.max(0, Math.min(1, ((px - x1) * dx + (py - y1) * dy) / len2));
  const nearX = x1 + t * dx, nearY = y1 + t * dy;
  return Math.sqrt((px - nearX) ** 2 + (py - nearY) ** 2);
}

/**
 * Distance from a point to an annular sector (arc band).
 * Point is in local space: arc center at origin, Y points UP,
 * sector sweeps CCW from angle 0 to sweepRad.
 */
function distToAnnularSector(px: number, py: number, innerR: number, outerR: number, sweepRad: number): number {
  const r = Math.sqrt(px * px + py * py);
  let theta = Math.atan2(py, px);
  if (theta < -0.001) theta += 2 * Math.PI;

  const inAngle = theta >= -0.001 && theta <= sweepRad + 0.001;

  if (inAngle) {
    if (r >= innerR && r <= outerR) return 0; // inside sector
    if (r < innerR) return innerR - r;
    return r - outerR;
  }

  // Outside angular range — check distance to radial edges and corner points
  let minDist = Infinity;

  // Start radial edge (angle 0, from innerR to outerR along +X)
  minDist = Math.min(minDist, distPointToSegment(px, py, innerR, 0, outerR, 0));

  // End radial edge (angle sweepRad)
  const ec = Math.cos(sweepRad), es = Math.sin(sweepRad);
  minDist = Math.min(minDist, distPointToSegment(px, py, innerR * ec, innerR * es, outerR * ec, outerR * es));

  // Also check distance to the arcs at the clamped angle
  const clampedTheta = Math.max(0, Math.min(sweepRad, theta < 0 ? theta + 2 * Math.PI : theta));
  if (clampedTheta >= 0 && clampedTheta <= sweepRad) {
    const oc = Math.cos(clampedTheta), os = Math.sin(clampedTheta);
    minDist = Math.min(minDist, Math.sqrt((px - outerR * oc) ** 2 + (py - outerR * os) ** 2));
    if (innerR > 0) {
      minDist = Math.min(minDist, Math.sqrt((px - innerR * oc) ** 2 + (py - innerR * os) ** 2));
    }
  }

  return minDist;
}

/** Distance from a point in world space to an OBB */
function pointToOBBDist(px: number, py: number, bx: number, by: number, bw: number, bh: number, brot: number): number {
  const bcx = bx + bw / 2, bcy = by + bh / 2;
  const rad = -(brot || 0) * Math.PI / 180;
  const cos = Math.cos(rad), sin = Math.sin(rad);
  const dx = px - bcx, dy = py - bcy;
  const lx = dx * cos - dy * sin;
  const ly = dx * sin + dy * cos;
  const hw = bw / 2, hh = bh / 2;
  const cx = Math.max(-hw, Math.min(hw, lx));
  const cy = Math.max(-hh, Math.min(hh, ly));
  return Math.sqrt((lx - cx) ** 2 + (ly - cy) ** 2);
}

// ─── Curved vs OBB: true geometric check ───

interface CurveParams {
  x: number; y: number; w: number; h: number;
  rotation: number; symbolId: string; curveAngle?: number;
}

/**
 * Check if an OBB violates spacing against an annular sector (curved conveyor).
 * Uses true geometric distance instead of AABB approximation.
 */
function checkCurvedVsOBB(
  curve: CurveParams,
  bx: number, by: number, bw: number, bh: number, brot: number,
  spacing: number
): boolean {
  const angle = curve.curveAngle || 90;
  const outerR = curve.w;
  const bandW = getCurveBandWidth(curve.symbolId);
  const innerR = Math.max(0, outerR - bandW);
  const curveRot = (curve.rotation || 0) * Math.PI / 180;
  const sweepRad = (angle * Math.PI) / 180;

  // Arc center in unrotated local space (bottom-left of bbox)
  const acx = curve.x;
  const acy = curve.y + curve.h;

  // Symbol center (rotation pivot)
  const scx = curve.x + curve.w / 2;
  const scy = curve.y + curve.h / 2;

  const { toLocal, toWorld } = createCurveTransforms(acx, acy, scx, scy, curveRot);

  // Get OBB corners in world space
  const otherCx = bx + bw / 2, otherCy = by + bh / 2;
  const oRad = (brot || 0) * Math.PI / 180;
  const oCos = Math.cos(oRad), oSin = Math.sin(oRad);
  const ohw = bw / 2, ohh = bh / 2;

  const corners: [number, number][] = [
    [otherCx + ohw * oCos - ohh * oSin, otherCy + ohw * oSin + ohh * oCos],
    [otherCx - ohw * oCos - ohh * oSin, otherCy - ohw * oSin + ohh * oCos],
    [otherCx - ohw * oCos + ohh * oSin, otherCy - ohw * oSin - ohh * oCos],
    [otherCx + ohw * oCos + ohh * oSin, otherCy + ohw * oSin - ohh * oCos],
  ];

  // Check OBB corners against the annular sector
  for (const [wx, wy] of corners) {
    const [lx, ly] = toLocal(wx, wy);
    if (distToAnnularSector(lx, ly, innerR, outerR, sweepRad) < spacing) return true;
  }

  // Check edge midpoints and quarter-points for better coverage on long edges
  for (let i = 0; i < 4; i++) {
    const [x1, y1] = corners[i];
    const [x2, y2] = corners[(i + 1) % 4];
    for (const t of [0.25, 0.5, 0.75]) {
      const [lx, ly] = toLocal(x1 + (x2 - x1) * t, y1 + (y2 - y1) * t);
      if (distToAnnularSector(lx, ly, innerR, outerR, sweepRad) < spacing) return true;
    }
  }

  // Check arc sample points against the OBB
  const SAMPLES = Math.max(4, Math.ceil(angle / 10));
  for (let i = 0; i <= SAMPLES; i++) {
    const a = (i / SAMPLES) * sweepRad;
    const cos = Math.cos(a), sin = Math.sin(a);

    const [owx, owy] = toWorld(acx + outerR * cos, acy - outerR * sin);
    if (pointToOBBDist(owx, owy, bx, by, bw, bh, brot) < spacing) return true;

    if (innerR > 0) {
      const [iwx, iwy] = toWorld(acx + innerR * cos, acy - innerR * sin);
      if (pointToOBBDist(iwx, iwy, bx, by, bw, bh, brot) < spacing) return true;
    }
  }

  return false;
}

// ─── Curved vs Spur: geometric check ───

function checkCurvedVsSpur(
  curve: CurveParams,
  spurVerts: [number, number][],
  spacing: number
): boolean {
  const angle = curve.curveAngle || 90;
  const outerR = curve.w;
  const bandW = getCurveBandWidth(curve.symbolId);
  const innerR = Math.max(0, outerR - bandW);
  const curveRot = (curve.rotation || 0) * Math.PI / 180;
  const sweepRad = (angle * Math.PI) / 180;

  const acx = curve.x;
  const acy = curve.y + curve.h;
  const scx = curve.x + curve.w / 2;
  const scy = curve.y + curve.h / 2;

  const { toLocal, toWorld } = createCurveTransforms(acx, acy, scx, scy, curveRot);

  // Check spur vertices + edge sample points against annular sector
  for (const [wx, wy] of spurVerts) {
    const [lx, ly] = toLocal(wx, wy);
    if (distToAnnularSector(lx, ly, innerR, outerR, sweepRad) < spacing) return true;
  }
  for (let i = 0; i < spurVerts.length; i++) {
    const [x1, y1] = spurVerts[i];
    const [x2, y2] = spurVerts[(i + 1) % spurVerts.length];
    for (const t of [0.25, 0.5, 0.75]) {
      const [lx, ly] = toLocal(x1 + (x2 - x1) * t, y1 + (y2 - y1) * t);
      if (distToAnnularSector(lx, ly, innerR, outerR, sweepRad) < spacing) return true;
    }
  }

  // Check arc sample points against spur polygon
  const SAMPLES = Math.max(4, Math.ceil(angle / 10));
  for (let i = 0; i <= SAMPLES; i++) {
    const a = (i / SAMPLES) * sweepRad;
    const cos = Math.cos(a), sin = Math.sin(a);

    const [owx, owy] = toWorld(acx + outerR * cos, acy - outerR * sin);
    if (pointToConvexPolygonDist(owx, owy, spurVerts) < spacing) return true;

    if (innerR > 0) {
      const [iwx, iwy] = toWorld(acx + innerR * cos, acy - innerR * sin);
      if (pointToConvexPolygonDist(iwx, iwy, spurVerts) < spacing) return true;
    }
  }

  return false;
}

// ─── Legacy AABB distance (for curved vs curved fallback) ───

function edgeDistance(
  ax: number, ay: number, aw: number, ah: number,
  bx: number, by: number, bw: number, bh: number
): number {
  const dx = Math.max(0, ax - (bx + bw), bx - (ax + aw));
  const dy = Math.max(0, ay - (by + bh), by - (ay + ah));
  return Math.sqrt(dx * dx + dy * dy);
}

// ─── Curved band AABBs (only for curved vs curved fallback) ───

function getCurvedBandAABBs(sym: CurveParams): AABB[] {
  const angle = sym.curveAngle || 90;
  const outerR = sym.w;
  const bandW = getCurveBandWidth(sym.symbolId);
  const innerR = Math.max(0, outerR - bandW);
  const rot = (sym.rotation || 0) * Math.PI / 180;
  const sweepRad = (angle * Math.PI) / 180;
  const acx = sym.x;
  const acy = sym.y + sym.h;
  const scx = sym.x + sym.w / 2;
  const scy = sym.y + sym.h / 2;

  const cosR = Math.cos(rot), sinR = Math.sin(rot);
  function rotatePoint(px: number, py: number): [number, number] {
    if (!rot) return [px, py];
    const dx = px - scx, dy = py - scy;
    return [scx + dx * cosR - dy * sinR, scy + dx * sinR + dy * cosR];
  }

  const STEPS = Math.max(3, Math.ceil(angle / 10));
  const stepRad = sweepRad / STEPS;
  const boxes: AABB[] = [];

  for (let i = 0; i < STEPS; i++) {
    const a0 = i * stepRad, a1 = (i + 1) * stepRad;
    const p1 = rotatePoint(acx + outerR * Math.cos(a0), acy - outerR * Math.sin(a0));
    const p2 = rotatePoint(acx + outerR * Math.cos(a1), acy - outerR * Math.sin(a1));
    const p3 = rotatePoint(acx + innerR * Math.cos(a0), acy - innerR * Math.sin(a0));
    const p4 = rotatePoint(acx + innerR * Math.cos(a1), acy - innerR * Math.sin(a1));
    const minX = Math.min(p1[0], p2[0], p3[0], p4[0]);
    const minY = Math.min(p1[1], p2[1], p3[1], p4[1]);
    const maxX = Math.max(p1[0], p2[0], p3[0], p4[0]);
    const maxY = Math.max(p1[1], p2[1], p3[1], p4[1]);
    boxes.push({ x: minX, y: minY, w: maxX - minX, h: maxY - minY });
  }
  return boxes;
}

// ─── EPC decomposition into oriented quads ───

/** Get all collision quads for an EPC symbol in world space */
function getEpcCollisionQuads(
  symX: number, symY: number, symW: number, symH: number, rotation: number,
  waypoints: EpcWaypoint[]
): [number, number][][] {
  const quads: [number, number][][] = [];
  if (waypoints.length < 2) return quads;

  const rot = (rotation || 0) * Math.PI / 180;
  const cosR = Math.cos(rot), sinR = Math.sin(rot);
  const symCx = symX + symW / 2, symCy = symY + symH / 2;

  function toWorld(lx: number, ly: number): [number, number] {
    if (!rotation) return [lx, ly];
    const dx = lx - symCx, dy = ly - symCy;
    return [symCx + dx * cosR - dy * sinR, symCy + dx * sinR + dy * cosR];
  }

  /** Build an oriented quad from a segment (p0→p1) with given half-width */
  function segmentQuad(
    p0x: number, p0y: number, p1x: number, p1y: number, halfW: number
  ): [number, number][] {
    const dx = p1x - p0x, dy = p1y - p0y;
    const len = Math.sqrt(dx * dx + dy * dy);
    if (len === 0) return [];
    const nx = -dy / len * halfW, ny = dx / len * halfW;
    return [
      toWorld(p0x - nx, p0y - ny),
      toWorld(p1x - nx, p1y - ny),
      toWorld(p1x + nx, p1y + ny),
      toWorld(p0x + nx, p0y + ny),
    ];
  }

  /** Build an oriented box quad using shared geometry, then transform to world space. */
  function boxQuad(
    anchorX: number, anchorY: number,
    dirX: number, dirY: number,
    boxW: number, boxH: number,
    anchorSide: 'left' | 'right'
  ): [number, number][] {
    const corners = orientedBoxCorners(anchorX, anchorY, dirX, dirY, boxW, boxH, anchorSide);
    if (corners.length < 4) return [];
    return corners.map(([cx, cy]) => toWorld(cx, cy)) as [number, number][];
  }

  const ox = symX, oy = symY;

  // Line segment quads (use half-width of at least 1px for collision)
  const segHalfW = Math.max(EPC_LINE_WIDTH / 2, 0.5);
  for (let i = 0; i < waypoints.length - 1; i++) {
    const q = segmentQuad(
      ox + waypoints[i].x, oy + waypoints[i].y,
      ox + waypoints[i + 1].x, oy + waypoints[i + 1].y,
      segHalfW
    );
    if (q.length === 4) quads.push(q);
  }

  // Left box quad
  const p0 = waypoints[0], p1 = waypoints[1];
  const lbQ = boxQuad(
    ox + p0.x, oy + p0.y,
    p1.x - p0.x, p1.y - p0.y,
    EPC_LEFT_BOX.w, EPC_LEFT_BOX.h,
    'right'
  );
  if (lbQ.length === 4) quads.push(lbQ);

  // Right box quad
  const last = waypoints[waypoints.length - 1];
  const prev = waypoints[waypoints.length - 2];
  const rbQ = boxQuad(
    ox + last.x, oy + last.y,
    last.x - prev.x, last.y - prev.y,
    EPC_RIGHT_BOX.w, EPC_RIGHT_BOX.h,
    'left'
  );
  if (rbQ.length === 4) quads.push(rbQ);

  return quads;
}

/** Look up EPC waypoints for a symbol (from layout store or defaults) */
function getSymWaypoints(id: number): EpcWaypoint[] {
  const sym = layout.symbols.find(s => s.id === id);
  return sym?.epcWaypoints || EPC_DEFAULT_WAYPOINTS.map(wp => ({ ...wp }));
}

// ─── Main spacing violation check ───

/** Check if any quad from a set collides with a single polygon */
function anyQuadVsPolygon(
  quads: [number, number][][],
  bVerts: [number, number][],
  spacing: number
): boolean {
  for (const q of quads) {
    if (polygonSATWithSpacing(q, bVerts, spacing)) return true;
  }
  return false;
}

/** Check if any quad from set A collides with any quad from set B */
function anyQuadVsQuads(
  aQuads: [number, number][][],
  bQuads: [number, number][][],
  spacing: number
): boolean {
  for (const aq of aQuads) {
    for (const bq of bQuads) {
      if (polygonSATWithSpacing(aq, bq, spacing)) return true;
    }
  }
  return false;
}

/** Check any EPC quad against a curved symbol */
function anyQuadVsCurved(
  quads: [number, number][][],
  curve: CurveParams,
  spacing: number
): boolean {
  // Use the curved-vs-spur logic: each quad is a convex polygon
  for (const q of quads) {
    if (checkCurvedVsSpur(curve, q, spacing)) return true;
  }
  return false;
}

export function checkSpacingViolation(
  id: number, x: number, y: number, w: number, h: number, rotation: number,
  symbolId?: string, curveAngle?: number, w2?: number
): boolean {
  if (symbolId && SPACING_EXEMPT.has(symbolId)) return false;

  const spacing = layout.minSpacing;
  const isCurved = !!(symbolId && isCurvedType(symbolId));
  const isSpur = !!(symbolId && isSpurType(symbolId));
  const isEpc = !!(symbolId && isEpcType(symbolId));

  // Get EPC quads for "this" symbol if it's an EPC
  let epcQuads: [number, number][][] | null = null;
  if (isEpc) {
    const wps = getSymWaypoints(id);
    epcQuads = getEpcCollisionQuads(x, y, w, h, rotation, wps);
  }

  for (const sym of layout.symbols) {
    if (sym.id === id) continue;
    if (SPACING_EXEMPT.has(sym.symbolId)) continue;

    const symIsCurved = isCurvedType(sym.symbolId);
    const symIsSpur = isSpurType(sym.symbolId);
    const symIsEpc = isEpcType(sym.symbolId);

    // Get EPC quads for "other" symbol if it's EPC
    let symEpcQuads: [number, number][][] | null = null;
    if (symIsEpc) {
      const symWps = sym.epcWaypoints || EPC_DEFAULT_WAYPOINTS;
      symEpcQuads = getEpcCollisionQuads(sym.x, sym.y, sym.w, sym.h, sym.rotation, symWps);
    }

    // ── EPC vs EPC ──
    if (isEpc && symIsEpc && epcQuads && symEpcQuads) {
      if (anyQuadVsQuads(epcQuads, symEpcQuads, spacing)) return true;
      continue;
    }

    // ── EPC vs Curved ──
    if (isEpc && symIsCurved && epcQuads) {
      if (anyQuadVsCurved(epcQuads, sym, spacing)) return true;
      continue;
    }
    if (isCurved && symIsEpc && symEpcQuads) {
      if (anyQuadVsCurved(symEpcQuads, { x, y, w, h, rotation, symbolId: symbolId!, curveAngle }, spacing)) return true;
      continue;
    }

    // ── EPC vs Spur ──
    if (isEpc && symIsSpur && epcQuads) {
      const spurVerts = getSpurVertices(sym.x, sym.y, sym.w, sym.h, sym.w2 ?? sym.w, sym.rotation);
      if (anyQuadVsPolygon(epcQuads, spurVerts, spacing)) return true;
      continue;
    }
    if (isSpur && symIsEpc && symEpcQuads) {
      const spurVerts = getSpurVertices(x, y, w, h, w2 ?? w, rotation);
      if (anyQuadVsPolygon(symEpcQuads, spurVerts, spacing)) return true;
      continue;
    }

    // ── EPC vs OBB (regular symbol) ──
    if (isEpc && epcQuads && !symIsCurved && !symIsSpur && !symIsEpc) {
      const bVerts = getOBBVertices(sym.x, sym.y, sym.w, sym.h, sym.rotation);
      if (anyQuadVsPolygon(epcQuads, bVerts, spacing)) return true;
      continue;
    }
    if (symIsEpc && symEpcQuads && !isCurved && !isSpur && !isEpc) {
      const aVerts = getOBBVertices(x, y, w, h, rotation);
      if (anyQuadVsPolygon(symEpcQuads, aVerts, spacing)) return true;
      continue;
    }

    // ── Non-EPC logic (existing) ──
    if (!isCurved && !symIsCurved) {
      if (isSpur || symIsSpur) {
        const aVerts = isSpur
          ? getSpurVertices(x, y, w, h, w2 ?? w, rotation)
          : getOBBVertices(x, y, w, h, rotation);
        const bVerts = symIsSpur
          ? getSpurVertices(sym.x, sym.y, sym.w, sym.h, sym.w2 ?? sym.w, sym.rotation)
          : getOBBVertices(sym.x, sym.y, sym.w, sym.h, sym.rotation);
        if (polygonSATWithSpacing(aVerts, bVerts, spacing)) return true;
      } else {
        if (obbOverlapWithSpacing(x, y, w, h, rotation, sym.x, sym.y, sym.w, sym.h, sym.rotation, spacing)) {
          return true;
        }
      }
    } else if (isCurved && !symIsCurved) {
      if (symIsSpur) {
        const spurVerts = getSpurVertices(sym.x, sym.y, sym.w, sym.h, sym.w2 ?? sym.w, sym.rotation);
        if (checkCurvedVsSpur(
          { x, y, w, h, rotation, symbolId: symbolId!, curveAngle },
          spurVerts, spacing
        )) return true;
      } else {
        if (checkCurvedVsOBB(
          { x, y, w, h, rotation, symbolId: symbolId!, curveAngle },
          sym.x, sym.y, sym.w, sym.h, sym.rotation, spacing
        )) return true;
      }
    } else if (!isCurved && symIsCurved) {
      if (isSpur) {
        const spurVerts = getSpurVertices(x, y, w, h, w2 ?? w, rotation);
        if (checkCurvedVsSpur(sym, spurVerts, spacing)) return true;
      } else {
        if (checkCurvedVsOBB(
          sym,
          x, y, w, h, rotation, spacing
        )) return true;
      }
    } else {
      // Both curved: use band AABBs as fallback
      const aBoxes = getCurvedBandAABBs({ x, y, w, h, rotation, symbolId: symbolId!, curveAngle });
      const bBoxes = getCurvedBandAABBs(sym);
      for (const a of aBoxes) {
        for (const b of bBoxes) {
          if (edgeDistance(a.x, a.y, a.w, a.h, b.x, b.y, b.w, b.h) < spacing) return true;
        }
      }
    }
  }
  return false;
}

// ─── Grid snapping ───

export function snapToGrid(x: number, y: number, w?: number, h?: number, rotation?: number): { x: number; y: number } {
  if (!layout.snapEnabled) return { x, y };
  const size = layout.gridSize;
  if (!rotation || !w || !h) {
    return { x: Math.round(x / size) * size, y: Math.round(y / size) * size };
  }
  const aabb = getAABB(x, y, w, h, rotation);
  const snappedAabbX = Math.round(aabb.x / size) * size;
  const snappedAabbY = Math.round(aabb.y / size) * size;
  return {
    x: snappedAabbX + aabb.w / 2 - w / 2,
    y: snappedAabbY + aabb.h / 2 - h / 2,
  };
}

// ─── Find valid position ───

export function findValidPosition(
  id: number, x: number, y: number, w: number, h: number,
  symbolId: string, rotation: number, curveAngle?: number, w2?: number
): { x: number; y: number } {
  const snapped = snapToGrid(x, y, w, h, rotation);
  let sx = snapped.x, sy = snapped.y;
  if (!checkSpacingViolation(id, sx, sy, w, h, rotation, symbolId, curveAngle, w2)) return { x: sx, y: sy };

  const step = layout.snapEnabled ? layout.gridSize : 1;
  const searchRadius = 20;
  for (let r = 1; r <= searchRadius; r++) {
    for (let dx = -r; dx <= r; dx++) {
      for (let dy = -r; dy <= r; dy++) {
        if (Math.abs(dx) !== r && Math.abs(dy) !== r) continue;
        const cx = sx + dx * step;
        const cy = sy + dy * step;
        const bb = getAABB(cx, cy, w, h, rotation);
        if (bb.x < 0 || bb.y < 0 || bb.x + bb.w > layout.canvasW || bb.y + bb.h > layout.canvasH) continue;
        if (!checkSpacingViolation(id, cx, cy, w, h, rotation, symbolId, curveAngle, w2)) return { x: cx, y: cy };
      }
    }
  }
  return { x: sx, y: sy };
}

export function clamp(value: number, min: number, max: number): number {
  return Math.max(min, Math.min(max, value));
}
