//! Multi-format mesh ingestion: STL/OBJ/glTF parsing into a renderer-agnostic [`TriMesh`].
//!
//! Defines [`ModelMesh`] (in [`contract`]), a thin Polymodel wrapper over
//! [`three_d_asset::TriMesh`] that carries format provenance, unit assumptions, and a
//! camera framing hint. STL, OBJ, and glTF (GLB, embedded-base64, or multi-file `.gltf`
//! with external companions) are parsed from in-memory bytes via `three-d-asset`, so this
//! crate compiles to `wasm32-unknown-unknown`. 3MF cannot target `wasm32` and is routed
//! to a remote parser (PM-49); the parser seam in [`parser`] is remote-ready.
//!
//! [`MeshFormat`] / [`LengthUnit`] / [`Units`] are owned by [`polymodel_renderer_protocol`]
//! (the leaf crate shared with the app) to avoid a circular dependency.

pub mod companions;
pub mod contract;
pub mod gltf;
pub mod obj;
pub mod parser;
pub mod stl;

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

use crate::contract::MeshLoadError;

/// Validate and finalize a parsed [`TriMesh`] shared by every loader.
///
/// Runs the geometry checks lifted from the STL path so STL, OBJ, and glTF all enforce
/// the same invariants, then derives geometry normals and normalizes positions to F32:
///
/// * no triangles, an out-of-range index, a non-finite position, or a point-sized
///   bounding box → [`MeshLoadError::Degenerate`];
/// * normals are geometry-derived (source facet/vertex normals are not relied upon);
/// * a vertex touched only by zero-area triangles gets a NaN normal, which is scrubbed to zero;
/// * OBJ's `Positions::F64` is downcast to `Positions::F32` (the renderer invariant).
///
/// Call this **after** any per-format transform baking, so the AABB and normals reflect
/// world-space positions.
pub fn validate(trimesh: &mut TriMesh) -> Result<(), MeshLoadError> {
    if trimesh.triangle_count() == 0 {
        return Err(MeshLoadError::Degenerate);
    }
    if !positions_all_finite(&trimesh.positions) {
        return Err(MeshLoadError::Degenerate);
    }
    let position_count = trimesh.positions.len() as u32;
    if trimesh
        .indices
        .to_u32()
        .is_some_and(|indices| indices.iter().any(|&i| i >= position_count))
    {
        return Err(MeshLoadError::Degenerate);
    }
    let size = trimesh.compute_aabb().size();
    if size.x.abs() < f32::EPSILON && size.y.abs() < f32::EPSILON && size.z.abs() < f32::EPSILON {
        return Err(MeshLoadError::Degenerate);
    }
    trimesh.compute_normals();
    if let Some(normals) = trimesh.normals.as_mut() {
        for n in normals {
            if !n.x.is_finite() || !n.y.is_finite() || !n.z.is_finite() {
                *n = Vec3::new(0.0, 0.0, 0.0);
            }
        }
    }
    if let Positions::F64(f64s) = &trimesh.positions {
        trimesh.positions = Positions::F32(
            f64s.iter()
                .map(|v| Vec3::new(v.x as f32, v.y as f32, v.z as f32))
                .collect(),
        );
    }
    Ok(())
}

fn positions_all_finite(positions: &Positions) -> bool {
    match positions {
        Positions::F32(vs) => vs
            .iter()
            .all(|v| v.x.is_finite() && v.y.is_finite() && v.z.is_finite()),
        Positions::F64(vs) => vs
            .iter()
            .all(|v| v.x.is_finite() && v.y.is_finite() && v.z.is_finite()),
    }
}

/// Flatten a parsed `three-d-asset` [`Model`](three_d_asset::Model) into a single
/// [`TriMesh`], shared by the OBJ and glTF loaders.
///
/// `three-d-asset`'s `Scene → Model` conversion accumulates each glTF node chain's world
/// transform onto [`Primitive::transformation`](three_d_asset::Primitive) while leaving
/// positions in local space, so each geometry is baked here (local → world) as it is
/// folded into one mesh with per-primitive index offsets. Non-triangle geometry (e.g.
/// point clouds) is dropped. All positions are unified to F32 (OBJ arrives as F64);
/// [`validate`] recomputes normals afterwards.
pub fn flatten_model(model: three_d_asset::Model) -> TriMesh {
    let mut positions: Vec<Vec3> = Vec::new();
    let mut indices: Vec<u32> = Vec::new();
    for primitive in model.geometries {
        let Geometry::Triangles(mesh) = primitive.geometry else {
            continue;
        };
        let base = positions.len() as u32;
        for p in mesh.positions.to_f32() {
            positions.push(transform_point(primitive.transformation, p));
        }
        match mesh.indices {
            Indices::U32(values) => indices.extend(values.into_iter().map(|i| i + base)),
            Indices::U16(values) => indices.extend(values.into_iter().map(|i| i as u32 + base)),
            Indices::U8(values) => indices.extend(values.into_iter().map(|i| i as u32 + base)),
            Indices::None => indices.extend((0..mesh.positions.len() as u32).map(|i| i + base)),
        }
    }
    TriMesh {
        positions: Positions::F32(positions),
        indices: Indices::U32(indices),
        normals: None,
        tangents: None,
        uvs: None,
        colors: None,
    }
}

fn transform_point(m: Mat4, p: Vec3) -> Vec3 {
    Vec3::new(
        p.x * m.x.x + p.y * m.y.x + p.z * m.z.x + m.w.x,
        p.x * m.x.y + p.y * m.y.y + p.z * m.z.y + m.w.y,
        p.x * m.x.z + p.y * m.y.z + p.z * m.z.z + m.w.z,
    )
}

#[cfg(test)]
mod tests {
    use super::*;
    use three_d_asset::{Indices, SquareMatrix};

    fn trimesh(positions: &[[f32; 3]], indices: &[u32]) -> TriMesh {
        TriMesh {
            positions: Positions::F32(
                positions
                    .iter()
                    .map(|p| Vec3::new(p[0], p[1], p[2]))
                    .collect(),
            ),
            indices: Indices::U32(indices.to_vec()),
            normals: None,
            tangents: None,
            uvs: None,
            colors: None,
        }
    }

    #[test]
    fn validate_rejects_zero_triangles() {
        let mut m = trimesh(&[[0.0, 0.0, 0.0]], &[]);
        assert!(matches!(validate(&mut m), Err(MeshLoadError::Degenerate)));
    }

    #[test]
    fn validate_rejects_non_finite_position() {
        let mut m = trimesh(
            &[[f32::INFINITY, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
            &[0, 1, 2],
        );
        assert!(matches!(validate(&mut m), Err(MeshLoadError::Degenerate)));
    }

    #[test]
    fn validate_rejects_out_of_range_indices() {
        let mut m = trimesh(
            &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
            &[0, 1, 99],
        );
        assert!(matches!(validate(&mut m), Err(MeshLoadError::Degenerate)));
    }

    #[test]
    fn validate_rejects_point_sized_aabb() {
        let mut m = trimesh(&[[5.0, 5.0, 5.0]; 3], &[0, 1, 2]);
        assert!(matches!(validate(&mut m), Err(MeshLoadError::Degenerate)));
    }

    #[test]
    fn validate_derives_normals_and_normalizes_f64() {
        let mut m = TriMesh {
            positions: Positions::F64(
                [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]
                    .iter()
                    .map(|p| three_d_asset::Vector3::new(p[0], p[1], p[2]))
                    .collect(),
            ),
            indices: Indices::U32(vec![0, 1, 2]),
            normals: None,
            tangents: None,
            uvs: None,
            colors: None,
        };
        validate(&mut m).expect("a valid triangle parses");
        assert!(matches!(m.positions, Positions::F32(_)), "F64 -> F32");
        let normals = m.normals.expect("normals populated");
        assert_eq!(normals.len(), 3);
        assert!(
            normals
                .iter()
                .all(|n| n.x.is_finite() && n.y.is_finite() && n.z.is_finite())
        );
    }

    #[test]
    fn flatten_bakes_translation_into_positions() {
        let model = three_d_asset::Model {
            name: "test".into(),
            materials: Vec::new(),
            geometries: vec![
                three_d_asset::Primitive {
                    name: "a".into(),
                    transformation: Mat4::identity(),
                    animations: Vec::new(),
                    geometry: Geometry::Triangles(trimesh(
                        &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
                        &[0, 1, 2],
                    )),
                    material_index: None,
                },
                three_d_asset::Primitive {
                    name: "b".into(),
                    transformation: Mat4::identity(),
                    animations: Vec::new(),
                    geometry: Geometry::Triangles(trimesh(
                        &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
                        &[0, 1, 2],
                    )),
                    material_index: None,
                },
            ],
        };
        let flat = flatten_model(model);
        assert_eq!(flat.positions.len(), 6);
        assert_eq!(flat.indices.to_u32().unwrap(), vec![0, 1, 2, 3, 4, 5]);
    }
}
