// Koordinaten- & Numerik-Parser (docs/design/rhino-command-system.md §2.7).
//
// Parst rohen Command-Line-Text TOLERANT (Komma/Space/Semikolon als Trenner) zu
// einer Discriminated Union; die Engine löst die punktbezogenen Varianten mit
// `lastPoint` + Cursor-Richtung zu einem konkreten Modellpunkt auf.
//
// Unterstützte Formen:
//   `5,3`        → absolut X,Y
//   `5,3,2`      → absolut X,Y (Z ignoriert, 2D)
//   `r5,3`       → relativ zum letzten Punkt
//   `5<45`       → polar: Distanz 5 unter 45° vom letzten Punkt
//   `<45`        → nur Winkel-Constraint; Distanz folgt Maus/Zahl
//   `5`          → nackte Zahl = Distanz-Lock entlang Cursor-/Constraint-Richtung
//   sonst        → Roh-Text (Befehlsname / Optionsbuchstabe) — von der Engine geroutet
//
// Reine Funktionen, kein Zustand. Winkel in Grad, gegen die +X-Achse, CCW positiv
// (passend zur Y-flip-freien Modellkonvention: +Y ist „oben").

import type { Vec2 } from "../model/types";

/** Geparste, noch nicht aufgelöste Eingabe. */
export type ParsedInput =
  | { kind: "absolute"; point: Vec2 } // 5,3
  | { kind: "relative"; delta: Vec2 } // r5,3
  | { kind: "polar"; dist: number; angleDeg: number } // 5<45
  | { kind: "angle"; angleDeg: number } // <45
  | { kind: "distance"; value: number } // 5  (nackte Zahl)
  | { kind: "text"; text: string }; // Befehlsname / Option / unparsbar

const DEG = Math.PI / 180;

/** Tolerant: Zahl aus einem Token (erlaubt führendes +, Dezimalpunkt/-komma). */
function parseNumber(raw: string): number | null {
  const s = raw.trim().replace(",", "."); // Hilfsfall: einzelnes Dezimalkomma
  if (s === "" || s === "+" || s === "-") return null;
  const n = Number(s);
  return Number.isFinite(n) ? n : null;
}

/**
 * Zerlegt eine Koordinatenliste (X,Y[,Z]) tolerant in Zahlen. Trenner: Komma,
 * Semikolon oder Whitespace. Liefert null, wenn nicht ≥2 valide Zahlen vorliegen.
 * (Wir nutzen die echten Trenner, daher kollidiert das NICHT mit dem
 * Dezimalkomma-Hilfsfall der nackten Zahl.)
 */
function parseCoordList(raw: string): number[] | null {
  const parts = raw
    .trim()
    .split(/[\s,;]+/)
    .filter((p) => p.length > 0);
  if (parts.length < 2) return null;
  const nums: number[] = [];
  for (const p of parts) {
    const n = Number(p.trim());
    if (!Number.isFinite(n)) return null;
    nums.push(n);
  }
  return nums;
}

/**
 * Parst rohen Command-Line-Text zu einer {@link ParsedInput}. Befehlsnamen/
 * Optionsbuchstaben fallen als `{kind:"text"}` durch (die Engine routet sie).
 */
export function parseInput(raw: string): ParsedInput {
  const s = raw.trim();
  if (s === "") return { kind: "text", text: "" };

  // ── polar / Winkel: enthält '<' ───────────────────────────────────────────
  if (s.includes("<")) {
    const i = s.indexOf("<");
    const left = s.slice(0, i).trim();
    const right = s.slice(i + 1).trim();
    const angle = parseNumber(right);
    if (angle == null) return { kind: "text", text: s };
    if (left === "") return { kind: "angle", angleDeg: angle };
    const dist = parseNumber(left);
    if (dist == null) return { kind: "text", text: s };
    return { kind: "polar", dist, angleDeg: angle };
  }

  // ── relativ: führendes r / @ ──────────────────────────────────────────────
  if (/^[r@]/i.test(s)) {
    const rest = s.slice(1);
    const nums = parseCoordList(rest);
    if (nums && nums.length >= 2) {
      return { kind: "relative", delta: { x: nums[0], y: nums[1] } };
    }
    // `r5` → relative Distanz entlang Cursor-Richtung.
    const one = parseNumber(rest);
    if (one != null) return { kind: "distance", value: one };
    return { kind: "text", text: s };
  }

  // ── absolute Koordinate: ≥2 zahlen mit echtem Trenner ─────────────────────
  const coords = parseCoordList(s);
  if (coords && coords.length >= 2) {
    return { kind: "absolute", point: { x: coords[0], y: coords[1] } };
  }

  // ── nackte Zahl = Distanz-Lock ────────────────────────────────────────────
  const one = parseNumber(s);
  if (one != null) return { kind: "distance", value: one };

  // ── sonst: Text (Befehl/Option) ───────────────────────────────────────────
  return { kind: "text", text: s };
}

/** Punkt aus polarer Form (Distanz/Winkel) relativ zu einem Basispunkt. */
export function polarPoint(base: Vec2, dist: number, angleDeg: number): Vec2 {
  const a = angleDeg * DEG;
  return { x: base.x + Math.cos(a) * dist, y: base.y + Math.sin(a) * dist };
}

/**
 * Löst eine {@link ParsedInput} zu einem konkreten Modellpunkt auf — falls sie
 * sich als Punkt auflösen lässt. `lastPoint` ist der letzte gesetzte Punkt im
 * Befehl (für relativ/polar/Distanz), `cursorDir` die aktuelle (gesnappte)
 * Cursor-Richtung als Roh-Cursorpunkt (für nackte Zahl / reinen Winkel-Lock).
 *
 * Liefert null, wenn die Eingabe keinen Punkt ergibt (z. B. reiner Text oder
 * Distanz ohne Bezug).
 */
export function resolvePoint(
  parsed: ParsedInput,
  lastPoint: Vec2 | null,
  cursor: Vec2 | null,
): Vec2 | null {
  switch (parsed.kind) {
    case "absolute":
      return parsed.point;
    case "relative":
      if (!lastPoint) return parsed.delta; // ohne Bezug = vom Ursprung
      return { x: lastPoint.x + parsed.delta.x, y: lastPoint.y + parsed.delta.y };
    case "polar": {
      const base = lastPoint ?? { x: 0, y: 0 };
      return polarPoint(base, parsed.dist, parsed.angleDeg);
    }
    case "angle": {
      // Reiner Winkel-Lock: ohne Distanz braucht es den Cursor, um die Länge
      // entlang der gelockten Richtung zu projizieren.
      if (!lastPoint || !cursor) return null;
      const a = parsed.angleDeg * DEG;
      const dir = { x: Math.cos(a), y: Math.sin(a) };
      const dx = cursor.x - lastPoint.x;
      const dy = cursor.y - lastPoint.y;
      const proj = dx * dir.x + dy * dir.y; // skalare Projektion
      const len = Math.max(0, proj);
      return { x: lastPoint.x + dir.x * len, y: lastPoint.y + dir.y * len };
    }
    case "distance": {
      // Distanz-Lock: Richtung per Cursor, Länge per Zahl.
      if (!lastPoint || !cursor) return null;
      const dx = cursor.x - lastPoint.x;
      const dy = cursor.y - lastPoint.y;
      const l = Math.hypot(dx, dy);
      if (l < 1e-9) return { x: lastPoint.x + parsed.value, y: lastPoint.y };
      return {
        x: lastPoint.x + (dx / l) * parsed.value,
        y: lastPoint.y + (dy / l) * parsed.value,
      };
    }
    default:
      return null;
  }
}
