//! Neutrales 3D-Modell — das gemeinsame Ergebnis des Blender-Loaders
//! (engine::gltf). Konsumenten (render::props, collision, trigger) kennen
//! nur diese Struktur; welcher Parser sie erzeugt hat, ist ihnen egal — so
//! bleibt ein Format-Wechsel ein lokaler Loader-Tausch.
//!
//! Was ein Objekt *ist*, steuern seine Blender-Custom-Properties (keine
//! Namens-Präfixe — Namen bleiben frei für die Autorin), siehe
//! [`apply_props`]:
//!   `collide = true`    → sichtbar **und** Collider (konvexe Hülle des
//!                          Meshes, siehe collision::object_planes)
//!   `collide = "proxy"` → unsichtbarer Collider (reines Kollisionsvolumen)
//!   `signal  = <name>`  → Trigger; mit Collider Klick-Ziel, ohne Collider
//!                          unsichtbare Betretens-Zone (engine::trigger)
//!   nichts davon        → nur sichtbar
//!
//! Objektnamen sind zugleich die Interact-Keys: Blender-Suffixe
//! (`Thing.001`) strippt `signals::signal_key` beim Dispatch, nicht hier —
//! die Loader bleiben frei von Signal-Wissen.

use std::collections::HashMap;

/// Blender-Custom-Properties eines Objekts/Empties (glTF-`extras`),
/// Werte zu Strings vereinheitlicht. OBJ kann keine tragen → leer.
pub type Props = HashMap<String, String>;

pub struct Model {
    pub objects: Vec<Object>,
    /// Nodes ohne Mesh (Blender-Empties) — der Entity-Kanal: Marker für
    /// Spawn-Punkte, Trigger o.Ä., Bedeutung geben künftige Konsumenten.
    pub empties: Vec<Empty>,
    /// Distinkte Materialnamen in Auftrittsreihenfolge; `Object::tri_mats`
    /// indiziert hierhin. `""` steht für „Faces ohne Material" — der
    /// Konsument wählt dafür seinen Fallback (z.B. Platzhalter-Textur).
    pub materials: Vec<String>,
}

pub struct Object {
    pub name:     String,
    /// Positionen in Engine-Koords (Y-up, Blick −Z), Welt-Raum (Transforms
    /// vom Loader eingebacken), Maßstab 1:1 (Blender-Meter = Engine-Unit).
    pub verts:    Vec<[f32; 3]>,
    /// UVs parallel zu `verts`, V-Ursprung oben links — glTF- und zugleich
    /// `tga::Image`-Konvention, direkt sampelbar ohne Flip.
    pub uvs:      Vec<[f32; 2]>,
    pub tris:     Vec<[usize; 3]>,
    /// Material-Index je Dreieck, parallel zu `tris` (→ `Model::materials`).
    pub tri_mats: Vec<usize>,
    pub visible:  bool,
    pub collider: bool,
    pub props:    Props,
}

pub struct Empty {
    pub name:  String,
    /// Welt-Position (Engine-Koords).
    pub pos:   [f32; 3],
    pub props: Props,
}

/// Custom-Property-Schlüssel, der ein Objekt zum Collider macht (siehe
/// Modul-Doc): truthy (`true`, `1`, …) = sichtbar + Collider, `"proxy"` =
/// unsichtbarer Collider. `0`/`false`/`no`/leer zählen als aus, damit die
/// Autorin die Property abschalten kann, ohne sie zu löschen.
pub const COLLIDE_PROP: &str = "collide";

/// Custom-Property-Schlüssel, der ein Objekt zum Trigger macht (Wert =
/// Signal-Name für `signals::dispatch`). Die Collider-Frage entscheidet die
/// Semantik — anfassbar = klickbar, durchlaufbar = Betretens-Zone:
///   `collide` + `signal` → solide **und** klickbar (Point-and-Click)
///   nur `signal`        → unsichtbare Trigger-Zone, feuert beim Betreten
///                          (einmal je Eintritt; erneut erst nach Verlassen)
pub const SIGNAL_PROP: &str = "signal";

/// Custom-Property-Schlüssel für Empty-Rollen (der Entity-Kanal):
/// `role = "spawn"` markiert den Spieler-Startpunkt (Fußpunkt =
/// Empty-Position). Weitere Rollen geben künftige Konsumenten.
pub const ROLE_PROP: &str = "role";
pub const ROLE_SPAWN: &str = "spawn";

/// Die Property-Regeln anwenden, sobald `props` gelesen sind (Objekte
/// starten sichtbar und ohne Collider): `collide` schaltet den Collider,
/// `"proxy"` und Betretens-Zonen sind reine Volumen und werden nie gerendert.
pub fn apply_props(o: &mut Object) {
    if let Some(v) = o.props.get(COLLIDE_PROP) {
        let v = v.to_ascii_lowercase();
        if !matches!(v.as_str(), "" | "0" | "false" | "no") {
            o.collider = true;
            if v == "proxy" { o.visible = false; }
        }
    }
    if !o.collider && o.props.contains_key(SIGNAL_PROP) {
        o.visible = false;
    }
}

/// Test-Helfer: Einheitswürfel-Objekt bei `min..min+1` (Quads CCW von außen,
/// Fan-trianguliert wie der Loader) mit optionalen `collide`-/`signal`-
/// Properties — geteilt von trigger-/session-/collision-Tests.
#[cfg(test)]
pub(crate) fn test_cube(name: &str, min: [f32; 3], collide: Option<&str>, signal: Option<&str>) -> Object {
    let v = |dx: f32, dy: f32, dz: f32| [min[0] + dx, min[1] + dy, min[2] + dz];
    let verts = vec![
        v(0.0, 0.0, 0.0), v(1.0, 0.0, 0.0), v(1.0, 1.0, 0.0), v(0.0, 1.0, 0.0),
        v(0.0, 0.0, 1.0), v(1.0, 0.0, 1.0), v(1.0, 1.0, 1.0), v(0.0, 1.0, 1.0),
    ];
    let quads = [
        [4, 5, 6, 7], [0, 3, 2, 1], [1, 2, 6, 5],
        [0, 4, 7, 3], [3, 7, 6, 2], [0, 1, 5, 4],
    ];
    let mut tris = Vec::new();
    for q in quads {
        tris.push([q[0], q[1], q[2]]);
        tris.push([q[0], q[2], q[3]]);
    }
    let n = tris.len();
    let mut props = Props::new();
    if let Some(c) = collide { props.insert(COLLIDE_PROP.into(), c.into()); }
    if let Some(s) = signal { props.insert(SIGNAL_PROP.into(), s.into()); }
    let mut o = Object {
        name: name.into(),
        uvs: vec![[0.0, 0.0]; verts.len()],
        verts, tris,
        tri_mats: vec![0; n],
        visible: true, collider: false, props,
    };
    apply_props(&mut o);
    o
}

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

    #[test]
    fn collide_prop_rules() {
        // truthy → sichtbar + Collider; "proxy" → unsichtbarer Collider.
        let o = test_cube("Kiste", [0.0; 3], Some("true"), None);
        assert!(o.visible && o.collider);
        let o = test_cube("Wand", [0.0; 3], Some("proxy"), None);
        assert!(!o.visible && o.collider);
        // Abschaltwerte zählen als aus; ohne Property nur sichtbar.
        let o = test_cube("Aus", [0.0; 3], Some("0"), None);
        assert!(o.visible && !o.collider);
        let o = test_cube("Deko", [0.0; 3], None, None);
        assert!(o.visible && !o.collider);
    }

    #[test]
    fn signal_without_collider_is_invisible_zone() {
        let o = test_cube("Zone", [0.0; 3], None, Some("betreten"));
        assert!(!o.visible && !o.collider);
        // Mit Collider bleibt das Objekt sichtbar (Klick-Ziel).
        let o = test_cube("Kiste", [0.0; 3], Some("1"), Some("kiste"));
        assert!(o.visible && o.collider);
    }
}
