// Geometrie-Helfer für Öffnungen (Fenster/Türen) — reine Funktionen, die eine
// `Opening` relativ zu ihrer Wirts-Wand in konkrete Welt-Geometrie auflösen:
//   • die Öffnungs-Lücke entlang der Wandachse (from/to in Metern),
//   • das Türblatt (Scharnier → offenes/geschlossenes Ende) + Schwenkbogen,
//   • die Fenster-Rahmen-/Glaslinien quer über die Lücke,
//   • die vertikale Lage (UK/OK der Öffnung, absolut in Metern).
//
// Da JEDE Größe aus der aktuellen Wandachse (`wall.start`/`wall.end`) abgeleitet
// wird, folgt eine Öffnung ihrer Wand automatisch, sobald die Wand bewegt wird —
// es gibt keine eingebackenen Weltkoordinaten.
//
// Bezeichner englisch, Kommentare deutsch (CONVENTIONS.md).

import type { Opening, Project, SashDef, Vec2, Wall } from "../model/types";
import { getWallType, wallTypeThickness } from "../model/types";
import { add, along, leftNormal, normalize, scale, sub } from "../model/geometry";
import { wallReferenceOffset, wallVerticalExtent } from "../model/wall";

const DEG = Math.PI / 180;

/** Achslänge einer Wand (Meter). */
export function wallAxisLength(wall: Wall): number {
  return Math.hypot(wall.end.x - wall.start.x, wall.end.y - wall.start.y);
}

/**
 * Öffnungs-Intervall entlang der Wandachse (from..to in Metern), auf die
 * Achslänge geklemmt. Liefert null, wenn das Intervall entartet (Breite ≤ 0 oder
 * vollständig außerhalb der Achse).
 */
export function openingInterval(
  wall: Wall,
  o: Opening,
): { from: number; to: number } | null {
  const axis = wallAxisLength(wall);
  const from = Math.max(0, Math.min(o.position, axis));
  const to = Math.max(from, Math.min(o.position + o.width, axis));
  if (to - from < 1e-4) return null;
  return { from, to };
}

/**
 * Vertikale Lage einer Öffnung (absolute Z-Werte in Metern) relativ zur Wand-UK.
 * Türen sitzen am Boden (sillHeight 0); Fenster ab Brüstung. Die OK wird auf die
 * Wand-OK geklemmt, damit die Öffnung nie über den Wandkopf hinausragt.
 */
export function openingVerticalExtent(
  project: Project,
  wall: Wall,
  o: Opening,
): { zBottom: number; zTop: number } {
  const { zBottom, zTop } = wallVerticalExtent(project, wall);
  const sill = Math.max(0, o.sillHeight);
  const oBottom = zBottom + sill;
  const oTop = Math.min(zTop, oBottom + o.height);
  return { zBottom: oBottom, zTop: oTop };
}

/** Einheits-Laufrichtung (u) und linke Normale (n) der Wandachse. */
export function wallAxisFrame(wall: Wall): { u: Vec2; n: Vec2 } {
  const u = normalize(sub(wall.end, wall.start));
  return { u, n: leftNormal(u) };
}

/** Die beiden Pfosten-Punkte (auf der Achse) einer Öffnung im Grundriss. */
export function openingJambs(
  wall: Wall,
  o: Opening,
): { jambStart: Vec2; jambEnd: Vec2 } | null {
  const iv = openingInterval(wall, o);
  if (!iv) return null;
  return {
    jambStart: along(wall.start, wall.end, iv.from),
    jambEnd: along(wall.start, wall.end, iv.to),
  };
}

/**
 * Die Lücken-Rechteck-Ecken (über die volle Wanddicke), z. B. um im Plan die
 * Öffnung als Aussparung zu markieren (Auswahl-Highlight). Reihenfolge:
 * jambStart(+n/2) → jambEnd(+n/2) → jambEnd(−n/2) → jambStart(−n/2).
 */
export function openingGapQuad(project: Project, wall: Wall, o: Opening): Vec2[] | null {
  const jambs = openingJambs(wall, o);
  if (!jambs) return null;
  const { n } = wallAxisFrame(wall);
  const total = wallTypeThickness(getWallType(project, wall));
  const refOff = wallReferenceOffset(wall, total);
  const outer = refOff + total / 2;
  const inner = refOff - total / 2;
  const { jambStart, jambEnd } = jambs;
  return [
    add(jambStart, scale(n, outer)),
    add(jambEnd, scale(n, outer)),
    add(jambEnd, scale(n, inner)),
    add(jambStart, scale(n, inner)),
  ];
}

/** Mittelpunkt einer Öffnung im Grundriss (Achse) — für HUD/Auswahl-Anker. */
export function openingCenter(wall: Wall, o: Opening): Vec2 | null {
  const iv = openingInterval(wall, o);
  if (!iv) return null;
  return along(wall.start, wall.end, (iv.from + iv.to) / 2);
}

// ── Tür-Symbol (Plan) ────────────────────────────────────────────────────────

/** Aufgelöste Tür-Symbol-Geometrie im Grundriss (Weltkoordinaten). */
export interface DoorSymbol {
  /** Scharnierpunkt (auf der Achse). */
  hinge: Vec2;
  /** Ende des Blatts in GEÖFFNETER Stellung. */
  openEnd: Vec2;
  /** Ende des Blatts in GESCHLOSSENER Stellung (Bogen-Startpunkt). */
  closedEnd: Vec2;
  /** Radius (= Türbreite) des Schwenkbogens. */
  radius: number;
  /** Beide Pfosten-Punkte (für Rahmen-/Anschlagstriche). */
  jambStart: Vec2;
  jambEnd: Vec2;
  /** Achs-Normale (für kurze Anschlagstriche quer zur Wand). */
  normal: Vec2;
}

/**
 * Löst die Tür-Symbolik auf. `swing` wählt die Aufschlagseite (linke/rechte
 * Achs-Normale), `openingDir` ("out") spiegelt sie auf die andere Seite,
 * `hinge` wählt den Scharnier-Pfosten, `swingAngle` (Grad, Default 90) bestimmt,
 * wie weit das Blatt in der offenen Stellung aufsteht.
 */
export function doorSymbol(wall: Wall, o: Opening): DoorSymbol | null {
  const jambs = openingJambs(wall, o);
  if (!jambs) return null;
  const { u, n } = wallAxisFrame(wall);
  const { jambStart, jambEnd } = jambs;
  const width = Math.hypot(jambEnd.x - jambStart.x, jambEnd.y - jambStart.y);

  const swingSign = (o.swing ?? "left") === "left" ? 1 : -1;
  const dirSign = (o.openingDir ?? "in") === "in" ? 1 : -1;
  const swingDir = scale(n, swingSign * dirSign);

  const hinge = (o.hinge ?? "start") === "start" ? jambStart : jambEnd;
  // Geschlossene Richtung zeigt vom Scharnier zum GEGENüberliegenden Pfosten.
  const closedDir = (o.hinge ?? "start") === "start" ? u : scale(u, -1);

  const angle = (o.swingAngle ?? 90) * DEG;
  // Offenes Blatt-Ende: um `angle` von der geschlossenen Richtung zur
  // Schwenkrichtung gedreht (Interpolation über die Basis closedDir/swingDir).
  const openDir = {
    x: closedDir.x * Math.cos(angle) + swingDir.x * Math.sin(angle),
    y: closedDir.y * Math.cos(angle) + swingDir.y * Math.sin(angle),
  };
  return {
    hinge,
    openEnd: add(hinge, scale(openDir, width)),
    closedEnd: add(hinge, scale(closedDir, width)),
    radius: width,
    jambStart,
    jambEnd,
    normal: n,
  };
}

/** Ein einzelnes Türblatt (Grundriss): Scharnier + geschlossenes/offenes Ende + Radius. */
export interface DoorLeaf {
  hinge: Vec2;
  openEnd: Vec2;
  closedEnd: Vec2;
  radius: number;
}

/** Blatt-Geometrie aus Scharnier, geschlossener Richtung, Schwenkrichtung, Winkel, Breite. */
function computeLeaf(
  hinge: Vec2,
  closedDir: Vec2,
  swingDir: Vec2,
  angle: number,
  width: number,
): DoorLeaf {
  const openDir = {
    x: closedDir.x * Math.cos(angle) + swingDir.x * Math.sin(angle),
    y: closedDir.y * Math.cos(angle) + swingDir.y * Math.sin(angle),
  };
  return {
    hinge,
    openEnd: add(hinge, scale(openDir, width)),
    closedEnd: add(hinge, scale(closedDir, width)),
    radius: width,
  };
}

/**
 * Türblätter im Grundriss (1 oder 2 Flügel). Einflüglig: ein Blatt über die
 * volle Breite (Scharnier je `o.hinge`). Zweiflüglig (`leafCount === 2`): zwei
 * halbbreite Blätter, an den GEGENüberliegenden Pfosten angeschlagen, beide zur
 * selben Seite schwenkend (symmetrische Doppeltür / Stulptür). `doorSymbol`
 * bleibt unverändert (einflügelig) für Bestandspfade/Parität; dies ist additiv.
 */
export function doorLeaves(wall: Wall, o: Opening, leafCount: number): DoorLeaf[] {
  const jambs = openingJambs(wall, o);
  if (!jambs) return [];
  const { u, n } = wallAxisFrame(wall);
  const { jambStart, jambEnd } = jambs;
  const width = Math.hypot(jambEnd.x - jambStart.x, jambEnd.y - jambStart.y);
  const swingSign = (o.swing ?? "left") === "left" ? 1 : -1;
  const dirSign = (o.openingDir ?? "in") === "in" ? 1 : -1;
  const swingDir = scale(n, swingSign * dirSign);
  const angle = (o.swingAngle ?? 90) * DEG;
  if (leafCount >= 2) {
    const hw = width / 2;
    return [
      computeLeaf(jambStart, u, swingDir, angle, hw),
      computeLeaf(jambEnd, scale(u, -1), swingDir, angle, hw),
    ];
  }
  const hinge = (o.hinge ?? "start") === "start" ? jambStart : jambEnd;
  const closedDir = (o.hinge ?? "start") === "start" ? u : scale(u, -1);
  return [computeLeaf(hinge, closedDir, swingDir, angle, width)];
}

// ── Fenster-Symbol (Plan) ────────────────────────────────────────────────────

/** Aufgelöste Fenster-Linien im Grundriss (Weltkoordinaten). */
export interface WindowSymbol {
  /** Rahmen-Rechteck (volle Dicke) als Ecken. */
  frame: Vec2[];
  /** Glaslinien-Segmente quer über die Lücke (1–3 parallele Linien). */
  glassLines: [Vec2, Vec2][];
  /**
   * Flügel-/Pfosten-Trennlinien, jeweils von einer Laibungsseite zur anderen
   * (parallel zu den Laibungslinien, quer zur Öffnungsrichtung). Ist `sashes`
   * (dritter Parameter von {@link windowSymbol}) gesetzt, ergeben sich die
   * Positionen aus den Flügel-/Pfostenbreiten (siehe `sashSegments`); sonst
   * (Alt-Fall) `wingCount − 1` gleichmäßig verteilte Querlinien. Entspricht den
   * Mittelpfosten in `_make_oeffnung_pieces` des Rhino-Plugins (`oeff_fluegel`).
   */
  mullionLines: [Vec2, Vec2][];
  /**
   * Flügelrahmen (Flügelkontur) je ÖFFENBAREM Flügel (Dreh/Kipp/Drehkipp/
   * Schiebe): ein Rechteck INNERHALB des Blendrahmens (Schachtelung
   * Blendrahmen → Flügelrahmen → Glas), das den öffenbaren Flügel vom festen
   * Feld unterscheidet. Feste Flügel (`opening:"fest"`) bekommen KEINEN
   * Flügelrahmen — dort sitzt das Glas direkt im Blendrahmen. Nur befüllt im
   * neuen (spec-)Pfad; leer im Alt-Fall (kein `spec`).
   */
  sashFrames: Vec2[][];
  /**
   * (Alt-Feld, für Rückwärtskompatibilität der Schnittstelle beibehalten) — im
   * neuen Pfad leer; die Öffenbarkeit wird jetzt über {@link sashFrames}
   * ausgedrückt statt über eine Diagonale quer über die Box.
   */
  sashOpeningLines: [Vec2, Vec2][];
}

/**
 * Render-Parameter des Fensters für die reiche 2D-Symbolik (aus dem
 * referenzierten {@link WindowType}): Flügeltabelle, Profilbreite, Rahmentiefe
 * und Schichteinzug. Fehlt der Spec (kein Typ auflösbar), zeichnet
 * {@link windowSymbol} das schlichte Alt-Symbol (`o.wingCount`, volle Wanddicke).
 */
export interface WindowRenderSpec {
  sashes: SashDef[];
  /** Sichtbare Profilbreite (Meter) von Blend-/Flügelrahmen in der Öffnungsebene. */
  frameWidth: number;
  /** Rahmentiefe quer zur Wand (Meter); fehlt ⇒ volle Wanddicke. */
  frameDepth?: number;
  /** Schichteinzug: Abstand der Rahmen-Vorderkante von {@link insetFace} (Meter). */
  insetFromFace?: number;
  /** Bezugsfläche des Schichteinzugs; Default "aussen". */
  insetFace?: "aussen" | "innen";
}

/** Ein Segment (Flügel/Pfosten) der Flügeleinteilung, mit aufgelöster Breite. */
interface SashSegment {
  sash: SashDef;
  /** Abstand vom Laibungsstart in Metern (0 = jambStart). */
  from: number;
  /** Abstand vom Laibungsstart in Metern. */
  to: number;
}

/** Default-Pfostenbreite (Meter), wenn ein `kind:"pfosten"`-Eintrag keine `postWidth` angibt. */
const DEFAULT_POST_WIDTH = 0.06;

/**
 * Löst eine Flügeleinteilung ({@link SashDef}-Liste) zu konkreten Segment-
 * Intervallen (Meter, gemessen ab Laibungsstart) über die gegebene
 * Gesamtbreite auf: `kind:"pfosten"` und Flügel mit `autoWidth === false`
 * erhalten ihre feste Breite (`postWidth` bzw. `width`), die restlichen
 * (`autoWidth`, Default) teilen sich die verbleibende Breite gleichmäßig.
 * Degeneriert bei GLEICHMÄSSIGEN Flügeln (alle `autoWidth`, keine Pfosten) zur
 * bisherigen `wingCount`-Gleichverteilung (Regressionssicherheit).
 */
function sashSegments(sashes: SashDef[], totalWidth: number): SashSegment[] {
  if (sashes.length === 0) return [];
  let fixedSum = 0;
  let autoCount = 0;
  for (const s of sashes) {
    if (s.kind === "pfosten") fixedSum += Math.max(0, s.postWidth ?? DEFAULT_POST_WIDTH);
    else if (s.autoWidth === false) fixedSum += Math.max(0, s.width ?? 0);
    else autoCount++;
  }
  const remaining = Math.max(0, totalWidth - fixedSum);
  const autoWidth = autoCount > 0 ? remaining / autoCount : 0;
  const segs: SashSegment[] = [];
  let cursor = 0;
  for (const s of sashes) {
    const w =
      s.kind === "pfosten"
        ? Math.max(0, s.postWidth ?? DEFAULT_POST_WIDTH)
        : s.autoWidth === false
          ? Math.max(0, s.width ?? 0)
          : autoWidth;
    segs.push({ sash: s, from: cursor, to: cursor + w });
    cursor += w;
  }
  return segs;
}

/**
 * Löst die Fenster-Symbolik auf: ein Rahmen-Rechteck über die Lücke (volle
 * Wanddicke) plus 1–3 Glaslinien (dünne parallele Linien in der Wandmitte,
 * `glassCount` steuert die Anzahl) plus optionale Flügel-/Pfosten-Trennlinien
 * und Öffnungsandeutungen je Flügel.
 *
 * `sashes` (optional) ist die aufgelöste Flügeltabelle (siehe
 * `sashesOfWindowType`) EINES referenzierten Fenstertyps. Fehlt sie (kein Typ
 * referenziert/auflösbar), bleibt die Mittelpfosten-Berechnung beim
 * BISHERIGEN Verhalten — `o.wingCount` gleichmäßig verteilt, KEINE
 * Öffnungsandeutungen — exakt wie vor dieser Erweiterung (Rückwärtskompatibilität).
 */
export function windowSymbol(
  project: Project,
  wall: Wall,
  o: Opening,
  glassCount: number,
  spec?: WindowRenderSpec,
): WindowSymbol | null {
  const jambs = openingJambs(wall, o);
  if (!jambs) return null;
  const { n } = wallAxisFrame(wall);
  const total = wallTypeThickness(getWallType(project, wall));
  const refOff = wallReferenceOffset(wall, total);
  const outer = refOff + total / 2;
  const inner = refOff - total / 2;
  const { jambStart, jambEnd } = jambs;

  // ── Reicher Pfad (typisiertes Fenster mit Flügeltabelle) ─────────────────
  // Blendrahmen an der realen Einbaulage (Schichteinzug), je Flügel
  // Blendrahmen → Flügelrahmen (öffenbar) → Glas. Feste Flügel: Glas direkt im
  // Blendrahmen (kein Flügelrahmen) — so unterscheidet sich fest von öffenbar.
  if (spec && spec.sashes.length > 0) {
    const fw = Math.max(0.01, spec.frameWidth);
    const depth = spec.frameDepth && spec.frameDepth > 1e-9 ? spec.frameDepth : total;
    const inset = Math.max(0, spec.insetFromFace ?? 0);
    const halfT = total / 2;
    const sign = (spec.insetFace ?? "aussen") === "innen" ? -1 : 1;
    // Rahmen-Vorderkante bündig mit der Bezugsfläche, um `inset` zurück; der
    // Körper reicht `depth` zur Wandmitte. Auf die Wanddicke geklemmt (mirror
    // von resolveFrameNormalRange im 3D). Alles relativ zur Referenzlinie.
    let frontRel = sign * (halfT - inset);
    let backRel = frontRel - sign * depth;
    frontRel = Math.max(-halfT, Math.min(halfT, frontRel));
    backRel = Math.max(-halfT, Math.min(halfT, backRel));
    const frameOuterN = refOff + Math.max(frontRel, backRel);
    const frameInnerN = refOff + Math.min(frontRel, backRel);
    const bandDepth = frameOuterN - frameInnerN;

    const frame = [
      add(jambStart, scale(n, frameOuterN)),
      add(jambEnd, scale(n, frameOuterN)),
      add(jambEnd, scale(n, frameInnerN)),
      add(jambStart, scale(n, frameInnerN)),
    ];

    const axisWidth = Math.hypot(jambEnd.x - jambStart.x, jambEnd.y - jambStart.y);
    const segs = sashSegments(spec.sashes, axisWidth);

    // Trennlinien (Pfosten/Mittelpfosten) an den inneren Segmentgrenzen, über
    // die volle Rahmentiefe.
    const mullionLines: [Vec2, Vec2][] = [];
    for (let i = 0; i < segs.length - 1; i++) {
      const onAxis = along(jambStart, jambEnd, segs[i].to);
      mullionLines.push([add(onAxis, scale(n, frameOuterN)), add(onAxis, scale(n, frameInnerN))]);
    }

    const sashFrames: Vec2[][] = [];
    const glassLines: [Vec2, Vec2][] = [];
    const count = Math.max(1, glassCount);
    // Öffenbarer Flügel sitzt `reveal` tiefer im Blendrahmen (n- und u-Richtung).
    const reveal = Math.min(bandDepth * 0.3, fw * 0.7);
    for (const seg of segs) {
      if (seg.sash.kind === "pfosten") continue;
      const clearFrom = seg.from + fw;
      const clearTo = seg.to - fw;
      if (clearTo - clearFrom <= 1e-6) continue;
      const operable = !!seg.sash.opening && seg.sash.opening !== "fest";
      const gOuterN = frameOuterN - (operable ? reveal : 0);
      const gInnerN = frameInnerN + (operable ? reveal : 0);
      if (operable) {
        const sFrom = along(jambStart, jambEnd, clearFrom);
        const sTo = along(jambStart, jambEnd, clearTo);
        sashFrames.push([
          add(sFrom, scale(n, gOuterN)),
          add(sTo, scale(n, gOuterN)),
          add(sTo, scale(n, gInnerN)),
          add(sFrom, scale(n, gInnerN)),
        ]);
      }
      // Glas innerhalb des Flügels (bei öffenbar zusätzlich in u eingerückt).
      const gInsetU = operable ? fw * 0.6 : 0;
      const gFrom = clearFrom + gInsetU;
      const gTo = clearTo - gInsetU;
      if (gTo - gFrom <= 1e-6) continue;
      const gMid = (gOuterN + gInnerN) / 2;
      const gBand = gOuterN - gInnerN;
      const step = count > 1 ? Math.min(total / 10, gBand / (count + 1)) : 0;
      const a = along(jambStart, jambEnd, gFrom);
      const b = along(jambStart, jambEnd, gTo);
      for (let i = 0; i < count; i++) {
        const off = gMid + (count === 1 ? 0 : (i - (count - 1) / 2) * step);
        glassLines.push([add(a, scale(n, off)), add(b, scale(n, off))]);
      }
    }
    return { frame, glassLines, mullionLines, sashFrames, sashOpeningLines: [] };
  }

  // ── Alt-Pfad (kein Typ auflösbar) ────────────────────────────────────────
  // UNVERÄNDERT (Rust-Parität kernel2d): Rahmen an voller Wanddicke, Glaslinien
  // mittig, (wingCount−1) gleichmäßige Mittelpfosten. Keine Flügelrahmen.
  const frame = [
    add(jambStart, scale(n, outer)),
    add(jambEnd, scale(n, outer)),
    add(jambEnd, scale(n, inner)),
    add(jambStart, scale(n, inner)),
  ];
  const glassLines: [Vec2, Vec2][] = [];
  const count = Math.max(1, glassCount);
  for (let i = 0; i < count; i++) {
    const spread = total / 8;
    const off = refOff + (count === 1 ? 0 : (i - (count - 1) / 2) * spread * 2);
    glassLines.push([add(jambStart, scale(n, off)), add(jambEnd, scale(n, off))]);
  }
  const mullionLines: [Vec2, Vec2][] = [];
  const wings = Math.max(1, Math.min(4, Math.round(o.wingCount ?? 1)));
  if (wings > 1) {
    const postDepth = total * 0.6;
    const pOuter = refOff + postDepth / 2;
    const pInner = refOff - postDepth / 2;
    for (let i = 1; i < wings; i++) {
      const t = i / wings;
      const onAxis = add(jambStart, scale(sub(jambEnd, jambStart), t));
      mullionLines.push([add(onAxis, scale(n, pOuter)), add(onAxis, scale(n, pInner))]);
    }
  }
  return { frame, glassLines, mullionLines, sashFrames: [], sashOpeningLines: [] };
}
