//! STL ingestion: parse in-memory bytes into a [`ModelMesh`] via `stl_io`.
//!
//! Both binary and ASCII STL are handled by `stl_io::read_stl`, which probes the
//! 80-byte header for a leading `"solid "` to pick its reader. A binary file whose
//! header happens to start with `"solid "` is mis-routed to the ASCII reader and fails;
//! on a parse error we neutralise the header in an owned copy and retry exactly once, so
//! `stl_io` re-probes and picks its binary reader. We deliberately do **not** try to
//! classify binary-vs-ASCII ourselves (size-based detection is an unreliable heuristic,
//! and `stl_io` exposes no public binary-only entry point).
//!
//! Normals are geometry-derived ([`three_d_asset::TriMesh::compute_normals`]); STL facet
//! normals are not relied upon. No filesystem or network access, so this is safe to call
//! from a `wasm32-unknown-unknown` build.

use std::io::Cursor;

use three_d_asset::{Indices, Positions, TriMesh, Vec3};

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

/// Parse STL bytes (binary or ASCII) into a [`ModelMesh`].
pub fn load_stl(bytes: &[u8]) -> Result<ModelMesh, MeshLoadError> {
    if bytes.is_empty() {
        return Err(MeshLoadError::Empty);
    }

    let mesh = stl_io::read_stl(&mut Cursor::new(bytes)).or_else(|_| {
        let mut buf = bytes.to_vec();
        if buf.len() >= 80 {
            buf[..80].fill(0);
        }
        stl_io::read_stl(&mut Cursor::new(buf))
    });

    build_model_mesh(mesh?)
}

fn build_model_mesh(mesh: stl_io::IndexedMesh) -> Result<ModelMesh, MeshLoadError> {
    let positions = Positions::F32(
        mesh.vertices
            .iter()
            .map(|v| Vec3::new(v.0[0], v.0[1], v.0[2]))
            .collect(),
    );
    let indices = Indices::U32(
        mesh.faces
            .iter()
            .flat_map(|f| f.vertices)
            .map(|i| i as u32)
            .collect(),
    );

    let mut trimesh = TriMesh {
        positions,
        indices,
        normals: None,
        tangents: None,
        uvs: None,
        colors: None,
    };

    crate::validate(&mut trimesh)?;

    Ok(ModelMesh {
        trimesh,
        format: MeshFormat::Stl,
        units: Units::unitless_assumed_mm(),
    })
}

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

    fn tri(a: [f32; 3], b: [f32; 3], c: [f32; 3]) -> stl_io::Triangle {
        stl_io::Triangle {
            normal: stl_io::Normal::new([0.0, 0.0, 0.0]),
            vertices: [
                stl_io::Vertex::new(a),
                stl_io::Vertex::new(b),
                stl_io::Vertex::new(c),
            ],
        }
    }

    fn binary_stl(triangles: &[stl_io::Triangle]) -> Vec<u8> {
        let mut buf = Vec::new();
        stl_io::write_stl(&mut buf, triangles.iter()).expect("write_stl should encode");
        assert!(buf.len() >= 80);
        buf
    }

    fn solid_header_binary_stl(triangles: &[stl_io::Triangle]) -> Vec<u8> {
        let mut buf = binary_stl(triangles);
        buf[..6].copy_from_slice(b"solid ");
        buf
    }

    fn tetrahedron_stl() -> Vec<u8> {
        let v = [
            [0.0, 0.0, 0.0],
            [1.0, 0.0, 0.0],
            [0.0, 1.0, 0.0],
            [0.0, 0.0, 1.0],
        ];
        binary_stl(&[
            tri(v[0], v[2], v[1]),
            tri(v[0], v[1], v[3]),
            tri(v[0], v[3], v[2]),
            tri(v[1], v[2], v[3]),
        ])
    }

    fn assert_approx(actual: f32, expected: f32) {
        assert!(
            (actual - expected).abs() < 1e-5,
            "expected ~{expected}, got {actual}"
        );
    }

    #[test]
    fn parses_binary_stl() {
        let model = load_stl(&tetrahedron_stl()).expect("binary STL should parse");
        assert_eq!(model.format, MeshFormat::Stl);
        assert_eq!(
            model.units,
            Units {
                source: LengthUnit::Unknown,
                assumed: Some(LengthUnit::Millimeter),
            }
        );
        assert_eq!(model.trimesh.triangle_count(), 4);
        assert!(model.trimesh.positions.len() >= 4);
        let normals = model
            .trimesh
            .normals
            .as_ref()
            .expect("normals should be populated");
        assert_eq!(normals.len(), model.trimesh.positions.len());
    }

    #[test]
    fn parses_ascii_stl() {
        let ascii = b"\
solid polymodel
  facet normal 0 0 1
    outer loop
      vertex 0 0 0
      vertex 1 0 0
      vertex 0 1 0
    endloop
  endfacet
endsolid polymodel\n";
        let model = load_stl(ascii).expect("ASCII STL should parse");
        assert_eq!(model.format, MeshFormat::Stl);
        assert_eq!(model.trimesh.triangle_count(), 1);
        assert!(model.trimesh.normals.is_some());
    }

    #[test]
    fn parses_solid_header_binary_via_retry() {
        let model = load_stl(&solid_header_binary_stl(&[tri(
            [0.0, 0.0, 0.0],
            [1.0, 0.0, 0.0],
            [0.0, 1.0, 0.0],
        )]))
        .expect("'solid '-header binary should parse via retry");
        assert_eq!(model.trimesh.triangle_count(), 1);
        assert!(model.trimesh.normals.is_some());
    }

    #[test]
    fn parses_planar_mesh_is_not_degenerate() {
        let model = load_stl(&binary_stl(&[tri(
            [10.0, 0.0, 0.0],
            [14.0, 0.0, 0.0],
            [10.0, 0.0, 4.0],
        )]))
        .expect("planar mesh should parse");
        assert_eq!(model.trimesh.triangle_count(), 1);
    }

    #[test]
    fn camera_fit_off_center() {
        let model = load_stl(&binary_stl(&[tri(
            [10.0, 0.0, 0.0],
            [14.0, 0.0, 0.0],
            [10.0, 0.0, 4.0],
        )]))
        .expect("off-centre mesh should parse");
        let fit = model.camera_fit();
        assert_approx(fit.center[0], 12.0);
        assert_approx(fit.center[1], 0.0);
        assert_approx(fit.center[2], 2.0);
        assert_approx(fit.radius, 2.0);
    }

    #[test]
    fn collinear_mesh_yields_finite_normals() {
        let model = load_stl(&binary_stl(&[tri(
            [0.0, 0.0, 0.0],
            [1.0, 0.0, 0.0],
            [2.0, 0.0, 0.0],
        )]))
        .expect("collinear mesh should parse (not degenerate)");
        let normals = model
            .trimesh
            .normals
            .as_ref()
            .expect("normals should be populated");
        assert!(
            normals
                .iter()
                .all(|n| { n.x.is_finite() && n.y.is_finite() && n.z.is_finite() }),
            "normals must be finite even for zero-area geometry"
        );
    }

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

    #[test]
    fn error_parse_garbage() {
        let garbage = vec![b'G'; 200];
        assert!(matches!(load_stl(&garbage), Err(MeshLoadError::Parse(_))));
    }

    #[test]
    fn error_parse_truncated() {
        let truncated = &tetrahedron_stl()[..90];
        assert!(matches!(load_stl(truncated), Err(MeshLoadError::Parse(_))));
    }

    #[test]
    fn error_zero_triangles() {
        let mut empty = vec![0u8; 84];
        empty[80..84].copy_from_slice(&0u32.to_le_bytes());
        assert!(matches!(load_stl(&empty), Err(MeshLoadError::Degenerate)));
    }

    #[test]
    fn error_point_sized_aabb() {
        let point = binary_stl(&[tri([5.0, 5.0, 5.0], [5.0, 5.0, 5.0], [5.0, 5.0, 5.0])]);
        assert!(matches!(load_stl(&point), Err(MeshLoadError::Degenerate)));
    }

    #[test]
    fn error_non_finite_position() {
        let inf = binary_stl(&[tri(
            [f32::INFINITY, 0.0, 0.0],
            [1.0, 0.0, 0.0],
            [0.0, 1.0, 0.0],
        )]);
        assert!(matches!(load_stl(&inf), Err(MeshLoadError::Degenerate)));
    }

    #[test]
    fn local_body_chest_fixture_parses() {
        let path = concat!(
            env!("CARGO_MANIFEST_DIR"),
            "/public/models/body_f_chest-v4.stl"
        );
        let bytes = std::fs::read(path)
            .expect("local fixture public/models/body_f_chest-v4.stl should be committed");
        let model = load_stl(&bytes).expect("local STL fixture should parse");
        assert_eq!(model.format, MeshFormat::Stl);
        assert_eq!(
            model.units,
            Units {
                source: LengthUnit::Unknown,
                assumed: Some(LengthUnit::Millimeter),
            }
        );
        assert!(model.trimesh.normals.is_some());
        assert!(model.trimesh.triangle_count() > 0);
    }
}
