//! OBJ ingestion: parse in-memory bytes into a [`ModelMesh`] via `three-d-asset`.
//!
//! `three-d-asset` parses Wavefront OBJ through `wavefront_obj`, which
//! fan-triangulates convex polygons at parse time and drops Line/Point primitives;
//! concave polygons are out of scope. OBJ carries no unit, so the loader assumes
//! millimetres. There is no filesystem or network access — an `.mtl` material sidecar is
//! not fetched (out of scope) — so this is safe to call from a `wasm32-unknown-unknown`
//! build.

use three_d_asset::io;

use crate::mesh::contract::{MeshFormat, MeshLoadError, ModelMesh, Units};

/// Parse OBJ bytes into a [`ModelMesh`].
///
/// Convex polygons are fan-triangulated; Line and Point primitives are dropped. OBJ is
/// unitless, so the result assumes millimetres. Positions are normalized to F32 by the
/// shared validator (OBJ arrives as F64).
pub fn load_obj(bytes: &[u8]) -> Result<ModelMesh, MeshLoadError> {
    if bytes.is_empty() {
        return Err(MeshLoadError::Empty);
    }
    let model: three_d_asset::Model = io::deserialize("asset.obj", bytes.to_vec())
        .map_err(|e| MeshLoadError::Decode(e.to_string()))?;
    let mut trimesh = crate::mesh::flatten_model(model);
    crate::mesh::validate(&mut trimesh)?;
    Ok(ModelMesh {
        trimesh,
        format: MeshFormat::Obj,
        units: Units::unitless_assumed_mm(),
    })
}

#[cfg(test)]
mod tests {
    use super::load_obj;
    use crate::mesh::contract::{LengthUnit, MeshFormat, MeshLoadError, Units};
    use three_d_asset::Positions;

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

    #[test]
    fn parses_quad_cube_fixture() {
        let model = load_obj(&fixture("cube.obj")).expect("cube.obj should parse");
        assert_eq!(model.format, MeshFormat::Obj);
        assert_eq!(
            model.units,
            Units {
                source: LengthUnit::Unknown,
                assumed: Some(LengthUnit::Millimeter),
            }
        );
        // 6 convex quads fan-triangulate to 12 triangles.
        assert_eq!(model.trimesh.triangle_count(), 12);
        // OBJ's F64 positions are normalized to F32.
        assert!(matches!(model.trimesh.positions, Positions::F32(_)));
        let normals = model
            .trimesh
            .normals
            .as_ref()
            .expect("geometry normals should be populated");
        assert_eq!(normals.len(), model.trimesh.positions.len());
        assert!(
            normals
                .iter()
                .all(|n| n.x.is_finite() && n.y.is_finite() && n.z.is_finite()),
            "normals must be finite"
        );
    }

    #[test]
    fn error_empty() {
        assert!(matches!(load_obj(&[]), Err(MeshLoadError::Empty)));
    }

    #[test]
    fn error_parse_garbage() {
        let garbage = b"this is definitely not a valid OBJ file !@#$";
        assert!(matches!(load_obj(garbage), Err(MeshLoadError::Decode(_))));
    }

    #[test]
    fn error_zero_triangles() {
        // Vertices but no faces: parses to an empty mesh.
        let verts_only = b"v 0 0 0\nv 1 0 0\nv 0 1 0\n";
        assert!(matches!(
            load_obj(verts_only),
            Err(MeshLoadError::Degenerate)
        ));
    }

    #[test]
    fn error_point_sized_aabb() {
        // A face collapsed to a single point has a point-sized AABB.
        let point = b"v 5 5 5\nf 1 1 1\n";
        assert!(matches!(load_obj(point), Err(MeshLoadError::Degenerate)));
    }
}
