//! Brush-Collision: Swept-AABB-Trace gegen konvexe Brushes (Quake-Hull-Idee).
//!
//! Ein Brush ist der Schnitt seiner Halbräume `{ n·x ≤ d }` (n nach außen) —
//! genau die Ebenen, die auch render::brush rekonstruiert, hier aber in
//! **Engine-Koords** und **vollständig** (keine Koplanar-Elimination, kein
//! Culling: für Collision zählt das ganze solide Volumen).
//!
//! 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.
//!
//! Headless wie der Rest von `engine`: hängt nur an `map` (für die Brush-Ebenen
//! und die geteilte Koordinaten-Umrechnung). Der Player ruft `trace` in
//! `player::step`; gebaut wird die Welt einmal vom Renderer aus der `Map`.

use crate::engine::map::{self, Map};

/// 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 Brushes der Map.
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 {
    /// Kollisionswelt aus allen Brushes der Map bauen. Jede Brush-Face liefert
    /// eine Ebene (aus drei Punkten, nach Engine-Koords gedreht).
    pub fn build(world: &Map) -> Self {
        let mut brushes = Vec::new();
        for ent in &world.entities {
            for b in &ent.brushes {
                if b.faces.len() < 4 { continue; } // kein geschlossenes Volumen
                let planes = b.faces.iter().map(|f| plane_from(&f.plane)).collect();
                brushes.push(ConvexBrush { planes });
            }
        }
        Self { brushes }
    }

    /// Leere Welt (keine Brushes) — Default, bis eine Map geladen ist.
    pub fn empty() -> Self {
        Self { brushes: Vec::new() }
    }

    /// 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_brush(b, start, end, half) {
                if nearest.as_ref().map_or(true, |n| h.frac < n.frac) {
                    nearest = Some(h);
                }
            }
        }
        nearest
    }
}

/// Segment `start→end` gegen einen aufgeblasenen konvexen Brush clippen.
/// `None`, wenn das Segment den Brush verfehlt oder der Start schon drin steckt
/// (dann nicht blocken — sonst bliebe der Player hängen).
fn trace_brush(b: &ConvexBrush, 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 &b.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
    }
}

/// Ebene aus drei Face-Punkten (Quake-Reihenfolge), gedreht nach Engine-Koords.
/// Wie render::brush::plane, aber direkt im Engine-System: weil `to_engine` die
/// Orientierung erhält (det +1), zeigt `cross(c−a, b−a)` weiter nach außen.
fn plane_from(p: &[[f32; 3]; 3]) -> Plane {
    let a = map::to_engine(p[0]);
    let b = map::to_engine(p[1]);
    let c = map::to_engine(p[2]);
    let n = normalize(cross(sub(c, a), sub(b, a)));
    Plane { n, d: dot(n, a) }
}

// --- kleine Vektor-Helfer (privat, wie render::brush; 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 builds_brushes_from_map() {
        // Ein achsenparalleler Quader-Brush (Quake-Koords).
        let src = r#"
{
"classname" "worldspawn"
{
( 0 0 0 ) ( 0 1 0 ) ( 0 0 1 ) t 0 0 0 1 1
( 0 0 0 ) ( 0 0 1 ) ( 1 0 0 ) t 0 0 0 1 1
( 0 0 0 ) ( 1 0 0 ) ( 0 1 0 ) t 0 0 0 1 1
( 64 64 64 ) ( 64 65 64 ) ( 65 64 64 ) t 0 0 0 1 1
( 64 64 64 ) ( 65 64 64 ) ( 64 64 65 ) t 0 0 0 1 1
( 64 64 64 ) ( 64 64 65 ) ( 64 65 64 ) t 0 0 0 1 1
}
}
"#;
        let m = map::parse(src);
        let w = CollisionWorld::build(&m);
        assert_eq!(w.brushes.len(), 1);
        assert_eq!(w.brushes[0].planes.len(), 6);
        // Engine-Koords des Quake-Würfels [0,64]³: x[0,2], y[0,2], z[-2,0]
        // (Drehung (x,z,−y)·1/32). Strahl von außerhalb (−X) hindurch.
        let h = w.trace([-1.0, 1.0, -1.0], [3.0, 1.0, -1.0], PT);
        assert!(h.is_some(), "Strahl sollte den Brush treffen");
    }
}
