// Druck-SVG aus einem Plan (generatePlan → Primitive). Eigenständiges,
// massstäbliches SVG für den VEKTOR-PDF-Export — NICHT vom Bildschirm-DOM
// abgeleitet, sondern direkt aus den Plan-Primitiven (Polygon/Linie/Bogen).
//
// Kern-Idee (vgl. CONVENTIONS.md, docs/design/plans-output.md §3): Der Plan IST
// Papier-Space. Welt-Meter werden im gewählten Massstab 1:N nach Papier-
// Millimeter abgebildet (1 m = 1000/N mm). Das SVG-Benutzerkoordinatensystem ist
// damit „mm": die viewBox ist in mm, Strichstärken (`stroke-width`) sind echte
// physische mm — KEIN non-scaling-stroke (anders als die Bildschirm-PlanView).
//
// Schraffuren werden NICHT als <pattern> geschrieben (PDF-Konverter übernehmen
// Pattern oft nicht sauber), sondern als echte, auf das Polygon geklippte
// Vektorlinien direkt ins SVG. So bleibt das PDF rein vektoriell und scharf.
//
// Bezeichner englisch, Kommentare deutsch (CONVENTIONS.md).

import type { HatchRender, Plan, Primitive } from "../plan/generatePlan";
import type { Vec2 } from "../model/types";

/** SVG-Namespace für die programmatisch erzeugten Knoten. */
const SVG_NS = "http://www.w3.org/2000/svg";

/**
 * Standard-Stift-Stufen (mm). Beliebige Kategorie-Strichstärken werden auf die
 * NÄCHSTHÖHERE dieser ISO-nahen Stufen gequantelt, damit der Plan saubere,
 * unterscheidbare Linienstärken trägt (dünne Hilfslinien ≠ dicke Umrisse).
 */
const PEN_STEPS = [0.13, 0.18, 0.25, 0.35, 0.5, 0.7, 1.0] as const;

/** Untergrenze einer sichtbaren Druck-Strichstärke (mm) — vermeidet 0-Linien. */
const MIN_PEN_MM = 0.13;

/** Quantelt eine mm-Strichstärke auf die nächste Stift-Stufe (≥ MIN_PEN_MM). */
function quantizePen(mm: number): number {
  const w = Math.max(MIN_PEN_MM, mm);
  for (const step of PEN_STEPS) if (w <= step + 1e-6) return step;
  return PEN_STEPS[PEN_STEPS.length - 1];
}

/** Ergebnis des Print-SVG-Aufbaus: das Element + Blattmasse (mm). */
export interface PrintSvg {
  /** Das fertige, in mm bemaßte <svg>-Element (für svg2pdf). */
  svg: SVGSVGElement;
  /** Blattbreite in mm (= viewBox-Breite). */
  widthMm: number;
  /** Blatthöhe in mm (= viewBox-Höhe). */
  heightMm: number;
  /** Massstäblicher Inhalts-Rahmen (mm) innerhalb des Blattes (Plan-Bounds). */
  content: { x: number; y: number; w: number; h: number };
}

export interface PrintSvgOptions {
  /** Massstab-Nenner N (1:N). 1 m = 1000/N mm auf dem Papier. */
  scaleDenominator: number;
  /** Blattbreite in mm (Papierformat, schon Hoch/Quer berücksichtigt). */
  pageWidthMm: number;
  /** Blatthöhe in mm. */
  pageHeightMm: number;
  /** Rand um den Plan-Inhalt in mm (weisser Blattrand). */
  marginMm?: number;
}

/** Modell-Meter → Papier-Millimeter im Massstab 1:N. */
function mmPerMeter(n: number): number {
  return 1000 / n;
}

/**
 * Bildet einen Welt-Punkt (Meter) auf Papier-Millimeter ab. Der Inhalt wird so
 * verschoben, dass die Plan-Bounds (in mm) zentriert auf dem Blatt liegen.
 * Plan-Y zeigt nach oben → SVG-Y nach unten (Spiegelung), daher das Minus.
 */
interface Mapper {
  (p: Vec2): Vec2;
}

/**
 * Erzeugt das Druck-SVG aus einem Plan. Reihenfolge der Primitive bleibt
 * erhalten (Zeichenreihenfolge = Stapelung); Schraffuren werden als geklippte
 * Linien zwischen Grundfüllung und Umriss eingefügt.
 */
export function planToPrintSvg(plan: Plan, opts: PrintSvgOptions): PrintSvg {
  const n = opts.scaleDenominator;
  const k = mmPerMeter(n); // mm je Meter
  // `marginMm` ist Teil des Vertrags (Aufrufer kann einen Rand wünschen); da der
  // Inhalt zentriert wird, liegt er ohnehin innerhalb des Blattes. Wir lesen ihn
  // bewusst, ohne ihn aktuell für einen Versatz zu nutzen (zentrierte Lage).
  void (opts.marginMm ?? 10);

  const b = plan.bounds;
  // Inhalts-Maße in mm (massstäblich), unverschoben.
  const contentWmm = (b.maxX - b.minX) * k;
  const contentHmm = (b.maxY - b.minY) * k;

  // Inhalt mittig aufs Blatt setzen. Der nutzbare Bereich ist Blatt minus Rand;
  // wir zentrieren den Plan-Block darin (clippen aber NICHT — bei zu grossem
  // Inhalt läuft er über das Blatt hinaus, was der Aufrufer per Format/Massstab
  // vermeidet; ein Warnhinweis liegt in der UI).
  const offsetXmm = (opts.pageWidthMm - contentWmm) / 2;
  // SVG-Y zeigt nach unten: der oberste Welt-Punkt (maxY) liegt oben auf dem
  // Blatt. Wir mappen maxY → offsetYmm.
  const offsetYmm = (opts.pageHeightMm - contentHmm) / 2;

  const map: Mapper = (p) => ({
    x: offsetXmm + (p.x - b.minX) * k,
    y: offsetYmm + (b.maxY - p.y) * k,
  });

  const svg = document.createElementNS(SVG_NS, "svg") as SVGSVGElement;
  svg.setAttribute("xmlns", SVG_NS);
  // Breite/Höhe als REINE Zahlen (= viewBox-Einheiten), KEINE „mm"-Einheit: so
  // bildet svg2pdf die viewBox (in mm-Benutzereinheiten) 1:1 auf das mm-Ziel
  // (doc.svg {width,height}) ab. Eine „297mm"-Einheit würde svg2pdf als
  // physische Größe lesen und zusätzlich pt-skalieren (Faktor 25.4/72) → falsch.
  svg.setAttribute("width", String(opts.pageWidthMm));
  svg.setAttribute("height", String(opts.pageHeightMm));
  svg.setAttribute("viewBox", `0 0 ${opts.pageWidthMm} ${opts.pageHeightMm}`);

  // Weisses Blatt (deckt die volle Seite).
  const bg = document.createElementNS(SVG_NS, "rect");
  bg.setAttribute("x", "0");
  bg.setAttribute("y", "0");
  bg.setAttribute("width", String(opts.pageWidthMm));
  bg.setAttribute("height", String(opts.pageHeightMm));
  bg.setAttribute("fill", "#ffffff");
  svg.appendChild(bg);

  // Jedes Primitiv in mm-Knoten übersetzen.
  for (const p of plan.primitives) {
    appendPrimitive(svg, p, map, k);
  }

  return {
    svg,
    widthMm: opts.pageWidthMm,
    heightMm: opts.pageHeightMm,
    content: { x: offsetXmm, y: offsetYmm, w: contentWmm, h: contentHmm },
  };
}

/** Schreibt ein einzelnes Primitiv (mit ggf. Schraffur) in das SVG. */
function appendPrimitive(
  svg: SVGSVGElement,
  p: Primitive,
  map: Mapper,
  mmPerM: number,
): void {
  const opacity = p.greyed ? "0.3" : undefined;
  switch (p.kind) {
    case "polygon":
      appendPolygon(svg, p, map, mmPerM, opacity);
      break;
    case "line": {
      const a = map(p.a);
      const b = map(p.b);
      const ln = lineNode(a, b, p.color ?? "#111111", quantizePen(p.weightMm), p.dash, mmPerM);
      if (opacity) ln.setAttribute("opacity", opacity);
      svg.appendChild(ln);
      break;
    }
    case "arc": {
      const node = arcNode(p, map, mmPerM);
      if (opacity) node.setAttribute("opacity", opacity);
      svg.appendChild(node);
      break;
    }
  }
}

/**
 * Polygon mit Füllung/Umriss/Schraffur. Reihenfolge wie in der PlanView:
 *  • pattern "none"   → reine Füllung (oder „none") + Umriss.
 *  • pattern "solid"  → Vollfüllung in der Hatch-Farbe + Umriss.
 *  • Linien/Kreuz/Dämmung → Grundfüllung, dann geklippte Schraffurlinien, dann Umriss.
 */
function appendPolygon(
  svg: SVGSVGElement,
  p: Extract<Primitive, { kind: "polygon" }>,
  map: Mapper,
  mmPerM: number,
  opacity: string | undefined,
): void {
  const ptsMm = p.pts.map(map);
  const ptsAttr = ptsMm.map((s) => `${round(s.x)},${round(s.y)}`).join(" ");
  const strokeMm = p.strokeWidthMm > 0 ? quantizePen(p.strokeWidthMm) : 0;

  const g = document.createElementNS(SVG_NS, "g");
  if (opacity) g.setAttribute("opacity", opacity);

  const pattern = p.hatch.pattern;
  if (pattern === "none") {
    g.appendChild(polyNode(ptsAttr, p.fill, p.stroke, strokeMm));
  } else if (pattern === "solid") {
    g.appendChild(polyNode(ptsAttr, p.hatch.color, p.stroke, strokeMm));
  } else {
    // Grundfüllung (ohne Umriss), dann die geklippten Schraffurlinien, dann der
    // Umriss obenauf (separat, damit er die Schraffur-Enden überdeckt).
    if (p.fill !== "none") {
      g.appendChild(polyNode(ptsAttr, p.fill, "none", 0));
    }
    for (const seg of hatchLines(ptsMm, p.hatch, mmPerM)) {
      g.appendChild(
        lineNode(
          seg.a,
          seg.b,
          p.hatch.color,
          quantizePen(p.hatch.lineWeight),
          p.hatch.dash,
          mmPerM,
        ),
      );
    }
    if (strokeMm > 0) {
      g.appendChild(polyNode(ptsAttr, "none", p.stroke, strokeMm));
    }
  }

  svg.appendChild(g);
}

/** Erzeugt einen <polygon>-Knoten (mm-Koordinaten als Attribut-String). */
function polyNode(
  ptsAttr: string,
  fill: string,
  stroke: string,
  strokeMm: number,
): SVGElement {
  const el = document.createElementNS(SVG_NS, "polygon");
  el.setAttribute("points", ptsAttr);
  el.setAttribute("fill", fill);
  if (stroke !== "none" && strokeMm > 0) {
    el.setAttribute("stroke", stroke);
    el.setAttribute("stroke-width", String(strokeMm));
    el.setAttribute("stroke-linejoin", "round");
  } else {
    el.setAttribute("stroke", "none");
  }
  return el;
}

/** Erzeugt einen <line>-Knoten in mm; Strichmuster (mm Welt → mm Papier). */
function lineNode(
  a: Vec2,
  b: Vec2,
  color: string,
  strokeMm: number,
  dash: number[] | null | undefined,
  _mmPerM: number,
): SVGElement {
  const el = document.createElementNS(SVG_NS, "line");
  el.setAttribute("x1", String(round(a.x)));
  el.setAttribute("y1", String(round(a.y)));
  el.setAttribute("x2", String(round(b.x)));
  el.setAttribute("y2", String(round(b.y)));
  el.setAttribute("stroke", color);
  el.setAttribute("stroke-width", String(strokeMm));
  el.setAttribute("stroke-linecap", "round");
  if (dash && dash.length) {
    // Strichmuster ist in mm Papier definiert (wie weightMm), daher direkt.
    el.setAttribute("stroke-dasharray", dash.map((d) => round(d)).join(" "));
  }
  return el;
}

/**
 * Erzeugt einen Bogen als <path> (A-Kommando). Der Bogen-Radius ist in Metern →
 * Papier-mm. Drehrichtung aus dem Kreuzprodukt im Papier-Raum (Y nach unten).
 */
function arcNode(
  p: Extract<Primitive, { kind: "arc" }>,
  map: Mapper,
  mmPerM: number,
): SVGElement {
  const c = map(p.center);
  const from = map(p.from);
  const to = map(p.to);
  const r = p.r * mmPerM;
  const v1 = { x: from.x - c.x, y: from.y - c.y };
  const v2 = { x: to.x - c.x, y: to.y - c.y };
  const cross = v1.x * v2.y - v1.y * v2.x;
  const sweep = cross > 0 ? 1 : 0;
  const d = `M ${round(from.x)} ${round(from.y)} A ${round(r)} ${round(r)} 0 0 ${sweep} ${round(to.x)} ${round(to.y)}`;
  const el = document.createElementNS(SVG_NS, "path");
  el.setAttribute("d", d);
  el.setAttribute("fill", "none");
  el.setAttribute("stroke", "#111111");
  el.setAttribute("stroke-width", String(quantizePen(p.weightMm)));
  if (p.dash && p.dash.length) {
    el.setAttribute("stroke-dasharray", p.dash.map((x) => round(x)).join(" "));
  }
  return el;
}

/** Auf 3 Nachkommastellen runden (kompaktes, exaktes SVG). */
function round(v: number): number {
  return Math.round(v * 1000) / 1000;
}

// ── Schraffur als geklippte Vektorlinien ─────────────────────────────────────

interface Seg {
  a: Vec2;
  b: Vec2;
}

/**
 * Erzeugt die Schraffurlinien eines Polygons (in Papier-mm), auf das Polygon
 * geklippt. Die Kachelgrösse der Bildschirm-PlanView (8 px bzw. 14 px bei
 * „insulation", × Maßstab) wird hier in einen mm-Linienabstand übersetzt, damit
 * der Druck der Bildschirm-Dichte nahekommt:
 *   • diagonal/crosshatch: parallele Linienscharen im Winkel `angle`.
 *   • crosshatch: zusätzlich die um 90° gedrehte Schar.
 *   • insulation: vereinfacht als eine dichtere diagonale Schar (echte Wellen-
 *     linie wäre als Pfad möglich; eine Linienschar liest sich im Druck sauber
 *     und bleibt rein vektoriell — offener Punkt im Report).
 */
function hatchLines(polyMm: Vec2[], hatch: HatchRender, mmPerM: number): Seg[] {
  // Kachel der PlanView: 8 viewBox-Einheiten (= px bei dpi-Bezug) × scale. In der
  // PlanView ist 1 viewBox-Einheit = 1/PX_PER_M m = 1/90 m. Auf Papier sind das
  // (1/90)·mmPerM mm. So bleibt die Strich-Dichte massstäblich konsistent.
  const unitMm = (1 / 90) * mmPerM;
  const tileMm = (hatch.pattern === "insulation" ? 10 : 8) * hatch.scale * unitMm;
  const spacing = Math.max(0.5, tileMm); // Mindestabstand, sonst zu dicht

  const angle = (hatch.angle * Math.PI) / 180;
  const segs: Seg[] = [];
  fillParallel(polyMm, angle, spacing, segs);
  if (hatch.pattern === "crosshatch") {
    fillParallel(polyMm, angle + Math.PI / 2, spacing, segs);
  }
  if (hatch.pattern === "insulation") {
    // Zweite, gegenläufige Schar deutet die Dämmungs-Textur an (dichter Filz).
    fillParallel(polyMm, angle - Math.PI / 4, spacing, segs);
  }
  return segs;
}

/**
 * Füllt das Polygon mit parallelen Linien im gegebenen Winkel/Abstand und klippt
 * jede Linie auf das Polygon (Scanline über die Polygon-Schnittpunkte). Robust
 * für konvexe wie konkave (einfache) Polygone.
 */
function fillParallel(
  poly: Vec2[],
  angle: number,
  spacing: number,
  out: Seg[],
): void {
  if (poly.length < 3 || spacing <= 0) return;
  // Linienrichtung d und Normale nrm (Projektionsachse).
  const dir = { x: Math.cos(angle), y: Math.sin(angle) };
  const nrm = { x: -dir.y, y: dir.x };

  // Projektion der Polygonpunkte auf die Normale → [tMin, tMax]-Band der Linien.
  let tMin = Infinity;
  let tMax = -Infinity;
  for (const p of poly) {
    const t = p.x * nrm.x + p.y * nrm.y;
    if (t < tMin) tMin = t;
    if (t > tMax) tMax = t;
  }
  // Linien an t = tMin + i·spacing; für jede die Schnitt-Intervalle ermitteln.
  const start = Math.ceil(tMin / spacing) * spacing;
  for (let t = start; t <= tMax; t += spacing) {
    const xs: number[] = []; // Parameter entlang dir der Schnittpunkte
    for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
      const a = poly[j];
      const b = poly[i];
      const ta = a.x * nrm.x + a.y * nrm.y;
      const tb = b.x * nrm.x + b.y * nrm.y;
      // Schneidet die Kante das Niveau t?
      if (ta === tb) continue;
      const f = (t - ta) / (tb - ta);
      if (f < 0 || f > 1) continue;
      const px = a.x + (b.x - a.x) * f;
      const py = a.y + (b.y - a.y) * f;
      xs.push(px * dir.x + py * dir.y); // Position entlang dir
    }
    if (xs.length < 2) continue;
    xs.sort((u, v) => u - v);
    // Paarweise Intervalle = Polygon-Inneres entlang der Linie.
    for (let k = 0; k + 1 < xs.length; k += 2) {
      const u0 = xs[k];
      const u1 = xs[k + 1];
      if (u1 - u0 < 1e-6) continue;
      // Punkt = t·nrm + u·dir.
      out.push({
        a: { x: nrm.x * t + dir.x * u0, y: nrm.y * t + dir.y * u0 },
        b: { x: nrm.x * t + dir.x * u1, y: nrm.y * t + dir.y * u1 },
      });
    }
  }
}
