// Dach-Geometrie: leitet aus einem (rechteckigen) Grundriss-Umriss + Dachform +
// Neigung die Grundriss-Linien (Traufe/First/Grat/Knick) UND die 3D-Dachflächen
// ab. Bewusst auf der BOUNDING-BOX des Umrisses gerechnet (First entlang einer
// Hauptachse) — die gängige, intuitive Vereinfachung, die alle Standardformen
// (Flach/Pult/Sattel/Walm/Mansarde/Zelt) exakt und ohne Straight-Skeleton abdeckt.
//
// Koordinaten: Grundriss (x, y) in Metern; 3D-Punkte als [x, y, z] mit z = Höhe.
// Reine Datenschicht (kein React/Store), voll unit-testbar.
//
// Bezeichner englisch, Kommentare deutsch (CONVENTIONS.md).

import type { Project, Roof, Vec2 } from "../model/types";
import { getFloor } from "../model/types";

/** 3D-Punkt [x, y, z=Höhe] in Modell-Metern. */
export type Vec3 = [number, number, number];

/** Eine ebene Dachfläche als Polygon (3D-Eckpunkte in Umlaufreihenfolge). */
export interface RoofPlane {
  pts: Vec3[];
}

/** Vollständige Dach-Geometrie (Grundriss-Linien + 3D-Flächen). */
export interface RoofGeometry {
  /** Traufe-Umriss (Grundriss, inkl. Überstand) als geschlossener Ring. */
  eaves: Vec2[];
  /** Firstlinie(n) im Grundriss (leer bei Flach/Zelt). */
  ridges: [Vec2, Vec2][];
  /** Gratlinien (Walm/Zelt: Ecke → First/Spitze). */
  hips: [Vec2, Vec2][];
  /** Knicklinien (Mansarde). */
  breaks: [Vec2, Vec2][];
  /** 3D-Dachflächen. */
  planes: RoofPlane[];
  /** Giebelflächen (vertikale Polygone an Sattel-/Mansarde-Enden). */
  gables: Vec3[][];
  /** Firsthöhe über der Traufe (Meter). */
  ridgeHeight: number;
}

const DEG = Math.PI / 180;

/** Bounding-Box eines Umrisses. */
export function roofBBox(outline: Vec2[]): { x0: number; y0: number; x1: number; y1: number } {
  let x0 = Infinity;
  let y0 = Infinity;
  let x1 = -Infinity;
  let y1 = -Infinity;
  for (const p of outline) {
    if (p.x < x0) x0 = p.x;
    if (p.y < y0) y0 = p.y;
    if (p.x > x1) x1 = p.x;
    if (p.y > y1) y1 = p.y;
  }
  return { x0, y0, x1, y1 };
}

/**
 * Traufhöhe (absolutes Z) eines Dachs: `baseElevation`, sonst die OBERKANTE des
 * zugehörigen Geschosses (baseElevation + floorHeight) — das Dach sitzt also
 * standardmäßig auf der Geschossdecke auf.
 */
export function roofBaseElevation(project: Project, roof: Roof): number {
  if (roof.baseElevation !== undefined) return roof.baseElevation;
  try {
    const floor = getFloor(project, roof.floorId);
    return (floor.baseElevation ?? 0) + (floor.floorHeight ?? 0);
  } catch {
    return 0;
  }
}

const P = (x: number, y: number): Vec2 => ({ x, y });

/**
 * Kanonische Berechnung mit First entlang der X-Achse auf dem Rechteck
 * [x0,x1]×[y0,y1] (Traufe, inkl. Überstand) auf Traufhöhe `e`. Die „y"-First-
 * Richtung wird im Wrapper durch Transponieren (x↔y) erreicht.
 */
function computeCanonical(
  x0: number,
  y0: number,
  x1: number,
  y1: number,
  e: number,
  roof: Roof,
): RoofGeometry {
  const xc = (x0 + x1) / 2;
  const yc = (y0 + y1) / 2;
  const halfW = (x1 - x0) / 2;
  const halfD = (y1 - y0) / 2;
  const a = Math.max(0, roof.pitchDeg) * DEG;
  const tan = Math.tan(a);
  const h = halfD * tan; // Firsthöhe (Sattel/Walm/Mansarde)

  const eaves: Vec2[] = [P(x0, y0), P(x1, y0), P(x1, y1), P(x0, y1)];
  const flat = (): RoofGeometry => ({
    eaves,
    ridges: [],
    hips: [],
    breaks: [],
    planes: [{ pts: [[x0, y0, e], [x1, y0, e], [x1, y1, e], [x0, y1, e]] }],
    gables: [],
    ridgeHeight: 0,
  });

  switch (roof.shape) {
    case "flach":
      return flat();

    case "pult": {
      // Eine Fläche: tiefe Seite y0 auf Traufe, hohe Seite y1 auf e+H.
      const H = (y1 - y0) * tan;
      return {
        eaves,
        ridges: [[P(x0, y1), P(x1, y1)]], // hohe Kante (First-Ersatz)
        hips: [],
        breaks: [],
        planes: [{ pts: [[x0, y0, e], [x1, y0, e], [x1, y1, e + H], [x0, y1, e + H]] }],
        gables: [
          [[x0, y0, e], [x0, y1, e + H], [x0, y1, e]],
          [[x1, y0, e], [x1, y1, e + H], [x1, y1, e]],
        ],
        ridgeHeight: H,
      };
    }

    case "sattel": {
      const rz = e + h;
      return {
        eaves,
        ridges: [[P(x0, yc), P(x1, yc)]],
        hips: [],
        breaks: [],
        planes: [
          { pts: [[x0, y0, e], [x1, y0, e], [x1, yc, rz], [x0, yc, rz]] },
          { pts: [[x0, y1, e], [x1, y1, e], [x1, yc, rz], [x0, yc, rz]] },
        ],
        gables: [
          [[x0, y0, e], [x0, y1, e], [x0, yc, rz]],
          [[x1, y0, e], [x1, y1, e], [x1, yc, rz]],
        ],
        ridgeHeight: h,
      };
    }

    case "walm": {
      const rz = e + h;
      // First um die halbe Tiefe an beiden Enden verkürzt (45°-Grate bei
      // gleicher Neigung). Bei schmalem Baukörper (halfW<halfD) degeneriert er
      // zum Punkt (xc) → Walm wird zeltartig.
      let ra = x0 + halfD;
      let rb = x1 - halfD;
      if (ra > rb) ra = rb = xc;
      return {
        eaves,
        ridges: [[P(ra, yc), P(rb, yc)]],
        hips: [
          [P(x0, y0), P(ra, yc)],
          [P(x0, y1), P(ra, yc)],
          [P(x1, y0), P(rb, yc)],
          [P(x1, y1), P(rb, yc)],
        ],
        breaks: [],
        planes: [
          { pts: [[x0, y0, e], [x1, y0, e], [rb, yc, rz], [ra, yc, rz]] },
          { pts: [[x0, y1, e], [x1, y1, e], [rb, yc, rz], [ra, yc, rz]] },
          { pts: [[x0, y0, e], [x0, y1, e], [ra, yc, rz]] },
          { pts: [[x1, y0, e], [x1, y1, e], [rb, yc, rz]] },
        ],
        gables: [],
        ridgeHeight: h,
      };
    }

    case "mansarde": {
      // Mansarde: steile untere Neigung (pitchDeg) bis zur Knicklinie, dann
      // flache obere (pitchUpperDeg ?? halbe Hauptneigung). Untertyp steuert, ob
      // die Knickform an ZWEI Seiten sitzt (Giebel) oder RINGSUM (Walm/Zelt).
      const aLow = a;
      const aUp = Math.max(0, roof.pitchUpperDeg ?? roof.pitchDeg / 2) * DEG;
      const ratio = Math.min(0.49, Math.max(0.05, roof.mansardKneeRatio ?? 0.4));
      const mType = roof.mansardType ?? "giebel";

      if (mType === "giebel") {
        // Mansard-Satteldach: Knick nur an den beiden Traufseiten, Giebel an den
        // Enden (bisheriges Verhalten, jetzt mit parametrierbarem Knick).
        const d1 = halfD * ratio;
        const z1 = e + d1 * Math.tan(aLow);
        const z2 = z1 + (halfD - d1) * Math.tan(aUp);
        const yf = y0 + d1;
        const yb = y1 - d1;
        return {
          eaves,
          ridges: [[P(x0, yc), P(x1, yc)]],
          hips: [],
          breaks: [
            [P(x0, yf), P(x1, yf)],
            [P(x0, yb), P(x1, yb)],
          ],
          planes: [
            { pts: [[x0, y0, e], [x1, y0, e], [x1, yf, z1], [x0, yf, z1]] },
            { pts: [[x0, yf, z1], [x1, yf, z1], [x1, yc, z2], [x0, yc, z2]] },
            { pts: [[x0, y1, e], [x1, y1, e], [x1, yb, z1], [x0, yb, z1]] },
            { pts: [[x0, yb, z1], [x1, yb, z1], [x1, yc, z2], [x0, yc, z2]] },
          ],
          gables: [
            [[x0, y0, e], [x0, yf, z1], [x0, yc, z2], [x0, yb, z1], [x0, y1, e]],
            [[x1, y0, e], [x1, yf, z1], [x1, yc, z2], [x1, yb, z1], [x1, y1, e]],
          ],
          ridgeHeight: z2 - e,
        };
      }

      // Allseitige Mansarde (Walm/Zelt): steiler Sockel ringsum bis zur
      // Knicklinie (inneres Rechteck), darüber flacher Walm bzw. flache Spitze.
      const dLow = Math.min(halfW, halfD) * ratio;
      const z1 = e + dLow * Math.tan(aLow);
      const ix0 = x0 + dLow;
      const iy0 = y0 + dLow;
      const ix1 = x1 - dLow;
      const iy1 = y1 - dLow;
      const ihalfD = (iy1 - iy0) / 2;
      const ihalfW = (ix1 - ix0) / 2;
      // Sockel-Trapeze (4 Seiten, steil) + Sockel-Grate (Ecken aussen→innen) +
      // Knicklinie (inneres Rechteck) sind beiden allseitigen Formen gemein.
      const socketPlanes = [
        { pts: [[x0, y0, e], [x1, y0, e], [ix1, iy0, z1], [ix0, iy0, z1]] as Vec3[] },
        { pts: [[x1, y1, e], [x0, y1, e], [ix0, iy1, z1], [ix1, iy1, z1]] as Vec3[] },
        { pts: [[x0, y1, e], [x0, y0, e], [ix0, iy0, z1], [ix0, iy1, z1]] as Vec3[] },
        { pts: [[x1, y0, e], [x1, y1, e], [ix1, iy1, z1], [ix1, iy0, z1]] as Vec3[] },
      ];
      const socketHips: [Vec2, Vec2][] = [
        [P(x0, y0), P(ix0, iy0)],
        [P(x1, y0), P(ix1, iy0)],
        [P(x1, y1), P(ix1, iy1)],
        [P(x0, y1), P(ix0, iy1)],
      ];
      const kneeBreaks: [Vec2, Vec2][] = [
        [P(ix0, iy0), P(ix1, iy0)],
        [P(ix1, iy0), P(ix1, iy1)],
        [P(ix1, iy1), P(ix0, iy1)],
        [P(ix0, iy1), P(ix0, iy0)],
      ];

      if (mType === "walm") {
        // Oberer Walm auf dem inneren Rechteck (First entlang X).
        const z2 = z1 + ihalfD * Math.tan(aUp);
        let ra = ix0 + ihalfD;
        let rb = ix1 - ihalfD;
        if (ra > rb) ra = rb = xc;
        return {
          eaves,
          ridges: [[P(ra, yc), P(rb, yc)]],
          hips: [
            ...socketHips,
            [P(ix0, iy0), P(ra, yc)],
            [P(ix1, iy0), P(rb, yc)],
            [P(ix1, iy1), P(rb, yc)],
            [P(ix0, iy1), P(ra, yc)],
          ],
          breaks: kneeBreaks,
          planes: [
            ...socketPlanes,
            { pts: [[ix0, iy0, z1], [ix1, iy0, z1], [rb, yc, z2], [ra, yc, z2]] },
            { pts: [[ix1, iy1, z1], [ix0, iy1, z1], [ra, yc, z2], [rb, yc, z2]] },
            { pts: [[ix0, iy1, z1], [ix0, iy0, z1], [ra, yc, z2]] },
            { pts: [[ix1, iy0, z1], [ix1, iy1, z1], [rb, yc, z2]] },
          ],
          gables: [],
          ridgeHeight: z2 - e,
        };
      }

      // "zelt": flache Spitze über der Mitte des inneren Rechtecks.
      const z2 = z1 + Math.min(ihalfW, ihalfD) * Math.tan(aUp);
      return {
        eaves,
        ridges: [],
        hips: [
          ...socketHips,
          [P(ix0, iy0), P(xc, yc)],
          [P(ix1, iy0), P(xc, yc)],
          [P(ix1, iy1), P(xc, yc)],
          [P(ix0, iy1), P(xc, yc)],
        ],
        breaks: kneeBreaks,
        planes: [
          ...socketPlanes,
          { pts: [[ix0, iy0, z1], [ix1, iy0, z1], [xc, yc, z2]] },
          { pts: [[ix1, iy0, z1], [ix1, iy1, z1], [xc, yc, z2]] },
          { pts: [[ix1, iy1, z1], [ix0, iy1, z1], [xc, yc, z2]] },
          { pts: [[ix0, iy1, z1], [ix0, iy0, z1], [xc, yc, z2]] },
        ],
        gables: [],
        ridgeHeight: z2 - e,
      };
    }

    case "zelt": {
      // Allseitig zur Spitze über der Mitte.
      const hz = Math.min(halfW, halfD) * tan;
      const az = e + hz;
      return {
        eaves,
        ridges: [],
        hips: [
          [P(x0, y0), P(xc, yc)],
          [P(x1, y0), P(xc, yc)],
          [P(x1, y1), P(xc, yc)],
          [P(x0, y1), P(xc, yc)],
        ],
        breaks: [],
        planes: [
          { pts: [[x0, y0, e], [x1, y0, e], [xc, yc, az]] },
          { pts: [[x1, y0, e], [x1, y1, e], [xc, yc, az]] },
          { pts: [[x1, y1, e], [x0, y1, e], [xc, yc, az]] },
          { pts: [[x0, y1, e], [x0, y0, e], [xc, yc, az]] },
        ],
        gables: [],
        ridgeHeight: hz,
      };
    }
  }
}

const swap2 = (p: Vec2): Vec2 => ({ x: p.y, y: p.x });
const swap3 = (p: Vec3): Vec3 => [p[1], p[0], p[2]];
const swapLine = (l: [Vec2, Vec2]): [Vec2, Vec2] => [swap2(l[0]), swap2(l[1])];

/**
 * Dach-Geometrie aus Form/Neigung/Überstand + Traufhöhe `eavesZ`. Rechnet auf
 * der Bounding-Box des Umrisses, um `overhang` nach aussen geweitet. Für
 * `ridgeAxis:"y"` wird kanonisch (First entlang X) gerechnet und danach x↔y
 * transponiert.
 */
export function roofGeometry(roof: Roof, eavesZ: number): RoofGeometry {
  const bb = roofBBox(roof.outline);
  const oe = Math.max(0, roof.overhang); // Traufüberstand (senkrecht zum First)
  const og = Math.max(0, roof.overhangGable ?? roof.overhang); // Ortgang (entlang First)
  // Der First liegt entlang der `ridgeAxis`-Achse: dort wirkt der Ortgang-, quer
  // dazu der Traufüberstand. In OUTLINE-Koordinaten je nach Firstrichtung mappen.
  const ox = roof.ridgeAxis === "x" ? og : oe;
  const oy = roof.ridgeAxis === "x" ? oe : og;
  let x0 = bb.x0 - ox;
  let y0 = bb.y0 - oy;
  let x1 = bb.x1 + ox;
  let y1 = bb.y1 + oy;
  const flip = roof.ridgeAxis === "y";
  if (flip) {
    [x0, y0, x1, y1] = [y0, x0, y1, x1];
  }
  const g = computeCanonical(x0, y0, x1, y1, eavesZ, roof);
  if (!flip) return g;
  return {
    eaves: g.eaves.map(swap2),
    ridges: g.ridges.map(swapLine),
    hips: g.hips.map(swapLine),
    breaks: g.breaks.map(swapLine),
    planes: g.planes.map((pl) => ({ pts: pl.pts.map(swap3) })),
    gables: g.gables.map((poly) => poly.map(swap3)),
    ridgeHeight: g.ridgeHeight,
  };
}
