//! Collision: Swept-AABB-Trace gegen konvexe Volumen (Quake-Hull-Idee —
//! „Brush" heißt hier: der Schnitt seiner Halbräume `{ n·x ≤ d }`,
//! n nach außen).
//!
//! Die Box wird nicht selbst getract, sondern per Minkowski-Aufblasung in den
//! Ebenen versenkt: jede Ebene rückt um die auf ihre Normale projizierte
//! Box-Halbgröße nach außen (`d' = d + |n|·half`). Damit wird Box-vs-Brush zur
//! Punkt-vs-aufgeblasener-Brush-Frage, und die Trace ist ein simpler
//! Halbraum-Clip des Segments (Eintritts-/Austritts-Bruch). Ein `SKIN` hält
//! den Mittelpunkt eine Haaresbreite vor der Fläche, damit der Folgeframe nicht
//! sofort wieder im Kontakt steckt.
//!
//! Gefüllt wird die Welt aus Blender-Modellen ([`add_model`]): jedes Objekt
//! mit `collide`-Property wird zur **konvexen Hülle** seiner Vertices
//! ([`hull_planes`], inkrementeller Quickhull) — das Mesh selbst darf also
//! konkav sein, kollidiert wird gegen seine Hülle. Ergänzt um die sechs
//! achsparallelen AABB-Ebenen (Quake-„Bevel planes"): die `|n|·half`-
//! Aufblasung ist nur eine konservative Näherung der Minkowski-Summe; die
//! Axial-Ebenen ziehen sie an Kanten schräger Hüllen wieder stramm.
//!
//! Headless wie der Rest von `engine`: hängt nur an `model` (Quelle der
//! Ebenen). Der Player ruft `trace` in `player::step`; gebaut wird die
//! Welt einmal vom Frontend aus den Modellen.

use crate::engine::model::{Model, Object};

/// Mindestabstand (units), den der Box-Mittelpunkt vor einer Fläche hält —
/// verhindert Re-Kollision/Jitter im Folgeframe. ~1 cm, unsichtbar.
const SKIN: f32 = 0.01;

/// Eine nach außen orientierte Ebene `n·x ≤ d` (innen = Halbraum).
pub(crate) struct Plane {
    pub(crate) n: [f32; 3],
    pub(crate) d: f32,
}

/// Ein konvexer Brush = Schnitt seiner Halbräume.
struct ConvexBrush {
    planes: Vec<Plane>,
}

/// Die statische Kollisionswelt: alle soliden Volumen der geladenen Modelle.
pub struct CollisionWorld {
    brushes: Vec<ConvexBrush>,
}

/// Ergebnis einer Trace: Bruchteil entlang des Segments bis zum Kontakt und
/// die nach außen zeigende Trefferebenen-Normale (zum Gleiten/Boden-Erkennen).
pub struct Hit {
    pub frac:   f32,
    pub normal: [f32; 3],
}

impl CollisionWorld {
    /// Leere Welt — Ausgangszustand, bis Modelle geladen sind.
    pub fn empty() -> Self {
        Self { brushes: Vec::new() }
    }

    /// Collider eines Blender-Modells hinzufügen: jedes Objekt mit
    /// `collider = true` (aus der `collide`-Property) wird als konvexer
    /// Brush übernommen (siehe [`object_planes`]).
    pub fn add_model(&mut self, model: &Model) {
        for o in &model.objects {
            if !o.collider { continue; }
            match object_planes(o) {
                Some(planes) => self.brushes.push(ConvexBrush { planes }),
                None => eprintln!("[collision] {}: kein Volumen — ignoriert", o.name),
            }
        }
    }

    /// Eine AABB (Halbmaße `half`) von `start` nach `end` (Box-Mittelpunkte)
    /// sweepen. Liefert den frühesten Kontakt über alle Brushes, sonst `None`.
    pub fn trace(&self, start: [f32; 3], end: [f32; 3], half: [f32; 3]) -> Option<Hit> {
        let mut nearest: Option<Hit> = None;
        for b in &self.brushes {
            if let Some(h) = trace_planes(&b.planes, start, end, half)
                && nearest.as_ref().is_none_or(|n| h.frac < n.frac) {
                nearest = Some(h);
            }
        }
        nearest
    }
}

/// Das konvexe Ebenen-Set eines Modell-Objekts: die **konvexe Hülle** seiner
/// Vertices ([`hull_planes`]) plus die sechs achsparallelen AABB-Ebenen
/// (Bevel-Ebenen, siehe Modul-Doc) — das Mesh darf also konkav sein. Flache
/// Objekte (kein Hüll-Volumen) fallen auf ihre Face-Ebenen zurück, mit den
/// Bevel-Ebenen ergibt das die dünne AABB-Platte (brauchbar für flächige
/// Trigger-Zonen). `None`, wenn gar kein geschlossenes Volumen entsteht.
pub(crate) fn object_planes(o: &Object) -> Option<Vec<Plane>> {
    if o.verts.is_empty() { return None; }
    let mut planes = hull_planes(&o.verts).unwrap_or_else(|| face_planes(o));

    // Axiale Bevel-Ebenen aus der AABB (push_plane dedupt achsparallele
    // Duplikate weg).
    let (mut bb_min, mut bb_max) = (o.verts[0], o.verts[0]);
    for v in &o.verts {
        for k in 0..3 {
            bb_min[k] = bb_min[k].min(v[k]);
            bb_max[k] = bb_max[k].max(v[k]);
        }
    }
    for k in 0..3 {
        let mut n = [0.0f32; 3];
        n[k] = 1.0;
        push_plane(&mut planes, n, bb_max[k]);
        n[k] = -1.0;
        push_plane(&mut planes, n, -bb_min[k]);
    }
    (planes.len() >= 4).then_some(planes) // sonst kein geschlossenes Volumen
}

/// Ebene anfügen, wenn keine (nahezu) gleiche schon da ist — Koplanar-Dedupe
/// für Hüll-Dreiecke und Bevel-Ebenen.
fn push_plane(planes: &mut Vec<Plane>, n: [f32; 3], d: f32) {
    if !planes.iter().any(|p| dot(p.n, n) > 0.999 && (p.d - d).abs() < 1e-4) {
        planes.push(Plane { n, d });
    }
}

/// Fallback für degenerierte (flache) Objekte: Ebenen direkt aus den
/// Dreiecks-Flächen (koplanare dedupliziert). OBJ-/glTF-Winding ist CCW von
/// außen → cross(b−a, c−a) zeigt nach außen.
fn face_planes(o: &Object) -> Vec<Plane> {
    let mut planes: Vec<Plane> = Vec::new();
    for t in &o.tris {
        let (a, b, c) = (o.verts[t[0]], o.verts[t[1]], o.verts[t[2]]);
        let n = cross(sub(b, a), sub(c, a));
        if dot(n, n) < 1e-12 { continue; } // degeneriert (Länge ~0)
        let n = normalize(n);
        push_plane(&mut planes, n, dot(n, a));
    }
    planes
}

/// Toleranz der Hüllen-Konstruktion (units): Punkte näher als das an einer
/// Ebene gelten als koplanar. Absolut statt relativ — Maßstab ist fix
/// (1 unit = 1 Blender-Meter), und die Trace arbeitet mit ~SKIN-Auflösung.
const HULL_EPS: f32 = 1e-4;

/// Ein Hüll-Dreieck während der Konstruktion; `alive = false` = durch einen
/// später eingefügten Punkt ersetzt (Tombstone statt teurem Entfernen).
struct HullFace {
    v:     [usize; 3],
    n:     [f32; 3],
    d:     f32,
    alive: bool,
}

/// Hüll-Dreieck `(x, y, z)`, so orientiert, dass der Referenzpunkt `w`
/// (ein Punkt im Inneren des Start-Simplex) hinter der Ebene liegt.
fn hull_face(pts: &[[f32; 3]], x: usize, y: usize, z: usize, w: usize) -> HullFace {
    let n = normalize(cross(sub(pts[y], pts[x]), sub(pts[z], pts[x])));
    let d = dot(n, pts[x]);
    if dot(n, pts[w]) > d {
        HullFace { v: [x, z, y], n: [-n[0], -n[1], -n[2]], d: -d, alive: true }
    } else {
        HullFace { v: [x, y, z], n, d, alive: true }
    }
}

/// Konvexe Hülle einer Punktwolke als nach außen orientierte Ebenen
/// (inkrementeller Quickhull: Start-Tetraeder aus Extrempunkten, dann jeden
/// Außenpunkt einfügen — sichtbare Faces sterben, der Horizont wird mit
/// neuen Dreiecken zum Punkt geschlossen). `None` bei degenerierter Eingabe
/// (alle Punkte ~kollinear/koplanar) — kein Volumen zum Aufspannen.
fn hull_planes(pts: &[[f32; 3]]) -> Option<Vec<Plane>> {
    if pts.len() < 4 { return None; }

    // Start-Simplex: das fernste Paar unter den sechs Achsen-Extrema, dazu
    // der fernste Punkt zur Kante und der fernste zur aufgespannten Ebene.
    let mut ext = [0usize; 6];
    for (i, p) in pts.iter().enumerate() {
        for k in 0..3 {
            if p[k] < pts[ext[k]][k]     { ext[k] = i; }
            if p[k] > pts[ext[3 + k]][k] { ext[3 + k] = i; }
        }
    }
    let (mut a, mut b, mut best) = (0, 0, -1.0);
    for &i in &ext {
        for &j in &ext {
            let d = sub(pts[i], pts[j]);
            let d = dot(d, d);
            if d > best { best = d; (a, b) = (i, j); }
        }
    }
    if best < HULL_EPS * HULL_EPS { return None; } // alle Punkte ~identisch

    let ab = sub(pts[b], pts[a]);
    let (mut c, mut best) = (0, -1.0);
    for (i, p) in pts.iter().enumerate() {
        let x = cross(ab, sub(*p, pts[a]));
        let x = dot(x, x);
        if x > best { best = x; c = i; }
    }
    // |cross|² = Abstand² · |ab|² → Vergleich gegen EPS²·|ab|².
    if best < HULL_EPS * HULL_EPS * dot(ab, ab) { return None; } // kollinear

    let n = normalize(cross(ab, sub(pts[c], pts[a])));
    let (mut e, mut best) = (0, -1.0);
    for (i, p) in pts.iter().enumerate() {
        let x = (dot(n, *p) - dot(n, pts[a])).abs();
        if x > best { best = x; e = i; }
    }
    if best < HULL_EPS { return None; } // koplanar

    // Tetraeder-Faces; als Innen-Referenz dient je der vierte Eckpunkt.
    let mut faces = vec![
        hull_face(pts, a, b, c, e),
        hull_face(pts, a, b, e, c),
        hull_face(pts, a, c, e, b),
        hull_face(pts, b, c, e, a),
    ];

    for p in 0..pts.len() {
        // Faces, die den Punkt sehen (Punkt liegt vor ihrer Ebene).
        let visible: Vec<usize> = faces.iter().enumerate()
            .filter(|(_, f)| f.alive && dot(f.n, pts[p]) > f.d + HULL_EPS)
            .map(|(i, _)| i)
            .collect();
        if visible.is_empty() { continue; } // schon in der Hülle

        // Horizont: gerichtete Kanten sichtbarer Faces, deren Gegenkante
        // nicht selbst zu einem sichtbaren Face gehört. Weil die Windings
        // konsistent sind, taucht jede innere Kante genau einmal je Richtung
        // auf — der Horizont ist der Rest.
        let mut edges: Vec<(usize, usize)> = Vec::new();
        for &fi in &visible {
            let [x, y, z] = faces[fi].v;
            edges.extend([(x, y), (y, z), (z, x)]);
            faces[fi].alive = false;
        }
        for &(x, y) in &edges {
            if edges.contains(&(y, x)) { continue; } // innere Kante
            // Horizont-Kante behält ihre Richtung → (x, y, p) zeigt nach
            // außen. Degenerierte Dreiecke (p ~auf der Kante) überspringen.
            let nn = cross(sub(pts[y], pts[x]), sub(pts[p], pts[x]));
            if dot(nn, nn) < 1e-12 { continue; }
            let n = normalize(nn);
            faces.push(HullFace { v: [x, y, p], n, d: dot(n, pts[x]), alive: true });
        }
    }

    let mut planes: Vec<Plane> = Vec::new();
    for f in faces.iter().filter(|f| f.alive) {
        push_plane(&mut planes, f.n, f.d); // koplanare Hüll-Dreiecke dedupen
    }
    Some(planes)
}

/// Überlappt eine AABB (Mittelpunkt/Halbmaße) das konvexe Volumen? Gleiche
/// Minkowski-Aufblasung wie die Trace — „berühren" zählt als drin.
pub(crate) fn box_touches(planes: &[Plane], center: [f32; 3], half: [f32; 3]) -> bool {
    planes.iter().all(|p| {
        let d = p.d + p.n[0].abs() * half[0] + p.n[1].abs() * half[1] + p.n[2].abs() * half[2];
        dot(p.n, center) <= d
    })
}

/// Segment `start→end` gegen ein aufgeblasenes konvexes Ebenen-Set clippen.
/// `None`, wenn das Segment das Volumen verfehlt oder der Start schon drin
/// steckt (dann nicht blocken — sonst bliebe der Player hängen).
pub(crate) fn trace_planes(planes: &[Plane], start: [f32; 3], end: [f32; 3], half: [f32; 3]) -> Option<Hit> {
    let mut enter = f32::NEG_INFINITY; // größter Eintritts-Bruch
    let mut leave = 1.0f32;            // kleinster Austritts-Bruch
    let mut normal = [0.0f32; 3];
    let mut entered = false;           // überhaupt eine Eintrittsebene gefunden?
    let mut started_outside = false;

    for p in planes {
        // Ebene um die Box-Halbgröße nach außen aufblasen (Minkowski).
        let d = p.d + p.n[0].abs() * half[0] + p.n[1].abs() * half[1] + p.n[2].abs() * half[2];
        let ds = dot(p.n, start) - d;
        let de = dot(p.n, end) - d;

        if ds > 0.0 { started_outside = true; }
        if ds > 0.0 && de > 0.0 { return None; }   // ganz außerhalb dieser Ebene
        if ds <= 0.0 && de <= 0.0 { continue; }     // ganz innerhalb dieser Ebene

        if ds > de {
            // Eintritt (außen → innen): SKIN-Rückzug, damit der Mittelpunkt
            // knapp vor der Fläche stoppt. `enter` darf dabei leicht negativ
            // werden (Kontakt liegt im SKIN-Band) — das wird unten auf 0
            // geklemmt, nicht verworfen, sonst rutschte ein ruhender Körper
            // im Folgeframe durch die Fläche.
            let f = (ds - SKIN) / (ds - de);
            if f > enter { enter = f; normal = p.n; entered = true; }
        } else {
            // Austritt (innen → außen).
            let f = ds / (ds - de);
            if f < leave { leave = f; }
        }
    }

    // Treffer nur, wenn der Start außerhalb lag (sonst säße man fest), eine
    // Eintrittsebene existiert und das Eintrittsintervall vor dem Segmentende
    // beginnt.
    if started_outside && entered && enter < leave && enter < 1.0 {
        Some(Hit { frac: enter.max(0.0), normal })
    } else {
        None
    }
}

// --- kleine Vektor-Helfer (privat; ein gemeinsames engine::vec3 lohnt
// erst, falls ein dritter Nutzer auftaucht) ----------------------------------

fn sub(a: [f32; 3], b: [f32; 3]) -> [f32; 3] { [a[0] - b[0], a[1] - b[1], a[2] - b[2]] }
fn dot(a: [f32; 3], b: [f32; 3]) -> f32 { a[0] * b[0] + a[1] * b[1] + a[2] * b[2] }

fn cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
    [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
}

fn normalize(a: [f32; 3]) -> [f32; 3] {
    let len = dot(a, a).sqrt();
    if len > 0.0 { [a[0] / len, a[1] / len, a[2] / len] } else { a }
}

#[cfg(test)]
pub(crate) fn aabb_brush(min: [f32; 3], max: [f32; 3]) -> Vec<Plane> {
    vec![
        Plane { n: [ 1.0, 0.0, 0.0], d:  max[0] }, Plane { n: [-1.0, 0.0, 0.0], d: -min[0] },
        Plane { n: [0.0,  1.0, 0.0], d:  max[1] }, Plane { n: [0.0, -1.0, 0.0], d: -min[1] },
        Plane { n: [0.0, 0.0,  1.0], d:  max[2] }, Plane { n: [0.0, 0.0, -1.0], d: -min[2] },
    ]
}

#[cfg(test)]
pub(crate) fn from_brushes(brushes: Vec<Vec<Plane>>) -> CollisionWorld {
    CollisionWorld {
        brushes: brushes.into_iter().map(|planes| ConvexBrush { planes }).collect(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    // Punktförmige Box (half=0) → reine Strahl-vs-Brush-Trace.
    const PT: [f32; 3] = [0.0, 0.0, 0.0];

    #[test]
    fn trace_hits_near_face_with_normal() {
        let w = from_brushes(vec![aabb_brush([0.0, 0.0, 0.0], [2.0, 2.0, 2.0])]);
        // Von x=-1 nach x=3 (Gesamtweg 4), trifft die −X-Fläche bei x≈0.
        let h = w.trace([-1.0, 1.0, 1.0], [3.0, 1.0, 1.0], PT).unwrap();
        assert!((h.frac - 0.25).abs() < 0.02, "frac={}", h.frac);
        assert!(h.normal[0] < -0.5, "normal sollte -X sein: {:?}", h.normal);
    }

    #[test]
    fn trace_misses_returns_none() {
        let w = from_brushes(vec![aabb_brush([0.0, 0.0, 0.0], [2.0, 2.0, 2.0])]);
        // Läuft oberhalb der Box vorbei.
        assert!(w.trace([-1.0, 5.0, 1.0], [3.0, 5.0, 1.0], PT).is_none());
    }

    #[test]
    fn trace_starting_inside_does_not_block() {
        let w = from_brushes(vec![aabb_brush([0.0, 0.0, 0.0], [2.0, 2.0, 2.0])]);
        // Start mitten im Brush → kein Hit (sonst säße man fest).
        assert!(w.trace([1.0, 1.0, 1.0], [5.0, 1.0, 1.0], PT).is_none());
    }

    #[test]
    fn aabb_expands_by_half_extents() {
        let w = from_brushes(vec![aabb_brush([0.0, 0.0, 0.0], [2.0, 2.0, 2.0])]);
        // Box mit halber Breite 0.5: Kontakt schon bei x≈-0.5 statt 0.
        let h = w.trace([-2.0, 1.0, 1.0], [2.0, 1.0, 1.0], [0.5, 0.5, 0.5]).unwrap();
        // Weg 4, Kontakt bei x≈-0.5 → frac≈(−0.5−(−2))/4 = 0.375.
        assert!((h.frac - 0.375).abs() < 0.02, "frac={}", h.frac);
    }

    #[test]
    fn collider_object_becomes_convex_brush() {
        use crate::engine::model::{test_cube, Model};
        let model = Model {
            objects: vec![
                test_cube("Box",  [0.0, 0.0, 0.0], Some("proxy"), None),
                test_cube("Deko", [5.0, 5.0, 5.0], None, None),
            ],
            empties: Vec::new(),
            materials: vec![String::new()],
        };
        let mut w = CollisionWorld::empty();
        w.add_model(&model);

        // Nur Box (Deko hat kein collide); die Hülle des Würfels dedupt auf
        // 6 Ebenen, die axialen Bevel-Ebenen sind mit ihnen identisch.
        assert_eq!(w.brushes.len(), 1);
        assert_eq!(w.brushes[0].planes.len(), 6);

        // Strahl von −X hindurch: Kontakt an x≈0 mit −X-Normale.
        let h = w.trace([-1.0, 0.5, 0.5], [3.0, 0.5, 0.5], PT).unwrap();
        assert!((h.frac - 0.25).abs() < 0.02, "frac={}", h.frac);
        assert!(h.normal[0] < -0.5, "normal sollte -X sein: {:?}", h.normal);
    }

    #[test]
    fn hull_of_concave_mesh_spans_the_notch() {
        // Konkaves „L": zwei Würfel-Objekte liefern die Punktwolke eines
        // L-Prismas — [0,2]×[0,1]×[0,1] plus [0,1]×[1,2]×[0,1]. Die Hülle
        // muss die Kerbe (x>1, y>1) mit der Schräge x+y=3 überspannen.
        let mut verts = Vec::new();
        for (min, max) in [([0.0f32, 0.0, 0.0], [2.0f32, 1.0, 1.0]),
                           ([0.0, 1.0, 0.0], [1.0, 2.0, 1.0])] {
            for &x in &[min[0], max[0]] {
                for &y in &[min[1], max[1]] {
                    for &z in &[min[2], max[2]] { verts.push([x, y, z]); }
                }
            }
        }
        let planes = hull_planes(&verts).unwrap();
        // 6 AABB-Seiten der Gesamtbox + 1 Schräge über der Kerbe.
        assert_eq!(planes.len(), 7);
        let diag = planes.iter()
            .find(|p| p.n[0] > 0.5 && p.n[1] > 0.5)
            .expect("Schräge über der Kerbe fehlt");
        assert!((dot(diag.n, [2.0, 1.0, 0.5]) - diag.d).abs() < 1e-3,
            "Schräge sollte durch (2,1,·) laufen");

        // Ein Punkt in der Kerbe, unter der Schräge, liegt in der Hülle.
        assert!(box_touches(&planes, [1.2, 1.2, 0.5], PT));
        // Die Ecke der Kerbe (über der Schräge) liegt außerhalb.
        assert!(!box_touches(&planes, [1.9, 1.9, 0.5], PT));
    }

    #[test]
    fn degenerate_objects_fall_back_or_are_rejected() {
        use crate::engine::model::{Object, Props};
        // Flaches Quad in der XY-Ebene: keine Hülle, aber der Face-Ebenen-
        // Fallback + Bevels ergeben die dünne Platte.
        let flat = Object {
            name: "Zone".into(),
            verts: vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.0, 1.0, 0.0]],
            uvs: vec![[0.0, 0.0]; 4],
            tris: vec![[0, 1, 2], [0, 2, 3]],
            tri_mats: vec![0; 2],
            visible: true, collider: false, props: Props::new(),
        };
        let planes = object_planes(&flat).unwrap();
        assert!(box_touches(&planes, [0.5, 0.5, 0.0], PT));
        assert!(!box_touches(&planes, [0.5, 0.5, 1.0], PT));

        // Ohne Vertices gibt es nichts.
        let empty = Object { verts: Vec::new(), uvs: Vec::new(), tris: Vec::new(),
            tri_mats: Vec::new(), ..flat };
        assert!(object_planes(&empty).is_none());
    }

}
