//! Quake-`.map`-Parser (Standard-Format, wie TrenchBroom es mit dem
//! Generic-Game exportiert).
//!
//! Struktur: eine Datei ist eine Liste von Entities `{...}`. Eine Entity
//! hält Key/Value-Properties (`"key" "value"`) und null oder mehr Brushes
//! `{...}`. Ein Brush ist eine Liste von Faces; jede Face ist eine Ebene
//! aus drei Punkten plus Textur-Ausrichtung:
//!
//!   ( x y z ) ( x y z ) ( x y z ) TEXTUR offX offY rot scaleX scaleY
//!
//! Hier wird nur geparst — Geometrie bleibt im Quake-Koordinatensystem
//! (Z-up, Brush = Schnitt der Halbräume); die Umrechnung ins Engine-System
//! und der Halbraum-Schnitt sind Sache des Konsumenten (render::brush).

use std::collections::HashMap;
use std::fs::read_to_string;

pub struct Map {
    pub entities: Vec<Entity>,
}

#[derive(Default)]
pub struct Entity {
    pub props:   HashMap<String, String>,
    pub brushes: Vec<Brush>,
}

impl Entity {
    // Genutzt für Spawn-Auflösung (info_player_start) und künftige
    // Entity-/Signal-Bindung.
    pub fn classname(&self) -> Option<&str> {
        self.props.get("classname").map(String::as_str)
    }
}

pub struct Brush {
    pub faces: Vec<Face>,
}

/// Eine Brush-Face: Ebene aus drei Punkten (Quake-Reihenfolge, im
/// Uhrzeigersinn von vorn gesehen) plus Textur-Ausrichtung. Die
/// Ausrichtungsfelder werden erst vom Textur-Schritt gebraucht.
pub struct Face {
    pub plane:   [[f32; 3]; 3],
    pub texture: String,
    #[allow(dead_code)]
    pub offset:   [f32; 2],
    #[allow(dead_code)]
    pub rotation: f32,
    #[allow(dead_code)]
    pub scale:    [f32; 2],
}

/// Quake-Einheiten pro Engine-Einheit (32 ≈ klassische „1 Meter"-Annahme).
/// Single Source für Render- (render::brush) und Collision-Pfad
/// (engine::collision), damit beide denselben Maßstab benutzen.
pub const MAP_SCALE: f32 = 1.0 / 32.0;

/// Quake-Koords (Z-up) → Engine-Koords (Y-up, Blick −Z), skaliert:
/// `(x, y, z) → (x, z, −y) · MAP_SCALE`. Die Drehung erhält die Orientierung
/// (det +1) — nach außen zeigende Normalen bleiben außen.
pub fn to_engine(p: [f32; 3]) -> [f32; 3] {
    [p[0] * MAP_SCALE, p[2] * MAP_SCALE, -p[1] * MAP_SCALE]
}

pub fn load(path: &str) -> Map {
    let src = read_to_string(path).unwrap_or_else(|e| panic!("map load {path}: {e}"));
    parse(&src)
}

pub fn parse(src: &str) -> Map {
    let mut entities = Vec::new();
    let mut cur_entity: Option<Entity> = None;
    let mut cur_brush:  Option<Brush>  = None;

    for raw in src.lines() {
        // Kommentar (`// …`) abschneiden, dann trimmen.
        let line = match raw.split_once("//") {
            Some((code, _)) => code.trim(),
            None => raw.trim(),
        };
        if line.is_empty() { continue; }

        match line {
            "{" => {
                // Erstes `{` öffnet eine Entity, ein weiteres einen Brush.
                if cur_entity.is_none() {
                    cur_entity = Some(Entity::default());
                } else {
                    cur_brush = Some(Brush { faces: Vec::new() });
                }
            }
            "}" => {
                if let Some(brush) = cur_brush.take() {
                    cur_entity.as_mut().unwrap().brushes.push(brush);
                } else if let Some(entity) = cur_entity.take() {
                    entities.push(entity);
                }
            }
            _ => {
                if let Some(brush) = cur_brush.as_mut() {
                    if let Some(face) = parse_face(line) {
                        brush.faces.push(face);
                    }
                } else if let Some(entity) = cur_entity.as_mut() {
                    if let Some((k, v)) = parse_property(line) {
                        entity.props.insert(k, v);
                    }
                }
            }
        }
    }
    Map { entities }
}

/// `"key" "value"` — Wert darf leer sein.
fn parse_property(line: &str) -> Option<(String, String)> {
    let mut it = line.split('"');
    it.next()?;            // vor dem ersten "
    let key = it.next()?;  // key
    it.next()?;            // zwischen den Paaren
    let val = it.next()?;  // value
    Some((key.to_string(), val.to_string()))
}

/// `( x y z ) ( x y z ) ( x y z ) TEX offX offY rot sx sy`
fn parse_face(line: &str) -> Option<Face> {
    let t: Vec<&str> = line.split_whitespace().collect();
    let mut i = 0;
    let mut plane = [[0.0f32; 3]; 3];
    for p in 0..3 {
        if *t.get(i)? != "(" { return None; }
        for k in 0..3 {
            plane[p][k] = t.get(i + 1 + k)?.parse().ok()?;
        }
        if *t.get(i + 4)? != ")" { return None; }
        i += 5;
    }
    let texture = (*t.get(i)?).to_string();
    let nums: Option<Vec<f32>> = t[i + 1..i + 6].iter().map(|s| s.parse().ok()).collect();
    let n = nums?;
    Some(Face {
        plane,
        texture,
        offset:   [n[0], n[1]],
        rotation: n[2],
        scale:    [n[3], n[4]],
    })
}

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

    const SAMPLE: &str = r#"
// entity 0
{
"classname" "worldspawn"
"wad" ""
// brush 0
{
( -48 -64 -48 ) ( -48 -63 -48 ) ( -48 -64 -47 ) placeholder 32 -32 0 1 1
( -48 -64 -48 ) ( -48 -64 -47 ) ( -47 -64 -48 ) placeholder -16 -32 0 1 1
( -48 -64 -48 ) ( -47 -64 -48 ) ( -48 -63 -48 ) placeholder -16 -32 0 1 1
( 80 64 -16 ) ( 80 65 -16 ) ( 81 64 -16 ) placeholder -16 -32 0 1 1
( 80 64 -16 ) ( 81 64 -16 ) ( 80 64 -15 ) placeholder -16 -32 0 1 1
( 80 64 -16 ) ( 80 64 -15 ) ( 80 65 -16 ) placeholder 32 -32 0 1 1
}
}
"#;

    #[test]
    fn parses_entity_brush_and_face() {
        let m = parse(SAMPLE);
        assert_eq!(m.entities.len(), 1);
        let e = &m.entities[0];
        assert_eq!(e.classname(), Some("worldspawn"));
        assert_eq!(e.props.get("wad").map(String::as_str), Some(""));
        assert_eq!(e.brushes.len(), 1);
        let b = &e.brushes[0];
        assert_eq!(b.faces.len(), 6);

        let f = &b.faces[0];
        assert_eq!(f.texture, "placeholder");
        assert_eq!(f.plane[0], [-48.0, -64.0, -48.0]);
        assert_eq!(f.plane[2], [-48.0, -64.0, -47.0]);
        assert_eq!(f.offset, [32.0, -32.0]);
        assert_eq!(f.scale, [1.0, 1.0]);
    }
}
