//! Mesh contract types: the renderer-agnostic [`ModelMesh`] and its provenance.

// `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};

/// Source format of a loaded mesh.
///
/// STL today; OBJ/glTF/3MF direct upload is reserved for PM-23. There is intentionally
/// no binary/ASCII sub-label for STL — reliably telling them apart is the hard part, and
/// both yield the same mesh.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MeshFormat {
    /// Stereolithography (binary or ASCII).
    Stl,
}

/// A length unit.
///
/// STL is unitless, so a loaded STL reports [`LengthUnit::Unknown`] as its source and
/// relies on an assumed convention.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LengthUnit {
    Millimeter,
    Centimeter,
    Meter,
    Inch,
    /// A thousandth of an inch — "thou" in machining, "mil" in PCB/electronics.
    Thou,
    Foot,
    Unknown,
}

/// Unit handling for a loaded model.
///
/// `source` is the unit declared by the file; STL declares none, so this is
/// [`LengthUnit::Unknown`]. `assumed` is the convention applied when the source is
/// unknown — millimetres for Polymodel's STL ingestion.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Units {
    /// The unit declared by the file (STL: [`LengthUnit::Unknown`]).
    pub source: LengthUnit,
    /// The convention assumed when `source` is unknown.
    pub assumed: Option<LengthUnit>,
}

impl Units {
    /// STL unit handling: source unknown, assume millimetres.
    pub fn stl_assumed_mm() -> Self {
        Self {
            source: LengthUnit::Unknown,
            assumed: Some(LengthUnit::Millimeter),
        }
    }
}

/// 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`]); STL 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),
    /// The mesh parsed but is degenerate: no triangles, a non-finite position, or a
    /// point-sized bounding box.
    #[error("mesh is degenerate")]
    Degenerate,
    /// The format is not yet supported (reserved for PM-23).
    #[error("unsupported mesh format")]
    UnsupportedFormat,
}
