//! glTF/GLB ingestion: parse in-memory bytes (+ companions) into a [`ModelMesh`] via
//! `three-d-asset`.
//!
//! GLB BIN chunks and embedded-base64 `data:` buffers resolve from the primary bytes
//! alone. A multi-file `.gltf` resolves its external `.bin`/texture companions from the
//! source's [`MeshResources`], inserted under their exact raw URI so `three-d-asset`'s
//! `base_path.join(uri)` lookup is exact (it is not asked to discover dependencies
//! itself). Textures are decoded during deserialize (the `image`/`png` features are on)
//! and then discarded — [`TriMesh`](three_d_asset::TriMesh) carries no materials — but a
//! referenced-but-missing or undecodable texture fails the whole parse. glTF's base unit
//! is the metre. No filesystem or network access, so this is safe from
//! `wasm32-unknown-unknown`.

use three_d_asset::io;

use crate::contract::{LengthUnit, MeshFormat, MeshLoadError, ModelMesh, Units};
use crate::parser::MeshSource;

/// Parse a glTF/GLB asset (plus companions) into a [`ModelMesh`].
///
/// The GLB magic (`b"glTF"`) is sniffed to pick the `.glb` vs `.gltf` key. Node world
/// transforms are baked into positions by the shared flattener; geometry normals are
/// derived by the shared validator.
pub fn load_gltf(source: &MeshSource<'_>) -> Result<ModelMesh, MeshLoadError> {
    if source.primary.is_empty() {
        return Err(MeshLoadError::Empty);
    }
    let key = if source.primary.starts_with(b"glTF") {
        "asset.glb"
    } else {
        "asset.gltf"
    };
    let mut assets = io::RawAssets::new();
    assets.insert(key, source.primary.to_vec());
    for (uri, bytes) in source.resources.iter() {
        assets.insert(uri, bytes.to_vec());
    }
    let model: three_d_asset::Model = assets
        .deserialize(key)
        .map_err(|e| MeshLoadError::Decode(e.to_string()))?;
    let mut trimesh = crate::flatten_model(model);
    crate::validate(&mut trimesh)?;
    Ok(ModelMesh {
        trimesh,
        format: MeshFormat::Gltf,
        units: Units::declared(LengthUnit::Meter),
    })
}

#[cfg(test)]
mod tests {
    use super::load_gltf;
    use crate::contract::{LengthUnit, MeshFormat, MeshLoadError, ModelMesh, Units};
    use crate::parser::{MeshResources, MeshSource};

    fn fixture(name: &str) -> Vec<u8> {
        let path = format!("{}/public/models/{name}", env!("CARGO_MANIFEST_DIR"));
        std::fs::read(&path)
            .unwrap_or_else(|_| panic!("glTF fixture {name} should be committed at {path}"))
    }

    fn companions(files: &[(&str, &str)]) -> Vec<(String, Vec<u8>)> {
        files
            .iter()
            .map(|(uri, file)| (uri.to_string(), fixture(file)))
            .collect()
    }

    fn assert_meter_units(model: &ModelMesh) {
        assert_eq!(
            model.units,
            Units {
                source: LengthUnit::Meter,
                assumed: None,
            }
        );
    }

    #[test]
    fn parses_self_contained_glb() {
        let primary = fixture("box.glb");
        let source = MeshSource {
            format: MeshFormat::Gltf,
            primary: &primary,
            resources: MeshResources::empty(),
        };
        let model = load_gltf(&source).expect("box.glb should parse");
        assert_eq!(model.format, MeshFormat::Gltf);
        assert_meter_units(&model);
        assert_eq!(model.trimesh.triangle_count(), 12);
        assert!(model.trimesh.normals.is_some());
    }

    #[test]
    fn parses_glb_with_baked_transform_and_multiple_primitives() {
        let primary = fixture("box_translated.glb");
        let source = MeshSource {
            format: MeshFormat::Gltf,
            primary: &primary,
            resources: MeshResources::empty(),
        };
        let model = load_gltf(&source).expect("box_translated.glb should parse");
        assert_eq!(model.trimesh.triangle_count(), 24);
        let center = model.aabb().center();
        assert!(
            (center.x - 10.0).abs() < 0.5,
            "node translation (+10 X) is baked into world-space positions: got center.x = {}",
            center.x
        );
    }

    #[test]
    fn parses_embedded_data_url_gltf() {
        let primary = fixture("box_embedded.gltf");
        let source = MeshSource {
            format: MeshFormat::Gltf,
            primary: &primary,
            resources: MeshResources::empty(),
        };
        let model = load_gltf(&source).expect("embedded-base64 .gltf should parse");
        assert_meter_units(&model);
        assert_eq!(model.trimesh.triangle_count(), 12);
    }

    #[test]
    fn parses_external_buffer_gltf() {
        let primary = fixture("box.gltf");
        let comps = companions(&[("box.bin", "box.bin")]);
        let source = MeshSource {
            format: MeshFormat::Gltf,
            primary: &primary,
            resources: MeshResources::new(&comps),
        };
        let model = load_gltf(&source).expect("external-buffer .gltf should parse");
        assert_meter_units(&model);
        assert_eq!(model.trimesh.triangle_count(), 12);
    }

    #[test]
    fn external_buffer_resolves_exact_companion_not_substring_fallback() {
        let primary = fixture("box.gltf");
        let real = fixture("box.bin");
        let comps = vec![
            ("box.bin".to_string(), real),
            (
                "box.bin.bak".to_string(),
                b"garbage that is not a glTF buffer".to_vec(),
            ),
        ];
        let source = MeshSource {
            format: MeshFormat::Gltf,
            primary: &primary,
            resources: MeshResources::new(&comps),
        };
        let model = load_gltf(&source).expect("exact box.bin must win over box.bin.bak");
        assert_eq!(model.trimesh.triangle_count(), 12);
    }

    #[test]
    fn parses_textured_gltf_with_external_png() {
        let primary = fixture("box_textured.gltf");
        let comps = companions(&[
            ("box_textured.bin", "box_textured.bin"),
            ("box_textured.png", "box_textured.png"),
        ]);
        let source = MeshSource {
            format: MeshFormat::Gltf,
            primary: &primary,
            resources: MeshResources::new(&comps),
        };
        let model = load_gltf(&source).expect("textured .gltf should parse");
        assert_meter_units(&model);
        assert_eq!(model.trimesh.triangle_count(), 12);
    }

    #[test]
    fn error_missing_buffer_companion() {
        let primary = fixture("box.gltf");
        let source = MeshSource {
            format: MeshFormat::Gltf,
            primary: &primary,
            resources: MeshResources::empty(),
        };
        assert!(matches!(load_gltf(&source), Err(MeshLoadError::Decode(_))));
    }

    #[test]
    fn error_missing_texture_companion() {
        let primary = fixture("box_textured.gltf");
        let comps = companions(&[("box_textured.bin", "box_textured.bin")]);
        let source = MeshSource {
            format: MeshFormat::Gltf,
            primary: &primary,
            resources: MeshResources::new(&comps),
        };
        assert!(matches!(load_gltf(&source), Err(MeshLoadError::Decode(_))));
    }

    #[test]
    fn error_empty() {
        let source = MeshSource {
            format: MeshFormat::Gltf,
            primary: &[],
            resources: MeshResources::empty(),
        };
        assert!(matches!(load_gltf(&source), Err(MeshLoadError::Empty)));
    }

    #[test]
    fn error_parse_garbage() {
        let source = MeshSource {
            format: MeshFormat::Gltf,
            primary: b"{ not valid gltf json",
            resources: MeshResources::empty(),
        };
        assert!(matches!(load_gltf(&source), Err(MeshLoadError::Decode(_))));
    }
}
