//! Native `MeshFoundry` implementation and bounded Second Life mesh decoding.

#![allow(clippy::cast_possible_truncation)] // Quantized protocol values are checked first.
#![allow(clippy::missing_errors_doc)] // Public signatures mirror the mapped API.
#![allow(clippy::must_use_candidate)] // Attributes are not part of the mapped surface.
#![allow(clippy::needless_pass_by_value)] // Mapped value parameters are owned.
#![allow(clippy::unnecessary_wraps)] // Mapped constructors are fallible.
#![allow(clippy::unused_self)] // IRendering operations are instance methods.

use std::collections::HashMap;
use std::io::Read as _;

use flate2::read::ZlibDecoder;
use libremetaverse::rendering::{
    DetailLevel, Face, FaceMask, FacetedMesh, IRendering, MeshSkinData, SimpleMesh, Vertex,
    VertexWeight,
};
use libremetaverse::{Primitive, PrimitiveTextureEntryFace};
use libremetaverse_imaging::ManagedImage;
use libremetaverse_prim_mesher::SculptMesh;
use libremetaverse_rendering_simple::SimpleRenderer;
use libremetaverse_structured_data::{OSD, OSDMap, deserialize_llsd_binary_prefix};
use libremetaverse_types::compat::Object;
use libremetaverse_types::{Error, Vector2, Vector3, Vector4};

const MAX_ASSET_BYTES: usize = 64 * 1024 * 1024;
const MAX_SECTION_BYTES: usize = 64 * 1024 * 1024;
const MAX_SECTIONS: usize = 256;
const MAX_SUBMESHES: usize = 256;
const MAX_FACE_VERTICES: usize = 65_536;
const MAX_TOTAL_VERTICES: usize = 1_000_000;
const MAX_TOTAL_INDICES: usize = 6_000_000;
const MAX_JOINTS: usize = 512;
const MAX_HULLS: usize = 65_536;
const MAX_HULL_VERTICES: usize = 1_000_000;

/// Mesh-only per-face domains that are not representable by the mapped `Face`
/// fields. Consumers can recover this value with `face.user_data.downcast_ref`.
#[derive(Debug)]
pub struct MeshFaceAux {
    /// Tangent vectors in vertex order; `w` is bitangent handedness.
    pub tangents: Vec<Vector4>,
}

/// Full native renderer for prims, sculpts, terrain, and LLSD mesh assets.
pub struct MeshFoundry;

impl MeshFoundry {
    pub(crate) fn native_new() -> Result<Self, Error> {
        Ok(Self)
    }

    pub(crate) fn native_generate_faceted_mesh(
        &self,
        prim: Primitive,
        lod: DetailLevel,
    ) -> Result<Option<FacetedMesh>, Error> {
        SimpleRenderer::new()?
            .generate_faceted_mesh(prim, lod)
            .map(Some)
    }

    pub(crate) fn native_generate_simple_mesh(
        &self,
        prim: Primitive,
        lod: DetailLevel,
        _with_normals: bool,
    ) -> Result<Option<SimpleMesh>, Error> {
        // The native pipeline always retains normals and UVs. This is a strict
        // improvement over the legacy non-normal overload and keeps one stable
        // vertex contract for downstream consumers.
        SimpleRenderer::new()?
            .generate_simple_mesh(prim, lod)
            .map(Some)
    }

    pub(crate) fn native_generate_faceted_sculpt_mesh(
        &self,
        prim: Primitive,
        sculpt_texture: ManagedImage,
        lod: DetailLevel,
    ) -> Result<Option<FacetedMesh>, Error> {
        SimpleRenderer::new()?
            .generate_faceted_sculpt_mesh(prim, sculpt_texture, lod)
            .map(Some)
    }

    pub(crate) fn native_generate_simple_sculpt_mesh(
        &self,
        prim: Primitive,
        sculpt_texture: ManagedImage,
        lod: DetailLevel,
    ) -> Result<Option<SimpleMesh>, Error> {
        SimpleRenderer::new()?
            .generate_simple_sculpt_mesh(prim, sculpt_texture, lod)
            .map(Some)
    }

    pub(crate) fn native_transform_tex_coords(
        &self,
        vertices: &mut Vec<Vertex>,
        center: Vector3,
        te_face: PrimitiveTextureEntryFace,
        prim_scale: Vector3,
    ) -> Result<(), Error> {
        SimpleRenderer::new()?.transform_tex_coords(vertices, center, te_face, prim_scale)
    }

    pub(crate) fn native_generate_faceted_mesh_mesh(
        &self,
        prim: Primitive,
        mesh_data: Vec<u8>,
        requested_lod: DetailLevel,
    ) -> Result<Option<FacetedMesh>, Error> {
        let source = prim.id;
        let unpacked = unpack_mesh_map(&mesh_data, source)?;
        let (lod, faces) = select_lod(&unpacked, requested_lod)
            .ok_or_else(|| render_error(source, "mesh asset has no visual LOD"))?;
        let skin = unpacked
            .get("skin")
            .map(|value| decode_skin(value, source))
            .transpose()?;
        let mut mesh = decode_faceted(&prim, faces, skin, lod, source)?;
        apply_sculpt_modifiers(&prim, &mut mesh)?;
        Ok(Some(mesh))
    }

    pub(crate) fn native_unpack_mesh(&self, asset_data: Vec<u8>) -> Result<OSDMap, Error> {
        let decoded = unpack_mesh_map(&asset_data, libremetaverse_types::UUID::zero())?;
        OSDMap::new_with_dictionary(decoded)
    }

    pub(crate) fn native_mesh_sub_mesh_as_simple_mesh(
        &self,
        prim: Primitive,
        compressed_mesh_data: Vec<u8>,
    ) -> Result<Option<SimpleMesh>, Error> {
        let source = prim.id;
        let value = decompress_osd(&compressed_mesh_data, source)?;
        let OSD::Array(submeshes) = value else {
            return Err(render_error(source, "mesh section is not a submesh array"));
        };
        if submeshes.len() > MAX_SUBMESHES {
            return Err(render_error(source, "mesh submesh count exceeds limit"));
        }
        let mut faces = Vec::new();
        let mut total_vertices = 0usize;
        let mut total_indices = 0usize;
        for (index, submesh) in submeshes.iter().enumerate() {
            if let Some(face) = decode_submesh(&prim, submesh, index, None, source)? {
                total_vertices = checked_total(
                    total_vertices,
                    face.vertices.len(),
                    MAX_TOTAL_VERTICES,
                    source,
                    "mesh vertex budget exceeded",
                )?;
                total_indices = checked_total(
                    total_indices,
                    face.indices.len(),
                    MAX_TOTAL_INDICES,
                    source,
                    "mesh index budget exceeded",
                )?;
                faces.push(face);
            }
        }
        flatten_faces(faces, source).map(Some)
    }

    pub(crate) fn native_mesh_sub_mesh_as_convex_hulls(
        &self,
        prim: Primitive,
        compressed_mesh_data: Vec<u8>,
        bounding_hull: Option<&mut Vec<Vector3>>,
    ) -> Result<Vec<Vec<Vector3>>, Error> {
        let source = prim.id;
        let value = decompress_osd(&compressed_mesh_data, source)?;
        let map = as_map(&value)
            .ok_or_else(|| render_error(source, "physics convex section is not a map"))?;
        let min = map
            .get("Min")
            .map(|value| osd_vector3(value, source, "invalid convex minimum"))
            .transpose()?
            .unwrap_or(Vector3 {
                x: -0.5,
                y: -0.5,
                z: -0.5,
            });
        let max = map
            .get("Max")
            .map(|value| osd_vector3(value, source, "invalid convex maximum"))
            .transpose()?
            .unwrap_or(Vector3 {
                x: 0.5,
                y: 0.5,
                z: 0.5,
            });
        validate_domain3(min, max, source, "invalid convex position domain")?;

        let decoded_bounding = map
            .get("BoundingVerts")
            .and_then(osd_binary)
            .map(|bytes| decode_quantized_positions(bytes, min, max, source))
            .transpose()?;
        if let Some(output) = bounding_hull {
            output.clear();
            if let Some(vertices) = decoded_bounding {
                *output = vertices;
            }
        }

        let Some(counts) = map.get("HullList").and_then(osd_binary) else {
            return Ok(Vec::new());
        };
        let positions = map
            .get("Positions")
            .and_then(osd_binary)
            .ok_or_else(|| render_error(source, "convex hull positions are missing"))?;
        if counts.len() > MAX_HULLS {
            return Err(render_error(source, "convex hull count exceeds limit"));
        }
        let mut cursor = 0usize;
        let mut total = 0usize;
        let mut hulls = Vec::with_capacity(counts.len());
        for encoded_count in counts {
            let count = if *encoded_count == 0 {
                256usize
            } else {
                usize::from(*encoded_count)
            };
            total = checked_total(
                total,
                count,
                MAX_HULL_VERTICES,
                source,
                "convex hull vertex budget exceeded",
            )?;
            let byte_count = count
                .checked_mul(6)
                .ok_or_else(|| render_error(source, "convex hull byte count overflow"))?;
            let end = cursor
                .checked_add(byte_count)
                .ok_or_else(|| render_error(source, "convex hull offset overflow"))?;
            let bytes = positions
                .get(cursor..end)
                .ok_or_else(|| render_error(source, "truncated convex hull positions"))?;
            hulls.push(decode_quantized_positions(bytes, min, max, source)?);
            cursor = end;
        }
        if cursor != positions.len() {
            return Err(render_error(source, "trailing convex hull positions"));
        }
        Ok(hulls)
    }

    pub(crate) fn native_terrain_mesh(
        &self,
        z_map: Vec<Vec<f32>>,
        x_begin: f32,
        x_end: f32,
        y_begin: f32,
        y_end: f32,
    ) -> Result<Face, Error> {
        let source = libremetaverse_types::UUID::zero();
        let mesh = SculptMesh::from_height_rows(z_map, x_begin, x_end, y_begin, y_end, true)
            .map_err(|_| render_error(source, "generate bounded terrain geometry"))?;
        if mesh.coords.len() > MAX_FACE_VERTICES
            || mesh.coords.len() != mesh.normals.len()
            || mesh.coords.len() != mesh.uvs.len()
        {
            return Err(render_error(source, "invalid terrain vertex domains"));
        }
        let vertices = mesh
            .coords
            .iter()
            .zip(&mesh.normals)
            .zip(&mesh.uvs)
            .map(|((position, normal), uv)| {
                checked_vertex(
                    Vector3 {
                        x: position.x,
                        y: position.y,
                        z: position.z,
                    },
                    Vector3 {
                        x: normal.x,
                        y: normal.y,
                        z: normal.z,
                    },
                    Vector2 { x: uv.u, y: uv.v },
                    source,
                )
            })
            .collect::<Result<Vec<_>, _>>()?;
        let mut indices = Vec::with_capacity(
            mesh.faces
                .len()
                .checked_mul(3)
                .ok_or_else(|| render_error(source, "terrain index count overflow"))?,
        );
        for face in mesh.faces {
            push_triangle(
                &mut indices,
                face.v1,
                face.v2,
                face.v3,
                vertices.len(),
                source,
            )?;
        }
        make_face(
            0,
            vertices,
            indices,
            PrimitiveTextureEntryFace::default(),
            Object::Undefined,
            Vector3::one(),
            source,
        )
    }
}

impl IRendering for MeshFoundry {
    fn generate_faceted_mesh(
        &self,
        prim: Primitive,
        lod: DetailLevel,
    ) -> Result<Option<FacetedMesh>, Error> {
        self.native_generate_faceted_mesh(prim, lod)
    }

    fn generate_faceted_sculpt_mesh(
        &self,
        prim: Primitive,
        sculpt_texture: ManagedImage,
        lod: DetailLevel,
    ) -> Result<Option<FacetedMesh>, Error> {
        self.native_generate_faceted_sculpt_mesh(prim, sculpt_texture, lod)
    }

    fn generate_simple_mesh(
        &self,
        prim: Primitive,
        lod: DetailLevel,
    ) -> Result<Option<SimpleMesh>, Error> {
        self.native_generate_simple_mesh(prim, lod, true)
    }

    fn generate_simple_sculpt_mesh(
        &self,
        prim: Primitive,
        sculpt_texture: ManagedImage,
        lod: DetailLevel,
    ) -> Result<Option<SimpleMesh>, Error> {
        self.native_generate_simple_sculpt_mesh(prim, sculpt_texture, lod)
    }

    fn transform_tex_coords(
        &self,
        vertices: &mut Vec<Vertex>,
        center: Vector3,
        te_face: PrimitiveTextureEntryFace,
        prim_scale: Vector3,
    ) -> Result<(), Error> {
        self.native_transform_tex_coords(vertices, center, te_face, prim_scale)
    }
}

fn unpack_mesh_map(
    asset_data: &[u8],
    source: libremetaverse_types::UUID,
) -> Result<HashMap<String, OSD>, Error> {
    if asset_data.is_empty() || asset_data.len() > MAX_ASSET_BYTES {
        return Err(render_error(source, "mesh asset size is out of range"));
    }
    let (header_value, payload_start) = deserialize_llsd_binary_prefix(asset_data)
        .map_err(|_| render_error(source, "decode mesh asset header"))?;
    let OSD::Map(header) = header_value else {
        return Err(render_error(source, "mesh asset header is not a map"));
    };
    if header.len() > MAX_SECTIONS {
        return Err(render_error(source, "mesh section count exceeds limit"));
    }
    let mut output = HashMap::with_capacity(header.len() + 1);
    output.insert("asset_header".to_owned(), OSD::Map(header.clone()));
    for (name, value) in header {
        let OSD::Map(info) = &value else {
            output.insert(name, value);
            continue;
        };
        let (Some(offset), Some(size)) = (
            info.get("offset").and_then(osd_integer),
            info.get("size").and_then(osd_integer),
        ) else {
            output.insert(name, value);
            continue;
        };
        if offset < 0 || size == 0 {
            output.insert(name, value);
            continue;
        }
        let offset = usize::try_from(offset)
            .map_err(|_| render_error(source, "mesh section offset is invalid"))?;
        let size = usize::try_from(size)
            .map_err(|_| render_error(source, "mesh section size is invalid"))?;
        if size > MAX_SECTION_BYTES {
            return Err(render_error(
                source,
                "compressed mesh section exceeds limit",
            ));
        }
        let begin = payload_start
            .checked_add(offset)
            .ok_or_else(|| render_error(source, "mesh section offset overflow"))?;
        let end = begin
            .checked_add(size)
            .ok_or_else(|| render_error(source, "mesh section size overflow"))?;
        let section = asset_data
            .get(begin..end)
            .ok_or_else(|| render_error(source, "mesh section is outside the asset"))?;
        output.insert(name, decompress_osd(section, source)?);
    }
    Ok(output)
}

fn decompress_osd(compressed: &[u8], source: libremetaverse_types::UUID) -> Result<OSD, Error> {
    if compressed.is_empty() || compressed.len() > MAX_SECTION_BYTES {
        return Err(render_error(
            source,
            "compressed mesh section size is out of range",
        ));
    }
    let mut zlib = ZlibDecoder::new(compressed);
    let mut output = Vec::new();
    (&mut zlib)
        .take((MAX_SECTION_BYTES + 1) as u64)
        .read_to_end(&mut output)
        .map_err(|_| render_error(source, "decompress mesh section"))?;
    if output.len() > MAX_SECTION_BYTES {
        return Err(render_error(
            source,
            "decompressed mesh section exceeds limit",
        ));
    }
    let (value, _) = deserialize_llsd_binary_prefix(&output)
        .map_err(|_| render_error(source, "decode mesh section LLSD"))?;
    Ok(value)
}

fn select_lod(
    unpacked: &HashMap<String, OSD>,
    requested: DetailLevel,
) -> Option<(DetailLevel, &[OSD])> {
    let order = [
        requested,
        DetailLevel::Highest,
        DetailLevel::High,
        DetailLevel::Medium,
        DetailLevel::Low,
    ];
    for (index, candidate) in order.into_iter().enumerate() {
        if index > 0 && candidate == requested {
            continue;
        }
        let key = lod_key(candidate);
        if let Some(OSD::Array(values)) = unpacked.get(key)
            && !values.is_empty()
        {
            return Some((candidate, values));
        }
    }
    None
}

const fn lod_key(lod: DetailLevel) -> &'static str {
    match lod {
        DetailLevel::Highest => "high_lod",
        DetailLevel::High => "medium_lod",
        DetailLevel::Medium => "low_lod",
        DetailLevel::Low => "lowest_lod",
    }
}

fn decode_faceted(
    prim: &Primitive,
    submeshes: &[OSD],
    skin_data: Option<MeshSkinData>,
    _lod: DetailLevel,
    source: libremetaverse_types::UUID,
) -> Result<FacetedMesh, Error> {
    if submeshes.len() > MAX_SUBMESHES {
        return Err(render_error(source, "mesh submesh count exceeds limit"));
    }
    let mut faces = Vec::with_capacity(submeshes.len());
    let mut total_vertices = 0usize;
    let mut total_indices = 0usize;
    for (index, submesh) in submeshes.iter().enumerate() {
        if let Some(face) = decode_submesh(prim, submesh, index, skin_data.as_ref(), source)? {
            total_vertices = checked_total(
                total_vertices,
                face.vertices.len(),
                MAX_TOTAL_VERTICES,
                source,
                "mesh vertex budget exceeded",
            )?;
            total_indices = checked_total(
                total_indices,
                face.indices.len(),
                MAX_TOTAL_INDICES,
                source,
                "mesh index budget exceeded",
            )?;
            faces.push(face);
        }
    }
    Ok(FacetedMesh { faces, skin_data })
}

#[allow(clippy::too_many_lines)] // One ordered pass validates parallel mesh domains.
fn decode_submesh(
    prim: &Primitive,
    value: &OSD,
    face_index: usize,
    skin: Option<&MeshSkinData>,
    source: libremetaverse_types::UUID,
) -> Result<Option<Face>, Error> {
    let map = as_map(value).ok_or_else(|| render_error(source, "mesh submesh is not a map"))?;
    if map.get("NoGeometry").and_then(osd_boolean).unwrap_or(false) {
        return Ok(None);
    }
    let positions = map
        .get("Position")
        .and_then(osd_binary)
        .ok_or_else(|| render_error(source, "mesh positions are missing"))?;
    if positions.is_empty() || !positions.len().is_multiple_of(6) {
        return Err(render_error(source, "mesh position buffer is malformed"));
    }
    let vertex_count = positions.len() / 6;
    if vertex_count > MAX_FACE_VERTICES {
        return Err(render_error(
            source,
            "mesh face exceeds 16-bit vertex range",
        ));
    }
    let (position_min, position_max) = domain3(
        map.get("PositionDomain"),
        Vector3 {
            x: -0.5,
            y: -0.5,
            z: -0.5,
        },
        Vector3 {
            x: 0.5,
            y: 0.5,
            z: 0.5,
        },
        source,
        "invalid mesh position domain",
    )?;
    let normals = map.get("Normal").and_then(osd_binary);
    if normals.is_some_and(|bytes| bytes.len() != vertex_count * 6) {
        return Err(render_error(source, "mesh normal buffer is misaligned"));
    }
    let tex_coords = map.get("TexCoord0").and_then(osd_binary);
    if tex_coords.is_some_and(|bytes| bytes.len() != vertex_count * 4) {
        return Err(render_error(source, "mesh primary UV buffer is misaligned"));
    }
    let (primary_min, primary_max) = if tex_coords.is_some() {
        domain2(
            Some(
                map.get("TexCoord0Domain")
                    .ok_or_else(|| render_error(source, "primary UV domain is missing"))?,
            ),
            Vector2::zero(),
            Vector2::zero(),
            source,
            "invalid primary UV domain",
        )?
    } else {
        (Vector2::zero(), Vector2::zero())
    };
    let tex_coords1 = map.get("TexCoord1").and_then(osd_binary);
    if tex_coords1.is_some_and(|bytes| bytes.len() != vertex_count * 4) {
        return Err(render_error(
            source,
            "mesh secondary UV buffer is misaligned",
        ));
    }
    let (secondary_min, secondary_max) = if tex_coords1.is_some() {
        domain2(
            map.get("TexCoord1Domain"),
            Vector2::zero(),
            Vector2::zero(),
            source,
            "invalid secondary UV domain",
        )?
    } else {
        (Vector2::zero(), Vector2::zero())
    };
    let tangent_bytes = map.get("Tangent").and_then(osd_binary);
    if tangent_bytes.is_some_and(|bytes| bytes.len() != vertex_count * 8) {
        return Err(render_error(source, "mesh tangent buffer is misaligned"));
    }

    let mut vertices = Vec::with_capacity(vertex_count);
    let mut secondary = tex_coords1.map(|_| Vec::with_capacity(vertex_count));
    for index in 0..vertex_count {
        let p = index * 6;
        let t = index * 4;
        let position = Vector3 {
            x: dequantize(
                read_u16(positions, p, source)?,
                position_min.x,
                position_max.x,
            ),
            y: dequantize(
                read_u16(positions, p + 2, source)?,
                position_min.y,
                position_max.y,
            ),
            z: dequantize(
                read_u16(positions, p + 4, source)?,
                position_min.z,
                position_max.z,
            ),
        };
        let normal = if let Some(bytes) = normals {
            normalize3(
                Vector3 {
                    x: dequantize(read_u16(bytes, p, source)?, -1.0, 1.0),
                    y: dequantize(read_u16(bytes, p + 2, source)?, -1.0, 1.0),
                    z: dequantize(read_u16(bytes, p + 4, source)?, -1.0, 1.0),
                },
                Vector3::unit_z(),
            )
        } else {
            Vector3::zero()
        };
        let tex_coord = if let Some(bytes) = tex_coords {
            Vector2 {
                x: dequantize(read_u16(bytes, t, source)?, primary_min.x, primary_max.x),
                y: dequantize(
                    read_u16(bytes, t + 2, source)?,
                    primary_min.y,
                    primary_max.y,
                ),
            }
        } else {
            Vector2::zero()
        };
        vertices.push(checked_vertex(position, normal, tex_coord, source)?);
        if let (Some(bytes), Some(output)) = (tex_coords1, secondary.as_mut()) {
            output.push(Vector2 {
                x: dequantize(
                    read_u16(bytes, t, source)?,
                    secondary_min.x,
                    secondary_max.x,
                ),
                y: dequantize(
                    read_u16(bytes, t + 2, source)?,
                    secondary_min.y,
                    secondary_max.y,
                ),
            });
        }
    }

    let triangles = map
        .get("TriangleList")
        .and_then(osd_binary)
        .ok_or_else(|| render_error(source, "mesh triangle list is missing"))?;
    if !triangles.len().is_multiple_of(6) || triangles.len() / 2 > MAX_TOTAL_INDICES {
        return Err(render_error(source, "mesh triangle buffer is malformed"));
    }
    let mut indices = Vec::with_capacity(triangles.len() / 2);
    for offset in (0..triangles.len()).step_by(6) {
        let a = read_u16(triangles, offset, source)?;
        let b = read_u16(triangles, offset + 2, source)?;
        let c = read_u16(triangles, offset + 4, source)?;
        for index in [a, b, c] {
            if usize::from(index) >= vertex_count {
                return Err(render_error(source, "mesh triangle index is out of range"));
            }
            indices.push(index);
        }
    }
    if normals.is_none() {
        generate_normals(&mut vertices, &indices);
    }
    let tangents = if let Some(bytes) = tangent_bytes {
        decode_tangents(bytes, vertex_count, source)?
    } else {
        generate_tangents(&vertices, &indices)
    };
    let weights = match (skin, map.get("Weights").and_then(osd_binary)) {
        (Some(skin), Some(bytes)) => Some(decode_weights(
            bytes,
            skin.joint_names.len(),
            vertex_count,
            source,
        )?),
        (None, Some(_)) => {
            return Err(render_error(source, "mesh weights have no skin section"));
        }
        _ => None,
    };
    let normalized_scale = map
        .get("NormalizedScale")
        .map(|value| osd_vector3(value, source, "invalid normalized face scale"))
        .transpose()?
        .unwrap_or(Vector3::one());
    let texture = texture_for_face(prim, face_index, source)?;
    let mut face = make_face(
        i32::try_from(face_index).map_err(|_| render_error(source, "mesh face id overflow"))?,
        vertices,
        indices,
        texture,
        Object::opaque(MeshFaceAux { tangents }),
        normalized_scale,
        source,
    )?;
    face.tex_coords1 = secondary;
    face.weights = weights;
    Ok(Some(face))
}

fn decode_skin(value: &OSD, source: libremetaverse_types::UUID) -> Result<MeshSkinData, Error> {
    let map =
        as_map(value).ok_or_else(|| render_error(source, "mesh skin section is not a map"))?;
    let names = match map.get("joint_names") {
        Some(OSD::Array(values)) if values.len() <= MAX_JOINTS => values
            .iter()
            .map(|value| {
                value
                    .as_string()
                    .map_err(|_| render_error(source, "invalid skin joint name"))
            })
            .collect::<Result<Vec<_>, _>>()?,
        Some(OSD::Array(_)) => return Err(render_error(source, "skin joint count exceeds limit")),
        Some(_) => return Err(render_error(source, "skin joint names are not an array")),
        None => Vec::new(),
    };
    let mut data = MeshSkinData::new()?;
    data.joint_names = names;
    data.inverse_bind_matrices = decode_matrix_array(
        map.get("inverse_bind_matrix"),
        data.joint_names.len(),
        false,
        source,
        "invalid inverse bind matrices",
    )?;
    data.alt_inverse_bind_matrices = decode_matrix_array(
        map.get("alt_inverse_bind_matrix"),
        data.joint_names.len(),
        true,
        source,
        "invalid alternate inverse bind matrices",
    )?;
    data.bind_shape_matrix = match map.get("bind_shape_matrix") {
        Some(value) => decode_matrix(value, source, "invalid bind shape matrix")?,
        None => identity_matrix().to_vec(),
    };
    data.pelvis_offset = match map.get("pelvis_offset") {
        Some(value) => {
            let value = value
                .as_real()
                .map_err(|_| render_error(source, "invalid pelvis offset"))?
                as f32;
            if !value.is_finite() {
                return Err(render_error(source, "non-finite pelvis offset"));
            }
            value
        }
        None => 0.0,
    };
    data.lock_scale_if_joint_position = map
        .get("lock_scale_if_joint_position")
        .map(OSD::as_boolean)
        .transpose()
        .map_err(|_| render_error(source, "invalid skin scale lock"))?
        .unwrap_or(false);
    Ok(data)
}

fn decode_matrix_array(
    value: Option<&OSD>,
    joint_count: usize,
    optional: bool,
    source: libremetaverse_types::UUID,
    context: &'static str,
) -> Result<Vec<f32>, Error> {
    let Some(value) = value else {
        if optional {
            return Ok(Vec::new());
        }
        return Ok((0..joint_count).flat_map(|_| identity_matrix()).collect());
    };
    let OSD::Array(matrices) = value else {
        return Err(render_error(source, context));
    };
    if matrices.len() != joint_count {
        return Err(render_error(source, context));
    }
    let mut output = Vec::with_capacity(joint_count * 16);
    for matrix in matrices {
        output.extend(decode_matrix(matrix, source, context)?);
    }
    Ok(output)
}

fn decode_matrix(
    value: &OSD,
    source: libremetaverse_types::UUID,
    context: &'static str,
) -> Result<Vec<f32>, Error> {
    let OSD::Array(values) = value else {
        return Err(render_error(source, context));
    };
    if values.len() != 16 {
        return Err(render_error(source, context));
    }
    values
        .iter()
        .map(|value| {
            let value = value.as_real().map_err(|_| render_error(source, context))? as f32;
            if value.is_finite() {
                Ok(value)
            } else {
                Err(render_error(source, context))
            }
        })
        .collect()
}

fn decode_weights(
    bytes: &[u8],
    joint_count: usize,
    vertex_count: usize,
    source: libremetaverse_types::UUID,
) -> Result<Vec<VertexWeight>, Error> {
    if joint_count == 0 || bytes.len() > vertex_count.saturating_mul(13) {
        return Err(render_error(source, "mesh skin weights are out of range"));
    }
    let mut cursor = 0usize;
    let mut output = Vec::with_capacity(vertex_count);
    for _ in 0..vertex_count {
        if cursor >= bytes.len() {
            output.push(default_weight());
            continue;
        }
        let mut joints = [0_i32; 4];
        let mut weights = [0.0_f32; 4];
        let mut count = 0usize;
        while cursor < bytes.len() && count < 4 {
            let joint = bytes[cursor];
            cursor += 1;
            if joint == 0xff {
                break;
            }
            if usize::from(joint) >= joint_count {
                return Err(render_error(
                    source,
                    "mesh skin joint index is out of range",
                ));
            }
            let end = cursor
                .checked_add(2)
                .ok_or_else(|| render_error(source, "mesh weight offset overflow"))?;
            let raw = bytes
                .get(cursor..end)
                .ok_or_else(|| render_error(source, "truncated mesh skin weight"))?;
            cursor = end;
            joints[count] = i32::from(joint);
            weights[count] =
                (f32::from(u16::from_le_bytes([raw[0], raw[1]])) / 65_535.0).clamp(0.001, 0.999);
            count += 1;
        }
        let total: f32 = weights.iter().sum();
        if total > 0.0 {
            for weight in &mut weights {
                *weight /= total;
            }
        } else {
            weights[0] = 1.0;
        }
        output.push(VertexWeight {
            joint0: joints[0],
            joint1: joints[1],
            joint2: joints[2],
            joint3: joints[3],
            weight0: weights[0],
            weight1: weights[1],
            weight2: weights[2],
            weight3: weights[3],
        });
    }
    if cursor != bytes.len() {
        return Err(render_error(source, "trailing mesh skin weights"));
    }
    Ok(output)
}

const fn default_weight() -> VertexWeight {
    VertexWeight {
        joint0: 0,
        joint1: 0,
        joint2: 0,
        joint3: 0,
        weight0: 1.0,
        weight1: 0.0,
        weight2: 0.0,
        weight3: 0.0,
    }
}

fn decode_tangents(
    bytes: &[u8],
    vertex_count: usize,
    source: libremetaverse_types::UUID,
) -> Result<Vec<Vector4>, Error> {
    let mut tangents = Vec::with_capacity(vertex_count);
    for index in 0..vertex_count {
        let offset = index * 8;
        let tangent = normalize3(
            Vector3 {
                x: dequantize(read_u16(bytes, offset, source)?, -1.0, 1.0),
                y: dequantize(read_u16(bytes, offset + 2, source)?, -1.0, 1.0),
                z: dequantize(read_u16(bytes, offset + 4, source)?, -1.0, 1.0),
            },
            Vector3::unit_x(),
        );
        let handedness = dequantize(read_u16(bytes, offset + 6, source)?, -1.0, 1.0);
        tangents.push(Vector4 {
            x: tangent.x,
            y: tangent.y,
            z: tangent.z,
            w: if handedness < 0.0 { -1.0 } else { 1.0 },
        });
    }
    Ok(tangents)
}

fn generate_normals(vertices: &mut [Vertex], indices: &[u16]) {
    let mut normals = vec![Vector3::zero(); vertices.len()];
    for triangle in indices.chunks_exact(3) {
        let a = vertices[usize::from(triangle[0])].position;
        let b = vertices[usize::from(triangle[1])].position;
        let c = vertices[usize::from(triangle[2])].position;
        let normal = cross(sub3(b, a), sub3(c, a));
        for index in triangle {
            normals[usize::from(*index)] = add3(normals[usize::from(*index)], normal);
        }
    }
    for (vertex, normal) in vertices.iter_mut().zip(normals) {
        vertex.normal = normalize3(normal, Vector3::unit_z());
    }
}

fn generate_tangents(vertices: &[Vertex], indices: &[u16]) -> Vec<Vector4> {
    let mut tangent_sum = vec![Vector3::zero(); vertices.len()];
    let mut bitangent_sum = vec![Vector3::zero(); vertices.len()];
    for triangle in indices.chunks_exact(3) {
        let ia = usize::from(triangle[0]);
        let ib = usize::from(triangle[1]);
        let ic = usize::from(triangle[2]);
        let edge1 = sub3(vertices[ib].position, vertices[ia].position);
        let edge2 = sub3(vertices[ic].position, vertices[ia].position);
        let duv1 = sub2(vertices[ib].tex_coord, vertices[ia].tex_coord);
        let duv2 = sub2(vertices[ic].tex_coord, vertices[ia].tex_coord);
        let determinant = duv1.x * duv2.y - duv1.y * duv2.x;
        if determinant.abs() <= f32::EPSILON || !determinant.is_finite() {
            continue;
        }
        let inverse = 1.0 / determinant;
        let tangent = scale3(sub3(scale3(edge1, duv2.y), scale3(edge2, duv1.y)), inverse);
        let bitangent = scale3(sub3(scale3(edge2, duv1.x), scale3(edge1, duv2.x)), inverse);
        for index in [ia, ib, ic] {
            tangent_sum[index] = add3(tangent_sum[index], tangent);
            bitangent_sum[index] = add3(bitangent_sum[index], bitangent);
        }
    }
    vertices
        .iter()
        .enumerate()
        .map(|(index, vertex)| {
            let projected = sub3(
                tangent_sum[index],
                scale3(vertex.normal, dot(vertex.normal, tangent_sum[index])),
            );
            let tangent = normalize3(projected, orthogonal_tangent(vertex.normal));
            let handedness = if dot(cross(vertex.normal, tangent), bitangent_sum[index]) < 0.0 {
                -1.0
            } else {
                1.0
            };
            Vector4 {
                x: tangent.x,
                y: tangent.y,
                z: tangent.z,
                w: handedness,
            }
        })
        .collect()
}

fn apply_sculpt_modifiers(prim: &Primitive, mesh: &mut FacetedMesh) -> Result<(), Error> {
    let Some(sculpt) = prim.sculpt.as_ref() else {
        return Ok(());
    };
    let reflect = sculpt.mirror();
    let invert = sculpt.invert();
    let reverse = reflect ^ invert;
    if !reflect && !invert {
        return Ok(());
    }
    for face in &mut mesh.faces {
        for vertex in &mut face.vertices {
            if reflect {
                vertex.position.x = -vertex.position.x;
                vertex.normal.x = -vertex.normal.x;
            }
            if invert {
                vertex.normal = scale3(vertex.normal, -1.0);
            }
        }
        if let Some(aux) = face.user_data.downcast_ref::<MeshFaceAux>() {
            let tangents = aux
                .tangents
                .iter()
                .map(|tangent| Vector4 {
                    x: if reflect { -tangent.x } else { tangent.x },
                    y: tangent.y,
                    z: tangent.z,
                    w: if reverse { -tangent.w } else { tangent.w },
                })
                .collect();
            face.user_data = Object::opaque(MeshFaceAux { tangents });
        }
        if reverse {
            for triangle in face.indices.chunks_exact_mut(3) {
                triangle.swap(1, 2);
            }
        }
        update_face_bounds(face)?;
    }
    Ok(())
}

fn flatten_faces(
    faces: Vec<Face>,
    source: libremetaverse_types::UUID,
) -> Result<SimpleMesh, Error> {
    let mut vertices = Vec::new();
    let mut indices = Vec::new();
    for face in faces {
        let base = vertices.len();
        let combined = base
            .checked_add(face.vertices.len())
            .ok_or_else(|| render_error(source, "simple mesh vertex count overflow"))?;
        if combined > MAX_FACE_VERTICES {
            return Err(render_error(
                source,
                "simple mesh exceeds 16-bit vertex range",
            ));
        }
        for index in face.indices {
            let global = base
                .checked_add(usize::from(index))
                .ok_or_else(|| render_error(source, "simple mesh index overflow"))?;
            if global >= combined {
                return Err(render_error(source, "simple mesh index is out of range"));
            }
            indices.push(
                u16::try_from(global)
                    .map_err(|_| render_error(source, "simple mesh index exceeds 16 bits"))?,
            );
        }
        vertices.extend(face.vertices);
    }
    Ok(SimpleMesh { vertices, indices })
}

fn make_face(
    id: i32,
    vertices: Vec<Vertex>,
    indices: Vec<u16>,
    texture_face: PrimitiveTextureEntryFace,
    user_data: Object,
    normalized_scale: Vector3,
    source: libremetaverse_types::UUID,
) -> Result<Face, Error> {
    if vertices.is_empty() {
        return Err(render_error(source, "render face has no vertices"));
    }
    let mut face = Face {
        begin_s: 0,
        begin_t: 0,
        center: Vector3::zero(),
        edge: Vec::new(),
        id,
        indices,
        mask: FaceMask::SINGLE,
        max_extent: Vector3::zero(),
        min_extent: Vector3::zero(),
        normalized_scale,
        num_s: 0,
        num_t: 0,
        tex_coords1: None,
        texture_face,
        user_data,
        vertices,
        weights: None,
    };
    update_face_bounds(&mut face)?;
    Ok(face)
}

fn update_face_bounds(face: &mut Face) -> Result<(), Error> {
    let Some(first) = face.vertices.first() else {
        return Err(Error::Argument);
    };
    let mut min = first.position;
    let mut max = first.position;
    for vertex in &face.vertices[1..] {
        min.x = min.x.min(vertex.position.x);
        min.y = min.y.min(vertex.position.y);
        min.z = min.z.min(vertex.position.z);
        max.x = max.x.max(vertex.position.x);
        max.y = max.y.max(vertex.position.y);
        max.z = max.z.max(vertex.position.z);
    }
    face.min_extent = min;
    face.max_extent = max;
    face.center = Vector3 {
        x: (min.x + max.x) * 0.5,
        y: (min.y + max.y) * 0.5,
        z: (min.z + max.z) * 0.5,
    };
    Ok(())
}

fn texture_for_face(
    prim: &Primitive,
    index: usize,
    source: libremetaverse_types::UUID,
) -> Result<PrimitiveTextureEntryFace, Error> {
    let Some(textures) = prim.textures.as_ref() else {
        return Ok(PrimitiveTextureEntryFace::default());
    };
    let index =
        u32::try_from(index).map_err(|_| render_error(source, "texture face index overflow"))?;
    textures
        .get_face(index)
        .map_err(|_| render_error(source, "texture face index is out of range"))
        .map(|face| face.cloned().unwrap_or_default())
}

fn checked_vertex(
    position: Vector3,
    normal: Vector3,
    tex_coord: Vector2,
    source: libremetaverse_types::UUID,
) -> Result<Vertex, Error> {
    if !finite3(position) || !finite3(normal) || !finite2(tex_coord) {
        return Err(render_error(
            source,
            "renderer produced a non-finite vertex",
        ));
    }
    Ok(Vertex {
        position,
        normal,
        tex_coord,
    })
}

fn push_triangle(
    indices: &mut Vec<u16>,
    a: i32,
    b: i32,
    c: i32,
    vertex_count: usize,
    source: libremetaverse_types::UUID,
) -> Result<(), Error> {
    for index in [a, b, c] {
        let index =
            usize::try_from(index).map_err(|_| render_error(source, "negative renderer index"))?;
        if index >= vertex_count {
            return Err(render_error(source, "renderer index is out of range"));
        }
        indices.push(
            u16::try_from(index)
                .map_err(|_| render_error(source, "renderer index exceeds 16 bits"))?,
        );
    }
    Ok(())
}

fn domain3(
    value: Option<&OSD>,
    default_min: Vector3,
    default_max: Vector3,
    source: libremetaverse_types::UUID,
    context: &'static str,
) -> Result<(Vector3, Vector3), Error> {
    let Some(value) = value else {
        return Ok((default_min, default_max));
    };
    let map = as_map(value).ok_or_else(|| render_error(source, context))?;
    let min = map
        .get("Min")
        .ok_or_else(|| render_error(source, context))
        .and_then(|value| osd_vector3(value, source, context))?;
    let max = map
        .get("Max")
        .ok_or_else(|| render_error(source, context))
        .and_then(|value| osd_vector3(value, source, context))?;
    validate_domain3(min, max, source, context)?;
    Ok((min, max))
}

fn domain2(
    value: Option<&OSD>,
    default_min: Vector2,
    default_max: Vector2,
    source: libremetaverse_types::UUID,
    context: &'static str,
) -> Result<(Vector2, Vector2), Error> {
    let Some(value) = value else {
        return Ok((default_min, default_max));
    };
    let map = as_map(value).ok_or_else(|| render_error(source, context))?;
    let min = map
        .get("Min")
        .ok_or_else(|| render_error(source, context))?
        .as_vector2()
        .map_err(|_| render_error(source, context))?;
    let max = map
        .get("Max")
        .ok_or_else(|| render_error(source, context))?
        .as_vector2()
        .map_err(|_| render_error(source, context))?;
    if !finite2(min) || !finite2(max) || min.x > max.x || min.y > max.y {
        return Err(render_error(source, context));
    }
    Ok((min, max))
}

fn validate_domain3(
    min: Vector3,
    max: Vector3,
    source: libremetaverse_types::UUID,
    context: &'static str,
) -> Result<(), Error> {
    if !finite3(min) || !finite3(max) || min.x > max.x || min.y > max.y || min.z > max.z {
        Err(render_error(source, context))
    } else {
        Ok(())
    }
}

fn decode_quantized_positions(
    bytes: &[u8],
    min: Vector3,
    max: Vector3,
    source: libremetaverse_types::UUID,
) -> Result<Vec<Vector3>, Error> {
    if !bytes.len().is_multiple_of(6) || bytes.len() / 6 > MAX_HULL_VERTICES {
        return Err(render_error(source, "convex position buffer is malformed"));
    }
    (0..bytes.len())
        .step_by(6)
        .map(|offset| {
            Ok(Vector3 {
                x: dequantize(read_u16(bytes, offset, source)?, min.x, max.x),
                y: dequantize(read_u16(bytes, offset + 2, source)?, min.y, max.y),
                z: dequantize(read_u16(bytes, offset + 4, source)?, min.z, max.z),
            })
        })
        .collect()
}

fn read_u16(bytes: &[u8], offset: usize, source: libremetaverse_types::UUID) -> Result<u16, Error> {
    let bytes = bytes
        .get(offset..offset + 2)
        .ok_or_else(|| render_error(source, "truncated quantized mesh value"))?;
    Ok(u16::from_le_bytes([bytes[0], bytes[1]]))
}

fn checked_total(
    current: usize,
    additional: usize,
    limit: usize,
    source: libremetaverse_types::UUID,
    context: &'static str,
) -> Result<usize, Error> {
    let total = current
        .checked_add(additional)
        .ok_or_else(|| render_error(source, context))?;
    if total > limit {
        Err(render_error(source, context))
    } else {
        Ok(total)
    }
}

fn osd_vector3(
    value: &OSD,
    source: libremetaverse_types::UUID,
    context: &'static str,
) -> Result<Vector3, Error> {
    let value = value
        .as_vector3()
        .map_err(|_| render_error(source, context))?;
    if finite3(value) {
        Ok(value)
    } else {
        Err(render_error(source, context))
    }
}

const fn as_map(value: &OSD) -> Option<&HashMap<String, OSD>> {
    if let OSD::Map(map) = value {
        Some(map)
    } else {
        None
    }
}

const fn osd_binary(value: &OSD) -> Option<&Vec<u8>> {
    if let OSD::Binary(bytes) = value {
        Some(bytes)
    } else {
        None
    }
}

const fn osd_integer(value: &OSD) -> Option<i32> {
    if let OSD::Integer(value) = value {
        Some(*value)
    } else {
        None
    }
}

const fn osd_boolean(value: &OSD) -> Option<bool> {
    if let OSD::Boolean(value) = value {
        Some(*value)
    } else {
        None
    }
}

const fn dequantize(value: u16, min: f32, max: f32) -> f32 {
    min + (value as f32 / 65_535.0) * (max - min)
}

fn normalize3(value: Vector3, fallback: Vector3) -> Vector3 {
    let length_squared = dot(value, value);
    if length_squared > f32::EPSILON && length_squared.is_finite() {
        scale3(value, length_squared.sqrt().recip())
    } else {
        fallback
    }
}

fn orthogonal_tangent(normal: Vector3) -> Vector3 {
    let axis = if normal.z.abs() < 0.9 {
        Vector3::unit_z()
    } else {
        Vector3::unit_y()
    };
    normalize3(cross(axis, normal), Vector3::unit_x())
}

const fn identity_matrix() -> [f32; 16] {
    [
        1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
    ]
}

const fn add3(left: Vector3, right: Vector3) -> Vector3 {
    Vector3 {
        x: left.x + right.x,
        y: left.y + right.y,
        z: left.z + right.z,
    }
}

const fn sub3(left: Vector3, right: Vector3) -> Vector3 {
    Vector3 {
        x: left.x - right.x,
        y: left.y - right.y,
        z: left.z - right.z,
    }
}

const fn sub2(left: Vector2, right: Vector2) -> Vector2 {
    Vector2 {
        x: left.x - right.x,
        y: left.y - right.y,
    }
}

const fn scale3(value: Vector3, scalar: f32) -> Vector3 {
    Vector3 {
        x: value.x * scalar,
        y: value.y * scalar,
        z: value.z * scalar,
    }
}

const fn dot(left: Vector3, right: Vector3) -> f32 {
    left.x * right.x + left.y * right.y + left.z * right.z
}

const fn cross(left: Vector3, right: Vector3) -> Vector3 {
    Vector3 {
        x: left.y * right.z - left.z * right.y,
        y: left.z * right.x - left.x * right.z,
        z: left.x * right.y - left.y * right.x,
    }
}

const fn finite2(value: Vector2) -> bool {
    value.x.is_finite() && value.y.is_finite()
}

const fn finite3(value: Vector3) -> bool {
    value.x.is_finite() && value.y.is_finite() && value.z.is_finite()
}

const fn render_error(source: libremetaverse_types::UUID, context: &'static str) -> Error {
    Error::Rendering { source, context }
}
