//! Mesh contract types: the renderer-agnostic [`ModelMesh`] and its provenance.
//!
//! [`MeshFormat`], [`LengthUnit`], and [`Units`] are re-exported from
//! [`polymodel_renderer_protocol`] so the app, worker, and mesh crate share a single
//! definition without the mesh crate pulling `three-d-asset` into the app.

pub use polymodel_renderer_protocol::{LengthUnit, MeshFormat, Units};

// `AxisAlignedBoundingBox` appears in the public `ModelMesh::aabb` signature, and
// `TriMesh` is a public field type, so both are re-exported alongside the contract.
pub use three_d_asset::{AxisAlignedBoundingBox, TriMesh};

/// Camera framing hint derived from a mesh's axis-aligned bounding box: the centre the
/// camera should orbit and a bounding-radius estimate to frame it.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CameraFit {
    /// AABB centre.
    pub center: [f32; 3],
    /// Half the largest AABB extent.
    pub radius: f32,
}

/// A Polymodel mesh: a [`TriMesh`] plus format provenance and unit handling.
///
/// Normals are geometry-derived (via [`TriMesh::compute_normals`]); source facet
/// normals are not relied upon. Bounds and camera framing come from [`Self::aabb`].
#[derive(Debug)]
pub struct ModelMesh {
    /// The underlying triangle mesh.
    pub trimesh: TriMesh,
    /// The format this mesh was loaded from.
    pub format: MeshFormat,
    /// Unit provenance and assumption.
    pub units: Units,
}

impl ModelMesh {
    /// The axis-aligned bounding box over all vertex positions.
    pub fn aabb(&self) -> AxisAlignedBoundingBox {
        self.trimesh.compute_aabb()
    }

    /// Camera framing derived from [`Self::aabb`]: `center` is the box centre and
    /// `radius` is half the largest box extent.
    pub fn camera_fit(&self) -> CameraFit {
        let aabb = self.aabb();
        let center = aabb.center();
        let size = aabb.size();
        CameraFit {
            center: [center.x, center.y, center.z],
            radius: size.x.max(size.y).max(size.z) / 2.0,
        }
    }
}

/// Errors that can occur loading a mesh.
#[derive(Debug, thiserror::Error)]
pub enum MeshLoadError {
    /// The input was empty (zero bytes).
    #[error("mesh input is empty")]
    Empty,
    /// The bytes could not be parsed as a valid mesh of the expected format.
    #[error("failed to parse STL: {0}")]
    Parse(#[from] std::io::Error),
    /// A format parsed by `three-d-asset` (OBJ/glTF) failed to decode, including a
    /// missing or undecodable companion resource (buffer/texture).
    #[error("mesh decode failed: {0}")]
    Decode(String),
    /// The mesh parsed but is degenerate: no triangles, an out-of-range index, a
    /// non-finite position, or a point-sized bounding box.
    #[error("mesh is degenerate")]
    Degenerate,
    /// The format is not supported by the active parser. `Threemf` returns this when no
    /// remote parser is wired in (PM-49 will supply one).
    #[error("unsupported mesh format")]
    UnsupportedFormat,
}

/// Lightweight stats for reporting back to the main thread.
impl ModelMesh {
    /// Number of unique vertex positions.
    pub fn vertex_count(&self) -> u32 {
        self.trimesh.positions.len() as u32
    }

    /// Number of triangles.
    pub fn triangle_count(&self) -> u32 {
        self.trimesh.triangle_count() as u32
    }
}
