/** Pure hit-testing functions — no module state, no side effects */

import { isResizable, isCurvedType, isSpurType, isEpcType, isInductionType, getSymbolGroup, EPC_CONFIG, INDUCTION_CONFIG, getCurveGeometry } from '../symbols.js';
import { layout } from '../stores/layout.svelte.js';
import { THEME } from './render-theme.js';
import type { PlacedSymbol } from '../types.js';

/** Transform a point into a symbol's unrotated, unmirrored local space */
export function toSymbolLocal(px: number, py: number, sym: PlacedSymbol): { x: number; y: number } {
  if (!sym.rotation && !sym.mirrored) return { x: px, y: py };
  const scx = sym.x + sym.w / 2;
  const scy = sym.y + sym.h / 2;
  const rad = (-(sym.rotation || 0) * Math.PI) / 180;
  const cos = Math.cos(rad), sin = Math.sin(rad);
  const dx = px - scx, dy = py - scy;
  // Inverse of render transform: un-rotate then un-mirror
  let rx = dx * cos - dy * sin;
  const ry = dx * sin + dy * cos;
  if (sym.mirrored) rx = -rx;
  return { x: rx + scx, y: ry + scy };
}

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

export function pointInTrapezoid(px: number, py: number, sym: PlacedSymbol): boolean {
  const w2 = sym.w2 ?? sym.w;
  const t = (py - sym.y) / sym.h;
  const maxX = sym.x + w2 + t * (sym.w - w2);
  return px >= sym.x && px <= maxX && py >= sym.y && py <= sym.y + sym.h;
}

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

/** Hit test EPC line segment midpoints for adding waypoints. Returns insert index or -1 */
export function hitEpcSegmentMidpoint(cx: number, cy: number, sym: PlacedSymbol): number {
  if (!isEpcType(sym.symbolId)) return -1;
  const waypoints = sym.epcWaypoints || EPC_CONFIG.defaultWaypoints;
  const { x: lx, y: ly } = toSymbolLocal(cx, cy, sym);
  const hs = 8;
  for (let i = 0; i < waypoints.length - 1; i++) {
    const mx = sym.x + (waypoints[i].x + waypoints[i + 1].x) / 2;
    const my = sym.y + (waypoints[i].y + waypoints[i + 1].y) / 2;
    const dx = lx - mx, dy = ly - (my + THEME.epcWaypoint.hintOffsetY);
    if (dx * dx + dy * dy <= hs * hs) return i + 1;
  }
  return -1;
}

/** Find nearest EPC segment to a point. Returns insert index or -1.
 *  Used for right-click add-waypoint — matches any point near a segment, not just midpoints. */
export function hitEpcSegmentNearest(cx: number, cy: number, sym: PlacedSymbol): number {
  if (!isEpcType(sym.symbolId)) return -1;
  const waypoints = sym.epcWaypoints || EPC_CONFIG.defaultWaypoints;
  const { x: lx, y: ly } = toSymbolLocal(cx, cy, sym);
  const maxDist = 12;
  let bestIdx = -1;
  let bestDist = maxDist;
  for (let i = 0; i < waypoints.length - 1; i++) {
    const ax = sym.x + waypoints[i].x, ay = sym.y + waypoints[i].y;
    const bx = sym.x + waypoints[i + 1].x, by = sym.y + waypoints[i + 1].y;
    const dx = bx - ax, dy = by - ay;
    const len2 = dx * dx + dy * dy;
    if (len2 === 0) continue;
    const t = Math.max(0, Math.min(1, ((lx - ax) * dx + (ly - ay) * dy) / len2));
    const nx = ax + t * dx, ny = ay + t * dy;
    const dist = Math.sqrt((lx - nx) ** 2 + (ly - ny) ** 2);
    if (dist < bestDist) {
      bestDist = dist;
      bestIdx = i + 1;
    }
  }
  return bestIdx;
}

/** Hit test resize handles. Caller must verify single-selection. */
export function hitResizeHandle(cx: number, cy: number, sym: PlacedSymbol): 'left' | 'right' | 'spur-top' | 'spur-bottom' | null {
  if (!isResizable(sym.symbolId)) return null;

  const hs = THEME.resizeHandle.hitSize;
  const { x: lx, y: ly } = toSymbolLocal(cx, cy, sym);

  if (isCurvedType(sym.symbolId)) {
    const { arcCx, arcCy, outerR } = getCurveGeometry(sym.symbolId, sym.x, sym.y, sym.w, sym.h);
    if (pointInRect(lx, ly, arcCx + outerR - hs / 2, arcCy - hs / 2, hs, hs)) return 'right';
    return null;
  }

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

  if (isInductionType(sym.symbolId)) {
    const stripTopY = sym.y + sym.h * INDUCTION_CONFIG.stripTopFrac;
    const stripBottomY = sym.y + sym.h * INDUCTION_CONFIG.stripBottomFrac;
    const stripMidY = (stripTopY + stripBottomY) / 2 - hs / 2;
    if (pointInRect(lx, ly, sym.x + sym.w - hs / 2, stripMidY, hs, hs)) return 'right';
    return null;
  }

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

/** Check if a point is inside an arc band (annular sector) */
function pointInArcBand(px: number, py: number, sym: PlacedSymbol): boolean {
  const angle = sym.curveAngle || 90;
  const { arcCx, arcCy, outerR, innerR } = getCurveGeometry(sym.symbolId, sym.x, sym.y, sym.w, sym.h);
  const dx = px - arcCx, dy = -(py - arcCy); // flip Y to math convention (Y up)
  const r = Math.sqrt(dx * dx + dy * dy);
  if (r < innerR || r > outerR) return false;
  let theta = Math.atan2(dy, dx);
  if (theta < -0.01) theta += 2 * Math.PI;
  const sweepRad = (angle * Math.PI) / 180;
  return theta >= -0.01 && theta <= sweepRad + 0.01;
}

/** Check if a point is inside the induction shape (arrow head + strip) */
function pointInInduction(px: number, py: number, sym: PlacedSymbol): boolean {
  const hw = INDUCTION_CONFIG.headWidth;
  const stripTopY = sym.y + sym.h * INDUCTION_CONFIG.stripTopFrac;
  const stripBottomY = sym.y + sym.h * INDUCTION_CONFIG.stripBottomFrac;
  // Check strip rect
  if (px >= sym.x + hw * 0.55 && px <= sym.x + sym.w && py >= stripTopY && py <= stripBottomY) return true;
  // Check arrow head (approximate as bounding rect of arrow points)
  if (px >= sym.x && px <= sym.x + hw && py >= sym.y && py <= sym.y + sym.h) return true;
  return false;
}

/** Check if a point is near the EPC shape (line segments + end boxes) */
function pointInEpc(px: number, py: number, sym: PlacedSymbol): boolean {
  const waypoints = sym.epcWaypoints || EPC_CONFIG.defaultWaypoints;
  const hitMargin = 8; // generous click target around thin lines
  const ox = sym.x, oy = sym.y;

  // Check left box
  const lb = EPC_CONFIG.leftBox;
  if (waypoints.length >= 2) {
    const p0x = ox + waypoints[0].x, p0y = oy + waypoints[0].y;
    const p1x = ox + waypoints[1].x, p1y = oy + waypoints[1].y;
    const angle = Math.atan2(p1y - p0y, p1x - p0x);
    const cos = Math.cos(-angle), sin = Math.sin(-angle);
    const dx = px - p0x, dy = py - p0y;
    const lx = dx * cos - dy * sin, ly = dx * sin + dy * cos;
    if (lx >= -lb.w - hitMargin && lx <= hitMargin && ly >= -lb.h / 2 - hitMargin && ly <= lb.h / 2 + hitMargin) return true;
  }

  // Check right box
  const rb = EPC_CONFIG.rightBox;
  if (waypoints.length >= 2) {
    const last = waypoints[waypoints.length - 1];
    const prev = waypoints[waypoints.length - 2];
    const plx = ox + last.x, ply = oy + last.y;
    const angle = Math.atan2(ply - (oy + prev.y), plx - (ox + prev.x)) - Math.PI / 2;
    const cos = Math.cos(-angle), sin = Math.sin(-angle);
    const dx = px - plx, dy = py - ply;
    const lx = dx * cos - dy * sin, ly = dx * sin + dy * cos;
    if (lx >= -rb.w - hitMargin && lx <= hitMargin && ly >= -rb.h / 2 - hitMargin && ly <= rb.h / 2 + hitMargin) return true;
  }

  // Check line segments
  for (let i = 0; i < waypoints.length - 1; i++) {
    const ax = ox + waypoints[i].x, ay = oy + waypoints[i].y;
    const bx = ox + waypoints[i + 1].x, by = oy + waypoints[i + 1].y;
    const sdx = bx - ax, sdy = by - ay;
    const len2 = sdx * sdx + sdy * sdy;
    if (len2 === 0) continue;
    const t = Math.max(0, Math.min(1, ((px - ax) * sdx + (py - ay) * sdy) / len2));
    const nx = ax + t * sdx, ny = ay + t * sdy;
    const dist = Math.sqrt((px - nx) ** 2 + (py - ny) ** 2);
    if (dist <= hitMargin) return true;
  }

  return false;
}

/** Hit test a single symbol against its actual shape */
function pointInSymbol(px: number, py: number, sym: PlacedSymbol): boolean {
  if (isCurvedType(sym.symbolId)) return pointInArcBand(px, py, sym);
  if (isSpurType(sym.symbolId)) return pointInTrapezoid(px, py, sym);
  if (isInductionType(sym.symbolId)) return pointInInduction(px, py, sym);
  if (isEpcType(sym.symbolId)) return pointInEpc(px, py, sym);
  return pointInRect(px, py, sym.x, sym.y, sym.w, sym.h);
}

/** Hit test all symbols, returning the id of the topmost hit, or null */
export function hitTestSymbols(
  cx: number, cy: number,
  symbols: PlacedSymbol[]
): number | null {
  for (let i = symbols.length - 1; i >= 0; i--) {
    const sym = symbols[i];
    if (sym.hidden || layout.hiddenGroups.has(getSymbolGroup(sym.symbolId))) continue;
    const local = toSymbolLocal(cx, cy, sym);
    if (pointInSymbol(local.x, local.y, sym)) return sym.id;
  }
  return null;
}
