//! Validated reader for Linden Lab's legacy `.llm` avatar mesh format.

use std::collections::BTreeMap;
use std::path::Path;

use libremetaverse_types::compat::{Object, SortedList};
use libremetaverse_types::{Error, Vector2, Vector3};

use crate::rendering::{
    LindenMeshFace, LindenMeshMorph, LindenMeshMorphVertex, LindenMeshSkinWeightElement,
    LindenMeshVertex, LindenMeshVertexRemap, LindenSkeleton,
};

const MESH_HEADER: &str = "Linden Binary Mesh 1.0";
const MORPH_FOOTER: &str = "End Morphs";
const MAX_MESH_BYTES: usize = 64 * 1024 * 1024;
const MAX_MORPHS: usize = 16_384;
const MORPH_VERTEX_BYTES: usize = 48;

/// Complete native state for a full Linden avatar mesh.
#[derive(Clone, Debug)]
pub struct LindenMesh {
    pub min_pixel_width: f32,
    pub morphs: Vec<LindenMeshMorph>,
    pub skin_weights: Vec<LindenMeshSkinWeightElement>,
    name: String,
    header: String,
    has_weights: bool,
    has_detail_tex_coords: bool,
    position: Vector3,
    rotation_angles: Vector3,
    scale: Vector3,
    num_vertices: u16,
    vertices: Vec<LindenMeshVertex>,
    num_faces: u16,
    faces: Vec<LindenMeshFace>,
    num_skin_joints: u16,
    skin_joints: Vec<String>,
    num_remaps: i32,
    vertex_remaps: Vec<LindenMeshVertexRemap>,
    lod_meshes: SortedList<i32, Object>,
    skeleton: Option<LindenSkeleton>,
}

/// Complete native state for a reference-only Linden LOD mesh.
#[derive(Clone, Debug, PartialEq)]
pub struct ReferenceMesh {
    pub min_pixel_width: f32,
    pub header: String,
    pub has_weights: bool,
    pub has_detail_tex_coords: bool,
    pub position: Vector3,
    pub rotation_angles: Vector3,
    pub rotation_order: u8,
    pub scale: Vector3,
    pub num_faces: u16,
    pub faces: Vec<LindenMeshFace>,
}

impl LindenMesh {
    pub(crate) fn native_new(
        name: String,
        skeleton: Option<LindenSkeleton>,
    ) -> Result<Self, Error> {
        Ok(Self {
            min_pixel_width: 0.0,
            morphs: Vec::new(),
            skin_weights: Vec::new(),
            name,
            header: String::new(),
            has_weights: false,
            has_detail_tex_coords: false,
            position: Vector3::zero(),
            rotation_angles: Vector3::zero(),
            scale: Vector3::zero(),
            num_vertices: 0,
            vertices: Vec::new(),
            num_faces: 0,
            faces: Vec::new(),
            num_skin_joints: 0,
            skin_joints: Vec::new(),
            num_remaps: 0,
            vertex_remaps: Vec::new(),
            lod_meshes: SortedList(BTreeMap::new()),
            skeleton: Some(skeleton.unwrap_or(LindenSkeleton::load_with_method()?)),
        })
    }

    pub(crate) fn native_load_mesh(&mut self, filename: &str) -> Result<(), Error> {
        let bytes = read_mesh_file(filename)?;
        let parsed = ParsedFullMesh::parse(&bytes)?;

        self.header = parsed.header;
        self.has_weights = parsed.has_weights;
        self.has_detail_tex_coords = parsed.has_detail_tex_coords;
        self.position = parsed.position;
        self.rotation_angles = parsed.rotation_angles;
        self.scale = parsed.scale;
        self.num_vertices = parsed.num_vertices;
        self.vertices = parsed.vertices;
        self.num_faces = parsed.num_faces;
        self.faces = parsed.faces;
        self.num_skin_joints = parsed.num_skin_joints;
        self.skin_joints = parsed.skin_joints;
        self.morphs = parsed.morphs;
        self.num_remaps = parsed.num_remaps;
        self.vertex_remaps = parsed.vertex_remaps;
        self.expand_skin_weights()?;
        Ok(())
    }

    pub(crate) fn native_load_lod_mesh(
        &mut self,
        level: i32,
        filename: &str,
    ) -> Result<Object, Error> {
        let object = if is_eye_lod(filename) {
            let mut mesh = Self::native_new(String::new(), None)?;
            mesh.native_load_mesh(filename)?;
            Object::opaque(mesh)
        } else {
            let mut mesh = ReferenceMesh::native_new()?;
            mesh.native_load_mesh(filename)?;
            Object::opaque(mesh)
        };
        self.lod_meshes.0.insert(level, object.clone());
        Ok(object)
    }

    pub(crate) fn native_load_reference_mesh(
        &mut self,
        lod_level: i32,
        filename: &str,
    ) -> Result<ReferenceMesh, Error> {
        if is_eye_lod(filename) {
            return Err(Error::Argument);
        }
        let mut mesh = ReferenceMesh::native_new()?;
        mesh.native_load_mesh(filename)?;
        self.lod_meshes
            .0
            .insert(lod_level, Object::opaque(mesh.clone()));
        Ok(mesh)
    }

    fn expand_skin_weights(&mut self) -> Result<(), Error> {
        self.skin_weights.clear();
        let Some(skeleton) = &self.skeleton else {
            return Ok(());
        };
        let mut joints =
            skeleton.build_expanded_joint_list(Box::new(self.skin_joints.clone().into_iter()))?;
        if joints.is_empty() {
            match self.name.as_str() {
                "eyeBallLeftMesh" => joints.extend(["mEyeLeft".to_owned(), "mSkull".to_owned()]),
                "eyeBallRightMesh" => {
                    joints.extend(["mEyeRight".to_owned(), "mSkull".to_owned()]);
                }
                _ => {}
            }
        }
        if joints.is_empty() {
            return Ok(());
        }
        self.skin_weights.reserve(self.vertices.len());
        for vertex in &self.vertices {
            let raw_index = vertex.weight.floor() as i32;
            let bone_index = raw_index - 1;
            let blend = (vertex.weight - raw_index as f32).clamp(0.0, 1.0);
            let valid_pair = usize::try_from(bone_index)
                .ok()
                .filter(|index| index.saturating_add(1) < joints.len());
            let weight = if let Some(index) = valid_pair {
                LindenMeshSkinWeightElement {
                    bone1: joints[index].clone(),
                    bone2: joints[index + 1].clone(),
                    weight1: 1.0 - blend,
                    weight2: blend,
                }
            } else {
                let nearest = bone_index.clamp(0, joints.len().saturating_sub(1) as i32) as usize;
                LindenMeshSkinWeightElement {
                    bone1: joints[nearest].clone(),
                    bone2: "mPelvis".to_owned(),
                    weight1: 1.0,
                    weight2: 0.0,
                }
            };
            self.skin_weights.push(weight);
        }
        Ok(())
    }

    pub(crate) fn native_faces(&self) -> Vec<LindenMeshFace> {
        self.faces.clone()
    }
    pub(crate) fn native_set_faces(&mut self, value: Vec<LindenMeshFace>) {
        self.num_faces = value.len().try_into().unwrap_or(u16::MAX);
        self.faces = value;
    }
    pub(crate) fn native_has_detail_tex_coords(&self) -> bool {
        self.has_detail_tex_coords
    }
    pub(crate) fn native_set_has_detail_tex_coords(&mut self, value: bool) {
        self.has_detail_tex_coords = value;
    }
    pub(crate) fn native_has_weights(&self) -> bool {
        self.has_weights
    }
    pub(crate) fn native_set_has_weights(&mut self, value: bool) {
        self.has_weights = value;
    }
    pub(crate) fn native_header(&self) -> String {
        self.header.clone()
    }
    pub(crate) fn native_set_header(&mut self, value: String) {
        self.header = value;
    }
    pub(crate) fn native_lod_meshes(&self) -> SortedList<i32, Object> {
        self.lod_meshes.clone()
    }
    pub(crate) fn native_set_lod_meshes(&mut self, value: SortedList<i32, Object>) {
        self.lod_meshes = value;
    }
    pub(crate) fn native_name(&self) -> String {
        self.name.clone()
    }
    pub(crate) fn native_set_name(&mut self, value: String) {
        self.name = value;
    }
    pub(crate) fn native_num_faces(&self) -> u16 {
        self.num_faces
    }
    pub(crate) fn native_set_num_faces(&mut self, value: u16) {
        self.num_faces = value;
    }
    pub(crate) fn native_num_remaps(&self) -> i32 {
        self.num_remaps
    }
    pub(crate) fn native_set_num_remaps(&mut self, value: i32) {
        self.num_remaps = value;
    }
    pub(crate) fn native_num_skin_joints(&self) -> u16 {
        self.num_skin_joints
    }
    pub(crate) fn native_set_num_skin_joints(&mut self, value: u16) {
        self.num_skin_joints = value;
    }
    pub(crate) fn native_num_vertices(&self) -> u16 {
        self.num_vertices
    }
    pub(crate) fn native_set_num_vertices(&mut self, value: u16) {
        self.num_vertices = value;
    }
    pub(crate) fn native_position(&self) -> Vector3 {
        self.position
    }
    pub(crate) fn native_set_position(&mut self, value: Vector3) {
        self.position = value;
    }
    pub(crate) fn native_rotation_angles(&self) -> Vector3 {
        self.rotation_angles
    }
    pub(crate) fn native_set_rotation_angles(&mut self, value: Vector3) {
        self.rotation_angles = value;
    }
    pub(crate) fn native_scale(&self) -> Vector3 {
        self.scale
    }
    pub(crate) fn native_set_scale(&mut self, value: Vector3) {
        self.scale = value;
    }
    pub(crate) fn native_skeleton(&self) -> Option<LindenSkeleton> {
        self.skeleton.clone()
    }
    pub(crate) fn native_set_skeleton(&mut self, value: Option<LindenSkeleton>) {
        self.skeleton = value;
    }
    pub(crate) fn native_skin_joints(&self) -> Vec<String> {
        self.skin_joints.clone()
    }
    pub(crate) fn native_set_skin_joints(&mut self, value: Vec<String>) {
        self.num_skin_joints = value.len().try_into().unwrap_or(u16::MAX);
        self.skin_joints = value;
    }
    pub(crate) fn native_vertex_remaps(&self) -> Vec<LindenMeshVertexRemap> {
        self.vertex_remaps.clone()
    }
    pub(crate) fn native_set_vertex_remaps(&mut self, value: Vec<LindenMeshVertexRemap>) {
        self.num_remaps = value.len().try_into().unwrap_or(i32::MAX);
        self.vertex_remaps = value;
    }
    pub(crate) fn native_vertices(&self) -> Vec<LindenMeshVertex> {
        self.vertices.clone()
    }
    pub(crate) fn native_set_vertices(&mut self, value: Vec<LindenMeshVertex>) {
        self.num_vertices = value.len().try_into().unwrap_or(u16::MAX);
        self.vertices = value;
    }
}

impl ReferenceMesh {
    pub(crate) fn native_new() -> Result<Self, Error> {
        Ok(Self {
            min_pixel_width: 0.0,
            header: String::new(),
            has_weights: false,
            has_detail_tex_coords: false,
            position: Vector3::zero(),
            rotation_angles: Vector3::zero(),
            rotation_order: 0,
            scale: Vector3::zero(),
            num_faces: 0,
            faces: Vec::new(),
        })
    }

    pub(crate) fn native_load_mesh(&mut self, filename: &str) -> Result<(), Error> {
        let bytes = read_mesh_file(filename)?;
        let mut reader = Reader::new(&bytes);
        self.header = reader.fixed_string(24, "reference mesh header")?;
        validate_header(&self.header)?;
        self.has_weights = reader.byte("reference weights flag")? != 0;
        self.has_detail_tex_coords = reader.byte("reference detail flag")? != 0;
        self.position = reader.vector3("reference position")?;
        self.rotation_angles = reader.vector3("reference rotation")?;
        self.rotation_order = reader.byte("reference rotation order")?;
        self.scale = reader.vector3("reference scale")?;
        self.num_faces = reader.u16("reference face count")?;
        self.faces = read_faces(&mut reader, self.num_faces)?;
        reader.finish("reference mesh trailing bytes")?;
        Ok(())
    }
}

#[derive(Debug)]
struct ParsedFullMesh {
    header: String,
    has_weights: bool,
    has_detail_tex_coords: bool,
    position: Vector3,
    rotation_angles: Vector3,
    scale: Vector3,
    num_vertices: u16,
    vertices: Vec<LindenMeshVertex>,
    num_faces: u16,
    faces: Vec<LindenMeshFace>,
    num_skin_joints: u16,
    skin_joints: Vec<String>,
    morphs: Vec<LindenMeshMorph>,
    num_remaps: i32,
    vertex_remaps: Vec<LindenMeshVertexRemap>,
}

impl ParsedFullMesh {
    fn parse(bytes: &[u8]) -> Result<Self, Error> {
        let mut reader = Reader::new(bytes);
        let header = reader.fixed_string(24, "mesh header")?;
        validate_header(&header)?;
        let has_weights = reader.byte("weights flag")? != 0;
        let has_detail_tex_coords = reader.byte("detail flag")? != 0;
        let position = reader.vector3("position")?;
        let rotation_angles = reader.vector3("rotation")?;
        let _rotation_order = reader.byte("rotation order")?;
        let scale = reader.vector3("scale")?;
        let num_vertices = reader.u16("vertex count")?;
        let count = usize::from(num_vertices);
        let mut vertices = vec![empty_vertex(); count];
        for vertex in &mut vertices {
            vertex.coord = reader.vector3("vertex coordinates")?;
        }
        for vertex in &mut vertices {
            vertex.normal = reader.vector3("vertex normals")?;
        }
        for vertex in &mut vertices {
            vertex.bi_normal = reader.vector3("vertex binormals")?;
        }
        for vertex in &mut vertices {
            vertex.tex_coord = reader.vector2("vertex texture coordinates")?;
        }
        if has_detail_tex_coords {
            for vertex in &mut vertices {
                vertex.detail_tex_coord = reader.vector2("vertex detail texture coordinates")?;
            }
        }
        if has_weights {
            for vertex in &mut vertices {
                vertex.weight = reader.f32("vertex weight")?;
                if !vertex.weight.is_finite() {
                    return Err(parse_error(reader.position(), "non-finite vertex weight"));
                }
            }
        }
        let num_faces = reader.u16("face count")?;
        let faces = read_faces(&mut reader, num_faces)?;
        let (num_skin_joints, skin_joints) = if has_weights {
            let count = reader.u16("skin joint count")?;
            let mut joints = Vec::with_capacity(usize::from(count));
            for _ in 0..count {
                joints.push(reader.fixed_string(64, "skin joint name")?);
            }
            (count, joints)
        } else {
            (0, Vec::new())
        };
        let mut morphs = Vec::new();
        loop {
            let name = reader.fixed_string(64, "morph name")?;
            if name == MORPH_FOOTER {
                break;
            }
            if morphs.len() >= MAX_MORPHS {
                return Err(parse_error(reader.position(), "morph count exceeds limit"));
            }
            let signed_count = reader.i32("morph vertex count")?;
            let morph_count = usize::try_from(signed_count)
                .map_err(|_| parse_error(reader.position(), "negative morph vertex count"))?;
            reader.require(
                morph_count
                    .checked_mul(MORPH_VERTEX_BYTES)
                    .ok_or_else(|| parse_error(reader.position(), "morph size overflow"))?,
                "morph vertices",
            )?;
            let mut morph_vertices = Vec::with_capacity(morph_count);
            for _ in 0..morph_count {
                morph_vertices.push(LindenMeshMorphVertex {
                    vertex_index: reader.u32("morph vertex index")?,
                    coord: reader.vector3("morph coordinates")?,
                    normal: reader.vector3("morph normal")?,
                    bi_normal: reader.vector3("morph binormal")?,
                    tex_coord: reader.vector2("morph texture coordinates")?,
                });
            }
            morphs.push(LindenMeshMorph {
                name,
                num_vertices: signed_count,
                vertices: morph_vertices,
            });
        }
        let (num_remaps, vertex_remaps) = if reader.remaining() == 0 {
            (0, Vec::new())
        } else {
            let signed_count = reader.i32("vertex remap count")?;
            let count = usize::try_from(signed_count)
                .map_err(|_| parse_error(reader.position(), "negative vertex remap count"))?;
            reader.require(
                count
                    .checked_mul(8)
                    .ok_or_else(|| parse_error(reader.position(), "vertex remap size overflow"))?,
                "vertex remaps",
            )?;
            let mut remaps = Vec::with_capacity(count);
            for _ in 0..count {
                remaps.push(LindenMeshVertexRemap {
                    remap_source: reader.i32("remap source")?,
                    remap_destination: reader.i32("remap destination")?,
                });
            }
            (signed_count, remaps)
        };
        reader.finish("mesh trailing bytes")?;
        Ok(Self {
            header,
            has_weights,
            has_detail_tex_coords,
            position,
            rotation_angles,
            scale,
            num_vertices,
            vertices,
            num_faces,
            faces,
            num_skin_joints,
            skin_joints,
            morphs,
            num_remaps,
            vertex_remaps,
        })
    }
}

fn empty_vertex() -> LindenMeshVertex {
    LindenMeshVertex {
        bi_normal: Vector3::zero(),
        coord: Vector3::zero(),
        detail_tex_coord: Vector2::zero(),
        normal: Vector3::zero(),
        tex_coord: Vector2::zero(),
        weight: 0.0,
    }
}

fn read_faces(reader: &mut Reader<'_>, count: u16) -> Result<Vec<LindenMeshFace>, Error> {
    reader.require(usize::from(count) * 6, "face indices")?;
    let mut faces = Vec::with_capacity(usize::from(count));
    for _ in 0..count {
        faces.push(LindenMeshFace {
            indices: vec![
                reader.i16("face index")?,
                reader.i16("face index")?,
                reader.i16("face index")?,
            ],
        });
    }
    Ok(faces)
}

fn read_mesh_file(filename: &str) -> Result<Vec<u8>, Error> {
    let metadata = std::fs::metadata(filename).map_err(|_| Error::Argument)?;
    let length = usize::try_from(metadata.len()).map_err(|_| Error::Argument)?;
    if length > MAX_MESH_BYTES {
        return Err(Error::Argument);
    }
    let bytes = std::fs::read(filename).map_err(|_| Error::Argument)?;
    if bytes.len() > MAX_MESH_BYTES {
        return Err(Error::Argument);
    }
    Ok(bytes)
}

fn is_eye_lod(filename: &str) -> bool {
    Path::new(filename)
        .file_name()
        .and_then(|name| name.to_str())
        == Some("avatar_eye_1.llm")
}

fn validate_header(header: &str) -> Result<(), Error> {
    if header == MESH_HEADER {
        Ok(())
    } else {
        Err(parse_error(0, "unrecognized Linden mesh header"))
    }
}

pub(crate) fn trim_at_nul(value: &str) -> String {
    value
        .split_once('\0')
        .map_or(value, |(prefix, _)| prefix)
        .to_owned()
}

struct Reader<'a> {
    bytes: &'a [u8],
    position: usize,
}

impl<'a> Reader<'a> {
    fn new(bytes: &'a [u8]) -> Self {
        Self { bytes, position: 0 }
    }
    fn position(&self) -> usize {
        self.position
    }
    fn remaining(&self) -> usize {
        self.bytes.len().saturating_sub(self.position)
    }
    fn require(&self, count: usize, context: &'static str) -> Result<(), Error> {
        if count <= self.remaining() {
            Ok(())
        } else {
            Err(parse_error(self.position, context))
        }
    }
    fn take(&mut self, count: usize, context: &'static str) -> Result<&'a [u8], Error> {
        self.require(count, context)?;
        let start = self.position;
        self.position += count;
        Ok(&self.bytes[start..self.position])
    }
    fn byte(&mut self, context: &'static str) -> Result<u8, Error> {
        Ok(self.take(1, context)?[0])
    }
    fn i16(&mut self, context: &'static str) -> Result<i16, Error> {
        let bytes: [u8; 2] = self
            .take(2, context)?
            .try_into()
            .map_err(|_| parse_error(self.position, context))?;
        Ok(i16::from_le_bytes(bytes))
    }
    fn u16(&mut self, context: &'static str) -> Result<u16, Error> {
        let bytes: [u8; 2] = self
            .take(2, context)?
            .try_into()
            .map_err(|_| parse_error(self.position, context))?;
        Ok(u16::from_le_bytes(bytes))
    }
    fn i32(&mut self, context: &'static str) -> Result<i32, Error> {
        let bytes: [u8; 4] = self
            .take(4, context)?
            .try_into()
            .map_err(|_| parse_error(self.position, context))?;
        Ok(i32::from_le_bytes(bytes))
    }
    fn u32(&mut self, context: &'static str) -> Result<u32, Error> {
        let bytes: [u8; 4] = self
            .take(4, context)?
            .try_into()
            .map_err(|_| parse_error(self.position, context))?;
        Ok(u32::from_le_bytes(bytes))
    }
    fn f32(&mut self, context: &'static str) -> Result<f32, Error> {
        let bytes: [u8; 4] = self
            .take(4, context)?
            .try_into()
            .map_err(|_| parse_error(self.position, context))?;
        let value = f32::from_le_bytes(bytes);
        if value.is_finite() {
            Ok(value)
        } else {
            Err(parse_error(self.position, context))
        }
    }
    fn vector2(&mut self, context: &'static str) -> Result<Vector2, Error> {
        Ok(Vector2 {
            x: self.f32(context)?,
            y: self.f32(context)?,
        })
    }
    fn vector3(&mut self, context: &'static str) -> Result<Vector3, Error> {
        Ok(Vector3 {
            x: self.f32(context)?,
            y: self.f32(context)?,
            z: self.f32(context)?,
        })
    }
    fn fixed_string(&mut self, count: usize, context: &'static str) -> Result<String, Error> {
        Ok(trim_at_nul(&String::from_utf8_lossy(
            self.take(count, context)?,
        )))
    }
    fn finish(&self, context: &'static str) -> Result<(), Error> {
        if self.remaining() == 0 {
            Ok(())
        } else {
            Err(parse_error(self.position, context))
        }
    }
}

const fn parse_error(position: usize, context: &'static str) -> Error {
    Error::Parse { position, context }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn base_header(weights: bool, detail: bool) -> Vec<u8> {
        let mut bytes = [0_u8; 24].to_vec();
        bytes[..MESH_HEADER.len()].copy_from_slice(MESH_HEADER.as_bytes());
        bytes.push(u8::from(weights));
        bytes.push(u8::from(detail));
        for value in [1.0_f32, 2.0, 3.0, 0.1, 0.2, 0.3] {
            bytes.extend(value.to_le_bytes());
        }
        bytes.push(7);
        for value in [4.0_f32, 5.0, 6.0] {
            bytes.extend(value.to_le_bytes());
        }
        bytes
    }

    #[test]
    fn parses_full_mesh_arrays_morphs_and_remaps() {
        let mut bytes = base_header(false, true);
        bytes.extend(1_u16.to_le_bytes());
        for value in [
            1.0_f32, 2.0, 3.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.25, 0.75, 0.5, 0.5,
        ] {
            bytes.extend(value.to_le_bytes());
        }
        bytes.extend(1_u16.to_le_bytes());
        for index in [0_i16, 0, 0] {
            bytes.extend(index.to_le_bytes());
        }
        let mut morph_name = [0_u8; 64];
        morph_name[..5].copy_from_slice(b"smile");
        bytes.extend(morph_name);
        bytes.extend(1_i32.to_le_bytes());
        bytes.extend(0_u32.to_le_bytes());
        for value in [0.1_f32; 11] {
            bytes.extend(value.to_le_bytes());
        }
        let mut footer = [0_u8; 64];
        footer[..MORPH_FOOTER.len()].copy_from_slice(MORPH_FOOTER.as_bytes());
        bytes.extend(footer);
        bytes.extend(1_i32.to_le_bytes());
        bytes.extend(3_i32.to_le_bytes());
        bytes.extend(4_i32.to_le_bytes());
        let mesh = ParsedFullMesh::parse(&bytes).unwrap();
        assert_eq!(mesh.vertices.len(), 1);
        assert_eq!(mesh.faces[0].indices, vec![0, 0, 0]);
        assert_eq!(mesh.morphs[0].name, "smile");
        assert_eq!(mesh.vertex_remaps[0].remap_destination, 4);
    }

    #[test]
    fn rejects_bad_header_truncation_negative_counts_and_trailing_data() {
        assert!(ParsedFullMesh::parse(b"bad").is_err());
        let mut negative = base_header(false, false);
        negative.extend(0_u16.to_le_bytes());
        negative.extend(0_u16.to_le_bytes());
        let mut name = [0_u8; 64];
        name[0] = b'x';
        negative.extend(name);
        negative.extend((-1_i32).to_le_bytes());
        assert!(ParsedFullMesh::parse(&negative).is_err());
        let mut valid = base_header(false, false);
        valid.extend(0_u16.to_le_bytes());
        valid.extend(0_u16.to_le_bytes());
        let mut footer = [0_u8; 64];
        footer[..MORPH_FOOTER.len()].copy_from_slice(MORPH_FOOTER.as_bytes());
        valid.extend(footer);
        valid.push(1);
        assert!(ParsedFullMesh::parse(&valid).is_err());
    }

    #[test]
    fn trim_stops_at_first_nul() {
        assert_eq!(trim_at_nul("abc\0def\0"), "abc");
    }
}
