// Schnitt-/Ansichts-Pipeline: bindet den analytischen render3d-Schnitt-Extraktor
// (`section.rs`, WASM-Export `cut_section_json`) an das Dokumentmodell an.
//
// Ablauf:
//   1. Aus einer Zeichnungsebene (DrawingLevel, kind "section"/"elevation") wird
//      eine Schnittebene abgeleitet — die Grundriss-Schnittlinie `linePoints`
//      plus `directionSign` (Blickrichtung).
//   2. Das Projekt wird über `projectToModel3d({ layeredWalls: false })` zu
//      Wänden + Decken geflacht und als JSON an `cut_section_json` gereicht —
//      pro Wand EINE Voll-Box (der 3D-Viewport bekommt dagegen je Materiallage
//      eine Box). So bleiben die Cut-Polygone 1:1 zu `wallSegmentOwners`, und
//      die per-Material-Schnitt-Schraffur macht `splitWallLayers` TS-seitig.
//   3. Das zurückgegebene `SectionOutput` (Cut-Polygone + sichtbare/verdeckte
//      Kanten, alles in (u, v)-Metern) wird in `generateSectionPlan`
//      (generatePlan.ts) zu Plan-Primitiven übersetzt.
//
// Einheiten: METER. Koordinaten der Ausgabe: u = horizontal entlang der Ebene,
// v = absolute Höhe (world.y). Siehe src-tauri/render3d/src/section.rs.

import type { Ceiling, DrawingLevel, Project, Vec2, Wall } from "../model/types";
import { getCeilingType, getComponent, getWallType, openingsOfWall } from "../model/types";
import { ceilingVerticalExtent, wallVerticalExtent } from "../model/wall";
import { dot, leftNormal, normalize, sub } from "../model/geometry";
import { openingInterval, openingVerticalExtent } from "../geometry/opening";
import { projectToModel3d } from "./toWalls3d";
import { loadEngine3d } from "../engine/engine3d";
import {
  HATCH_INK,
  HATCH_PAPER,
  resolveCeilingSectionStyle,
  resolveForeground,
  resolveHatch,
  resolveWallSectionStyle,
} from "./generatePlan";
import type { HatchRender } from "./generatePlan";

/** Bauteil-Referenz eines Cut-Polygons/einer Kante (Art + Eingabe-Index). */
export interface SectionComponentRef {
  kind: "wall" | "slab";
  index: number;
}

/** Ein geschnittenes Bauteil-Rechteck in (u, v)-Metern (Ring, implizit geschlossen). */
export interface SectionCutPolygon {
  component: SectionComponentRef;
  /** Albedo-Farbe des Bauteils (RGB 0..1) — Rohwert aus dem 3D-Modell (Fallback). */
  color: [number, number, number];
  /** Ring-Ecken (u, v). */
  pts: Array<[number, number]>;
  /**
   * Echte Poché-Füllfarbe des geschnittenen Bauteils (tragende Wandschicht
   * bzw. erste Deckenschicht), von `computeSection` aufgelöst. Fehlt, wenn das
   * Quell-Bauteil nicht auflösbar war — der Aufrufer (generateSectionPlan)
   * fällt dann auf `color` zurück.
   */
  fill?: string;
  /** Echte Schnitt-Schraffur des geschnittenen Bauteils, analog zu `fill`. */
  hatch?: HatchRender;
  /**
   * Verschneidungs-Rang der Schicht dieses Bands (`Component.joinPriority` des
   * Ursprungs-Bauteils). Steuert die Schnitt-Boolean-Dominanz in
   * `attachCutStyles`: ein Band mit STRIKT höherer Priorität schneidet ein
   * überlappendes schwächeres Band per Rechteck-Subtraktion weg (element-
   * übergreifend, z. B. Decken-Beton schneidet Wand-Putz). Fehlt der Wert
   * (unauflösbares Bauteil), nimmt das Band an keiner Subtraktion teil.
   */
  joinPriority?: number;
  /**
   * Bauteil-Identität der Schicht dieses Bands (`Component.id` des Ursprungs-
   * Bauteils bzw. — bei einschichtiger Poché — des tragenden Bauteils). Steuert
   * die MERGE-Regel in `subtractDominantBands`: berühren/überlappen sich zwei
   * Bänder mit GLEICHER `componentId` UND gleicher `joinPriority`, verschmelzen
   * sie zu einem Körper (keine innere Trennlinie durch gleichartiges Material —
   * analog zu `resolveJoinPriority` "merge" im Grundriss-Join-Modell). Fehlt der
   * Wert (unauflösbares Bauteil), nimmt das Band an keiner Verschmelzung teil.
   */
  componentId?: string;
}

/** Ein projiziertes Liniensegment in (u, v)-Metern. */
export interface SectionEdge {
  component: SectionComponentRef;
  a: [number, number];
  b: [number, number];
}

/** Vollständige Ausgabe eines Schnitt-/Ansichts-Laufs (JSON von `cut_section_json`). */
export interface SectionOutput {
  cutPolygons: SectionCutPolygon[];
  visibleEdges: SectionEdge[];
  hiddenEdges: SectionEdge[];
}

/** Minimal-Typ des WASM-Exports (das echte `.d.ts` entsteht erst beim wasm-Build). */
interface Engine3dSection {
  cut_section_json(
    modelJson: string,
    px: number,
    py: number,
    pz: number,
    nx: number,
    ny: number,
    nz: number,
  ): string;
}

/**
 * Eine Schnittebene in world-Koordinaten (Y-up): Punkt auf der Ebene +
 * Normale (= Blickrichtung, zeigt vom Betrachter ins Modell).
 */
export interface SectionPlaneSpec {
  point: [number, number, number];
  normal: [number, number, number];
}

/**
 * Leitet aus einer Schnitt-/Ansichtsebene die world-Schnittebene ab. Die
 * Grundriss-Schnittlinie `linePoints` [p0, p1] (Modell-Meter) liegt in der
 * XZ-Ebene (world.x = model.x, world.z = model.y); die Ebenennormale steht
 * senkrecht auf der Linie in der Horizontalen und wird über `directionSign`
 * (Voreinstellung +1) in die gewünschte Blickrichtung gedreht.
 *
 * Gibt `null` zurück, wenn die Ebene (noch) keine Schnittlinie trägt (z. B. eine
 * frisch angelegte Ansicht ohne gesetzte Linie) — der Aufrufer zeigt dann einen
 * Hinweis statt eines leeren Plans.
 */
export function sectionPlaneFromLevel(level: DrawingLevel): SectionPlaneSpec | null {
  const line = level.linePoints;
  if (!line) return null;
  const [p0, p1] = line;
  const dx = p1.x - p0.x;
  const dy = p1.y - p0.y;
  const len = Math.hypot(dx, dy);
  if (len < 1e-9) return null;
  const ux = dx / len;
  const uy = dy / len;
  // Links-Normale der Linie im Grundriss (n = (-u.y, u.x)), per directionSign
  // in die Blickrichtung gedreht. Modell (nx, ny) → world (nx, 0, ny).
  const sign = level.directionSign ?? 1;
  const nx = -uy * sign;
  const ny = ux * sign;
  return {
    point: [p0.x, 0, p0.y],
    normal: [nx, 0, ny],
  };
}

/**
 * Die in-plane-u-Weltrichtung der Schnittebene, projiziert in die Grundriss-
 * Ebene (Modell-2D: model.x = world.x, model.y = world.z).
 *
 * Muss EXAKT der u-Achse des WASM-Extraktors entsprechen (`SectionPlane::u_axis`
 * in section.rs): dort `u = normalize(cross(normal, up))` mit up = (0,1,0). Für
 * eine horizontale Normale `[Nx, Ny, Nz]` ist `cross(normal, (0,1,0)) =
 * (-Nz, 0, Nx)`; die Grundriss-Komponenten (x, z) sind also `(-Nz, Nx)`. Damit
 * wächst die Ausgabe-u-Koordinate genau in dieser Modell-Richtung — die Referenz,
 * gegen die die Wand-Aufbaurichtung projiziert wird. (Nicht normiert; für das
 * Vorzeichen des Skalarprodukts irrelevant.)
 */
export function sectionUAxisModel(plane: SectionPlaneSpec): Vec2 {
  const [nx, , nz] = plane.normal;
  return { x: -nz, y: nx };
}

/**
 * Ob die Schicht-Bänder einer Wand im Schnitt gegenüber der Default-Zuordnung
 * (layers[0] → uMin) umzukehren sind, damit sie mit der GRUNDRISS-Poché
 * übereinstimmen (Quelle der Wahrheit: `addWallPoche`/generatePlan).
 *
 * Konvention aus dem Grundriss: die Schichten stapeln entlang der linken Normalen
 * `n = leftNormal(u)` der Wandachse `u = normalize(end − start)`, von layers[0]
 * am kleinsten Offset (−T/2, `−n`-Seite) zu layers[n] am größten Offset (`+n`-
 * Seite). Die Aufbaurichtung layers[0] → layers[n] zeigt also nach `+n`. Der
 * Referenzlinien-Versatz (`wallReferenceOffset`) verschiebt nur die absolute
 * Lage, nicht diese Reihenfolge — daher hier ohne Belang.
 *
 * `splitWallLayers` legt layers[0] per Default an uMin (u wächst entlang der
 * Schnitt-u-Achse `uWorld`). Zeigt die Aufbaurichtung `+n` GEGEN `uWorld`
 * (Skalarprodukt < 0), muss die Band-Reihenfolge gespiegelt werden, damit
 * layers[0] auf der korrekten Weltseite (z. B. Aussenputz aussen) liegt und
 * gegenüberliegende Wände spiegelverkehrt erscheinen.
 */
export function wallLayersReversedInU(owner: Wall, uWorld: Vec2): boolean {
  const axis = sub(owner.end, owner.start);
  if (Math.hypot(axis.x, axis.y) < 1e-9) return false;
  const buildDir = leftNormal(normalize(axis)); // +n: layers[0] → layers[n]
  return dot(buildDir, uWorld) < 0;
}

// ── Cut-Polygon → Quell-Bauteil (für die echte Hatch-Auflösung) ────────────
// `cut_section_json` (section.rs) nummeriert jedes Cut-Polygon per
// `ComponentRef { kind, index }`, wobei `index` exakt die Position im
// `walls`/`slabs`-Array ist, das ihm als Modell-JSON übergeben wurde
// (`walls.iter().enumerate()` in section.rs) — also dieselbe Reihenfolge wie
// `projectToModel3d(project).walls`/`.slabs` (toWalls3d.ts).
//
// `projectToWalls3d` zerlegt EINE Wand mit Öffnungen in mehrere Teilquader
// (Pfeiler/Brüstung/Sturz) — die Emission ist also nicht 1:1 Wand→Element.
// Um einen `index` wieder auf seine Quellwand zurückzuführen, wird hier exakt
// dieselbe Segmentierungs-/Skip-Logik wie `emitWall`/`pushSegment`
// (toWalls3d.ts) repliziert — NICHT die Geometrie, nur welches Quellelement
// pro emittiertem Segment "besitzt". Bleibt diese Zählung nicht exakt parallel
// zu `emitWall`, verschieben sich die Indizes und die Hatch-Auflösung wird
// falsch (harmlos: der Aufrufer fällt dann pro Cut-Polygon auf die generische
// Schnitt-Schraffur zurück, siehe `resolveWallSectionStyle`/`generatePlan.ts`).
const OWNER_EPS = 1e-4;

/** Liefert je emittiertem Wand-Teilquader (in Emissions-Reihenfolge) die Quellwand. */
export function wallSegmentOwners(project: Project): Wall[] {
  const owners: Wall[] = [];
  const pushOwner = (wall: Wall, from: number, to: number, zBottom: number, zTop: number) => {
    if (to - from <= OWNER_EPS) return;
    if (zTop - zBottom <= OWNER_EPS) return;
    owners.push(wall);
  };
  for (const wall of project.walls) {
    const { zBottom, zTop } = wallVerticalExtent(project, wall);
    const axisLen = Math.hypot(wall.end.x - wall.start.x, wall.end.y - wall.start.y);
    if (axisLen < 1e-9 || zTop - zBottom <= OWNER_EPS) continue;

    const cutouts: Array<{ from: number; to: number; oBottom: number; oTop: number }> = [];
    for (const op of openingsOfWall(project, wall.id)) {
      const iv = openingInterval(wall, op);
      if (!iv) continue;
      const v = openingVerticalExtent(project, wall, op);
      cutouts.push({ from: iv.from, to: iv.to, oBottom: v.zBottom, oTop: v.zTop });
    }
    for (const d of project.doors ?? []) {
      if (d.hostWallId !== wall.id) continue;
      const from = Math.max(0, Math.min(d.position, axisLen));
      const to = Math.max(from, Math.min(d.position + d.width, axisLen));
      if (to - from < OWNER_EPS) continue;
      const oTop = Math.min(zTop, zBottom + d.height);
      cutouts.push({ from, to, oBottom: zBottom, oTop });
    }
    cutouts.sort((a, b) => a.from - b.from);

    if (cutouts.length === 0) {
      pushOwner(wall, 0, axisLen, zBottom, zTop);
      continue;
    }

    let cursor = 0;
    for (const { from, to, oBottom, oTop } of cutouts) {
      const segFrom = Math.max(cursor, from);
      if (from > cursor) pushOwner(wall, cursor, from, zBottom, zTop);
      if (to > segFrom) {
        if (oBottom > zBottom + OWNER_EPS) pushOwner(wall, segFrom, to, zBottom, oBottom);
        if (oTop < zTop - OWNER_EPS) pushOwner(wall, segFrom, to, oTop, zTop);
      }
      cursor = Math.max(cursor, to);
    }
    if (cursor < axisLen) pushOwner(wall, cursor, axisLen, zBottom, zTop);
  }
  return owners;
}

/** Liefert je emittierter Deckenplatte (in Emissions-Reihenfolge) die Quelldecke (1:1, keine Zerlegung). */
export function slabSegmentOwners(project: Project): Ceiling[] {
  const owners: Ceiling[] = [];
  for (const c of project.ceilings ?? []) {
    if (!c.outline || c.outline.length < 3) continue;
    const { zBottom, zTop } = ceilingVerticalExtent(project, c);
    if (zTop - zBottom <= OWNER_EPS) continue;
    owners.push(c);
  }
  return owners;
}

/**
 * Hängt an jedes Cut-Polygon von `output` (in-place) die echte Poché-Füllfarbe
 * + Schraffur des geschnittenen Bauteils an (`fill`/`hatch`), aufgelöst über
 * die Wand-/Decken-Besitzer-Listen. Nicht auflösbare Referenzen (Index
 * außerhalb der Besitzer-Liste, verwaistes `wallTypeId`/Bauteil) bleiben ohne
 * `fill`/`hatch` — `generateSectionPlan` fällt dafür auf die generische
 * Schnitt-Schraffur zurück.
 */
function attachCutStyles(output: SectionOutput, project: Project, plane: SectionPlaneSpec): void {
  if (output.cutPolygons.length === 0) return;
  // Schnitt-u-Weltachse (Modell-2D) — Referenz für die Aussen/Innen-Orientierung
  // der mehrschichtigen Wand-Bänder (siehe `wallLayersReversedInU`).
  const uWorld = sectionUAxisModel(plane);
  const walls = wallSegmentOwners(project);
  const slabs = slabSegmentOwners(project);

  // Phase 1 — ALLE Cut-Bänder (aus allen Wänden UND Decken, mehr- wie einschichtig)
  // als flache Liste sammeln, jedes mit seiner `joinPriority` getaggt (aus dem
  // Bauteil der Schicht bzw. dem repräsentativen Bauteil bei einschichtig).
  const bands: SectionCutPolygon[] = [];
  for (const cp of output.cutPolygons) {
    const ref = cp.component;
    if (ref.kind === "wall") {
      const owner = walls[ref.index];
      // Wand: bei mehrschichtigem Wandtyp wird das EINE geschnittene Rechteck in
      // stehende Teil-Rechtecke je Schicht quer zur Dicke (u) aufgeteilt — das
      // vertikale Gegenstück zur Decken-Zerlegung (dort quer zur Höhe v), siehe
      // `splitWallLayers`. Einschichtige Wände laufen unverändert über
      // `resolveWallSectionStyle` (ein repräsentatives Bauteil).
      if (owner) {
        const wt = getWallType(project, owner);
        if (wt.layers.length > 1) {
          bands.push(...splitWallLayers(cp, project, wt, owner, uWorld));
          continue;
        }
      }
      const style = owner ? resolveWallSectionStyle(project, owner) : null;
      if (style) {
        cp.fill = style.fill;
        cp.hatch = style.hatch;
      }
      cp.joinPriority = owner ? wallRepresentativePriority(project, owner) : undefined;
      cp.componentId = owner ? wallRepresentativeComponentId(project, owner) : undefined;
      bands.push(cp);
      continue;
    }
    // Decke: bei mehrschichtigem Deckentyp wird das EINE geschnittene Rechteck
    // (von der WASM-Schnitt-Extraktion) in liegende Teil-Rechtecke je Schicht
    // aufgeteilt (oben/OK → unten/UK) — siehe `splitSlabLayers`. Nur im
    // Schnitt sichtbar (im Grundriss bleibt die Decke bewusst flächig, siehe
    // `addCeilingPoche`).
    const owner = slabs[ref.index];
    if (owner) {
      const wt = getCeilingType(project, owner);
      if (wt.layers.length > 1) {
        bands.push(...splitSlabLayers(cp, project, wt));
        continue;
      }
      const style = resolveCeilingSectionStyle(project, owner);
      if (style) {
        cp.fill = style.fill;
        cp.hatch = style.hatch;
      }
      cp.joinPriority = ceilingRepresentativePriority(project, owner);
      cp.componentId = ceilingRepresentativeComponentId(project, owner);
    }
    bands.push(cp);
  }

  // Phase 2 — Boolean-Dominanz: die stärkere Schicht schneidet die schwächere.
  output.cutPolygons = subtractDominantBands(bands);
}

/**
 * Repräsentative `joinPriority` einer Wand für die Schnitt-Dominanz, wenn ihre
 * Poché einschichtig (ein Band über die volle Dicke) gezeichnet wird: die
 * HÖCHSTE joinPriority ihrer Schichten — dieselbe Regel, mit der
 * `resolveWallSectionStyle` das tragende Bauteil (dessen Füllung/Schraffur das
 * Band trägt) wählt. `undefined`, wenn der Wandtyp/keine Schicht auflösbar ist
 * (das Band nimmt dann an keiner Subtraktion teil).
 */
function wallRepresentativePriority(project: Project, wall: Wall): number | undefined {
  try {
    const wt = getWallType(project, wall);
    let best: number | undefined;
    for (const layer of wt.layers) {
      const p = getComponent(project, layer.componentId).joinPriority;
      if (best === undefined || p > best) best = p;
    }
    return best;
  } catch {
    return undefined;
  }
}

/**
 * Bauteil-Identität einer einschichtig gezeichneten Wand-Poché für die MERGE-
 * Regel: die `id` des tragenden Bauteils (höchste joinPriority) — exakt das
 * Bauteil, dessen Füllung/Schraffur `resolveWallSectionStyle` dem Band gibt und
 * dessen `joinPriority` `wallRepresentativePriority` liefert. `undefined`, wenn
 * Wandtyp/keine Schicht auflösbar ist.
 */
function wallRepresentativeComponentId(project: Project, wall: Wall): string | undefined {
  try {
    const wt = getWallType(project, wall);
    let bestId: string | undefined;
    let bestPrio = -Infinity;
    for (const layer of wt.layers) {
      const p = getComponent(project, layer.componentId).joinPriority;
      if (bestId === undefined || p > bestPrio) {
        bestId = layer.componentId;
        bestPrio = p;
      }
    }
    return bestId;
  } catch {
    return undefined;
  }
}

/**
 * Bauteil-Identität einer einschichtig gezeichneten Decken-Poché für die MERGE-
 * Regel: die `id` der ERSTEN Schicht — dasselbe Bauteil, das
 * `resolveCeilingSectionStyle`/`ceilingRepresentativePriority` verwenden.
 * `undefined`, wenn Deckentyp/keine Schicht auflösbar ist.
 */
function ceilingRepresentativeComponentId(project: Project, ceiling: Ceiling): string | undefined {
  try {
    const wt = getCeilingType(project, ceiling);
    const first = wt.layers[0];
    if (!first) return undefined;
    return first.componentId;
  } catch {
    return undefined;
  }
}

/**
 * Repräsentative `joinPriority` einer Decke für die Schnitt-Dominanz, wenn ihre
 * Poché einschichtig (ein Band über die volle Dicke) gezeichnet wird: die der
 * ERSTEN Schicht — dasselbe Bauteil, dessen Füllung/Schraffur
 * `resolveCeilingSectionStyle` dem Band gibt. `undefined`, wenn Deckentyp/keine
 * Schicht auflösbar ist.
 */
function ceilingRepresentativePriority(project: Project, ceiling: Ceiling): number | undefined {
  try {
    const wt = getCeilingType(project, ceiling);
    const first = wt.layers[0];
    if (!first) return undefined;
    return getComponent(project, first.componentId).joinPriority;
  } catch {
    return undefined;
  }
}

// ── Schnitt-Boolean-Dominanz (achsparallele Rechteck-Subtraktion) ───────────
// Kleine Toleranz für Überlappungs-/Rest-Rechtecke: Null-/Negativflächen und
// Rest-Streifen dünner als EPS werden verworfen (Float-Rauschen der u-/v-Meter).
const RECT_EPS = 1e-6;

/** Achsparalleles Rechteck in (u, v)-Metern. */
export interface Rect {
  uMin: number;
  uMax: number;
  vMin: number;
  vMax: number;
}

/** Bounding-Box eines (achsparallelen) Cut-Bands als `Rect`, oder `null` bei <3 Ecken. */
function rectOfBand(band: SectionCutPolygon): Rect | null {
  if (band.pts.length < 3) return null;
  const us = band.pts.map((p) => p[0]);
  const vs = band.pts.map((p) => p[1]);
  return {
    uMin: Math.min(...us),
    uMax: Math.max(...us),
    vMin: Math.min(...vs),
    vMax: Math.max(...vs),
  };
}

/**
 * Rechteck-minus-Rechteck (beide achsparallel): `base` ohne die Überlappung mit
 * `cutter`, zerlegt in 0–4 disjunkte Rest-Rechtecke (links/rechts über die volle
 * Höhe, dann oben/unten im mittleren u-Streifen — überlappungsfrei). Kein echter
 * Überlapp (Fläche ≤ EPS in u oder v) ⇒ `base` bleibt ganz. Vollständige
 * Überdeckung ⇒ leere Liste. Rest-Streifen dünner als EPS werden verworfen.
 */
export function subtractRect(base: Rect, cutter: Rect): Rect[] {
  const oMinU = Math.max(base.uMin, cutter.uMin);
  const oMaxU = Math.min(base.uMax, cutter.uMax);
  const oMinV = Math.max(base.vMin, cutter.vMin);
  const oMaxV = Math.min(base.vMax, cutter.vMax);
  // Kein (nennenswerter) Überlapp — `base` unverändert.
  if (oMaxU - oMinU <= RECT_EPS || oMaxV - oMinV <= RECT_EPS) return [base];

  const out: Rect[] = [];
  const push = (r: Rect) => {
    if (r.uMax - r.uMin > RECT_EPS && r.vMax - r.vMin > RECT_EPS) out.push(r);
  };
  // Links / rechts der Überlappung, jeweils über die volle Basis-Höhe.
  push({ uMin: base.uMin, uMax: oMinU, vMin: base.vMin, vMax: base.vMax });
  push({ uMin: oMaxU, uMax: base.uMax, vMin: base.vMin, vMax: base.vMax });
  // Unten / oben im mittleren u-Streifen (nur der von der Überlappung befreite Rest).
  push({ uMin: oMinU, uMax: oMaxU, vMin: base.vMin, vMax: oMinV });
  push({ uMin: oMinU, uMax: oMaxU, vMin: oMaxV, vMax: base.vMax });
  return out;
}

/**
 * Globale Schnitt-Dominanz: für jedes Band wird die Rechteck-Überlappung ALLER
 * Bänder mit STRIKT höherer `joinPriority` subtrahiert (elementübergreifend —
 * z. B. schneidet ein Decken-Beton-Band 100 ein überlappendes Wand-Putz-Band 10
 * weg). Gleiche Priorität schneidet NICHT (koexistiert → durchgehende Fläche).
 * Bänder ohne `joinPriority` (unauflösbares Bauteil) nehmen weder als Basis noch
 * als Cutter teil und bleiben unverändert erhalten.
 *
 * Kanten: jedes emittierte Rest-Rechteck erhält in `generateSectionPlan` seine
 * eigene Schnitt-Umrisslinie (unverändert). An der Schnittkante fällt die
 * Rest-Rechteck-Kante mit der Kante des stärkeren Bands zusammen — das ist die
 * gewünschte Material-/Fugenlinie zwischen den Schichten; im Inneren einer
 * Schicht entstehen durch die überlappungsfreie Zerlegung keine falschen Kanten.
 */
export function subtractDominantBands(bands: SectionCutPolygon[]): SectionCutPolygon[] {
  const rects = bands.map(rectOfBand);
  const out: SectionCutPolygon[] = [];
  for (let i = 0; i < bands.length; i++) {
    const band = bands[i];
    const base = rects[i];
    const prio = band.joinPriority;
    // Nicht-rechteckig oder ohne Priorität: unverändert durchreichen.
    if (base === null || prio === undefined) {
      out.push(band);
      continue;
    }
    let pieces: Rect[] = [base];
    for (let j = 0; j < bands.length && pieces.length > 0; j++) {
      if (j === i) continue;
      const otherPrio = bands[j].joinPriority;
      const cutter = rects[j];
      if (cutter === null || otherPrio === undefined || otherPrio <= prio) continue;
      const next: Rect[] = [];
      for (const p of pieces) next.push(...subtractRect(p, cutter));
      pieces = next;
    }
    for (const p of pieces) out.push(bandFromRect(band, p));
  }
  // MERGE-Regel: nach der Dominanz-Subtraktion angrenzende/überlappende Bänder
  // gleicher Komponente + Priorität zu einem Körper verschmelzen (keine innere
  // Trennlinie durch gleichartiges Material). Die Subtraktion oben bleibt davon
  // unberührt (bit-identisch) — hier werden nur gleichartige Reste vereinigt.
  return mergeSameComponentBands(out);
}

/**
 * Union zweier achsparalleler Rechtecke, ABER NUR wenn das Ergebnis EXAKT wieder
 * ein Rechteck ist — sonst `null`. Zwei Fälle liefern eine rechteckige Union
 * (jeweils mit `RECT_EPS`-Toleranz):
 *   • Enthaltensein: eines liegt vollständig im anderen ⇒ das umschließende.
 *   • Achs-Deckung + Berührung: in einer Achse deckungsgleich (gleiche min/max)
 *     UND in der anderen berührend/überlappend (kein Spalt) ⇒ Bounding-Box.
 * Teil-/Eck-Überlappungen (Union wäre L-förmig) und Bänder mit Spalt liefern
 * `null` — sie dürfen NICHT verschmolzen werden (die Bounding-Box deckte sonst
 * leere Fläche ab). Genau die im Schnitt vorkommenden bündigen Rechtecke.
 */
function rectUnionIfRect(a: Rect, b: Rect): Rect | null {
  const E = RECT_EPS;
  // Enthaltensein (inkl. Deckungsgleichheit): Union = umschließendes Rechteck.
  const aInB =
    b.uMin - a.uMin <= E && a.uMax - b.uMax <= E && b.vMin - a.vMin <= E && a.vMax - b.vMax <= E;
  if (aInB) return { uMin: b.uMin, uMax: b.uMax, vMin: b.vMin, vMax: b.vMax };
  const bInA =
    a.uMin - b.uMin <= E && b.uMax - a.uMax <= E && a.vMin - b.vMin <= E && b.vMax - a.vMax <= E;
  if (bInA) return { uMin: a.uMin, uMax: a.uMax, vMin: a.vMin, vMax: a.vMax };

  const uAligned = Math.abs(a.uMin - b.uMin) <= E && Math.abs(a.uMax - b.uMax) <= E;
  const vAligned = Math.abs(a.vMin - b.vMin) <= E && Math.abs(a.vMax - b.vMax) <= E;
  // Berührung/Überlappung in der jeweils ANDEREN Achse (kein Spalt größer EPS).
  const vTouch = a.vMax >= b.vMin - E && b.vMax >= a.vMin - E;
  const uTouch = a.uMax >= b.uMin - E && b.uMax >= a.uMin - E;
  if ((uAligned && vTouch) || (vAligned && uTouch)) {
    return {
      uMin: Math.min(a.uMin, b.uMin),
      uMax: Math.max(a.uMax, b.uMax),
      vMin: Math.min(a.vMin, b.vMin),
      vMax: Math.max(a.vMax, b.vMax),
    };
  }
  return null;
}

/**
 * MERGE-Phase der Schnitt-Dominanz (vgl. `resolveJoinPriority` "merge",
 * src/model/joins.ts): Bänder mit GLEICHER `componentId` UND gleicher
 * `joinPriority`, deren achsparallele Rechtecke sich berühren/überlappen und
 * eine rechteckige Union bilden (`rectUnionIfRect`), werden zu EINEM Band
 * zusammengefasst — so entsteht an der Berührkante keine innere Trennlinie durch
 * gleichartiges Material (z. B. zwei Backstein-Kerne, Beton-Decke an Beton-Wand).
 *
 * Bänder VERSCHIEDENER Komponenten (auch bei gleicher Priorität) bleiben
 * getrennt (coexist). Bänder ohne `componentId`/`joinPriority` oder ohne
 * Rechteck-Form nehmen nicht teil und werden unverändert durchgereicht — daher
 * ist die Phase für die bestehenden Dominanz-Tests (keine `componentId`) ein
 * No-op. Iterativ bis zum Fixpunkt (Ketten aus ≥3 Rechtecken verschmelzen
 * schrittweise); die Reihenfolge bleibt erhalten (das verschmolzene Rechteck
 * ersetzt das erste Band an seiner Position).
 */
function mergeSameComponentBands(bands: SectionCutPolygon[]): SectionCutPolygon[] {
  const out = bands.slice();
  for (let merged = true; merged; ) {
    merged = false;
    for (let i = 0; i < out.length && !merged; i++) {
      const bi = out[i];
      if (bi.componentId === undefined || bi.joinPriority === undefined) continue;
      const ri = rectOfBand(bi);
      if (!ri) continue;
      for (let j = i + 1; j < out.length; j++) {
        const bj = out[j];
        if (bj.componentId !== bi.componentId || bj.joinPriority !== bi.joinPriority) continue;
        const rj = rectOfBand(bj);
        if (!rj) continue;
        const u = rectUnionIfRect(ri, rj);
        if (!u) continue;
        out[i] = bandFromRect(bi, u);
        out.splice(j, 1);
        merged = true; // Fixpunkt-Neustart: neu entstandene Nachbarschaften erfassen
        break;
      }
    }
  }
  return out;
}

/** Erzeugt aus einem Rest-`Rect` ein Cut-Band, das Stil/Referenz von `src` erbt. */
function bandFromRect(src: SectionCutPolygon, r: Rect): SectionCutPolygon {
  return {
    component: src.component,
    color: src.color,
    pts: [
      [r.uMin, r.vMax],
      [r.uMax, r.vMax],
      [r.uMax, r.vMin],
      [r.uMin, r.vMin],
    ],
    fill: src.fill,
    hatch: src.hatch,
    joinPriority: src.joinPriority,
    componentId: src.componentId,
  };
}

/**
 * Zerlegt das EINE geschnittene Deckenrechteck (`cp`, vier Ecken in u/v-Metern)
 * in liegende Teil-Rechtecke je Schicht des Deckentyps `wt` — das Schnitt-
 * Gegenstück zur mehrschichtigen Wand-Poché (dort im Grundriss als Bänder quer
 * zur Achse; hier im Schnitt als Bänder über die Höhe v, oben/OK → unten/UK,
 * exakt wie der liegende Aufbau physisch stapelt). Die Modell-Schichtdicken
 * werden proportional auf die tatsächlich geschnittene Höhe skaliert (falls
 * `Ceiling.thickness` die Typ-Summe übersteuert). Jedes Teil-Rechteck trägt
 * Füllung + Schraffur seines Bauteils; die Fugen ergeben sich implizit aus den
 * aneinanderstoßenden Rechtecken (jedes bekommt in `generateSectionPlan` die
 * generische Schnitt-Umrisslinie).
 */
function splitSlabLayers(
  cp: SectionCutPolygon,
  project: Project,
  wt: ReturnType<typeof getCeilingType>,
): SectionCutPolygon[] {
  const totalT = wt.layers.reduce((s, l) => s + l.thickness, 0);
  const us = cp.pts.map((p) => p[0]);
  const vs = cp.pts.map((p) => p[1]);
  const uMin = Math.min(...us);
  const uMax = Math.max(...us);
  const vMin = Math.min(...vs);
  const vMax = Math.max(...vs); // vMax = OK (oben), vMin = UK (unten)
  const vSpan = vMax - vMin;
  if (totalT <= 1e-9 || vSpan <= 1e-9) return [cp];
  const scale = vSpan / totalT;

  const out: SectionCutPolygon[] = [];
  let acc = 0; // kumulierte Dicke von OK (oben) nach UK (unten)
  for (const layer of wt.layers) {
    const comp = getComponent(project, layer.componentId);
    const hatch = resolveHatch(project, comp.hatchId, undefined, resolveForeground(comp));
    const vTop = vMax - acc * scale;
    acc += layer.thickness;
    const vBottom = vMax - acc * scale;
    out.push({
      component: cp.component,
      color: cp.color,
      pts: [
        [uMin, vTop],
        [uMax, vTop],
        [uMax, vBottom],
        [uMin, vBottom],
      ],
      fill: hatch.pattern === "solid" ? HATCH_INK : HATCH_PAPER,
      hatch,
      joinPriority: comp.joinPriority,
      componentId: comp.id,
    });
  }
  return out;
}

/**
 * Achswinkel einer GESCHNITTENEN Wand im Schnitt-Koordinatensystem (Modell-
 * Konvention wie `wallAngleDeg` in resolveHatch): das Wand-Band steht vertikal
 * (Achse entlang v, atan2(1,0) = 90°). Damit drehen wandrelative Muster
 * (Dämmung, Backstein) im Schnitt genauso mit der Wand wie im Grundriss.
 */
const SECTION_WALL_AXIS_ANGLE_DEG = 90;

/**
 * Zerlegt das EINE geschnittene Wandrechteck (`cp`, vier Ecken in u/v-Metern) in
 * stehende Teil-Rechtecke je Schicht des Wandtyps `wt` — das Wand-Gegenstück zu
 * `splitSlabLayers`. Achsen-Spiegelung: eine Wand ist vertikal geschnitten, das
 * Schnittrechteck hat die volle HÖHE in v (vMin..vMax) und spannt über die DICKE
 * in u (uMin..uMax); die Schichten stapeln quer zur Dicke, also entlang u — im
 * Gegensatz zur Decke, die quer zur Höhe (v) geschichtet ist. v bleibt darum je
 * Band voll, u wird proportional zu den Schichtdicken zerlegt. Die Modell-
 * Schichtdicken werden auf die tatsächlich geschnittene Dicke (uSpan) skaliert.
 * Jedes Teil-Rechteck trägt Füllung + Schnitt-Schraffur seines Bauteils; die
 * Fugen ergeben sich implizit aus den aneinanderstoßenden Rechtecken.
 *
 * Reihenfolge/Orientierung: `WallType.layers` ist von einer Wandseite zur anderen
 * geordnet (layers[0] = Aussenseite). Welche u-Seite des Schnittrechtecks der
 * Aussenseite entspricht, folgt NICHT aus dem (u, v)-Rechteck allein, sondern aus
 * der Wandgeometrie + der Schnitt-u-Weltachse `uWorld`: `wallLayersReversedInU`
 * projiziert die Grundriss-Aufbaurichtung (`+leftNormal` der Achse, dieselbe
 * Konvention wie `addWallPoche`/generatePlan) auf `uWorld`. Ist das Vorzeichen
 * negativ, wird die Band-Reihenfolge gespiegelt (layers[0] → uMax statt uMin), so
 * dass Schnitt und Grundriss übereinstimmen und gegenüberliegende Wände spiegel-
 * verkehrt erscheinen (Dämmung aussen, Backstein innen).
 *
 * Annahme: rechteckiges Schnittpolygon (min/max-Bounding-Box auf `cp.pts`,
 * identisch zu `splitSlabLayers`).
 */
export function splitWallLayers(
  cp: SectionCutPolygon,
  project: Project,
  wt: ReturnType<typeof getWallType>,
  owner: Wall,
  uWorld: Vec2,
): SectionCutPolygon[] {
  const totalT = wt.layers.reduce((s, l) => s + l.thickness, 0);
  const us = cp.pts.map((p) => p[0]);
  const vs = cp.pts.map((p) => p[1]);
  const uMin = Math.min(...us);
  const uMax = Math.max(...us);
  const vMin = Math.min(...vs);
  const vMax = Math.max(...vs);
  const uSpan = uMax - uMin;
  if (totalT <= 1e-9 || uSpan <= 1e-9) return [cp];
  const scale = uSpan / totalT;

  // Aussen/Innen-Orientierung: per Default liegt layers[0] an uMin; zeigt die
  // Wand-Aufbaurichtung gegen die Schnitt-u-Achse, werden die Bänder gespiegelt.
  const layers = wallLayersReversedInU(owner, uWorld) ? [...wt.layers].reverse() : wt.layers;

  const out: SectionCutPolygon[] = [];
  let acc = 0; // kumulierte Dicke von uMin (erste einsortierte Schicht) nach uMax
  for (const layer of layers) {
    const comp = getComponent(project, layer.componentId);
    // Wandrelative Muster (relativeToWall, z. B. Dämmung/Backstein) folgen der
    // Wandachse — im Schnitt steht die geschnittene Wand VERTIKAL (Bandachse
    // entlang v, Modellwinkel 90°). Wie im Grundriss (`addWallPoche` übergibt
    // die Wandachse) wird der Achswinkel mitgegeben, sonst liefe die Dämmung
    // vertikal statt quer zur Wanddicke (horizontal).
    const hatch = resolveHatch(project, comp.hatchId, SECTION_WALL_AXIS_ANGLE_DEG, resolveForeground(comp));
    const uLeft = uMin + acc * scale;
    acc += layer.thickness;
    const uRight = uMin + acc * scale;
    out.push({
      component: cp.component,
      color: cp.color,
      pts: [
        [uLeft, vMax],
        [uRight, vMax],
        [uRight, vMin],
        [uLeft, vMin],
      ],
      fill: hatch.pattern === "solid" ? HATCH_INK : HATCH_PAPER,
      hatch,
      joinPriority: comp.joinPriority,
      componentId: comp.id,
    });
  }
  return out;
}

/**
 * Berechnet den Schnitt/die Ansicht der Ebene `level` gegen das Modell `project`
 * über den WASM-Extraktor. Asynchron (das render3d-WASM-Modul wird lazy geladen).
 *
 * Gibt `null` zurück, wenn die Ebene keine gültige Schnittlinie hat. Wirft, wenn
 * das WASM-Modul den Export (noch) nicht bereitstellt — der Aufrufer fängt das ab
 * und blendet einen Fallback ein (pkg3d muss dann über `npm run build:engine3d`
 * neu gebaut werden, damit `cut_section_json` verfügbar ist).
 */
export async function computeSection(
  project: Project,
  level: DrawingLevel,
): Promise<SectionOutput | null> {
  const plane = sectionPlaneFromLevel(level);
  if (!plane) return null;
  const mod = (await loadEngine3d()) as Engine3dSection;
  if (typeof mod.cut_section_json !== "function") {
    throw new Error(
      "render3d-WASM ohne cut_section_json — pkg3d neu bauen (npm run build:engine3d)",
    );
  }
  // Schnitt-Eingabe: EINE Voll-Box pro Wand (layeredWalls:false), damit die
  // Cut-Polygone 1:1 zu `wallSegmentOwners` (Öffnungs-Segmentierung) liegen und
  // `splitWallLayers` die materialspezifischen Schnitt-Bänder korrekt aus dem
  // EINEN Wandrechteck erzeugt. Der 3D-Viewer bekommt weiter per-Schicht-Boxen.
  const modelJson = JSON.stringify(projectToModel3d(project, { layeredWalls: false }));
  const json = mod.cut_section_json(
    modelJson,
    plane.point[0],
    plane.point[1],
    plane.point[2],
    plane.normal[0],
    plane.normal[1],
    plane.normal[2],
  );
  const output = JSON.parse(json) as SectionOutput;
  attachCutStyles(output, project, plane);
  return output;
}
