//! Native primitive and object appearance model.
//!
//! The wire decoders in this module accept untrusted simulator blobs. Every
//! cursor advance is checked and every variable-size section is bounded by the
//! caller-provided slice before data is committed to the model.

#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::cast_possible_wrap)]
#![allow(clippy::cast_precision_loss)]
#![allow(clippy::cast_sign_loss)]
#![allow(clippy::field_reassign_with_default)]
#![allow(clippy::inherent_to_string)]
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::missing_panics_doc)]
#![allow(clippy::must_use_candidate)]
#![allow(clippy::needless_pass_by_value)]
#![allow(clippy::should_implement_trait)]
#![allow(clippy::too_many_lines)]

use crate::{
    Animation, BitPack, Bumpiness, Grass, MappingType, MediaEntry, NameValue, Permissions,
    PrimitiveParticleSystemBlendFunc, PrimitiveParticleSystemParticleDataFlags,
    PrimitiveParticleSystemSourcePattern, PrimitivePhysicsProperties, PrimitiveTextureAnimMode,
    Shininess, Tree,
};
use libremetaverse_structured_data::{OSD, OSDMap};
use libremetaverse_types::compat::Object;
use libremetaverse_types::{
    AttachmentPoint, ClickAction, Color4, Error, HoleType, JointType, Material, ObjectCategory,
    PCode, PathCurve, PrimFlags, PrimType, ProfileCurve, Quaternion, SaleType, SculptType,
    SoundFlags, UUID, Vector2, Vector3, Vector4,
};
use std::collections::HashMap;
use std::fmt::Write as _;
use std::hash::{Hash, Hasher};
use std::time::{SystemTime, UNIX_EPOCH};

const MAX_OSD_FACE_ENTRIES: usize = 46;
const MAX_TEXTURE_ENTRY_BYTES: usize = 64 * 1024;
const MAX_EXTRA_PARAMS: usize = 64;
const MAX_EXTRA_PARAM_BYTES: usize = 64 * 1024;

fn checked_window(data: &[u8], pos: i32, length: usize) -> Result<&[u8], Error> {
    let start = usize::try_from(pos).map_err(|_| Error::IndexOutOfRange)?;
    let end = start.checked_add(length).ok_or(Error::IndexOutOfRange)?;
    data.get(start..end).ok_or(Error::IndexOutOfRange)
}

fn read_f32(data: &[u8], offset: usize) -> Result<f32, Error> {
    let bytes: [u8; 4] = data
        .get(offset..offset + 4)
        .ok_or(Error::IndexOutOfRange)?
        .try_into()
        .map_err(|_| Error::IndexOutOfRange)?;
    Ok(f32::from_le_bytes(bytes))
}

fn map(osd: OSD) -> Option<HashMap<String, OSD>> {
    match osd {
        OSD::Map(value) => Some(value),
        _ => None,
    }
}

fn map_real(values: Option<&HashMap<String, OSD>>, key: &str, default: f32) -> f32 {
    values
        .and_then(|map| map.get(key))
        .and_then(|value| value.as_real().ok())
        .map_or(default, |value| value as f32)
}

fn map_i32(values: Option<&HashMap<String, OSD>>, key: &str, default: i32) -> i32 {
    values
        .and_then(|map| map.get(key))
        .and_then(|value| value.as_integer().ok())
        .unwrap_or(default)
}

fn map_u32(values: Option<&HashMap<String, OSD>>, key: &str, default: u32) -> u32 {
    values
        .and_then(|map| map.get(key))
        .and_then(|value| value.as_u_integer().ok())
        .unwrap_or(default)
}

fn map_bool(values: Option<&HashMap<String, OSD>>, key: &str, default: bool) -> bool {
    values
        .and_then(|map| map.get(key))
        .and_then(|value| value.as_boolean().ok())
        .unwrap_or(default)
}

fn map_uuid(values: Option<&HashMap<String, OSD>>, key: &str, default: UUID) -> UUID {
    values
        .and_then(|map| map.get(key))
        .and_then(|value| value.as_uuid().ok())
        .unwrap_or(default)
}

fn map_vector3(values: Option<&HashMap<String, OSD>>, key: &str, default: Vector3) -> Vector3 {
    values
        .and_then(|map| map.get(key))
        .filter(|value| matches!(value, OSD::Array(items) if items.len() == 3))
        .and_then(|value| value.as_vector3().ok())
        .unwrap_or(default)
}

fn hash_i32<T: Hash>(value: &T) -> i32 {
    let mut hasher = std::collections::hash_map::DefaultHasher::new();
    value.hash(&mut hasher);
    hasher.finish() as i32
}

#[derive(Clone, Debug, PartialEq)]
pub struct PrimitiveConstructionData {
    pub material: Material,
    pub p_code: PCode,
    pub path_begin: f32,
    pub path_curve: PathCurve,
    pub path_end: f32,
    pub path_radius_offset: f32,
    pub path_revolutions: f32,
    pub path_scale_x: f32,
    pub path_scale_y: f32,
    pub path_shear_x: f32,
    pub path_shear_y: f32,
    pub path_skew: f32,
    pub path_taper_x: f32,
    pub path_taper_y: f32,
    pub path_twist: f32,
    pub path_twist_begin: f32,
    pub profile_begin: f32,
    pub profile_end: f32,
    pub profile_hollow: f32,
    pub state: u8,
    pub profile_curve_with_field: u8,
}

impl Default for PrimitiveConstructionData {
    fn default() -> Self {
        Self {
            material: Material::Stone,
            p_code: PCode::Prim,
            path_begin: 0.0,
            path_curve: PathCurve::Line,
            path_end: 1.0,
            path_radius_offset: 0.0,
            path_revolutions: 1.0,
            path_scale_x: 1.0,
            path_scale_y: 1.0,
            path_shear_x: 0.0,
            path_shear_y: 0.0,
            path_skew: 0.0,
            path_taper_x: 0.0,
            path_taper_y: 0.0,
            path_twist: 0.0,
            path_twist_begin: 0.0,
            profile_begin: 0.0,
            profile_end: 1.0,
            profile_hollow: 0.0,
            state: 0,
            profile_curve_with_field: ProfileCurve::Circle as u8,
        }
    }
}

impl PrimitiveConstructionData {
    pub fn new_with_constructor() -> Result<Self, Error> {
        Ok(Self::default())
    }

    pub fn new_with_construction_data(other: Self) -> Result<Self, Error> {
        Ok(other)
    }

    pub fn get_hash_code(&self) -> i32 {
        let floats = [
            self.path_begin,
            self.path_end,
            self.path_radius_offset,
            self.path_revolutions,
            self.path_scale_x,
            self.path_scale_y,
            self.path_shear_x,
            self.path_shear_y,
            self.path_skew,
            self.path_taper_x,
            self.path_taper_y,
            self.path_twist,
            self.path_twist_begin,
            self.profile_begin,
            self.profile_end,
            self.profile_hollow,
        ];
        floats.iter().fold(
            i32::from(self.profile_curve_with_field)
                ^ i32::from(self.path_curve as u8)
                ^ i32::from(self.material as u8)
                ^ i32::from(self.p_code as u8)
                ^ i32::from(self.state),
            |hash, value| hash ^ i32::from_ne_bytes(value.to_bits().to_ne_bytes()),
        )
    }

    pub fn attachment_point(&self) -> AttachmentPoint {
        attachment_point_from_u8(self.state.rotate_left(4))
    }

    pub fn set_attachment_point(&mut self, value: AttachmentPoint) {
        self.state = (value as u8).rotate_left(4);
    }

    pub fn path_begin_scale(&self) -> Vector2 {
        Vector2 {
            x: if self.path_scale_x > 1.0 {
                2.0 - self.path_scale_x
            } else {
                1.0
            },
            y: if self.path_scale_y > 1.0 {
                2.0 - self.path_scale_y
            } else {
                1.0
            },
        }
    }

    pub fn path_end_scale(&self) -> Vector2 {
        Vector2 {
            x: if self.path_scale_x < 1.0 {
                self.path_scale_x
            } else {
                1.0
            },
            y: if self.path_scale_y < 1.0 {
                self.path_scale_y
            } else {
                1.0
            },
        }
    }

    pub fn profile_curve_with_property(&self) -> ProfileCurve {
        profile_curve_from_u8(self.profile_curve_with_field & 0x0f)
    }

    pub fn set_profile_curve_with_property(&mut self, value: ProfileCurve) {
        self.profile_curve_with_field = (self.profile_curve_with_field & 0xf0) | value as u8;
    }

    pub fn profile_hole(&self) -> HoleType {
        HoleType(self.profile_curve_with_field & 0xf0)
    }

    pub fn set_profile_hole(&mut self, value: HoleType) {
        self.profile_curve_with_field = (self.profile_curve_with_field & 0x0f) | (value.0 & 0xf0);
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct PrimitiveFlexibleData {
    pub drag: f32,
    pub force: Vector3,
    pub gravity: f32,
    pub softness: i32,
    pub tension: f32,
    pub wind: f32,
}

impl Default for PrimitiveFlexibleData {
    fn default() -> Self {
        Self {
            drag: 0.0,
            force: Vector3::zero(),
            gravity: 0.0,
            softness: 0,
            tension: 0.0,
            wind: 0.0,
        }
    }
}

impl PrimitiveFlexibleData {
    pub fn new_with_constructor() -> Result<Self, Error> {
        Ok(Self::default())
    }

    pub fn new_with_bytes_int32(data: Vec<u8>, pos: i32) -> Result<Self, Error> {
        let bytes = checked_window(&data, pos, 16)?;
        Ok(Self {
            softness: i32::from(((bytes[0] & 0x80) >> 6) | ((bytes[1] & 0x80) >> 7)),
            tension: f32::from(bytes[0] & 0x7f) / 10.0,
            drag: f32::from(bytes[1] & 0x7f) / 10.0,
            gravity: f32::from(bytes[2]) / 10.0 - 10.0,
            wind: f32::from(bytes[3]) / 10.0,
            force: Vector3::new_with_bytes_int32(bytes[4..].to_vec(), 0)?,
        })
    }

    pub fn get_bytes(&self) -> Result<Vec<u8>, Error> {
        let mut data = vec![0_u8; 16];
        data[0] = ((self.softness & 2) << 6) as u8 | ((self.tension * 10.01) as u8 & 0x7f);
        data[1] = ((self.softness & 1) << 7) as u8 | ((self.drag * 10.01) as u8 & 0x7f);
        data[2] = ((self.gravity + 10.0) * 10.01) as u8;
        data[3] = (self.wind * 10.01) as u8;
        self.force.to_bytes(&mut data, 4)?;
        Ok(data)
    }

    pub fn get_osd(&self) -> Result<OSD, Error> {
        Ok(OSD::Map(HashMap::from([
            ("simulate_lod".into(), OSD::Integer(self.softness)),
            ("gravity".into(), OSD::Real(f64::from(self.gravity))),
            ("air_friction".into(), OSD::Real(f64::from(self.drag))),
            ("wind_sensitivity".into(), OSD::Real(f64::from(self.wind))),
            ("tension".into(), OSD::Real(f64::from(self.tension))),
            ("user_force".into(), OSD::from_vector3(self.force)?),
        ])))
    }

    pub fn from_osd(osd: OSD) -> Result<Self, Error> {
        let Some(values) = map(osd) else {
            return Ok(Self::default());
        };
        Ok(Self {
            softness: map_i32(Some(&values), "simulate_lod", 0),
            gravity: map_real(Some(&values), "gravity", 0.0),
            drag: map_real(Some(&values), "air_friction", 0.0),
            wind: map_real(Some(&values), "wind_sensitivity", 0.0),
            tension: map_real(Some(&values), "tension", 0.0),
            force: map_vector3(Some(&values), "user_force", Vector3::zero()),
        })
    }

    pub fn get_hash_code(&self) -> i32 {
        hash_i32(&self.get_bytes().unwrap_or_default())
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct PrimitiveLightData {
    pub color: Color4,
    pub cutoff: f32,
    pub falloff: f32,
    pub intensity: f32,
    pub radius: f32,
}

impl Default for PrimitiveLightData {
    fn default() -> Self {
        Self {
            color: Color4::black(),
            cutoff: 0.0,
            falloff: 0.0,
            intensity: 0.0,
            radius: 0.0,
        }
    }
}

impl PrimitiveLightData {
    pub fn new_with_constructor() -> Result<Self, Error> {
        Ok(Self::default())
    }

    pub fn new_with_bytes_int32(data: Vec<u8>, pos: i32) -> Result<Self, Error> {
        let bytes = checked_window(&data, pos, 16)?;
        let encoded = Color4::new_with_bytes_int32_boolean(bytes[..4].to_vec(), 0, false)?;
        Ok(Self {
            color: Color4 {
                r: encoded.r,
                g: encoded.g,
                b: encoded.b,
                a: 1.0,
            },
            intensity: encoded.a,
            radius: read_f32(bytes, 4)?,
            cutoff: read_f32(bytes, 8)?,
            falloff: read_f32(bytes, 12)?,
        })
    }

    pub fn get_bytes(&self) -> Result<Vec<u8>, Error> {
        let mut data = vec![0_u8; 16];
        Color4 {
            r: self.color.r,
            g: self.color.g,
            b: self.color.b,
            a: self.intensity.clamp(0.0, 1.0),
        }
        .to_bytes_with_bytes_int32(&mut data, 0)?;
        data[4..8].copy_from_slice(&self.radius.to_le_bytes());
        data[8..12].copy_from_slice(&self.cutoff.to_le_bytes());
        data[12..16].copy_from_slice(&self.falloff.to_le_bytes());
        Ok(data)
    }

    pub fn get_osd(&self) -> Result<OSD, Error> {
        Ok(OSD::Map(HashMap::from([
            ("color".into(), OSD::from_color4(self.color)?),
            ("intensity".into(), OSD::Real(f64::from(self.intensity))),
            ("radius".into(), OSD::Real(f64::from(self.radius))),
            ("cutoff".into(), OSD::Real(f64::from(self.cutoff))),
            ("falloff".into(), OSD::Real(f64::from(self.falloff))),
        ])))
    }

    pub fn from_osd(osd: OSD) -> Result<Self, Error> {
        let Some(values) = map(osd) else {
            return Ok(Self::default());
        };
        Ok(Self {
            color: values
                .get("color")
                .and_then(|v| v.as_color4().ok())
                .unwrap_or_else(Color4::black),
            intensity: map_real(Some(&values), "intensity", 0.0),
            radius: map_real(Some(&values), "radius", 0.0),
            cutoff: map_real(Some(&values), "cutoff", 0.0),
            falloff: map_real(Some(&values), "falloff", 0.0),
        })
    }

    pub fn get_hash_code(&self) -> i32 {
        hash_i32(&self.get_bytes().unwrap_or_default())
    }
    pub fn to_string(&self) -> String {
        format!(
            "Color: {:?} Intensity: {} Radius: {} Cutoff: {} Falloff: {}",
            self.color, self.intensity, self.radius, self.cutoff, self.falloff
        )
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct PrimitiveLightImage {
    pub light_texture: UUID,
    pub params: Vector3,
}

impl Default for PrimitiveLightImage {
    fn default() -> Self {
        Self {
            light_texture: UUID::zero(),
            params: Vector3::zero(),
        }
    }
}

impl PrimitiveLightImage {
    pub fn new_with_constructor() -> Result<Self, Error> {
        Ok(Self::default())
    }
    pub fn new_with_bytes_int32(data: Vec<u8>, pos: i32) -> Result<Self, Error> {
        let bytes = checked_window(&data, pos, 28)?;
        Ok(Self {
            light_texture: UUID::new_with_bytes_int32(bytes[..16].to_vec(), 0)?,
            params: Vector3::new_with_bytes_int32(bytes[16..].to_vec(), 0)?,
        })
    }
    pub fn get_bytes(&self) -> Result<Vec<u8>, Error> {
        let mut data = vec![0_u8; 28];
        self.light_texture.to_bytes(&mut data, 0)?;
        self.params.to_bytes(&mut data, 16)?;
        Ok(data)
    }
    pub fn get_osd(&self) -> Result<OSD, Error> {
        Ok(OSD::Map(HashMap::from([
            ("texture".into(), OSD::UUID(self.light_texture)),
            ("params".into(), OSD::from_vector3(self.params)?),
        ])))
    }
    pub fn from_osd(osd: OSD) -> Result<Self, Error> {
        let Some(values) = map(osd) else {
            return Ok(Self::default());
        };
        Ok(Self {
            light_texture: map_uuid(Some(&values), "texture", UUID::zero()),
            params: map_vector3(Some(&values), "params", Vector3::zero()),
        })
    }
    pub fn get_hash_code(&self) -> i32 {
        hash_i32(&self.get_bytes().unwrap_or_default())
    }
    pub fn to_string(&self) -> String {
        format!(
            "LightTexture: {} Params: {:?}",
            self.light_texture, self.params
        )
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct PrimitiveSculptData {
    pub sculpt_texture: UUID,
    type_bits: u8,
}

impl Default for PrimitiveSculptData {
    fn default() -> Self {
        Self {
            sculpt_texture: UUID::zero(),
            type_bits: SculptType::None as u8,
        }
    }
}

impl PrimitiveSculptData {
    pub fn new_with_constructor() -> Result<Self, Error> {
        Ok(Self::default())
    }
    pub fn new_with_bytes_int32(data: Vec<u8>, pos: i32) -> Result<Self, Error> {
        let bytes = checked_window(&data, pos, 17)?;
        Ok(Self {
            sculpt_texture: UUID::new_with_bytes_int32(bytes[..16].to_vec(), 0)?,
            type_bits: bytes[16],
        })
    }
    pub fn get_bytes(&self) -> Result<Vec<u8>, Error> {
        let mut out = self.sculpt_texture.get_bytes()?;
        out.push(self.type_bits);
        Ok(out)
    }
    pub fn get_osd(&self) -> Result<OSD, Error> {
        Ok(OSD::Map(HashMap::from([
            ("texture".into(), OSD::UUID(self.sculpt_texture)),
            ("type".into(), OSD::Integer(i32::from(self.type_bits))),
        ])))
    }
    pub fn from_osd(osd: OSD) -> Result<Self, Error> {
        let Some(values) = map(osd) else {
            return Ok(Self::default());
        };
        Ok(Self {
            sculpt_texture: map_uuid(Some(&values), "texture", UUID::zero()),
            type_bits: map_i32(Some(&values), "type", 0) as u8,
        })
    }
    pub fn get_hash_code(&self) -> i32 {
        self.sculpt_texture.get_hash_code() ^ i32::from(self.type_bits)
    }
    pub fn invert(&self) -> bool {
        self.type_bits & SculptType::Invert as u8 != 0
    }
    pub fn mirror(&self) -> bool {
        self.type_bits & SculptType::Mirror as u8 != 0
    }
    pub fn type_(&self) -> SculptType {
        sculpt_type_from_u8(self.type_bits & 7)
    }
    pub fn set_type_(&mut self, value: SculptType) {
        self.type_bits = value as u8;
    }
}

#[derive(Clone, Debug)]
pub struct PrimitiveObjectProperties {
    pub aggregate_perm_textures: u8,
    pub aggregate_perm_textures_owner: u8,
    pub aggregate_perms: u8,
    pub category: ObjectCategory,
    pub creation_date: SystemTime,
    pub creator_id: UUID,
    pub description: String,
    pub folder_id: UUID,
    pub from_task_id: UUID,
    pub group_id: UUID,
    pub inventory_serial: i16,
    pub item_id: UUID,
    pub last_owner_id: UUID,
    pub name: String,
    pub object_id: UUID,
    pub owner_id: UUID,
    pub ownership_cost: i32,
    pub permissions: Permissions,
    pub sale_price: i32,
    pub sale_type: SaleType,
    pub sit_name: String,
    pub texture_i_ds: Vec<UUID>,
    pub touch_name: String,
}

impl Default for PrimitiveObjectProperties {
    fn default() -> Self {
        Self {
            aggregate_perm_textures: 0,
            aggregate_perm_textures_owner: 0,
            aggregate_perms: 0,
            category: ObjectCategory::None,
            creation_date: UNIX_EPOCH,
            creator_id: UUID::zero(),
            description: String::new(),
            folder_id: UUID::zero(),
            from_task_id: UUID::zero(),
            group_id: UUID::zero(),
            inventory_serial: 0,
            item_id: UUID::zero(),
            last_owner_id: UUID::zero(),
            name: String::new(),
            object_id: UUID::zero(),
            owner_id: UUID::zero(),
            ownership_cost: 0,
            permissions: Permissions::no_permissions(),
            sale_price: 0,
            sale_type: SaleType::Not,
            sit_name: String::new(),
            texture_i_ds: Vec::new(),
            touch_name: String::new(),
        }
    }
}

impl PrimitiveObjectProperties {
    pub fn new() -> Result<Self, Error> {
        Ok(Self::default())
    }
    pub fn get_texture_id_bytes(&self) -> Result<Vec<u8>, Error> {
        let capacity = self
            .texture_i_ds
            .len()
            .checked_mul(16)
            .ok_or(Error::Argument)?;
        let mut bytes = Vec::with_capacity(capacity);
        for id in &self.texture_i_ds {
            bytes.extend_from_slice(&id.get_bytes()?);
        }
        Ok(bytes)
    }
    pub fn set_family_properties(&mut self, props: Self) -> Result<(), Error> {
        self.object_id = props.object_id;
        self.owner_id = props.owner_id;
        self.group_id = props.group_id;
        self.permissions = props.permissions;
        self.ownership_cost = props.ownership_cost;
        self.sale_type = props.sale_type;
        self.sale_price = props.sale_price;
        self.category = props.category;
        self.last_owner_id = props.last_owner_id;
        self.name = props.name;
        self.description = props.description;
        Ok(())
    }
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PrimitiveTextureAnimation {
    pub face: u32,
    pub flags: PrimitiveTextureAnimMode,
    pub length: f32,
    pub rate: f32,
    pub size_x: u32,
    pub size_y: u32,
    pub start: f32,
}

impl Default for PrimitiveTextureAnimation {
    fn default() -> Self {
        Self {
            face: 0,
            flags: PrimitiveTextureAnimMode::ANIM_OFF,
            length: 0.0,
            rate: 0.0,
            size_x: 0,
            size_y: 0,
            start: 0.0,
        }
    }
}

impl PrimitiveTextureAnimation {
    pub fn new(data: Vec<u8>, pos: i32) -> Result<Self, Error> {
        let Ok(bytes) = checked_window(&data, pos, 16) else {
            return Ok(Self::default());
        };
        Ok(Self {
            flags: PrimitiveTextureAnimMode(u32::from(bytes[0])),
            face: u32::from(bytes[1]),
            size_x: u32::from(bytes[2]),
            size_y: u32::from(bytes[3]),
            start: read_f32(bytes, 4)?,
            length: read_f32(bytes, 8)?,
            rate: read_f32(bytes, 12)?,
        })
    }
    pub fn get_bytes(&self) -> Result<Vec<u8>, Error> {
        let mut data = vec![
            self.flags.0 as u8,
            self.face as u8,
            self.size_x as u8,
            self.size_y as u8,
        ];
        data.extend_from_slice(&self.start.to_le_bytes());
        data.extend_from_slice(&self.length.to_le_bytes());
        data.extend_from_slice(&self.rate.to_le_bytes());
        Ok(data)
    }
    pub fn get_osd(&self) -> Result<OSD, Error> {
        Ok(OSD::Map(HashMap::from([
            ("face".into(), OSD::from_u_integer(self.face)?),
            ("flags".into(), OSD::from_u_integer(self.flags.0)?),
            ("length".into(), OSD::Real(f64::from(self.length))),
            ("rate".into(), OSD::Real(f64::from(self.rate))),
            ("size_x".into(), OSD::from_u_integer(self.size_x)?),
            ("size_y".into(), OSD::from_u_integer(self.size_y)?),
            ("start".into(), OSD::Real(f64::from(self.start))),
        ])))
    }
    pub fn from_osd(osd: OSD) -> Result<Self, Error> {
        let Some(values) = map(osd) else {
            return Ok(Self::default());
        };
        Ok(Self {
            face: map_u32(Some(&values), "face", 0),
            flags: PrimitiveTextureAnimMode(map_u32(Some(&values), "flags", 0)),
            length: map_real(Some(&values), "length", 0.0),
            rate: map_real(Some(&values), "rate", 0.0),
            size_x: map_u32(Some(&values), "size_x", 0),
            size_y: map_u32(Some(&values), "size_y", 0),
            start: map_real(Some(&values), "start", 0.0),
        })
    }
    pub fn equals_with_texture_animation(&self, other: Self) -> bool {
        *self == other
    }
    pub fn equals_with_object(&self, obj: Option<Object>) -> bool {
        obj.as_ref()
            .and_then(Object::downcast_ref::<Self>)
            .is_some_and(|other| self == other)
    }
    pub fn get_hash_code(&self) -> i32 {
        hash_i32(&self.get_bytes().unwrap_or_default())
    }
}

const ATTR_TEXTURE_ID: u32 = 1 << 0;
const ATTR_RGBA: u32 = 1 << 1;
const ATTR_REPEAT_U: u32 = 1 << 2;
const ATTR_REPEAT_V: u32 = 1 << 3;
const ATTR_OFFSET_U: u32 = 1 << 4;
const ATTR_OFFSET_V: u32 = 1 << 5;
const ATTR_ROTATION: u32 = 1 << 6;
const ATTR_MATERIAL: u32 = 1 << 7;
const ATTR_MEDIA: u32 = 1 << 8;
const ATTR_GLOW: u32 = 1 << 9;
const ATTR_MATERIAL_ID: u32 = 1 << 10;
const ATTR_RENDER_MATERIAL_ID: u32 = 1 << 11;
const ATTR_ALL: u32 = u32::MAX;

#[derive(Clone, Debug, PartialEq)]
struct TextureFaceData {
    rgba: Color4,
    repeat_u: f32,
    repeat_v: f32,
    offset_u: f32,
    offset_v: f32,
    rotation: f32,
    glow: f32,
    material: u8,
    media: u8,
    attributes: u32,
    texture_id: UUID,
    material_id: UUID,
    render_material_id: UUID,
}

impl Default for TextureFaceData {
    fn default() -> Self {
        Self {
            rgba: Color4::white(),
            repeat_u: 1.0,
            repeat_v: 1.0,
            offset_u: 0.0,
            offset_v: 0.0,
            rotation: 0.0,
            glow: 0.0,
            material: 0,
            media: 0,
            attributes: ATTR_ALL,
            texture_id: UUID::zero(),
            material_id: UUID::zero(),
            render_material_id: UUID::zero(),
        }
    }
}

#[derive(Debug)]
pub struct PrimitiveTextureEntryFace {
    data: std::sync::Arc<std::sync::RwLock<TextureFaceData>>,
    default: Option<std::sync::Arc<std::sync::RwLock<TextureFaceData>>>,
}

impl Default for PrimitiveTextureEntryFace {
    fn default() -> Self {
        Self::default_face()
    }
}

impl Clone for PrimitiveTextureEntryFace {
    fn clone(&self) -> Self {
        self.deep_clone()
    }
}

impl PartialEq for PrimitiveTextureEntryFace {
    fn eq(&self, other: &Self) -> bool {
        self.rgba() == other.rgba()
            && self.repeat_u() == other.repeat_u()
            && self.repeat_v() == other.repeat_v()
            && self.offset_u() == other.offset_u()
            && self.offset_v() == other.offset_v()
            && self.rotation() == other.rotation()
            && self.glow() == other.glow()
            && self.material() == other.material()
            && self.media() == other.media()
            && self.texture_id() == other.texture_id()
            && self.material_id() == other.material_id()
            && self.render_material_id() == other.render_material_id()
    }
}

impl PrimitiveTextureEntryFace {
    fn default_face() -> Self {
        Self {
            data: std::sync::Arc::new(std::sync::RwLock::new(TextureFaceData::default())),
            default: None,
        }
    }

    fn inheriting(default: std::sync::Arc<std::sync::RwLock<TextureFaceData>>) -> Self {
        let data = TextureFaceData {
            attributes: 0,
            ..TextureFaceData::default()
        };
        Self {
            data: std::sync::Arc::new(std::sync::RwLock::new(data)),
            default: Some(default),
        }
    }

    fn read_data(&self) -> std::sync::RwLockReadGuard<'_, TextureFaceData> {
        self.data
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
    }

    fn write_data(&self) -> std::sync::RwLockWriteGuard<'_, TextureFaceData> {
        self.data
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
    }

    fn inherited(&self) -> Option<std::sync::RwLockReadGuard<'_, TextureFaceData>> {
        self.default.as_ref().map(|value| {
            value
                .read()
                .unwrap_or_else(std::sync::PoisonError::into_inner)
        })
    }

    fn deep_clone(&self) -> Self {
        let default = self.default.as_ref().map(|value| {
            std::sync::Arc::new(std::sync::RwLock::new(
                value
                    .read()
                    .unwrap_or_else(std::sync::PoisonError::into_inner)
                    .clone(),
            ))
        });
        Self {
            data: std::sync::Arc::new(std::sync::RwLock::new(self.read_data().clone())),
            default,
        }
    }

    fn clone_with_default(
        &self,
        default: Option<std::sync::Arc<std::sync::RwLock<TextureFaceData>>>,
    ) -> Self {
        Self {
            data: std::sync::Arc::new(std::sync::RwLock::new(self.read_data().clone())),
            default,
        }
    }

    pub fn new(default_texture: Option<Self>) -> Result<Self, Error> {
        Ok(default_texture.map_or_else(Self::default_face, |face| Self::inheriting(face.data)))
    }

    pub fn clone(&self) -> Result<Object, Error> {
        Ok(Object::opaque(self.deep_clone()))
    }

    pub fn valid(&self) -> bool {
        self.default.is_some()
    }

    fn effective<T: Copy>(&self, attribute: u32, own: impl Fn(&TextureFaceData) -> T) -> T {
        let data = self.read_data();
        if data.attributes & attribute != 0 {
            return own(&data);
        }
        self.inherited()
            .map_or_else(|| own(&data), |default| own(&default))
    }

    fn material(&self) -> u8 {
        self.effective(ATTR_MATERIAL, |value| value.material)
    }
    fn media(&self) -> u8 {
        self.effective(ATTR_MEDIA, |value| value.media)
    }
    fn set_material(&mut self, value: u8) {
        let mut data = self.write_data();
        data.material = value;
        data.attributes |= ATTR_MATERIAL;
    }
    fn set_media(&mut self, value: u8) {
        let mut data = self.write_data();
        data.media = value;
        data.attributes |= ATTR_MEDIA;
    }

    pub fn rgba(&self) -> Color4 {
        self.effective(ATTR_RGBA, |value| value.rgba)
    }
    pub fn set_rgba(&mut self, value: Color4) {
        let mut data = self.write_data();
        data.rgba = value;
        data.attributes |= ATTR_RGBA;
    }
    pub fn repeat_u(&self) -> f32 {
        self.effective(ATTR_REPEAT_U, |value| value.repeat_u)
    }
    pub fn set_repeat_u(&mut self, value: f32) {
        let mut data = self.write_data();
        data.repeat_u = value;
        data.attributes |= ATTR_REPEAT_U;
    }
    pub fn repeat_v(&self) -> f32 {
        self.effective(ATTR_REPEAT_V, |value| value.repeat_v)
    }
    pub fn set_repeat_v(&mut self, value: f32) {
        let mut data = self.write_data();
        data.repeat_v = value;
        data.attributes |= ATTR_REPEAT_V;
    }
    pub fn offset_u(&self) -> f32 {
        self.effective(ATTR_OFFSET_U, |value| value.offset_u)
    }
    pub fn set_offset_u(&mut self, value: f32) {
        let mut data = self.write_data();
        data.offset_u = value;
        data.attributes |= ATTR_OFFSET_U;
    }
    pub fn offset_v(&self) -> f32 {
        self.effective(ATTR_OFFSET_V, |value| value.offset_v)
    }
    pub fn set_offset_v(&mut self, value: f32) {
        let mut data = self.write_data();
        data.offset_v = value;
        data.attributes |= ATTR_OFFSET_V;
    }
    pub fn rotation(&self) -> f32 {
        self.effective(ATTR_ROTATION, |value| value.rotation)
    }
    pub fn set_rotation(&mut self, value: f32) {
        let mut data = self.write_data();
        data.rotation = value;
        data.attributes |= ATTR_ROTATION;
    }
    pub fn glow(&self) -> f32 {
        self.effective(ATTR_GLOW, |value| value.glow)
    }
    pub fn set_glow(&mut self, value: f32) {
        let mut data = self.write_data();
        data.glow = value;
        data.attributes |= ATTR_GLOW;
    }
    pub fn texture_id(&self) -> UUID {
        self.effective(ATTR_TEXTURE_ID, |value| value.texture_id)
    }
    pub fn set_texture_id(&mut self, value: UUID) {
        let mut data = self.write_data();
        data.texture_id = value;
        data.attributes |= ATTR_TEXTURE_ID;
    }
    pub fn material_id(&self) -> UUID {
        self.effective(ATTR_MATERIAL_ID, |value| value.material_id)
    }
    pub fn set_material_id(&mut self, value: UUID) {
        let mut data = self.write_data();
        data.material_id = value;
        data.attributes |= ATTR_MATERIAL_ID;
    }
    pub fn render_material_id(&self) -> UUID {
        self.effective(ATTR_RENDER_MATERIAL_ID, |value| value.render_material_id)
    }
    pub fn set_render_material_id(&mut self, value: UUID) {
        let mut data = self.write_data();
        data.render_material_id = value;
        data.attributes |= ATTR_RENDER_MATERIAL_ID;
    }
    pub fn bump(&self) -> Bumpiness {
        bumpiness_from_u8(self.material() & 0x1f)
    }
    pub fn set_bump(&mut self, value: Bumpiness) {
        self.set_material((self.material() & 0xe0) | value as u8);
    }
    pub fn shiny(&self) -> Shininess {
        shininess_from_u8(self.material() & 0xc0)
    }
    pub fn set_shiny(&mut self, value: Shininess) {
        self.set_material((self.material() & 0x3f) | value as u8);
    }
    pub fn fullbright(&self) -> bool {
        self.material() & 0x20 != 0
    }
    pub fn set_fullbright(&mut self, value: bool) {
        self.set_material((self.material() & !0x20) | if value { 0x20 } else { 0 });
    }
    pub fn media_flags(&self) -> bool {
        self.media() & 1 != 0
    }
    pub fn set_media_flags(&mut self, value: bool) {
        self.set_media((self.media() & !1) | u8::from(value));
    }
    pub fn tex_map_type(&self) -> MappingType {
        mapping_type_from_u8(self.media() & 0x06)
    }
    pub fn set_tex_map_type(&mut self, value: MappingType) {
        self.set_media((self.media() & !0x06) | value as u8);
    }

    pub fn get_osd(&self, face_number: i32) -> Result<OSD, Error> {
        let mut values = HashMap::from([
            ("colors".into(), OSD::from_color4(self.rgba())?),
            ("scales".into(), OSD::Real(f64::from(self.repeat_u()))),
            ("scalet".into(), OSD::Real(f64::from(self.repeat_v()))),
            ("offsets".into(), OSD::Real(f64::from(self.offset_u()))),
            ("offsett".into(), OSD::Real(f64::from(self.offset_v()))),
            ("imagerot".into(), OSD::Real(f64::from(self.rotation()))),
            ("bump".into(), OSD::Integer(self.bump() as i32)),
            ("shiny".into(), OSD::Integer(self.shiny() as i32)),
            ("fullbright".into(), OSD::Boolean(self.fullbright())),
            ("media_flags".into(), OSD::Boolean(self.media_flags())),
            ("mapping".into(), OSD::Integer(self.tex_map_type() as i32)),
            ("glow".into(), OSD::Real(f64::from(self.glow()))),
            (
                "imageid".into(),
                OSD::UUID(
                    if self.texture_id() == PrimitiveTextureEntry::white_texture() {
                        UUID::zero()
                    } else {
                        self.texture_id()
                    },
                ),
            ),
            ("materialid".into(), OSD::UUID(self.material_id())),
            (
                "rendermaterialid".into(),
                OSD::UUID(self.render_material_id()),
            ),
        ]);
        if face_number >= 0 {
            values.insert("face_number".into(), OSD::Integer(face_number));
        }
        Ok(OSD::Map(values))
    }

    pub fn from_osd(
        osd: OSD,
        default_face: Option<Self>,
        face_number: &mut i32,
    ) -> Result<Self, Error> {
        let values = map(osd).ok_or(Error::Argument)?;
        *face_number = map_i32(Some(&values), "face_number", -1);
        let mut face =
            default_face.map_or_else(Self::default_face, |value| Self::inheriting(value.data));
        face.set_rgba(
            values
                .get("colors")
                .and_then(|v| v.as_color4().ok())
                .unwrap_or_else(Color4::white),
        );
        face.set_repeat_u(map_real(Some(&values), "scales", 1.0));
        face.set_repeat_v(map_real(Some(&values), "scalet", 1.0));
        face.set_offset_u(map_real(Some(&values), "offsets", 0.0));
        face.set_offset_v(map_real(Some(&values), "offsett", 0.0));
        face.set_rotation(map_real(Some(&values), "imagerot", 0.0));
        face.set_bump(bumpiness_from_u8(map_i32(Some(&values), "bump", 0) as u8));
        face.set_shiny(shininess_from_u8(map_i32(Some(&values), "shiny", 0) as u8));
        face.set_fullbright(map_bool(Some(&values), "fullbright", false));
        face.set_media_flags(map_bool(Some(&values), "media_flags", false));
        face.set_tex_map_type(mapping_type_from_u8(
            map_i32(Some(&values), "mapping", 0) as u8
        ));
        face.set_glow(map_real(Some(&values), "glow", 0.0));
        face.set_texture_id(map_uuid(Some(&values), "imageid", UUID::zero()));
        face.set_material_id(map_uuid(Some(&values), "materialid", UUID::zero()));
        face.set_render_material_id(map_uuid(Some(&values), "rendermaterialid", UUID::zero()));
        Ok(face)
    }

    pub fn get_hash_code(&self) -> i32 {
        hash_i32(&(
            self.rgba().get_hash_code(),
            self.repeat_u().to_bits(),
            self.repeat_v().to_bits(),
            self.offset_u().to_bits(),
            self.offset_v().to_bits(),
            self.rotation().to_bits(),
            self.glow().to_bits(),
            self.material(),
            self.media(),
            self.texture_id(),
            self.material_id(),
            self.render_material_id(),
        ))
    }

    pub fn to_string(&self) -> String {
        format!(
            "Color: {:?} RepeatU: {} RepeatV: {} OffsetU: {} OffsetV: {} Rotation: {} Bump: {:?} Shiny: {:?} Fullbright: {} Mapping: {:?} Media: {} Glow: {} ID: {} MaterialID: {} RenderMaterialID: {}",
            self.rgba(),
            self.repeat_u(),
            self.repeat_v(),
            self.offset_u(),
            self.offset_v(),
            self.rotation(),
            self.bump(),
            self.shiny(),
            self.fullbright(),
            self.tex_map_type(),
            self.media_flags(),
            self.glow(),
            self.texture_id(),
            self.material_id(),
            self.render_material_id()
        )
    }
}

#[derive(Debug, PartialEq)]
pub struct PrimitiveTextureEntry {
    pub default_texture: Option<PrimitiveTextureEntryFace>,
    pub face_textures: Vec<Option<PrimitiveTextureEntryFace>>,
}

impl Clone for PrimitiveTextureEntry {
    fn clone(&self) -> Self {
        let default_texture = self
            .default_texture
            .as_ref()
            .map(PrimitiveTextureEntryFace::deep_clone);
        let default_data = default_texture.as_ref().map(|face| face.data.clone());
        let face_textures = self
            .face_textures
            .iter()
            .map(|face| {
                face.as_ref()
                    .map(|face| face.clone_with_default(default_data.clone()))
            })
            .collect();
        Self {
            default_texture,
            face_textures,
        }
    }
}

impl PrimitiveTextureEntry {
    pub const MAX_FACES: i32 = 45;
    pub fn white_texture() -> UUID {
        UUID::new_with_string("5748decc-f629-461c-9a36-a35a221fe21f".into())
            .expect("fixed white texture UUID")
    }

    fn empty_faces() -> Vec<Option<PrimitiveTextureEntryFace>> {
        (0..Self::MAX_FACES).map(|_| None).collect()
    }

    pub fn new_with_uuid(default_texture_id: UUID) -> Result<Self, Error> {
        let mut default = PrimitiveTextureEntryFace::default_face();
        default.set_texture_id(default_texture_id);
        Ok(Self {
            default_texture: Some(default),
            face_textures: Self::empty_faces(),
        })
    }

    pub fn new_with_texture_entry_face(
        default_face: PrimitiveTextureEntryFace,
    ) -> Result<Self, Error> {
        let mut default = PrimitiveTextureEntryFace::default_face();
        default.set_bump(default_face.bump());
        default.set_fullbright(default_face.fullbright());
        default.set_media_flags(default_face.media_flags());
        default.set_offset_u(default_face.offset_u());
        default.set_offset_v(default_face.offset_v());
        default.set_repeat_u(default_face.repeat_u());
        default.set_repeat_v(default_face.repeat_v());
        default.set_rgba(default_face.rgba());
        default.set_rotation(default_face.rotation());
        default.set_glow(default_face.glow());
        default.set_shiny(default_face.shiny());
        default.set_tex_map_type(default_face.tex_map_type());
        default.set_texture_id(default_face.texture_id());
        default.set_material_id(default_face.material_id());
        default.set_render_material_id(default_face.render_material_id());
        Ok(Self {
            default_texture: Some(default),
            face_textures: Self::empty_faces(),
        })
    }

    pub fn new_with_texture_entry(source: Self) -> Result<Self, Error> {
        Ok(source.clone())
    }

    pub fn create_face(&mut self, index: u32) -> Result<&mut PrimitiveTextureEntryFace, Error> {
        let index = usize::try_from(index).map_err(|_| Error::IndexOutOfRange)?;
        if index >= Self::MAX_FACES as usize {
            return Err(Error::IndexOutOfRange);
        }
        let default = self
            .default_texture
            .as_ref()
            .ok_or(Error::InvalidOperation)?
            .data
            .clone();
        if self.face_textures[index].is_none() {
            self.face_textures[index] = Some(PrimitiveTextureEntryFace::inheriting(default));
        }
        self.face_textures[index]
            .as_mut()
            .ok_or(Error::InvalidOperation)
    }

    pub fn get_face(&self, index: u32) -> Result<Option<&PrimitiveTextureEntryFace>, Error> {
        let index = usize::try_from(index).map_err(|_| Error::IndexOutOfRange)?;
        if index >= Self::MAX_FACES as usize {
            return Err(Error::IndexOutOfRange);
        }
        Ok(self.face_textures[index]
            .as_ref()
            .or(self.default_texture.as_ref()))
    }

    pub fn get_osd(&self) -> Result<OSD, Error> {
        let Some(default) = &self.default_texture else {
            return Ok(OSD::Array(Vec::new()));
        };
        let mut faces = Vec::with_capacity(1 + self.face_textures.iter().flatten().count());
        faces.push(default.get_osd(-1)?);
        for (index, face) in self.face_textures.iter().enumerate() {
            if let Some(face) = face {
                faces.push(face.get_osd(index as i32)?);
            }
        }
        Ok(OSD::Array(faces))
    }

    pub fn from_osd(osd: OSD) -> Result<Self, Error> {
        let OSD::Array(faces) = osd else {
            return Self::new_with_uuid(UUID::zero());
        };
        if faces.is_empty() {
            return Self::new_with_uuid(UUID::zero());
        }
        if faces.len() > MAX_OSD_FACE_ENTRIES {
            return Err(Error::Argument);
        }
        let mut number = -1;
        let default = PrimitiveTextureEntryFace::from_osd(faces[0].clone(), None, &mut number)?;
        let mut entry = Self::new_with_texture_entry_face(default)?;
        for value in faces.into_iter().skip(1) {
            let default = entry
                .default_texture
                .as_ref()
                .ok_or(Error::InvalidOperation)?
                .deep_clone();
            let face = PrimitiveTextureEntryFace::from_osd(value, Some(default), &mut number)?;
            let index = usize::try_from(number).map_err(|_| Error::IndexOutOfRange)?;
            if index < Self::MAX_FACES as usize {
                let shared_default = entry
                    .default_texture
                    .as_ref()
                    .ok_or(Error::InvalidOperation)?
                    .data
                    .clone();
                entry.face_textures[index] = Some(face.clone_with_default(Some(shared_default)));
            }
        }
        Ok(entry)
    }

    pub fn get_hash_code(&self) -> i32 {
        self.default_texture
            .as_ref()
            .map_or(0, PrimitiveTextureEntryFace::get_hash_code)
            ^ self
                .face_textures
                .iter()
                .flatten()
                .fold(0, |hash, face| hash ^ face.get_hash_code())
    }
    pub fn to_string(&self) -> String {
        let mut output = format!(
            "Default Face: {}\n",
            self.default_texture
                .as_ref()
                .map_or_else(String::new, PrimitiveTextureEntryFace::to_string)
        );
        for (index, face) in self.face_textures.iter().enumerate() {
            if let Some(face) = face {
                let _ = writeln!(output, "Face {index}: {}", face.to_string());
            }
        }
        output
    }

    pub fn new_with_bytes_int32_int32(data: Vec<u8>, pos: i32, length: i32) -> Result<Self, Error> {
        let length = usize::try_from(length).map_err(|_| Error::Argument)?;
        if length > MAX_TEXTURE_ENTRY_BYTES {
            return Err(Error::Argument);
        }
        let bytes = checked_window(&data, pos, length)?;
        let mut entry = Self::new_with_uuid(UUID::zero())?;
        if length < 16 {
            return Ok(entry);
        }
        let mut cursor = 0_usize;
        entry
            .default_texture
            .as_mut()
            .ok_or(Error::InvalidOperation)?
            .set_texture_id(read_uuid(bytes, &mut cursor)?);
        decode_overrides(&mut entry, bytes, &mut cursor, 16, |face, bytes| {
            face.set_texture_id(
                UUID::new_with_bytes_int32(bytes.to_vec(), 0).unwrap_or_else(|_| UUID::zero()),
            );
        })?;
        let color = read_exact(bytes, &mut cursor, 4)?;
        entry
            .default_texture
            .as_mut()
            .ok_or(Error::InvalidOperation)?
            .set_rgba(Color4::new_with_bytes_int32_boolean(
                color.to_vec(),
                0,
                true,
            )?);
        decode_overrides(&mut entry, bytes, &mut cursor, 4, |face, bytes| {
            if let Ok(value) = Color4::new_with_bytes_int32_boolean(bytes.to_vec(), 0, true) {
                face.set_rgba(value);
            }
        })?;
        let value = read_f32(bytes, cursor)?;
        cursor += 4;
        entry
            .default_texture
            .as_mut()
            .ok_or(Error::InvalidOperation)?
            .set_repeat_u(value);
        decode_overrides(&mut entry, bytes, &mut cursor, 4, |face, bytes| {
            face.set_repeat_u(f32::from_le_bytes(bytes.try_into().unwrap_or([0; 4])));
        })?;
        let value = read_f32(bytes, cursor)?;
        cursor += 4;
        entry
            .default_texture
            .as_mut()
            .ok_or(Error::InvalidOperation)?
            .set_repeat_v(value);
        decode_overrides(&mut entry, bytes, &mut cursor, 4, |face, bytes| {
            face.set_repeat_v(f32::from_le_bytes(bytes.try_into().unwrap_or([0; 4])));
        })?;
        let value = te_offset_float_slice(read_exact(bytes, &mut cursor, 2)?)?;
        entry
            .default_texture
            .as_mut()
            .ok_or(Error::InvalidOperation)?
            .set_offset_u(value);
        decode_overrides(&mut entry, bytes, &mut cursor, 2, |face, bytes| {
            if let Ok(value) = te_offset_float_slice(bytes) {
                face.set_offset_u(value);
            }
        })?;
        let value = te_offset_float_slice(read_exact(bytes, &mut cursor, 2)?)?;
        entry
            .default_texture
            .as_mut()
            .ok_or(Error::InvalidOperation)?
            .set_offset_v(value);
        decode_overrides(&mut entry, bytes, &mut cursor, 2, |face, bytes| {
            if let Ok(value) = te_offset_float_slice(bytes) {
                face.set_offset_v(value);
            }
        })?;
        let value = te_rotation_float_slice(read_exact(bytes, &mut cursor, 2)?)?;
        entry
            .default_texture
            .as_mut()
            .ok_or(Error::InvalidOperation)?
            .set_rotation(value);
        decode_overrides(&mut entry, bytes, &mut cursor, 2, |face, bytes| {
            if let Ok(value) = te_rotation_float_slice(bytes) {
                face.set_rotation(value);
            }
        })?;
        let value = *read_exact(bytes, &mut cursor, 1)?
            .first()
            .ok_or(Error::IndexOutOfRange)?;
        entry
            .default_texture
            .as_mut()
            .ok_or(Error::InvalidOperation)?
            .set_material(value);
        decode_overrides(&mut entry, bytes, &mut cursor, 1, |face, bytes| {
            face.set_material(bytes[0]);
        })?;
        let value = *read_exact(bytes, &mut cursor, 1)?
            .first()
            .ok_or(Error::IndexOutOfRange)?;
        entry
            .default_texture
            .as_mut()
            .ok_or(Error::InvalidOperation)?
            .set_media(value);
        decode_overrides(&mut entry, bytes, &mut cursor, 1, |face, bytes| {
            face.set_media(bytes[0]);
        })?;
        if cursor >= bytes.len() {
            return Ok(entry);
        }
        let value = f32::from(
            *read_exact(bytes, &mut cursor, 1)?
                .first()
                .ok_or(Error::IndexOutOfRange)?,
        ) / 255.0;
        entry
            .default_texture
            .as_mut()
            .ok_or(Error::InvalidOperation)?
            .set_glow(value);
        decode_overrides(&mut entry, bytes, &mut cursor, 1, |face, bytes| {
            face.set_glow(f32::from(bytes[0]) / 255.0);
        })?;
        if bytes.len().saturating_sub(cursor) >= 16 {
            entry
                .default_texture
                .as_mut()
                .ok_or(Error::InvalidOperation)?
                .set_material_id(read_uuid(bytes, &mut cursor)?);
            decode_overrides(&mut entry, bytes, &mut cursor, 16, |face, bytes| {
                face.set_material_id(
                    UUID::new_with_bytes_int32(bytes.to_vec(), 0).unwrap_or_else(|_| UUID::zero()),
                );
            })?;
        }
        if bytes.len().saturating_sub(cursor) >= 16 {
            entry
                .default_texture
                .as_mut()
                .ok_or(Error::InvalidOperation)?
                .set_render_material_id(read_uuid(bytes, &mut cursor)?);
            decode_overrides_optional_terminator(
                &mut entry,
                bytes,
                &mut cursor,
                16,
                |face, bytes| {
                    face.set_render_material_id(
                        UUID::new_with_bytes_int32(bytes.to_vec(), 0)
                            .unwrap_or_else(|_| UUID::zero()),
                    );
                },
            )?;
        }
        Ok(entry)
    }

    pub fn get_bytes(&self) -> Result<Vec<u8>, Error> {
        let Some(default) = &self.default_texture else {
            return Ok(Vec::new());
        };
        let mut output = Vec::new();
        output.extend_from_slice(&default.texture_id().get_bytes()?);
        encode_face_values(
            self,
            &mut output,
            default.texture_id(),
            PrimitiveTextureEntryFace::texture_id,
            |value| value.get_bytes(),
            true,
        )?;
        output.extend_from_slice(&default.rgba().get_bytes_with_boolean(true)?);
        encode_face_values(
            self,
            &mut output,
            default.rgba(),
            PrimitiveTextureEntryFace::rgba,
            |value| value.get_bytes_with_boolean(true),
            true,
        )?;
        output.extend_from_slice(&default.repeat_u().to_le_bytes());
        encode_face_values(
            self,
            &mut output,
            default.repeat_u().to_bits(),
            |face| face.repeat_u().to_bits(),
            |value| Ok(f32::from_bits(value).to_le_bytes().to_vec()),
            true,
        )?;
        output.extend_from_slice(&default.repeat_v().to_le_bytes());
        encode_face_values(
            self,
            &mut output,
            default.repeat_v().to_bits(),
            |face| face.repeat_v().to_bits(),
            |value| Ok(f32::from_bits(value).to_le_bytes().to_vec()),
            true,
        )?;
        output.extend_from_slice(&te_offset_short(default.offset_u()).to_le_bytes());
        encode_face_values(
            self,
            &mut output,
            te_offset_short(default.offset_u()),
            |face| te_offset_short(face.offset_u()),
            |value| Ok(value.to_le_bytes().to_vec()),
            true,
        )?;
        output.extend_from_slice(&te_offset_short(default.offset_v()).to_le_bytes());
        encode_face_values(
            self,
            &mut output,
            te_offset_short(default.offset_v()),
            |face| te_offset_short(face.offset_v()),
            |value| Ok(value.to_le_bytes().to_vec()),
            true,
        )?;
        output.extend_from_slice(&te_rotation_short(default.rotation()).to_le_bytes());
        encode_face_values(
            self,
            &mut output,
            te_rotation_short(default.rotation()),
            |face| te_rotation_short(face.rotation()),
            |value| Ok(value.to_le_bytes().to_vec()),
            true,
        )?;
        output.push(default.material());
        encode_face_values(
            self,
            &mut output,
            default.material(),
            PrimitiveTextureEntryFace::material,
            |value| Ok(vec![value]),
            true,
        )?;
        output.push(default.media());
        encode_face_values(
            self,
            &mut output,
            default.media(),
            PrimitiveTextureEntryFace::media,
            |value| Ok(vec![value]),
            true,
        )?;
        output.push(te_glow_byte(default.glow()));
        encode_face_values(
            self,
            &mut output,
            te_glow_byte(default.glow()),
            |face| te_glow_byte(face.glow()),
            |value| Ok(vec![value]),
            true,
        )?;
        output.extend_from_slice(&default.material_id().get_bytes()?);
        encode_face_values(
            self,
            &mut output,
            default.material_id(),
            PrimitiveTextureEntryFace::material_id,
            |value| value.get_bytes(),
            true,
        )?;
        output.extend_from_slice(&default.render_material_id().get_bytes()?);
        encode_face_values(
            self,
            &mut output,
            default.render_material_id(),
            PrimitiveTextureEntryFace::render_material_id,
            |value| value.get_bytes(),
            false,
        )?;
        if output.len() > MAX_TEXTURE_ENTRY_BYTES {
            return Err(Error::Argument);
        }
        Ok(output)
    }
}

fn read_exact<'a>(data: &'a [u8], cursor: &mut usize, length: usize) -> Result<&'a [u8], Error> {
    let end = cursor.checked_add(length).ok_or(Error::IndexOutOfRange)?;
    let value = data.get(*cursor..end).ok_or(Error::IndexOutOfRange)?;
    *cursor = end;
    Ok(value)
}

fn read_uuid(data: &[u8], cursor: &mut usize) -> Result<UUID, Error> {
    UUID::new_with_bytes_int32(read_exact(data, cursor, 16)?.to_vec(), 0)
}

fn read_face_bitfield(data: &[u8], cursor: &mut usize) -> Result<Option<u64>, Error> {
    let mut bits = 0_u64;
    for _ in 0..10 {
        let byte = *read_exact(data, cursor, 1)?
            .first()
            .ok_or(Error::IndexOutOfRange)?;
        bits = bits.checked_shl(7).ok_or(Error::Argument)? | u64::from(byte & 0x7f);
        if byte & 0x80 == 0 {
            if bits >> PrimitiveTextureEntry::MAX_FACES != 0 {
                return Err(Error::Argument);
            }
            return Ok((bits != 0).then_some(bits));
        }
    }
    Err(Error::Argument)
}

fn face_bitfield_bytes(bits: u64) -> Vec<u8> {
    if bits == 0 {
        return vec![0];
    }
    let chunks = (64 - bits.leading_zeros() as usize).div_ceil(7);
    (0..chunks)
        .map(|index| {
            let shift = 7 * (chunks - index - 1);
            ((bits >> shift) as u8 & 0x7f) | if index + 1 < chunks { 0x80 } else { 0 }
        })
        .collect()
}

fn decode_overrides(
    entry: &mut PrimitiveTextureEntry,
    data: &[u8],
    cursor: &mut usize,
    value_size: usize,
    mut set: impl FnMut(&mut PrimitiveTextureEntryFace, &[u8]),
) -> Result<(), Error> {
    while let Some(bits) = read_face_bitfield(data, cursor)? {
        let value = read_exact(data, cursor, value_size)?;
        for face in 0..PrimitiveTextureEntry::MAX_FACES as usize {
            if bits & (1_u64 << face) != 0 {
                set(entry.create_face(face as u32)?, value);
            }
        }
    }
    Ok(())
}

fn decode_overrides_optional_terminator(
    entry: &mut PrimitiveTextureEntry,
    data: &[u8],
    cursor: &mut usize,
    value_size: usize,
    mut set: impl FnMut(&mut PrimitiveTextureEntryFace, &[u8]),
) -> Result<(), Error> {
    while *cursor < data.len() {
        let Some(bits) = read_face_bitfield(data, cursor)? else {
            break;
        };
        let value = read_exact(data, cursor, value_size)?;
        for face in 0..PrimitiveTextureEntry::MAX_FACES as usize {
            if bits & (1_u64 << face) != 0 {
                set(entry.create_face(face as u32)?, value);
            }
        }
    }
    Ok(())
}

fn encode_face_values<T: Copy + PartialEq>(
    entry: &PrimitiveTextureEntry,
    output: &mut Vec<u8>,
    default: T,
    get: impl Fn(&PrimitiveTextureEntryFace) -> T,
    bytes: impl Fn(T) -> Result<Vec<u8>, Error>,
    terminator: bool,
) -> Result<(), Error> {
    for (index, face) in entry.face_textures.iter().enumerate() {
        if let Some(face) = face {
            let value = get(face);
            if value != default {
                output.extend_from_slice(&face_bitfield_bytes(1_u64 << index));
                output.extend_from_slice(&bytes(value)?);
            }
        }
    }
    if terminator {
        output.push(0);
    }
    Ok(())
}

pub(crate) fn te_offset_short(offset: f32) -> i16 {
    (offset.clamp(-1.0, 1.0) * 32767.0).round_ties_even() as i16
}
fn te_offset_float_slice(bytes: &[u8]) -> Result<f32, Error> {
    Ok(f32::from(i16::from_le_bytes(
        bytes.try_into().map_err(|_| Error::IndexOutOfRange)?,
    )) / 32767.0)
}
pub(crate) fn te_rotation_short(rotation: f32) -> i16 {
    let two_pi = f64::from(std::f32::consts::TAU);
    let rotation = f64::from(rotation);
    let remainder = rotation - (rotation / two_pi).round_ties_even() * two_pi;
    (remainder / two_pi * 32768.0).round_ties_even() as i16
}
fn te_rotation_float_slice(bytes: &[u8]) -> Result<f32, Error> {
    Ok(f32::from(u16::from_le_bytes(
        bytes.try_into().map_err(|_| Error::IndexOutOfRange)?,
    )) / 32768.0
        * std::f32::consts::TAU)
}
pub(crate) fn te_glow_byte(glow: f32) -> u8 {
    (glow.clamp(0.0, 1.0) * 255.0) as u8
}
#[derive(Clone, Debug, PartialEq)]
pub struct PrimitiveParticleSystem {
    pub angular_velocity: Vector3,
    pub blend_func_dest: u8,
    pub blend_func_source: u8,
    pub burst_part_count: u8,
    pub burst_radius: f32,
    pub burst_rate: f32,
    pub burst_speed_max: f32,
    pub burst_speed_min: f32,
    pub crc: u32,
    pub inner_angle: f32,
    pub max_age: f32,
    pub outer_angle: f32,
    pub part_acceleration: Vector3,
    pub part_data_flags: PrimitiveParticleSystemParticleDataFlags,
    pub part_end_color: Color4,
    pub part_end_glow: f32,
    pub part_end_scale_x: f32,
    pub part_end_scale_y: f32,
    pub part_flags: u32,
    pub part_max_age: f32,
    pub part_start_color: Color4,
    pub part_start_glow: f32,
    pub part_start_scale_x: f32,
    pub part_start_scale_y: f32,
    pub pattern: PrimitiveParticleSystemSourcePattern,
    pub start_age: f32,
    pub target: UUID,
    pub texture: UUID,
}

impl Default for PrimitiveParticleSystem {
    fn default() -> Self {
        Self {
            angular_velocity: Vector3::zero(),
            blend_func_dest: PrimitiveParticleSystemBlendFunc::OneMinusSourceAlpha as u8,
            blend_func_source: PrimitiveParticleSystemBlendFunc::SourceAlpha as u8,
            burst_part_count: 0,
            burst_radius: 0.0,
            burst_rate: 0.0,
            burst_speed_max: 0.0,
            burst_speed_min: 0.0,
            crc: 0,
            inner_angle: 0.0,
            max_age: 0.0,
            outer_angle: 0.0,
            part_acceleration: Vector3::zero(),
            part_data_flags: PrimitiveParticleSystemParticleDataFlags::NONE,
            part_end_color: Color4::black(),
            part_end_glow: 0.0,
            part_end_scale_x: 0.0,
            part_end_scale_y: 0.0,
            part_flags: 0,
            part_max_age: 0.0,
            part_start_color: Color4::black(),
            part_start_glow: 0.0,
            part_start_scale_x: 0.0,
            part_start_scale_y: 0.0,
            pattern: PrimitiveParticleSystemSourcePattern::NONE,
            start_age: 0.0,
            target: UUID::zero(),
            texture: UUID::zero(),
        }
    }
}

impl PrimitiveParticleSystem {
    pub const LEGACY_DATA_BLOCK_SIZE: u8 = 86;
    pub const MAX_DATA_BLOCK_SIZE: u8 = 98;
    pub const PART_DATA_SIZE: u8 = 18;
    pub const SYS_DATA_SIZE: u8 = 68;

    pub fn new(data: Vec<u8>, pos: i32) -> Result<Self, Error> {
        let start = usize::try_from(pos).map_err(|_| Error::IndexOutOfRange)?;
        let size = data
            .len()
            .checked_sub(start)
            .ok_or(Error::IndexOutOfRange)?;
        if size > usize::from(Self::MAX_DATA_BLOCK_SIZE) {
            return Err(Error::Argument);
        }
        if size != usize::from(Self::LEGACY_DATA_BLOCK_SIZE) && size < 94 {
            return Err(Error::Argument);
        }
        let slice = data.get(start..).ok_or(Error::IndexOutOfRange)?.to_vec();
        let mut pack = BitPack::new(slice, 0)?;
        let mut value = Self::default();
        if size == usize::from(Self::LEGACY_DATA_BLOCK_SIZE) {
            value.unpack_system(&mut pack)?;
            value.unpack_legacy(&mut pack)?;
            return Ok(value);
        }
        let system_size = pack.unpack_u_bits(32)?;
        if system_size != u32::from(Self::SYS_DATA_SIZE) {
            return Err(Error::Argument);
        }
        value.unpack_system(&mut pack)?;
        let part_size = pack.unpack_u_bits(32)?;
        if !(u32::from(Self::PART_DATA_SIZE)..=u32::from(Self::PART_DATA_SIZE) + 4)
            .contains(&part_size)
        {
            return Err(Error::Argument);
        }
        let expected = usize::from(Self::SYS_DATA_SIZE)
            + usize::try_from(part_size).map_err(|_| Error::Argument)?
            + 8;
        if expected != size {
            return Err(Error::Argument);
        }
        value.unpack_legacy(&mut pack)?;
        if value.part_data_flags.0 & PrimitiveParticleSystemParticleDataFlags::DATA_GLOW.0 != 0 {
            value.part_start_glow = f32::from(pack.unpack_byte()?) / 255.0;
            value.part_end_glow = f32::from(pack.unpack_byte()?) / 255.0;
        }
        if value.part_data_flags.0 & PrimitiveParticleSystemParticleDataFlags::DATA_BLEND.0 != 0 {
            value.blend_func_source = pack.unpack_byte()?;
            value.blend_func_dest = pack.unpack_byte()?;
        }
        Ok(value)
    }

    fn unpack_system(&mut self, pack: &mut BitPack) -> Result<(), Error> {
        self.crc = pack.unpack_u_bits(32)?;
        self.part_flags = pack.unpack_u_bits(32)?;
        self.pattern = PrimitiveParticleSystemSourcePattern(pack.unpack_byte()?);
        self.max_age = pack.unpack_fixed(false, 8, 8)?;
        self.start_age = pack.unpack_fixed(false, 8, 8)?;
        self.inner_angle = pack.unpack_fixed(false, 3, 5)?;
        self.outer_angle = pack.unpack_fixed(false, 3, 5)?;
        self.burst_rate = pack.unpack_fixed(false, 8, 8)?;
        self.burst_radius = pack.unpack_fixed(false, 8, 8)?;
        self.burst_speed_min = pack.unpack_fixed(false, 8, 8)?;
        self.burst_speed_max = pack.unpack_fixed(false, 8, 8)?;
        self.burst_part_count = pack.unpack_byte()?;
        self.angular_velocity = Vector3 {
            x: pack.unpack_fixed(true, 8, 7)?,
            y: pack.unpack_fixed(true, 8, 7)?,
            z: pack.unpack_fixed(true, 8, 7)?,
        };
        self.part_acceleration = Vector3 {
            x: pack.unpack_fixed(true, 8, 7)?,
            y: pack.unpack_fixed(true, 8, 7)?,
            z: pack.unpack_fixed(true, 8, 7)?,
        };
        self.texture = pack.unpack_uuid()?;
        self.target = pack.unpack_uuid()?;
        Ok(())
    }

    fn unpack_legacy(&mut self, pack: &mut BitPack) -> Result<(), Error> {
        self.part_data_flags = PrimitiveParticleSystemParticleDataFlags(pack.unpack_u_bits(32)?);
        self.part_max_age = pack.unpack_fixed(false, 8, 8)?;
        self.part_start_color = unpack_color(pack)?;
        self.part_end_color = unpack_color(pack)?;
        self.part_start_scale_x = pack.unpack_fixed(false, 3, 5)?;
        self.part_start_scale_y = pack.unpack_fixed(false, 3, 5)?;
        self.part_end_scale_x = pack.unpack_fixed(false, 3, 5)?;
        self.part_end_scale_y = pack.unpack_fixed(false, 3, 5)?;
        Ok(())
    }

    fn pack_system(&self, pack: &mut BitPack) -> Result<(), Error> {
        pack.pack_bits_with_u_int32_int32(self.crc, 32)?;
        pack.pack_bits_with_u_int32_int32(self.part_flags, 32)?;
        pack.pack_bits_with_u_int32_int32(u32::from(self.pattern.0), 8)?;
        pack.pack_fixed(self.max_age, false, 8, 8)?;
        pack.pack_fixed(self.start_age, false, 8, 8)?;
        pack.pack_fixed(self.inner_angle, false, 3, 5)?;
        pack.pack_fixed(self.outer_angle, false, 3, 5)?;
        pack.pack_fixed(self.burst_rate, false, 8, 8)?;
        pack.pack_fixed(self.burst_radius, false, 8, 8)?;
        pack.pack_fixed(self.burst_speed_min, false, 8, 8)?;
        pack.pack_fixed(self.burst_speed_max, false, 8, 8)?;
        pack.pack_bits_with_u_int32_int32(u32::from(self.burst_part_count), 8)?;
        pack.pack_fixed(self.angular_velocity.x, true, 8, 7)?;
        pack.pack_fixed(self.angular_velocity.y, true, 8, 7)?;
        pack.pack_fixed(self.angular_velocity.z, true, 8, 7)?;
        pack.pack_fixed(self.part_acceleration.x, true, 8, 7)?;
        pack.pack_fixed(self.part_acceleration.y, true, 8, 7)?;
        pack.pack_fixed(self.part_acceleration.z, true, 8, 7)?;
        pack.pack_uuid(self.texture)?;
        pack.pack_uuid(self.target)?;
        Ok(())
    }

    fn pack_legacy(&self, pack: &mut BitPack, flags: u32) -> Result<(), Error> {
        pack.pack_bits_with_u_int32_int32(flags, 32)?;
        pack.pack_fixed(self.part_max_age, false, 8, 8)?;
        pack.pack_color(self.part_start_color)?;
        pack.pack_color(self.part_end_color)?;
        pack.pack_fixed(self.part_start_scale_x, false, 3, 5)?;
        pack.pack_fixed(self.part_start_scale_y, false, 3, 5)?;
        pack.pack_fixed(self.part_end_scale_x, false, 3, 5)?;
        pack.pack_fixed(self.part_end_scale_y, false, 3, 5)?;
        Ok(())
    }

    pub fn has_glow(&self) -> Result<bool, Error> {
        Ok(self.part_start_glow > 0.0 || self.part_end_glow > 0.0)
    }
    pub fn has_blend_func(&self) -> Result<bool, Error> {
        Ok(
            self.blend_func_source != PrimitiveParticleSystemBlendFunc::SourceAlpha as u8
                || self.blend_func_dest
                    != PrimitiveParticleSystemBlendFunc::OneMinusSourceAlpha as u8,
        )
    }
    pub fn is_legacy_compatible(&self) -> Result<bool, Error> {
        Ok(!self.has_glow()? && !self.has_blend_func()?)
    }

    pub fn get_bytes(&self) -> Result<Vec<u8>, Error> {
        let glow = self.has_glow()?;
        let blend = self.has_blend_func()?;
        let legacy = !glow && !blend;
        let size = usize::from(Self::LEGACY_DATA_BLOCK_SIZE)
            + if legacy { 0 } else { 8 }
            + if glow { 2 } else { 0 }
            + if blend { 2 } else { 0 };
        let mut pack = BitPack::new(vec![0_u8; size], 0)?;
        let mut flags = self.part_data_flags.0;
        if legacy {
            self.pack_system(&mut pack)?;
            self.pack_legacy(&mut pack, flags)?;
        } else {
            if glow {
                flags |= PrimitiveParticleSystemParticleDataFlags::DATA_GLOW.0;
            }
            if blend {
                flags |= PrimitiveParticleSystemParticleDataFlags::DATA_BLEND.0;
            }
            pack.pack_bits_with_u_int32_int32(u32::from(Self::SYS_DATA_SIZE), 32)?;
            self.pack_system(&mut pack)?;
            let part_size =
                u32::from(Self::PART_DATA_SIZE) + u32::from(glow) * 2 + u32::from(blend) * 2;
            pack.pack_bits_with_u_int32_int32(part_size, 32)?;
            self.pack_legacy(&mut pack, flags)?;
            if glow {
                pack.pack_bits_with_u_int32_int32(
                    u32::from(te_glow_byte(self.part_start_glow)),
                    8,
                )?;
                pack.pack_bits_with_u_int32_int32(u32::from(te_glow_byte(self.part_end_glow)), 8)?;
            }
            if blend {
                pack.pack_bits_with_u_int32_int32(u32::from(self.blend_func_source), 8)?;
                pack.pack_bits_with_u_int32_int32(u32::from(self.blend_func_dest), 8)?;
            }
        }
        Ok(pack.data)
    }

    pub fn get_osd(&self) -> Result<OSD, Error> {
        let mut values = HashMap::from([
            ("crc".into(), OSD::from_u_integer(self.crc)?),
            ("part_flags".into(), OSD::from_u_integer(self.part_flags)?),
            ("pattern".into(), OSD::Integer(i32::from(self.pattern.0))),
            ("max_age".into(), OSD::Real(f64::from(self.max_age))),
            ("start_age".into(), OSD::Real(f64::from(self.start_age))),
            ("inner_angle".into(), OSD::Real(f64::from(self.inner_angle))),
            ("outer_angle".into(), OSD::Real(f64::from(self.outer_angle))),
            ("burst_rate".into(), OSD::Real(f64::from(self.burst_rate))),
            (
                "burst_radius".into(),
                OSD::Real(f64::from(self.burst_radius)),
            ),
            (
                "burst_speed_min".into(),
                OSD::Real(f64::from(self.burst_speed_min)),
            ),
            (
                "burst_speed_max".into(),
                OSD::Real(f64::from(self.burst_speed_max)),
            ),
            (
                "burst_part_count".into(),
                OSD::Integer(i32::from(self.burst_part_count)),
            ),
            (
                "ang_velocity".into(),
                OSD::from_vector3(self.angular_velocity)?,
            ),
            (
                "part_acceleration".into(),
                OSD::from_vector3(self.part_acceleration)?,
            ),
            ("texture".into(), OSD::UUID(self.texture)),
            ("target".into(), OSD::UUID(self.target)),
            (
                "part_data_flags".into(),
                OSD::from_u_integer(self.part_data_flags.0)?,
            ),
            (
                "part_max_age".into(),
                OSD::Real(f64::from(self.part_max_age)),
            ),
            (
                "part_start_color".into(),
                OSD::from_color4(self.part_start_color)?,
            ),
            (
                "part_end_color".into(),
                OSD::from_color4(self.part_end_color)?,
            ),
            (
                "part_start_scale".into(),
                OSD::from_vector3(Vector3 {
                    x: self.part_start_scale_x,
                    y: self.part_start_scale_y,
                    z: 0.0,
                })?,
            ),
            (
                "part_end_scale".into(),
                OSD::from_vector3(Vector3 {
                    x: self.part_end_scale_x,
                    y: self.part_end_scale_y,
                    z: 0.0,
                })?,
            ),
        ]);
        if self.has_glow()? {
            values.insert(
                "part_start_glow".into(),
                OSD::Real(f64::from(self.part_start_glow)),
            );
            values.insert(
                "part_end_glow".into(),
                OSD::Real(f64::from(self.part_end_glow)),
            );
        }
        if self.has_blend_func()? {
            values.insert(
                "blendfunc_source".into(),
                OSD::Integer(i32::from(self.blend_func_source)),
            );
            values.insert(
                "blendfunc_dest".into(),
                OSD::Integer(i32::from(self.blend_func_dest)),
            );
        }
        Ok(OSD::Map(values))
    }

    pub fn from_osd(osd: OSD) -> Result<Self, Error> {
        let Some(values) = map(osd) else {
            return Ok(Self::default());
        };
        let values = Some(&values);
        let start_scale = map_vector3(values, "part_start_scale", Vector3::zero());
        let end_scale = map_vector3(values, "part_end_scale", Vector3::zero());
        Ok(Self {
            crc: map_u32(values, "crc", 0),
            part_flags: map_u32(values, "part_flags", 0),
            pattern: PrimitiveParticleSystemSourcePattern(map_i32(values, "pattern", 0) as u8),
            max_age: map_real(values, "max_age", 0.0),
            start_age: map_real(values, "start_age", 0.0),
            inner_angle: map_real(values, "inner_angle", 0.0),
            outer_angle: map_real(values, "outer_angle", 0.0),
            burst_rate: map_real(values, "burst_rate", 0.0),
            burst_radius: map_real(values, "burst_radius", 0.0),
            burst_speed_min: map_real(values, "burst_speed_min", 0.0),
            burst_speed_max: map_real(values, "burst_speed_max", 0.0),
            burst_part_count: map_i32(values, "burst_part_count", 0) as u8,
            angular_velocity: map_vector3(values, "ang_velocity", Vector3::zero()),
            part_acceleration: map_vector3(values, "part_acceleration", Vector3::zero()),
            texture: map_uuid(values, "texture", UUID::zero()),
            target: map_uuid(values, "target", UUID::zero()),
            part_data_flags: PrimitiveParticleSystemParticleDataFlags(map_u32(
                values,
                "part_data_flags",
                0,
            )),
            part_max_age: map_real(values, "part_max_age", 0.0),
            part_start_color: values
                .and_then(|m| m.get("part_start_color"))
                .and_then(|v| v.as_color4().ok())
                .unwrap_or_else(Color4::black),
            part_end_color: values
                .and_then(|m| m.get("part_end_color"))
                .and_then(|v| v.as_color4().ok())
                .unwrap_or_else(Color4::black),
            part_start_scale_x: start_scale.x,
            part_start_scale_y: start_scale.y,
            part_end_scale_x: end_scale.x,
            part_end_scale_y: end_scale.y,
            part_start_glow: map_real(values, "part_start_glow", 0.0),
            part_end_glow: map_real(values, "part_end_glow", 0.0),
            blend_func_source: map_i32(
                values,
                "blendfunc_source",
                PrimitiveParticleSystemBlendFunc::SourceAlpha as i32,
            ) as u8,
            blend_func_dest: map_i32(
                values,
                "blendfunc_dest",
                PrimitiveParticleSystemBlendFunc::OneMinusSourceAlpha as i32,
            ) as u8,
        })
    }

    pub fn equals_with_particle_system(&self, other: Self) -> bool {
        *self == other
    }
    pub fn equals_with_object(&self, obj: Option<Object>) -> bool {
        obj.as_ref()
            .and_then(Object::downcast_ref::<Self>)
            .is_some_and(|other| self == other)
    }
    pub fn get_hash_code(&self) -> i32 {
        hash_i32(&self.get_bytes().unwrap_or_default())
    }
}

fn unpack_color(pack: &mut BitPack) -> Result<Color4, Error> {
    Color4::new_with_byte_byte_byte_byte(
        pack.unpack_byte()?,
        pack.unpack_byte()?,
        pack.unpack_byte()?,
        pack.unpack_byte()?,
    )
}

pub struct Primitive {
    pub acceleration: Vector3,
    pub active_clients: i32,
    pub angular_velocity: Vector3,
    pub crc: u32,
    pub child_count: i32,
    pub click_action: ClickAction,
    pub collision_plane: Vector4,
    pub extended_mesh_flags: u32,
    pub face_media: Vec<MediaEntry>,
    pub flags: PrimFlags,
    pub flexible: Option<PrimitiveFlexibleData>,
    pub group_id: UUID,
    pub id: UUID,
    pub is_attachment: bool,
    pub joint: JointType,
    pub joint_axis_or_anchor: Vector3,
    pub joint_pivot: Vector3,
    pub light: Option<PrimitiveLightData>,
    pub light_map: Option<PrimitiveLightImage>,
    pub local_id: u32,
    pub media_url: String,
    pub media_version: String,
    pub name_values: Option<Vec<NameValue>>,
    pub owner_id: UUID,
    pub parent_id: u32,
    pub particle_sys: PrimitiveParticleSystem,
    pub physics_props: Option<PrimitivePhysicsProperties>,
    pub position: Vector3,
    pub prim_data: PrimitiveConstructionData,
    pub properties: Option<PrimitiveObjectProperties>,
    pub region_handle: u64,
    pub rotation: Quaternion,
    pub scale: Vector3,
    pub scratch_pad: Vec<u8>,
    pub sculpt: Option<PrimitiveSculptData>,
    pub signaled_animations: Option<Vec<Animation>>,
    pub sound: UUID,
    pub sound_flags: SoundFlags,
    pub sound_gain: f32,
    pub sound_radius: f32,
    pub tag: Option<Object>,
    pub text: String,
    pub text_color: Color4,
    pub texture_anim: PrimitiveTextureAnimation,
    pub textures: Option<PrimitiveTextureEntry>,
    pub tree_species: Tree,
    pub velocity: Vector3,
    pub(crate) unknown_extra_params: Vec<(u16, Vec<u8>)>,
}

impl std::fmt::Debug for Primitive {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        formatter
            .debug_struct("Primitive")
            .field("id", &self.id)
            .field("local_id", &self.local_id)
            .field("type", &self.type_())
            .finish_non_exhaustive()
    }
}

impl Clone for Primitive {
    fn clone(&self) -> Self {
        Self {
            acceleration: self.acceleration,
            active_clients: self.active_clients,
            angular_velocity: self.angular_velocity,
            crc: self.crc,
            child_count: self.child_count,
            click_action: self.click_action,
            collision_plane: self.collision_plane,
            extended_mesh_flags: self.extended_mesh_flags,
            face_media: self.face_media.clone(),
            flags: self.flags,
            flexible: self.flexible.clone(),
            group_id: self.group_id,
            id: self.id,
            is_attachment: self.is_attachment,
            joint: self.joint,
            joint_axis_or_anchor: self.joint_axis_or_anchor,
            joint_pivot: self.joint_pivot,
            light: self.light.clone(),
            light_map: self.light_map.clone(),
            local_id: self.local_id,
            media_url: self.media_url.clone(),
            media_version: self.media_version.clone(),
            name_values: self
                .name_values
                .as_ref()
                .map(|values| values.iter().map(clone_name_value).collect()),
            owner_id: self.owner_id,
            parent_id: self.parent_id,
            particle_sys: self.particle_sys.clone(),
            physics_props: self.physics_props.clone(),
            position: self.position,
            prim_data: self.prim_data.clone(),
            properties: self.properties.clone(),
            region_handle: self.region_handle,
            rotation: self.rotation,
            scale: self.scale,
            scratch_pad: self.scratch_pad.clone(),
            sculpt: self.sculpt.clone(),
            signaled_animations: self.signaled_animations.clone(),
            sound: self.sound,
            sound_flags: self.sound_flags,
            sound_gain: self.sound_gain,
            sound_radius: self.sound_radius,
            tag: self.tag.clone(),
            text: self.text.clone(),
            text_color: self.text_color,
            texture_anim: self.texture_anim,
            textures: self.textures.clone(),
            tree_species: self.tree_species,
            velocity: self.velocity,
            unknown_extra_params: self.unknown_extra_params.clone(),
        }
    }
}

fn clone_name_value(value: &NameValue) -> NameValue {
    NameValue {
        class_: value.class_,
        name: value.name.clone(),
        sendto: value.sendto,
        type_: value.type_,
        value: value.value.clone(),
    }
}

impl Primitive {
    pub fn new_with_constructor() -> Result<Self, Error> {
        Ok(crate::current_outfit::primitive_defaults())
    }
    pub fn new_with_primitive(prim: Self) -> Result<Self, Error> {
        Ok(prim.clone())
    }
    pub fn equals_with_primitive(&self, other: Option<Self>) -> bool {
        other.is_some_and(|value| self.id == value.id)
    }
    pub fn equals_with_object(&self, obj: Option<Object>) -> bool {
        obj.as_ref()
            .and_then(Object::downcast_ref::<Self>)
            .is_some_and(|other| self.id == other.id)
    }
    pub fn eq(lhs: Option<Self>, rhs: Option<Self>) -> bool {
        match (lhs, rhs) {
            (None, None) => true,
            (Some(left), Some(right)) => left.id == right.id,
            _ => false,
        }
    }
    pub fn ne(lhs: Option<Self>, rhs: Option<Self>) -> bool {
        !Self::eq(lhs, rhs)
    }

    pub fn get_children(
        &self,
        client: crate::GridClient,
    ) -> Result<Box<dyn Iterator<Item = Self>>, Error> {
        let Some(simulator) = client
            .native_network()?
            .find_simulator_with_u_int64(self.region_handle)?
        else {
            return Ok(Box::new(std::iter::empty()));
        };
        let children = simulator
            .objects_primitives
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .values()
            .filter(|primitive| primitive.parent_id == self.local_id)
            .cloned()
            .collect::<Vec<_>>();
        Ok(Box::new(children.into_iter()))
    }

    pub fn grass_species(&self) -> Grass {
        grass_from_u8(self.prim_data.state)
    }
    pub fn set_grass_species(&mut self, value: Grass) {
        self.prim_data.state = value as u8;
    }
    pub fn get_grass_definition(&self) -> Result<crate::GrassDefinition, Error> {
        crate::foliage_catalog::grass_by_id(self.prim_data.state)
    }
    pub fn get_tree_definition(&self) -> Result<crate::TreeDefinition, Error> {
        crate::foliage_catalog::TreeDefinitions::get(self.tree_species)
    }
    pub fn is_animated_object(&self) -> bool {
        self.extended_mesh_flags & 1 != 0
    }

    pub fn type_(&self) -> PrimType {
        if let Some(sculpt) = &self.sculpt
            && sculpt.type_() != SculptType::None
            && sculpt.sculpt_texture != UUID::zero()
        {
            return if sculpt.type_() == SculptType::Mesh {
                PrimType::Mesh
            } else {
                PrimType::Sculpt
            };
        }
        let profile = self.prim_data.profile_curve_with_property();
        if matches!(
            self.prim_data.path_curve,
            PathCurve::Line | PathCurve::Flexible
        ) {
            return match profile {
                ProfileCurve::Circle => PrimType::Cylinder,
                ProfileCurve::Square => PrimType::Box,
                ProfileCurve::IsoTriangle
                | ProfileCurve::EqualTriangle
                | ProfileCurve::RightTriangle => PrimType::Prism,
                ProfileCurve::HalfCircle => PrimType::Unknown,
            };
        }
        match self.prim_data.path_curve {
            PathCurve::Circle => match profile {
                ProfileCurve::Circle => {
                    if self.prim_data.path_scale_y > 0.75 {
                        PrimType::Sphere
                    } else {
                        PrimType::Torus
                    }
                }
                ProfileCurve::HalfCircle => PrimType::Sphere,
                ProfileCurve::EqualTriangle => PrimType::Ring,
                ProfileCurve::Square if self.prim_data.path_scale_y <= 0.75 => PrimType::Tube,
                _ => PrimType::Unknown,
            },
            PathCurve::Circle2 if profile == ProfileCurve::Circle => PrimType::Sphere,
            _ => PrimType::Unknown,
        }
    }

    pub fn get_osd(&self) -> Result<OSD, Error> {
        let path = OSD::Map(HashMap::from([
            (
                "begin".into(),
                OSD::Real(f64::from(self.prim_data.path_begin)),
            ),
            (
                "curve".into(),
                OSD::Integer(self.prim_data.path_curve as i32),
            ),
            ("end".into(), OSD::Real(f64::from(self.prim_data.path_end))),
            (
                "radius_offset".into(),
                OSD::Real(f64::from(self.prim_data.path_radius_offset)),
            ),
            (
                "revolutions".into(),
                OSD::Real(f64::from(self.prim_data.path_revolutions)),
            ),
            (
                "scale_x".into(),
                OSD::Real(f64::from(self.prim_data.path_scale_x)),
            ),
            (
                "scale_y".into(),
                OSD::Real(f64::from(self.prim_data.path_scale_y)),
            ),
            (
                "shear_x".into(),
                OSD::Real(f64::from(self.prim_data.path_shear_x)),
            ),
            (
                "shear_y".into(),
                OSD::Real(f64::from(self.prim_data.path_shear_y)),
            ),
            (
                "skew".into(),
                OSD::Real(f64::from(self.prim_data.path_skew)),
            ),
            (
                "taper_x".into(),
                OSD::Real(f64::from(self.prim_data.path_taper_x)),
            ),
            (
                "taper_y".into(),
                OSD::Real(f64::from(self.prim_data.path_taper_y)),
            ),
            (
                "twist".into(),
                OSD::Real(f64::from(self.prim_data.path_twist)),
            ),
            (
                "twist_begin".into(),
                OSD::Real(f64::from(self.prim_data.path_twist_begin)),
            ),
        ]));
        let profile = OSD::Map(HashMap::from([
            (
                "begin".into(),
                OSD::Real(f64::from(self.prim_data.profile_begin)),
            ),
            (
                "curve".into(),
                OSD::Integer(self.prim_data.profile_curve_with_property() as i32),
            ),
            (
                "hole".into(),
                OSD::Integer(i32::from(self.prim_data.profile_hole().0)),
            ),
            (
                "end".into(),
                OSD::Real(f64::from(self.prim_data.profile_end)),
            ),
            (
                "hollow".into(),
                OSD::Real(f64::from(self.prim_data.profile_hollow)),
            ),
        ]));
        let (name, description) = self.properties.as_ref().map_or_else(
            || ("Object".to_owned(), String::new()),
            |value| (value.name.clone(), value.description.clone()),
        );
        let mut values = HashMap::from([
            ("name".into(), OSD::String(name)),
            ("description".into(), OSD::String(description)),
            (
                "phantom".into(),
                OSD::Boolean(self.flags.0 & PrimFlags::PHANTOM.0 != 0),
            ),
            (
                "physical".into(),
                OSD::Boolean(self.flags.0 & PrimFlags::PHYSICS.0 != 0),
            ),
            ("position".into(), OSD::from_vector3(self.position)?),
            ("rotation".into(), OSD::from_quaternion(self.rotation)?),
            ("scale".into(), OSD::from_vector3(self.scale)?),
            ("pcode".into(), OSD::Integer(self.prim_data.p_code as i32)),
            (
                "material".into(),
                OSD::Integer(self.prim_data.material as i32),
            ),
            (
                "shadows".into(),
                OSD::Boolean(self.flags.0 & PrimFlags::CAST_SHADOWS.0 != 0),
            ),
            (
                "state".into(),
                OSD::Integer(i32::from(self.prim_data.state)),
            ),
            ("id".into(), OSD::UUID(self.id)),
            ("localid".into(), OSD::from_u_integer(self.local_id)?),
            ("parentid".into(), OSD::from_u_integer(self.parent_id)?),
            (
                "volume".into(),
                OSD::Map(HashMap::from([
                    ("path".into(), path),
                    ("profile".into(), profile),
                ])),
            ),
        ]);
        if let Some(value) = &self.textures {
            values.insert("textures".into(), value.get_osd()?);
        }
        if self.texture_anim.flags.0 & PrimitiveTextureAnimMode::ANIM_ON.0 != 0 {
            values.insert("texture_anim".into(), self.texture_anim.get_osd()?);
        }
        if let Some(value) = &self.light {
            values.insert("light".into(), value.get_osd()?);
        }
        if let Some(value) = &self.light_map {
            values.insert("light_image".into(), value.get_osd()?);
        }
        if let Some(value) = &self.flexible {
            values.insert("flex".into(), value.get_osd()?);
        }
        if let Some(value) = &self.sculpt {
            values.insert("sculpt".into(), value.get_osd()?);
        }
        Ok(OSD::Map(values))
    }

    pub fn from_osd(osd: OSD) -> Result<Self, Error> {
        let mut primitive = Self::new_with_constructor()?;
        let Some(values) = map(osd) else {
            return Ok(primitive);
        };
        let volume = values.get("volume").and_then(|value| {
            if let OSD::Map(map) = value {
                Some(map)
            } else {
                None
            }
        });
        let path = volume.and_then(|map| map.get("path")).and_then(|value| {
            if let OSD::Map(map) = value {
                Some(map)
            } else {
                None
            }
        });
        let profile = volume.and_then(|map| map.get("profile")).and_then(|value| {
            if let OSD::Map(map) = value {
                Some(map)
            } else {
                None
            }
        });
        let mut data = PrimitiveConstructionData::default();
        data.material =
            material_from_i32(map_i32(Some(&values), "material", Material::Stone as i32));
        data.p_code = pcode_from_i32(map_i32(Some(&values), "pcode", PCode::Prim as i32));
        data.state = map_i32(Some(&values), "state", 0) as u8;
        data.path_begin = map_real(path, "begin", 0.0);
        data.path_curve = path_curve_from_i32(map_i32(path, "curve", PathCurve::Line as i32));
        data.path_end = map_real(path, "end", 1.0);
        data.path_radius_offset = map_real(path, "radius_offset", 0.0);
        data.path_revolutions = map_real(path, "revolutions", 1.0);
        data.path_scale_x = map_real(path, "scale_x", 1.0);
        data.path_scale_y = map_real(path, "scale_y", 1.0);
        data.path_shear_x = map_real(path, "shear_x", 0.0);
        data.path_shear_y = map_real(path, "shear_y", 0.0);
        data.path_skew = map_real(path, "skew", 0.0);
        data.path_taper_x = map_real(path, "taper_x", 0.0);
        data.path_taper_y = map_real(path, "taper_y", 0.0);
        data.path_twist = map_real(path, "twist", 0.0);
        data.path_twist_begin = map_real(path, "twist_begin", 0.0);
        data.profile_begin = map_real(profile, "begin", 0.0);
        data.profile_end = map_real(profile, "end", 1.0);
        data.profile_hollow = map_real(profile, "hollow", 0.0);
        data.set_profile_curve_with_property(profile_curve_from_u8(map_i32(
            profile,
            "curve",
            ProfileCurve::Circle as i32,
        ) as u8));
        data.set_profile_hole(HoleType(map_i32(profile, "hole", 0) as u8));
        primitive.prim_data = data;
        primitive.flags = PrimFlags::NONE;
        if map_bool(Some(&values), "phantom", false) {
            primitive.flags.0 |= PrimFlags::PHANTOM.0;
        }
        if map_bool(Some(&values), "physical", false) {
            primitive.flags.0 |= PrimFlags::PHYSICS.0;
        }
        if map_bool(Some(&values), "shadows", false) {
            primitive.flags.0 |= PrimFlags::CAST_SHADOWS.0;
        }
        primitive.id = map_uuid(Some(&values), "id", UUID::zero());
        primitive.local_id = map_u32(Some(&values), "localid", 0);
        primitive.parent_id = map_u32(Some(&values), "parentid", 0);
        primitive.position = map_vector3(Some(&values), "position", Vector3::zero());
        primitive.scale = map_vector3(Some(&values), "scale", Vector3::one());
        primitive.rotation = values
            .get("rotation")
            .filter(|value| matches!(value, OSD::Array(items) if items.len() == 4))
            .and_then(|value| value.as_quaternion().ok())
            .unwrap_or_else(Quaternion::identity);
        if let Some(value) = values.get("flex") {
            primitive.flexible = Some(PrimitiveFlexibleData::from_osd(value.clone())?);
        }
        if let Some(value) = values.get("light") {
            primitive.light = Some(PrimitiveLightData::from_osd(value.clone())?);
        }
        if let Some(value) = values.get("light_image") {
            primitive.light_map = Some(PrimitiveLightImage::from_osd(value.clone())?);
        }
        if let Some(value) = values.get("sculpt") {
            primitive.sculpt = Some(PrimitiveSculptData::from_osd(value.clone())?);
        }
        if let Some(value) = values.get("textures") {
            primitive.textures = Some(PrimitiveTextureEntry::from_osd(value.clone())?);
        }
        if let Some(value) = values.get("texture_anim") {
            primitive.texture_anim = PrimitiveTextureAnimation::from_osd(value.clone())?;
        }
        let mut properties = PrimitiveObjectProperties::default();
        properties.name = values
            .get("name")
            .and_then(|v| v.as_string().ok())
            .unwrap_or_default();
        properties.description = values
            .get("description")
            .and_then(|v| v.as_string().ok())
            .unwrap_or_default();
        primitive.properties = Some(properties);
        Ok(primitive)
    }

    pub fn set_extra_params_from_bytes(&mut self, data: Vec<u8>, pos: i32) -> Result<i32, Error> {
        let start = usize::try_from(pos).map_err(|_| Error::IndexOutOfRange)?;
        let mut cursor = start;
        let count = usize::from(*data.get(cursor).ok_or(Error::IndexOutOfRange)?);
        cursor += 1;
        if count > MAX_EXTRA_PARAMS {
            return Err(Error::Argument);
        }
        self.unknown_extra_params.clear();
        for _ in 0..count {
            let header = data.get(cursor..cursor + 6).ok_or(Error::IndexOutOfRange)?;
            let type_ = u16::from_le_bytes([header[0], header[1]]);
            let length = usize::try_from(u32::from_le_bytes([
                header[2], header[3], header[4], header[5],
            ]))
            .map_err(|_| Error::Argument)?;
            cursor += 6;
            if length > MAX_EXTRA_PARAM_BYTES {
                return Err(Error::Argument);
            }
            let payload = data
                .get(cursor..cursor.checked_add(length).ok_or(Error::IndexOutOfRange)?)
                .ok_or(Error::IndexOutOfRange)?;
            match type_ {
                0x10 if length == 16 => {
                    self.flexible = Some(PrimitiveFlexibleData::new_with_bytes_int32(
                        payload.to_vec(),
                        0,
                    )?);
                }
                0x20 if length == 16 => {
                    self.light = Some(PrimitiveLightData::new_with_bytes_int32(
                        payload.to_vec(),
                        0,
                    )?);
                }
                0x40 if length == 28 => {
                    self.light_map = Some(PrimitiveLightImage::new_with_bytes_int32(
                        payload.to_vec(),
                        0,
                    )?);
                }
                0x30 | 0x60 if length == 17 => {
                    self.sculpt = Some(PrimitiveSculptData::new_with_bytes_int32(
                        payload.to_vec(),
                        0,
                    )?);
                }
                0x70 if length == 4 => {
                    self.extended_mesh_flags =
                        u32::from_le_bytes(payload.try_into().map_err(|_| Error::Argument)?);
                }
                _ => self.unknown_extra_params.push((type_, payload.to_vec())),
            }
            cursor += length;
        }
        i32::try_from(cursor - start).map_err(|_| Error::Argument)
    }

    pub fn get_extra_params_bytes(&self) -> Result<Vec<u8>, Error> {
        let mut params = Vec::<(u16, Vec<u8>)>::new();
        if let Some(value) = &self.flexible {
            params.push((0x10, value.get_bytes()?));
        }
        if let Some(value) = &self.light {
            params.push((0x20, value.get_bytes()?));
        }
        if let Some(value) = &self.light_map {
            params.push((0x40, value.get_bytes()?));
        }
        if let Some(value) = &self.sculpt {
            params.push((
                if value.type_() == SculptType::Mesh {
                    0x60
                } else {
                    0x30
                },
                value.get_bytes()?,
            ));
        }
        if self.extended_mesh_flags != 0 {
            params.push((0x70, self.extended_mesh_flags.to_le_bytes().to_vec()));
        }
        params.extend(self.unknown_extra_params.clone());
        if params.len() > MAX_EXTRA_PARAMS {
            return Err(Error::Argument);
        }
        let mut output = Vec::new();
        output.push(params.len() as u8);
        for (type_, payload) in params {
            if payload.len() > MAX_EXTRA_PARAM_BYTES {
                return Err(Error::Argument);
            }
            output.extend_from_slice(&type_.to_le_bytes());
            output.extend_from_slice(
                &u32::try_from(payload.len())
                    .map_err(|_| Error::Argument)?
                    .to_le_bytes(),
            );
            output.extend_from_slice(&payload);
        }
        Ok(output)
    }

    pub fn get_hash_code(&self) -> i32 {
        self.id.get_hash_code()
            ^ self.prim_data.get_hash_code()
            ^ self.local_id as i32
            ^ self.parent_id as i32
    }
    pub fn to_string(&self) -> String {
        if self.prim_data.p_code == PCode::Prim {
            format!("{:?} ({})", self.type_(), self.id)
        } else {
            format!("{:?} ({})", self.prim_data.p_code, self.id)
        }
    }
    pub fn pack_begin_cut(value: f32) -> Result<u16, Error> {
        Ok((value / 0.00002).round_ties_even() as u16)
    }
    pub fn pack_end_cut(value: f32) -> Result<u16, Error> {
        Ok(50_000_u16.wrapping_sub((value / 0.00002).round_ties_even() as u16))
    }
    pub fn pack_path_scale(value: f32) -> Result<u8, Error> {
        Ok(200_u8.wrapping_sub((value / 0.01).round_ties_even() as u8))
    }
    pub fn pack_path_shear(value: f32) -> Result<i8, Error> {
        Ok((value / 0.01).round_ties_even() as i8)
    }
    pub fn pack_path_twist(value: f32) -> Result<i8, Error> {
        Ok((value / 0.01).round_ties_even() as i8)
    }
    pub fn pack_path_taper(value: f32) -> Result<i8, Error> {
        Ok((value / 0.01).round_ties_even() as i8)
    }
    pub fn pack_path_revolutions(value: f32) -> Result<u8, Error> {
        Ok(((value - 1.0) / 0.015).round_ties_even() as u8)
    }
    pub fn pack_profile_hollow(value: f32) -> Result<u16, Error> {
        Ok((value / 0.00002).round_ties_even() as u16)
    }
    pub fn unpack_begin_cut(value: u16) -> Result<f32, Error> {
        Ok(f32::from(value) * 0.00002)
    }
    pub fn unpack_end_cut(value: u16) -> Result<f32, Error> {
        Ok(f32::from(50_000_u16.wrapping_sub(value)) * 0.00002)
    }
    pub fn unpack_path_scale(value: u8) -> Result<f32, Error> {
        Ok(f32::from(200_u8.wrapping_sub(value)) * 0.01)
    }
    pub fn unpack_path_shear(value: i8) -> Result<f32, Error> {
        Ok(f32::from(value) * 0.01)
    }
    pub fn unpack_path_twist(value: i8) -> Result<f32, Error> {
        Ok(f32::from(value) * 0.01)
    }
    pub fn unpack_path_taper(value: i8) -> Result<f32, Error> {
        Ok(f32::from(value) * 0.01)
    }
    pub fn unpack_path_revolutions(value: u8) -> Result<f32, Error> {
        Ok(f32::from(value).mul_add(0.015, 1.0))
    }
    pub fn unpack_profile_hollow(value: u16) -> Result<f32, Error> {
        Ok(f32::from(value) * 0.00002)
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct LegacyMaterial {
    id: UUID,
    normal_map: UUID,
    normal_map_offset_x: f64,
    normal_map_offset_y: f64,
    normal_map_repeat_x: f64,
    normal_map_repeat_y: f64,
    normal_map_rotation: f64,
    specular_map: UUID,
    specular_map_offset_x: f64,
    specular_map_offset_y: f64,
    specular_map_repeat_x: f64,
    specular_map_repeat_y: f64,
    specular_map_rotation: f64,
    specular_color: Color4,
    specular_exponent: u8,
    environment_intensity: u8,
    alpha_mask_cutoff: u8,
    diffuse_alpha_mode: crate::materials::LegacyMaterialAlphaMode,
}

impl Default for LegacyMaterial {
    fn default() -> Self {
        Self {
            id: UUID::zero(),
            normal_map: UUID::zero(),
            normal_map_offset_x: 0.0,
            normal_map_offset_y: 0.0,
            normal_map_repeat_x: 0.0,
            normal_map_repeat_y: 0.0,
            normal_map_rotation: 0.0,
            specular_map: UUID::zero(),
            specular_map_offset_x: 0.0,
            specular_map_offset_y: 0.0,
            specular_map_repeat_x: 0.0,
            specular_map_repeat_y: 0.0,
            specular_map_rotation: 0.0,
            specular_color: Color4::black(),
            specular_exponent: 0,
            environment_intensity: 0,
            alpha_mask_cutoff: 0,
            diffuse_alpha_mode: crate::materials::LegacyMaterialAlphaMode::None,
        }
    }
}

impl LegacyMaterial {
    const MULTIPLIER: f64 = 10_000.0;
    pub fn new_with_constructor() -> Result<Self, Error> {
        Ok(Self::default())
    }
    pub fn new_with_osd_map(map_orig: OSDMap) -> Result<Self, Error> {
        let values = map_orig.snapshot();
        let id_value = values.get("ID").ok_or(Error::InvalidOperation)?;
        let id = match id_value {
            OSD::UUID(value) => *value,
            OSD::Binary(_) | OSD::Array(_) => UUID::new_with_bytes_int32(id_value.as_binary()?, 0)?,
            _ => return Err(Error::InvalidOperation),
        };
        let Some(OSD::Map(material)) = values.get("Material") else {
            return Err(Error::InvalidOperation);
        };
        let scaled = |key: &str| -> f64 {
            material
                .get(key)
                .and_then(|value| value.as_integer().ok())
                .map_or(0.0, |value| f64::from(value) / Self::MULTIPLIER)
        };
        Ok(Self {
            id,
            normal_map: map_uuid(Some(material), "NormMap", UUID::zero()),
            normal_map_offset_x: scaled("NormOffsetX"),
            normal_map_offset_y: scaled("NormOffsetY"),
            normal_map_repeat_x: scaled("NormRepeatX"),
            normal_map_repeat_y: scaled("NormRepeatY"),
            normal_map_rotation: scaled("NormRotation"),
            specular_map: map_uuid(Some(material), "SpecMap", UUID::zero()),
            specular_map_offset_x: scaled("SpecOffsetX"),
            specular_map_offset_y: scaled("SpecOffsetY"),
            specular_map_repeat_x: scaled("SpecRepeatX"),
            specular_map_repeat_y: scaled("SpecRepeatY"),
            specular_map_rotation: scaled("SpecRotation"),
            specular_color: material
                .get("SpecColor")
                .and_then(|value| value.as_color4().ok())
                .unwrap_or_else(Color4::black),
            specular_exponent: map_i32(Some(material), "SpecExp", 0) as u8,
            environment_intensity: map_i32(Some(material), "EnvIntensity", 0) as u8,
            alpha_mask_cutoff: map_i32(Some(material), "AlphaMaskCutoff", 0) as u8,
            diffuse_alpha_mode: legacy_alpha_mode_from_i32(map_i32(
                Some(material),
                "DiffuseAlphaMode",
                0,
            )),
        })
    }
    pub fn id(&self) -> UUID {
        self.id
    }
    pub fn set_id(&mut self, value: UUID) {
        self.id = value;
    }
    pub fn normal_map(&self) -> UUID {
        self.normal_map
    }
    pub fn set_normal_map(&mut self, value: UUID) {
        self.normal_map = value;
    }
    pub fn normal_map_offset_x(&self) -> f64 {
        self.normal_map_offset_x
    }
    pub fn set_normal_map_offset_x(&mut self, value: f64) {
        self.normal_map_offset_x = value;
    }
    pub fn normal_map_offset_y(&self) -> f64 {
        self.normal_map_offset_y
    }
    pub fn set_normal_map_offset_y(&mut self, value: f64) {
        self.normal_map_offset_y = value;
    }
    pub fn normal_map_repeat_x(&self) -> f64 {
        self.normal_map_repeat_x
    }
    pub fn set_normal_map_repeat_x(&mut self, value: f64) {
        self.normal_map_repeat_x = value;
    }
    pub fn normal_map_repeat_y(&self) -> f64 {
        self.normal_map_repeat_y
    }
    pub fn set_normal_map_repeat_y(&mut self, value: f64) {
        self.normal_map_repeat_y = value;
    }
    pub fn normal_map_rotation(&self) -> f64 {
        self.normal_map_rotation
    }
    pub fn set_normal_map_rotation(&mut self, value: f64) {
        self.normal_map_rotation = value;
    }
    pub fn specular_map(&self) -> UUID {
        self.specular_map
    }
    pub fn set_specular_map(&mut self, value: UUID) {
        self.specular_map = value;
    }
    pub fn specular_map_offset_x(&self) -> f64 {
        self.specular_map_offset_x
    }
    pub fn set_specular_map_offset_x(&mut self, value: f64) {
        self.specular_map_offset_x = value;
    }
    pub fn specular_map_offset_y(&self) -> f64 {
        self.specular_map_offset_y
    }
    pub fn set_specular_map_offset_y(&mut self, value: f64) {
        self.specular_map_offset_y = value;
    }
    pub fn specular_map_repeat_x(&self) -> f64 {
        self.specular_map_repeat_x
    }
    pub fn set_specular_map_repeat_x(&mut self, value: f64) {
        self.specular_map_repeat_x = value;
    }
    pub fn specular_map_repeat_y(&self) -> f64 {
        self.specular_map_repeat_y
    }
    pub fn set_specular_map_repeat_y(&mut self, value: f64) {
        self.specular_map_repeat_y = value;
    }
    pub fn specular_map_rotation(&self) -> f64 {
        self.specular_map_rotation
    }
    pub fn set_specular_map_rotation(&mut self, value: f64) {
        self.specular_map_rotation = value;
    }
    pub fn specular_color(&self) -> Color4 {
        self.specular_color
    }
    pub fn set_specular_color(&mut self, value: Color4) {
        self.specular_color = value;
    }
    pub fn specular_exponent(&self) -> u8 {
        self.specular_exponent
    }
    pub fn set_specular_exponent(&mut self, value: u8) {
        self.specular_exponent = value;
    }
    pub fn environment_intensity(&self) -> u8 {
        self.environment_intensity
    }
    pub fn set_environment_intensity(&mut self, value: u8) {
        self.environment_intensity = value;
    }
    pub fn alpha_mask_cutoff(&self) -> u8 {
        self.alpha_mask_cutoff
    }
    pub fn set_alpha_mask_cutoff(&mut self, value: u8) {
        self.alpha_mask_cutoff = value;
    }
    pub fn diffuse_alpha_mode(&self) -> crate::materials::LegacyMaterialAlphaMode {
        self.diffuse_alpha_mode
    }
    pub fn set_diffuse_alpha_mode(&mut self, value: crate::materials::LegacyMaterialAlphaMode) {
        self.diffuse_alpha_mode = value;
    }
}

pub(crate) const fn profile_curve_from_u8(value: u8) -> ProfileCurve {
    match value {
        1 => ProfileCurve::Square,
        2 => ProfileCurve::IsoTriangle,
        3 => ProfileCurve::EqualTriangle,
        4 => ProfileCurve::RightTriangle,
        5 => ProfileCurve::HalfCircle,
        _ => ProfileCurve::Circle,
    }
}
pub(crate) const fn material_from_i32(value: i32) -> Material {
    match value {
        1 => Material::Metal,
        2 => Material::Glass,
        3 => Material::Wood,
        4 => Material::Flesh,
        5 => Material::Plastic,
        6 => Material::Rubber,
        7 => Material::Light,
        _ => Material::Stone,
    }
}
pub(crate) const fn pcode_from_i32(value: i32) -> PCode {
    match value {
        9 => PCode::Prim,
        47 => PCode::Avatar,
        95 => PCode::Grass,
        111 => PCode::NewTree,
        143 => PCode::ParticleSystem,
        255 => PCode::Tree,
        _ => PCode::None,
    }
}
pub(crate) const fn path_curve_from_i32(value: i32) -> PathCurve {
    match value {
        32 => PathCurve::Circle,
        48 => PathCurve::Circle2,
        64 => PathCurve::Test,
        128 => PathCurve::Flexible,
        _ => PathCurve::Line,
    }
}
const fn sculpt_type_from_u8(value: u8) -> SculptType {
    match value {
        1 => SculptType::Sphere,
        2 => SculptType::Torus,
        3 => SculptType::Plane,
        4 => SculptType::Cylinder,
        5 => SculptType::Mesh,
        _ => SculptType::None,
    }
}
const fn mapping_type_from_u8(value: u8) -> MappingType {
    match value {
        2 => MappingType::Planar,
        4 => MappingType::Spherical,
        6 => MappingType::Cylindrical,
        _ => MappingType::Default,
    }
}
const fn shininess_from_u8(value: u8) -> Shininess {
    match value {
        0x40 => Shininess::Low,
        0x80 => Shininess::Medium,
        0xc0 => Shininess::High,
        _ => Shininess::None,
    }
}
const fn bumpiness_from_u8(value: u8) -> Bumpiness {
    match value {
        1 => Bumpiness::Brightness,
        2 => Bumpiness::Darkness,
        3 => Bumpiness::Woodgrain,
        4 => Bumpiness::Bark,
        5 => Bumpiness::Bricks,
        6 => Bumpiness::Checker,
        7 => Bumpiness::Concrete,
        8 => Bumpiness::Crustytile,
        9 => Bumpiness::Cutstone,
        10 => Bumpiness::Discs,
        11 => Bumpiness::Gravel,
        12 => Bumpiness::Petridish,
        13 => Bumpiness::Siding,
        14 => Bumpiness::Stonetile,
        15 => Bumpiness::Stucco,
        16 => Bumpiness::Suction,
        17 => Bumpiness::Weave,
        _ => Bumpiness::None,
    }
}
const fn grass_from_u8(value: u8) -> Grass {
    match value {
        1 => Grass::Grass1,
        2 => Grass::Grass2,
        3 => Grass::Grass3,
        4 => Grass::Grass4,
        5 => Grass::Undergrowth1,
        _ => Grass::Grass0,
    }
}
const fn legacy_alpha_mode_from_i32(value: i32) -> crate::materials::LegacyMaterialAlphaMode {
    match value {
        1 => crate::materials::LegacyMaterialAlphaMode::Blend,
        2 => crate::materials::LegacyMaterialAlphaMode::Mask,
        3 => crate::materials::LegacyMaterialAlphaMode::Emissive,
        4 => crate::materials::LegacyMaterialAlphaMode::Default,
        _ => crate::materials::LegacyMaterialAlphaMode::None,
    }
}
const fn attachment_point_from_u8(value: u8) -> AttachmentPoint {
    match value {
        1 => AttachmentPoint::Chest,
        2 => AttachmentPoint::Skull,
        3 => AttachmentPoint::LeftShoulder,
        4 => AttachmentPoint::RightShoulder,
        5 => AttachmentPoint::LeftHand,
        6 => AttachmentPoint::RightHand,
        7 => AttachmentPoint::LeftFoot,
        8 => AttachmentPoint::RightFoot,
        9 => AttachmentPoint::Spine,
        10 => AttachmentPoint::Pelvis,
        11 => AttachmentPoint::Mouth,
        12 => AttachmentPoint::Chin,
        13 => AttachmentPoint::LeftEar,
        14 => AttachmentPoint::RightEar,
        15 => AttachmentPoint::LeftEyeball,
        16 => AttachmentPoint::RightEyeball,
        17 => AttachmentPoint::Nose,
        18 => AttachmentPoint::RightUpperArm,
        19 => AttachmentPoint::RightForearm,
        20 => AttachmentPoint::LeftUpperArm,
        21 => AttachmentPoint::LeftForearm,
        22 => AttachmentPoint::RightHip,
        23 => AttachmentPoint::RightUpperLeg,
        24 => AttachmentPoint::RightLowerLeg,
        25 => AttachmentPoint::LeftHip,
        26 => AttachmentPoint::LeftUpperLeg,
        27 => AttachmentPoint::LeftLowerLeg,
        28 => AttachmentPoint::Stomach,
        29 => AttachmentPoint::LeftPec,
        30 => AttachmentPoint::RightPec,
        31 => AttachmentPoint::HUDCenter2,
        32 => AttachmentPoint::HUDTopRight,
        33 => AttachmentPoint::HUDTop,
        34 => AttachmentPoint::HUDTopLeft,
        35 => AttachmentPoint::HUDCenter,
        36 => AttachmentPoint::HUDBottomLeft,
        37 => AttachmentPoint::HUDBottom,
        38 => AttachmentPoint::HUDBottomRight,
        39 => AttachmentPoint::Neck,
        40 => AttachmentPoint::Root,
        41 => AttachmentPoint::LeftHandRing,
        42 => AttachmentPoint::RightHandRing,
        43 => AttachmentPoint::TailBase,
        44 => AttachmentPoint::TailTip,
        45 => AttachmentPoint::LeftWing,
        46 => AttachmentPoint::RightWing,
        47 => AttachmentPoint::Jaw,
        48 => AttachmentPoint::AltLeftEar,
        49 => AttachmentPoint::AltRightEar,
        50 => AttachmentPoint::AltLeftEye,
        51 => AttachmentPoint::AltRightEye,
        52 => AttachmentPoint::Tongue,
        53 => AttachmentPoint::Groin,
        54 => AttachmentPoint::LeftHindFoot,
        55 => AttachmentPoint::RightHindFoot,
        _ => AttachmentPoint::Default,
    }
}

#[cfg(test)]
#[allow(clippy::float_cmp)] // Texture fixtures use exactly representable values.
mod tests {
    use super::*;

    #[test]
    fn texture_faces_inherit_live_defaults_and_clone_independently() {
        let mut entry = PrimitiveTextureEntry::new_with_uuid(UUID::zero()).unwrap();
        entry.create_face(7).unwrap();
        entry.default_texture.as_mut().unwrap().set_repeat_u(2.5);
        assert_eq!(entry.face_textures[7].as_ref().unwrap().repeat_u(), 2.5);

        entry.face_textures[7].as_mut().unwrap().set_repeat_u(4.0);
        entry.default_texture.as_mut().unwrap().set_repeat_u(3.0);
        assert_eq!(entry.face_textures[7].as_ref().unwrap().repeat_u(), 4.0);

        let copy = PrimitiveTextureEntry::new_with_texture_entry(entry.clone()).unwrap();
        entry.default_texture.as_mut().unwrap().set_repeat_v(8.0);
        assert_eq!(copy.default_texture.as_ref().unwrap().repeat_v(), 1.0);
    }

    #[test]
    fn texture_entry_rejects_unterminated_face_bitfields() {
        let mut malformed = vec![0_u8; 16];
        malformed.extend_from_slice(&[0x80; 10]);
        assert!(matches!(
            PrimitiveTextureEntry::new_with_bytes_int32_int32(
                malformed.clone(),
                0,
                malformed.len() as i32
            ),
            Err(Error::Argument)
        ));
    }

    #[test]
    fn particle_system_legacy_and_extended_bytes_are_stable() {
        let mut legacy = PrimitiveParticleSystem::default();
        legacy.crc = 0x1234_5678;
        legacy.part_flags = 0x8000_0001;
        legacy.pattern = PrimitiveParticleSystemSourcePattern::ANGLE_CONE;
        legacy.max_age = 12.5;
        legacy.burst_part_count = 17;
        legacy.part_data_flags = PrimitiveParticleSystemParticleDataFlags(
            PrimitiveParticleSystemParticleDataFlags::INTERP_COLOR.0 | 0x8000_0000,
        );
        legacy.part_start_color = Color4::white();
        legacy.part_end_color = Color4::black();
        legacy.part_start_scale_x = 1.0;
        legacy.part_start_scale_y = 2.0;
        let bytes = legacy.get_bytes().unwrap();
        assert_eq!(
            bytes.len(),
            usize::from(PrimitiveParticleSystem::LEGACY_DATA_BLOCK_SIZE)
        );
        let decoded = PrimitiveParticleSystem::new(bytes.clone(), 0).unwrap();
        assert_eq!(decoded.get_bytes().unwrap(), bytes);
        assert_ne!(decoded.part_data_flags.0 & 0x8000_0000, 0);

        let mut extended = legacy;
        extended.part_start_glow = 0.5;
        extended.part_end_glow = 1.0;
        extended.blend_func_source = PrimitiveParticleSystemBlendFunc::One as u8;
        extended.blend_func_dest = PrimitiveParticleSystemBlendFunc::Zero as u8;
        let bytes = extended.get_bytes().unwrap();
        assert_eq!(
            bytes.len(),
            usize::from(PrimitiveParticleSystem::MAX_DATA_BLOCK_SIZE)
        );
        let decoded = PrimitiveParticleSystem::new(bytes.clone(), 0).unwrap();
        assert_eq!(decoded.get_bytes().unwrap(), bytes);
        assert!(decoded.has_glow().unwrap());
        assert!(decoded.has_blend_func().unwrap());
        assert_ne!(
            decoded.part_data_flags.0 & PrimitiveParticleSystemParticleDataFlags::DATA_GLOW.0,
            0
        );
        assert_ne!(
            decoded.part_data_flags.0 & PrimitiveParticleSystemParticleDataFlags::DATA_BLEND.0,
            0
        );
    }

    #[test]
    fn particle_and_extra_parameter_decoders_are_bounded() {
        assert!(matches!(
            PrimitiveParticleSystem::new(
                vec![0; usize::from(PrimitiveParticleSystem::MAX_DATA_BLOCK_SIZE) + 1],
                0
            ),
            Err(Error::Argument)
        ));
        assert!(PrimitiveParticleSystem::new(vec![0; 85], 0).is_err());

        let unknown = vec![1, 0x99, 0x99, 3, 0, 0, 0, 1, 2, 3];
        let mut primitive = Primitive::new_with_constructor().unwrap();
        assert_eq!(
            primitive
                .set_extra_params_from_bytes(unknown.clone(), 0)
                .unwrap(),
            unknown.len() as i32
        );
        assert_eq!(primitive.get_extra_params_bytes().unwrap(), unknown);

        let oversized = (MAX_EXTRA_PARAM_BYTES as u32 + 1).to_le_bytes();
        let malformed = vec![
            1,
            0x99,
            0x99,
            oversized[0],
            oversized[1],
            oversized[2],
            oversized[3],
        ];
        assert!(matches!(
            primitive.set_extra_params_from_bytes(malformed, 0),
            Err(Error::Argument)
        ));
    }

    #[test]
    fn auxiliary_primitive_blobs_round_trip_without_losing_type_bits() {
        let mut flexible = PrimitiveFlexibleData::default();
        flexible.softness = 3;
        flexible.tension = 1.5;
        flexible.drag = 0.7;
        flexible.gravity = -4.0;
        flexible.wind = 0.2;
        flexible.force = Vector3 {
            x: 1.0,
            y: -2.0,
            z: 3.0,
        };
        let flexible_bytes = flexible.get_bytes().unwrap();
        assert_eq!(
            PrimitiveFlexibleData::new_with_bytes_int32(flexible_bytes.clone(), 0)
                .unwrap()
                .get_bytes()
                .unwrap(),
            flexible_bytes
        );

        let mut light = PrimitiveLightData::default();
        light.color = Color4 {
            r: 0.2,
            g: 0.4,
            b: 0.6,
            a: 1.0,
        };
        light.intensity = 0.8;
        light.radius = 12.0;
        light.cutoff = 0.5;
        light.falloff = 1.25;
        let light_bytes = light.get_bytes().unwrap();
        assert_eq!(
            PrimitiveLightData::new_with_bytes_int32(light_bytes.clone(), 0)
                .unwrap()
                .get_bytes()
                .unwrap(),
            light_bytes
        );

        let sculpt_bytes = {
            let mut bytes = UUID::zero().get_bytes().unwrap();
            bytes
                .push(SculptType::Mesh as u8 | SculptType::Invert as u8 | SculptType::Mirror as u8);
            bytes
        };
        let sculpt = PrimitiveSculptData::new_with_bytes_int32(sculpt_bytes.clone(), 0).unwrap();
        assert_eq!(sculpt.type_(), SculptType::Mesh);
        assert!(sculpt.invert());
        assert!(sculpt.mirror());
        assert_eq!(sculpt.get_bytes().unwrap(), sculpt_bytes);
    }

    #[test]
    fn legacy_material_and_media_osd_preserve_wire_values() {
        let id = UUID::random().unwrap();
        let normal = UUID::random().unwrap();
        let material = HashMap::from([
            ("NormMap".into(), OSD::UUID(normal)),
            ("NormOffsetX".into(), OSD::Integer(1250)),
            ("SpecExp".into(), OSD::Integer(42)),
            ("DiffuseAlphaMode".into(), OSD::Integer(2)),
        ]);
        let legacy = LegacyMaterial::new_with_osd_map(
            OSDMap::new_with_dictionary(HashMap::from([
                ("ID".into(), OSD::UUID(id)),
                ("Material".into(), OSD::Map(material)),
            ]))
            .unwrap(),
        )
        .unwrap();
        assert_eq!(legacy.id(), id);
        assert_eq!(legacy.normal_map(), normal);
        assert_eq!(legacy.normal_map_offset_x(), 0.125);
        assert_eq!(legacy.specular_exponent(), 42);
        assert_eq!(
            legacy.diffuse_alpha_mode(),
            crate::materials::LegacyMaterialAlphaMode::Mask
        );

        let mut media = MediaEntry::new().unwrap();
        media.auto_loop = true;
        media.current_url = "https://example.invalid/current".into();
        media.home_url = "https://example.invalid/home".into();
        media.white_list = vec!["https://example.invalid/*".into()];
        media.enable_white_list = true;
        media.width = 1024;
        media.height = 768;
        let decoded = MediaEntry::from_osd(OSD::Map(media.get_osd().unwrap().snapshot())).unwrap();
        assert_eq!(decoded, media);
        assert!(MediaEntry::from_osd(OSD::Undefined).unwrap().is_undefined());
    }
}
