//! Native linkset object model and lossless LLSD conversion.

#![allow(clippy::missing_errors_doc)] // Public signatures mirror the compatibility map.
#![allow(clippy::too_many_lines)] // Wire fields are intentionally explicit.

use std::collections::HashMap;
use std::time::{SystemTime, UNIX_EPOCH};

use libremetaverse_structured_data::{OSD, OSDMap};
use libremetaverse_types::{
    AssetType, ClickAction, Color4, InventoryType, PrimFlags, Quaternion, SaleType, SoundFlags,
    UUID, Vector2, Vector3,
};

use crate::{
    Error, InventoryItem, Permissions, Primitive, PrimitiveFlexibleData, PrimitiveLightData,
    PrimitiveObjectProperties, PrimitiveParticleSystem, PrimitiveParticleSystemParticleDataFlags,
    PrimitiveParticleSystemSourcePattern, PrimitiveSculptData, PrimitiveTextureEntry,
};

fn map(entries: impl IntoIterator<Item = (&'static str, OSD)>) -> Result<OSDMap, Error> {
    OSDMap::new_with_dictionary(
        entries
            .into_iter()
            .map(|(name, value)| (name.to_owned(), value))
            .collect(),
    )
}

fn required<'a>(values: &'a HashMap<String, OSD>, name: &str) -> Result<&'a OSD, Error> {
    values.get(name).ok_or(Error::Argument)
}

fn real(values: &HashMap<String, OSD>, name: &str) -> Result<f32, Error> {
    let value = required(values, name)?.as_real()? as f32;
    value.is_finite().then_some(value).ok_or(Error::Argument)
}

pub(crate) fn asset_type(value: i32) -> Result<AssetType, Error> {
    Ok(match value {
        -1 => AssetType::Unknown,
        0 => AssetType::Texture,
        1 => AssetType::Sound,
        2 => AssetType::CallingCard,
        3 => AssetType::Landmark,
        4 => AssetType::Script,
        5 => AssetType::Clothing,
        6 => AssetType::Object,
        7 => AssetType::Notecard,
        8 => AssetType::Folder,
        10 => AssetType::LSLText,
        11 => AssetType::LSLBytecode,
        12 => AssetType::TextureTGA,
        13 => AssetType::Bodypart,
        17 => AssetType::SoundWAV,
        18 => AssetType::ImageTGA,
        19 => AssetType::ImageJPEG,
        20 => AssetType::Animation,
        21 => AssetType::Gesture,
        22 => AssetType::Simstate,
        24 => AssetType::Link,
        25 => AssetType::LinkFolder,
        40 => AssetType::Widget,
        45 => AssetType::Person,
        49 => AssetType::Mesh,
        56 => AssetType::Settings,
        57 => AssetType::Material,
        _ => return Err(Error::Argument),
    })
}

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

impl PrimObjectFlexibleBlock {
    pub fn new() -> Result<Self, Error> {
        Ok(Self {
            drag: 0.0,
            force: Vector3::zero(),
            gravity: 0.0,
            softness: 0,
            tension: 0.0,
            wind: 0.0,
        })
    }

    pub fn serialize(&self) -> Result<OSDMap, Error> {
        map([
            ("softness", OSD::Integer(self.softness)),
            ("gravity", OSD::Real(f64::from(self.gravity))),
            ("drag", OSD::Real(f64::from(self.drag))),
            ("wind", OSD::Real(f64::from(self.wind))),
            ("tension", OSD::Real(f64::from(self.tension))),
            ("force", OSD::from_vector3(self.force)?),
        ])
    }

    pub fn deserialize(&mut self, map: OSDMap) -> Result<(), Error> {
        let values = map.snapshot();
        self.softness = required(&values, "softness")?.as_integer()?;
        self.gravity = real(&values, "gravity")?;
        self.drag = real(&values, "drag")?;
        self.wind = real(&values, "wind")?;
        self.tension = real(&values, "tension")?;
        self.force = required(&values, "force")?.as_vector3()?;
        Ok(())
    }
}

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

impl PrimObjectLightBlock {
    pub fn new() -> Result<Self, Error> {
        Ok(Self {
            color: Color4::default(),
            cutoff: 0.0,
            falloff: 0.0,
            intensity: 0.0,
            radius: 0.0,
        })
    }

    pub fn serialize(&self) -> Result<OSDMap, Error> {
        map([
            ("color", OSD::from_color4(self.color)?),
            ("intensity", OSD::Real(f64::from(self.intensity))),
            ("radius", OSD::Real(f64::from(self.radius))),
            ("falloff", OSD::Real(f64::from(self.falloff))),
            ("cutoff", OSD::Real(f64::from(self.cutoff))),
        ])
    }

    pub fn deserialize(&mut self, map: OSDMap) -> Result<(), Error> {
        let values = map.snapshot();
        self.color = required(&values, "color")?.as_color4()?;
        self.intensity = real(&values, "intensity")?;
        self.radius = real(&values, "radius")?;
        self.falloff = real(&values, "falloff")?;
        self.cutoff = real(&values, "cutoff")?;
        Ok(())
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct PrimObjectSculptBlock {
    pub texture: UUID,
    pub type_: i32,
}

impl PrimObjectSculptBlock {
    pub fn new() -> Result<Self, Error> {
        Ok(Self {
            texture: UUID::zero(),
            type_: 0,
        })
    }

    pub fn serialize(&self) -> Result<OSDMap, Error> {
        map([
            ("texture", OSD::UUID(self.texture)),
            ("type", OSD::Integer(self.type_)),
        ])
    }

    pub fn deserialize(&mut self, map: OSDMap) -> Result<(), Error> {
        let values = map.snapshot();
        self.texture = required(&values, "texture")?.as_uuid()?;
        self.type_ = required(&values, "type")?.as_integer()?;
        Ok(())
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct PrimObjectParticlesBlock {
    pub acceleration: Vector3,
    pub angular_velocity: Vector3,
    pub burst_particle_count: i32,
    pub burst_radius: f32,
    pub burst_rate: f32,
    pub burst_speed_max: f32,
    pub burst_speed_min: f32,
    pub data_flags: i32,
    pub flags: i32,
    pub inner_angle: f32,
    pub max_age: f32,
    pub outer_angle: f32,
    pub particle_end_color: Color4,
    pub particle_end_scale: Vector2,
    pub particle_max_age: f32,
    pub particle_start_color: Color4,
    pub particle_start_scale: Vector2,
    pub pattern: i32,
    pub start_age: f32,
    pub target_id: UUID,
    pub texture_id: UUID,
}

impl PrimObjectParticlesBlock {
    pub fn new() -> Result<Self, Error> {
        Ok(Self {
            acceleration: Vector3::zero(),
            angular_velocity: Vector3::zero(),
            burst_particle_count: 0,
            burst_radius: 0.0,
            burst_rate: 0.0,
            burst_speed_max: 0.0,
            burst_speed_min: 0.0,
            data_flags: 0,
            flags: 0,
            inner_angle: 0.0,
            max_age: 0.0,
            outer_angle: 0.0,
            particle_end_color: Color4::default(),
            particle_end_scale: Vector2::zero(),
            particle_max_age: 0.0,
            particle_start_color: Color4::default(),
            particle_start_scale: Vector2::zero(),
            pattern: 0,
            start_age: 0.0,
            target_id: UUID::zero(),
            texture_id: UUID::zero(),
        })
    }

    pub fn serialize(&self) -> Result<OSDMap, Error> {
        map([
            ("flags", OSD::Integer(self.flags)),
            ("pattern", OSD::Integer(self.pattern)),
            ("max_age", OSD::Real(f64::from(self.max_age))),
            ("start_age", OSD::Real(f64::from(self.start_age))),
            ("inner_angle", OSD::Real(f64::from(self.inner_angle))),
            ("outer_angle", OSD::Real(f64::from(self.outer_angle))),
            ("burst_rate", OSD::Real(f64::from(self.burst_rate))),
            ("burst_radius", OSD::Real(f64::from(self.burst_radius))),
            (
                "burst_speed_min",
                OSD::Real(f64::from(self.burst_speed_min)),
            ),
            (
                "burst_speed_max",
                OSD::Real(f64::from(self.burst_speed_max)),
            ),
            (
                "burst_particle_count",
                OSD::Integer(self.burst_particle_count),
            ),
            (
                "angular_velocity",
                OSD::from_vector3(self.angular_velocity)?,
            ),
            ("acceleration", OSD::from_vector3(self.acceleration)?),
            ("texture_id", OSD::UUID(self.texture_id)),
            ("target_id", OSD::UUID(self.target_id)),
            ("data_flags", OSD::Integer(self.data_flags)),
            (
                "particle_max_age",
                OSD::Real(f64::from(self.particle_max_age)),
            ),
            (
                "particle_start_color",
                OSD::from_color4(self.particle_start_color)?,
            ),
            (
                "particle_end_color",
                OSD::from_color4(self.particle_end_color)?,
            ),
            (
                "particle_start_scale",
                OSD::from_vector2(self.particle_start_scale)?,
            ),
            (
                "particle_end_scale",
                OSD::from_vector2(self.particle_end_scale)?,
            ),
        ])
    }

    pub fn deserialize(&mut self, map: OSDMap) -> Result<(), Error> {
        let values = map.snapshot();
        self.flags = required(&values, "flags")?.as_integer()?;
        self.pattern = required(&values, "pattern")?.as_integer()?;
        self.max_age = real(&values, "max_age")?;
        self.start_age = real(&values, "start_age")?;
        self.inner_angle = real(&values, "inner_angle")?;
        self.outer_angle = real(&values, "outer_angle")?;
        self.burst_rate = real(&values, "burst_rate")?;
        self.burst_radius = real(&values, "burst_radius")?;
        self.burst_speed_min = real(&values, "burst_speed_min")?;
        self.burst_speed_max = real(&values, "burst_speed_max")?;
        self.burst_particle_count = required(&values, "burst_particle_count")?.as_integer()?;
        self.angular_velocity = required(&values, "angular_velocity")?.as_vector3()?;
        self.acceleration = required(&values, "acceleration")?.as_vector3()?;
        self.texture_id = required(&values, "texture_id")?.as_uuid()?;
        self.target_id = required(&values, "target_id")?.as_uuid()?;
        self.data_flags = required(&values, "data_flags")?.as_integer()?;
        self.particle_max_age = real(&values, "particle_max_age")?;
        self.particle_start_color = required(&values, "particle_start_color")?.as_color4()?;
        self.particle_end_color = required(&values, "particle_end_color")?.as_color4()?;
        self.particle_start_scale = required(&values, "particle_start_scale")?.as_vector2()?;
        self.particle_end_scale = required(&values, "particle_end_scale")?.as_vector2()?;
        Ok(())
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct PrimObjectShapeBlock {
    pub path_begin: f32,
    pub path_curve: i32,
    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_curve: i32,
    pub profile_end: f32,
    pub profile_hollow: f32,
}

impl PrimObjectShapeBlock {
    pub fn new() -> Result<Self, Error> {
        Ok(Self {
            path_begin: 0.0,
            path_curve: 0,
            path_end: 0.0,
            path_radius_offset: 0.0,
            path_revolutions: 0.0,
            path_scale_x: 0.0,
            path_scale_y: 0.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_curve: 0,
            profile_end: 0.0,
            profile_hollow: 0.0,
        })
    }

    pub fn serialize(&self) -> Result<OSDMap, Error> {
        map([
            ("path_curve", OSD::Integer(self.path_curve)),
            ("path_begin", OSD::Real(f64::from(self.path_begin))),
            ("path_end", OSD::Real(f64::from(self.path_end))),
            ("path_scale_x", OSD::Real(f64::from(self.path_scale_x))),
            ("path_scale_y", OSD::Real(f64::from(self.path_scale_y))),
            ("path_shear_x", OSD::Real(f64::from(self.path_shear_x))),
            ("path_shear_y", OSD::Real(f64::from(self.path_shear_y))),
            ("path_twist", OSD::Real(f64::from(self.path_twist))),
            (
                "path_twist_begin",
                OSD::Real(f64::from(self.path_twist_begin)),
            ),
            (
                "path_radius_offset",
                OSD::Real(f64::from(self.path_radius_offset)),
            ),
            ("path_taper_x", OSD::Real(f64::from(self.path_taper_x))),
            ("path_taper_y", OSD::Real(f64::from(self.path_taper_y))),
            (
                "path_revolutions",
                OSD::Real(f64::from(self.path_revolutions)),
            ),
            ("path_skew", OSD::Real(f64::from(self.path_skew))),
            ("profile_curve", OSD::Integer(self.profile_curve)),
            ("profile_begin", OSD::Real(f64::from(self.profile_begin))),
            ("profile_end", OSD::Real(f64::from(self.profile_end))),
            ("profile_hollow", OSD::Real(f64::from(self.profile_hollow))),
        ])
    }

    pub fn deserialize(&mut self, map: OSDMap) -> Result<(), Error> {
        let values = map.snapshot();
        self.path_curve = required(&values, "path_curve")?.as_integer()?;
        self.path_begin = real(&values, "path_begin")?;
        self.path_end = real(&values, "path_end")?;
        self.path_scale_x = real(&values, "path_scale_x")?;
        self.path_scale_y = real(&values, "path_scale_y")?;
        self.path_shear_x = real(&values, "path_shear_x")?;
        self.path_shear_y = real(&values, "path_shear_y")?;
        self.path_twist = real(&values, "path_twist")?;
        self.path_twist_begin = real(&values, "path_twist_begin")?;
        self.path_radius_offset = real(&values, "path_radius_offset")?;
        self.path_taper_x = real(&values, "path_taper_x")?;
        self.path_taper_y = real(&values, "path_taper_y")?;
        self.path_revolutions = real(&values, "path_revolutions")?;
        self.path_skew = real(&values, "path_skew")?;
        self.profile_curve = required(&values, "profile_curve")?.as_integer()?;
        self.profile_begin = real(&values, "profile_begin")?;
        self.profile_end = real(&values, "profile_end")?;
        self.profile_hollow = real(&values, "profile_hollow")?;
        Ok(())
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct PrimObjectInventoryBlockItemBlock {
    pub asset_id: UUID,
    pub creation_date: SystemTime,
    pub creator_id: UUID,
    pub description: String,
    pub flags: i32,
    pub group_id: UUID,
    pub id: UUID,
    pub inv_type: InventoryType,
    pub last_owner_id: UUID,
    pub name: String,
    pub owner_id: UUID,
    pub perms_base: u32,
    pub perms_everyone: u32,
    pub perms_granter_id: UUID,
    pub perms_group: u32,
    pub perms_next_owner: u32,
    pub perms_owner: u32,
    pub type_: AssetType,
}

impl PrimObjectInventoryBlockItemBlock {
    pub fn new() -> Result<Self, Error> {
        Ok(Self {
            asset_id: UUID::zero(),
            creation_date: UNIX_EPOCH,
            creator_id: UUID::zero(),
            description: String::new(),
            flags: 0,
            group_id: UUID::zero(),
            id: UUID::zero(),
            inv_type: InventoryType::UNKNOWN,
            last_owner_id: UUID::zero(),
            name: String::new(),
            owner_id: UUID::zero(),
            perms_base: 0,
            perms_everyone: 0,
            perms_granter_id: UUID::zero(),
            perms_group: 0,
            perms_next_owner: 0,
            perms_owner: 0,
            type_: AssetType::Unknown,
        })
    }

    pub fn serialize(&self) -> Result<OSDMap, Error> {
        map([
            ("id", OSD::UUID(self.id)),
            ("name", OSD::String(self.name.clone())),
            ("owner_id", OSD::UUID(self.owner_id)),
            ("creator_id", OSD::UUID(self.creator_id)),
            ("group_id", OSD::UUID(self.group_id)),
            ("last_owner_id", OSD::UUID(self.last_owner_id)),
            ("perms_granter_id", OSD::UUID(self.perms_granter_id)),
            ("asset_id", OSD::UUID(self.asset_id)),
            ("asset_type", OSD::Integer(i32::from(self.type_ as i8))),
            ("inv_type", OSD::Integer(i32::from(self.inv_type.raw()))),
            ("description", OSD::String(self.description.clone())),
            ("perms_base", OSD::Integer(self.perms_base.cast_signed())),
            ("perms_owner", OSD::Integer(self.perms_owner.cast_signed())),
            ("perms_group", OSD::Integer(self.perms_group.cast_signed())),
            (
                "perms_everyone",
                OSD::Integer(self.perms_everyone.cast_signed()),
            ),
            (
                "perms_next_owner",
                OSD::Integer(self.perms_next_owner.cast_signed()),
            ),
            ("flags", OSD::Integer(self.flags)),
            ("creation_date", OSD::Date(self.creation_date)),
        ])
    }

    pub fn deserialize(&mut self, map: OSDMap) -> Result<(), Error> {
        let values = map.snapshot();
        self.id = required(&values, "id")?.as_uuid()?;
        self.name = required(&values, "name")?.as_string()?;
        self.owner_id = required(&values, "owner_id")?.as_uuid()?;
        self.creator_id = required(&values, "creator_id")?.as_uuid()?;
        self.group_id = required(&values, "group_id")?.as_uuid()?;
        self.last_owner_id = required(&values, "last_owner_id")?.as_uuid()?;
        self.perms_granter_id = required(&values, "perms_granter_id")?.as_uuid()?;
        self.asset_id = required(&values, "asset_id")?.as_uuid()?;
        self.type_ = asset_type(required(&values, "asset_type")?.as_integer()?)?;
        let inv_type = required(&values, "inv_type")?.as_integer()?;
        self.inv_type =
            InventoryType::from_raw(i8::try_from(inv_type).map_err(|_| Error::Argument)?);
        self.description = required(&values, "description")?.as_string()?;
        self.perms_base = required(&values, "perms_base")?.as_u_integer()?;
        self.perms_owner = required(&values, "perms_owner")?.as_u_integer()?;
        self.perms_group = required(&values, "perms_group")?.as_u_integer()?;
        self.perms_everyone = required(&values, "perms_everyone")?.as_u_integer()?;
        self.perms_next_owner = required(&values, "perms_next_owner")?.as_u_integer()?;
        self.flags = required(&values, "flags")?.as_integer()?;
        self.creation_date = required(&values, "creation_date")?.as_date()?;
        Ok(())
    }

    pub fn from_inventory_base(item: InventoryItem) -> Result<Self, Error> {
        let inventory_type = if item.inventory_type() == InventoryType::UNKNOWN
            && item.asset_type() == AssetType::LSLText
        {
            InventoryType::LSL
        } else {
            item.inventory_type()
        };
        let permissions = item.permissions();
        Ok(Self {
            asset_id: item.asset_uuid(),
            creation_date: item.creation_date(),
            creator_id: item.creator_id(),
            description: item.description(),
            flags: item.flags().cast_signed(),
            group_id: item.group_id(),
            id: item.base.uuid(),
            inv_type: inventory_type,
            last_owner_id: item.last_owner_id(),
            name: item.base.name(),
            owner_id: item.base.owner_id(),
            perms_base: permissions.base_mask.0,
            perms_everyone: permissions.everyone_mask.0,
            perms_granter_id: UUID::zero(),
            perms_group: permissions.group_mask.0,
            perms_next_owner: permissions.next_owner_mask.0,
            perms_owner: permissions.owner_mask.0,
            type_: item.asset_type(),
        })
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct PrimObjectInventoryBlock {
    pub items: Vec<PrimObjectInventoryBlockItemBlock>,
    pub serial: i32,
}

impl PrimObjectInventoryBlock {
    pub fn new() -> Result<Self, Error> {
        Ok(Self {
            items: Vec::new(),
            serial: 0,
        })
    }

    pub fn serialize(&self) -> Result<OSDMap, Error> {
        let items = self
            .items
            .iter()
            .map(|item| item.serialize().map(|map| OSD::Map(map.snapshot())))
            .collect::<Result<Vec<_>, _>>()?;
        map([
            ("serial", OSD::Integer(self.serial)),
            ("items", OSD::Array(items)),
        ])
    }

    pub fn deserialize(&mut self, map: OSDMap) -> Result<(), Error> {
        let values = map.snapshot();
        self.serial = required(&values, "serial")?.as_integer()?;
        self.items = match values.get("items") {
            Some(OSD::Array(items)) => items
                .iter()
                .map(|value| {
                    let OSD::Map(values) = value else {
                        return Err(Error::Argument);
                    };
                    let mut item = PrimObjectInventoryBlockItemBlock::new()?;
                    item.deserialize(OSDMap::new_with_dictionary(values.clone())?)?;
                    Ok(item)
                })
                .collect::<Result<Vec<_>, Error>>()?,
            None => Vec::new(),
            Some(_) => return Err(Error::Argument),
        };
        Ok(())
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct PrimObject {
    pub acceleration: Vector3,
    pub allowed_drop: bool,
    pub angular_velocity: Vector3,
    pub attachment_position: Vector3,
    pub attachment_rotation: Quaternion,
    pub before_attachment_rotation: Quaternion,
    pub camera_at_offset: Vector3,
    pub camera_eye_offset: Vector3,
    pub click_action: i32,
    pub collision_sound: UUID,
    pub collision_sound_volume: f32,
    pub creation_date: SystemTime,
    pub creator_id: UUID,
    pub description: String,
    pub die_at_edge: bool,
    pub flexible: PrimObjectFlexibleBlock,
    pub folder_id: UUID,
    pub group_id: UUID,
    pub id: UUID,
    pub inventory: PrimObjectInventoryBlock,
    pub last_attachment_point: i32,
    pub last_owner_id: UUID,
    pub light: PrimObjectLightBlock,
    pub link_number: i32,
    pub local_id: u32,
    pub material: i32,
    pub name: String,
    pub owner_id: UUID,
    pub p_code: i32,
    pub parent_id: u32,
    pub particles: PrimObjectParticlesBlock,
    pub pass_touches: bool,
    pub perms_base: u32,
    pub perms_everyone: u32,
    pub perms_group: u32,
    pub perms_next_owner: u32,
    pub perms_owner: u32,
    pub phantom: bool,
    pub position: Vector3,
    pub region_handle: u64,
    pub remote_script_access_pin: i32,
    pub return_at_edge: bool,
    pub rez_date: SystemTime,
    pub rotation: Quaternion,
    pub sale_price: i32,
    pub sale_type: i32,
    pub sandbox: bool,
    pub scale: Vector3,
    pub script_state: Vec<u8>,
    pub sculpt: PrimObjectSculptBlock,
    pub selected: bool,
    pub selector_id: UUID,
    pub shape: PrimObjectShapeBlock,
    pub sit_name: String,
    pub sit_offset: Vector3,
    pub sit_rotation: Quaternion,
    pub sound_flags: i32,
    pub sound_gain: f32,
    pub sound_id: UUID,
    pub sound_radius: f32,
    pub state: i32,
    pub temporary: bool,
    pub text: String,
    pub text_color: Color4,
    pub textures: Option<PrimitiveTextureEntry>,
    pub touch_name: String,
    pub use_physics: bool,
    pub velocity: Vector3,
    pub volume_detect: bool,
}

fn nested_map(values: &HashMap<String, OSD>, name: &str) -> Result<Option<OSDMap>, Error> {
    match values.get(name) {
        Some(OSD::Map(map)) => Ok(Some(OSDMap::new_with_dictionary(map.clone())?)),
        Some(_) => Err(Error::Argument),
        None => Ok(None),
    }
}

fn click_action(value: i32) -> Result<ClickAction, Error> {
    Ok(match value {
        0 => ClickAction::Touch,
        1 => ClickAction::Sit,
        2 => ClickAction::Buy,
        3 => ClickAction::Pay,
        4 => ClickAction::OpenTask,
        5 => ClickAction::PlayMedia,
        6 => ClickAction::OpenMedia,
        _ => return Err(Error::Argument),
    })
}

fn sale_type(value: i32) -> Result<SaleType, Error> {
    Ok(match value {
        0 => SaleType::Not,
        1 => SaleType::Original,
        2 => SaleType::Copy,
        3 => SaleType::Contents,
        _ => return Err(Error::Argument),
    })
}

fn has_flag(flags: PrimFlags, flag: PrimFlags) -> bool {
    flags.0 & flag.0 == flag.0
}

impl PrimObject {
    pub fn new() -> Result<Self, Error> {
        Ok(Self {
            acceleration: Vector3::zero(),
            allowed_drop: false,
            angular_velocity: Vector3::zero(),
            attachment_position: Vector3::zero(),
            attachment_rotation: Quaternion::identity(),
            before_attachment_rotation: Quaternion::identity(),
            camera_at_offset: Vector3::zero(),
            camera_eye_offset: Vector3::zero(),
            click_action: 0,
            collision_sound: UUID::zero(),
            collision_sound_volume: 0.0,
            creation_date: UNIX_EPOCH,
            creator_id: UUID::zero(),
            description: String::new(),
            die_at_edge: false,
            flexible: PrimObjectFlexibleBlock::new()?,
            folder_id: UUID::zero(),
            group_id: UUID::zero(),
            id: UUID::zero(),
            inventory: PrimObjectInventoryBlock::new()?,
            last_attachment_point: 0,
            last_owner_id: UUID::zero(),
            light: PrimObjectLightBlock::new()?,
            link_number: 0,
            local_id: 0,
            material: 0,
            name: String::new(),
            owner_id: UUID::zero(),
            p_code: 0,
            parent_id: 0,
            particles: PrimObjectParticlesBlock::new()?,
            pass_touches: false,
            perms_base: 0,
            perms_everyone: 0,
            perms_group: 0,
            perms_next_owner: 0,
            perms_owner: 0,
            phantom: false,
            position: Vector3::zero(),
            region_handle: 0,
            remote_script_access_pin: 0,
            return_at_edge: false,
            rez_date: UNIX_EPOCH,
            rotation: Quaternion::identity(),
            sale_price: 0,
            sale_type: 0,
            sandbox: false,
            scale: Vector3::zero(),
            script_state: Vec::new(),
            sculpt: PrimObjectSculptBlock::new()?,
            selected: false,
            selector_id: UUID::zero(),
            shape: PrimObjectShapeBlock::new()?,
            sit_name: String::new(),
            sit_offset: Vector3::zero(),
            sit_rotation: Quaternion::identity(),
            sound_flags: 0,
            sound_gain: 0.0,
            sound_id: UUID::zero(),
            sound_radius: 0.0,
            state: 0,
            temporary: false,
            text: String::new(),
            text_color: Color4::default(),
            textures: None,
            touch_name: String::new(),
            use_physics: false,
            velocity: Vector3::zero(),
            volume_detect: false,
        })
    }

    pub fn serialize(&self) -> Result<OSDMap, Error> {
        let mut values = HashMap::from([
            ("id".into(), OSD::UUID(self.id)),
            ("allowed_drop".into(), OSD::Boolean(self.allowed_drop)),
            (
                "attachment_position".into(),
                OSD::from_vector3(self.attachment_position)?,
            ),
            (
                "attachment_rotation".into(),
                OSD::from_quaternion(self.attachment_rotation)?,
            ),
            (
                "before_attachment_rotation".into(),
                OSD::from_quaternion(self.before_attachment_rotation)?,
            ),
            ("name".into(), OSD::String(self.name.clone())),
            ("description".into(), OSD::String(self.description.clone())),
            (
                "perms_base".into(),
                OSD::Integer(self.perms_base.cast_signed()),
            ),
            (
                "perms_owner".into(),
                OSD::Integer(self.perms_owner.cast_signed()),
            ),
            (
                "perms_group".into(),
                OSD::Integer(self.perms_group.cast_signed()),
            ),
            (
                "perms_everyone".into(),
                OSD::Integer(self.perms_everyone.cast_signed()),
            ),
            (
                "perms_next_owner".into(),
                OSD::Integer(self.perms_next_owner.cast_signed()),
            ),
            ("creator_identity".into(), OSD::UUID(self.creator_id)),
            ("owner_identity".into(), OSD::UUID(self.owner_id)),
            ("last_owner_identity".into(), OSD::UUID(self.last_owner_id)),
            ("group_identity".into(), OSD::UUID(self.group_id)),
            ("folder_id".into(), OSD::UUID(self.folder_id)),
            (
                "region_handle".into(),
                OSD::from_u_long(self.region_handle)?,
            ),
            ("click_action".into(), OSD::Integer(self.click_action)),
            ("collision_sound".into(), OSD::UUID(self.collision_sound)),
            (
                "collision_sound_volume".into(),
                OSD::Real(f64::from(self.collision_sound_volume)),
            ),
            (
                "last_attachment_point".into(),
                OSD::Integer(self.last_attachment_point),
            ),
            ("link_number".into(), OSD::Integer(self.link_number)),
            ("local_id".into(), OSD::Integer(self.local_id.cast_signed())),
            (
                "parent_id".into(),
                OSD::Integer(self.parent_id.cast_signed()),
            ),
            ("position".into(), OSD::from_vector3(self.position)?),
            ("rotation".into(), OSD::from_quaternion(self.rotation)?),
            ("velocity".into(), OSD::from_vector3(self.velocity)?),
            (
                "angular_velocity".into(),
                OSD::from_vector3(self.angular_velocity)?,
            ),
            ("acceleration".into(), OSD::from_vector3(self.acceleration)?),
            ("scale".into(), OSD::from_vector3(self.scale)?),
            ("sit_offset".into(), OSD::from_vector3(self.sit_offset)?),
            (
                "sit_rotation".into(),
                OSD::from_quaternion(self.sit_rotation)?,
            ),
            (
                "camera_eye_offset".into(),
                OSD::from_vector3(self.camera_eye_offset)?,
            ),
            (
                "camera_at_offset".into(),
                OSD::from_vector3(self.camera_at_offset)?,
            ),
            ("state".into(), OSD::Integer(self.state)),
            ("prim_code".into(), OSD::Integer(self.p_code)),
            ("material".into(), OSD::Integer(self.material)),
            ("pass_touches".into(), OSD::Boolean(self.pass_touches)),
            ("sound_id".into(), OSD::UUID(self.sound_id)),
            ("sound_gain".into(), OSD::Real(f64::from(self.sound_gain))),
            (
                "sound_radius".into(),
                OSD::Real(f64::from(self.sound_radius)),
            ),
            ("sound_flags".into(), OSD::Integer(self.sound_flags)),
            ("text_color".into(), OSD::from_color4(self.text_color)?),
            ("text".into(), OSD::String(self.text.clone())),
            ("sit_name".into(), OSD::String(self.sit_name.clone())),
            ("touch_name".into(), OSD::String(self.touch_name.clone())),
            ("selected".into(), OSD::Boolean(self.selected)),
            ("selector_id".into(), OSD::UUID(self.selector_id)),
            ("use_physics".into(), OSD::Boolean(self.use_physics)),
            ("phantom".into(), OSD::Boolean(self.phantom)),
            (
                "remote_script_access_pin".into(),
                OSD::Integer(self.remote_script_access_pin),
            ),
            ("volume_detect".into(), OSD::Boolean(self.volume_detect)),
            ("die_at_edge".into(), OSD::Boolean(self.die_at_edge)),
            ("return_at_edge".into(), OSD::Boolean(self.return_at_edge)),
            ("temporary".into(), OSD::Boolean(self.temporary)),
            ("sandbox".into(), OSD::Boolean(self.sandbox)),
            ("creation_date".into(), OSD::Date(self.creation_date)),
            ("rez_date".into(), OSD::Date(self.rez_date)),
            ("sale_price".into(), OSD::Integer(self.sale_price)),
            ("sale_type".into(), OSD::Integer(self.sale_type)),
            (
                "script_state".into(),
                OSD::Binary(self.script_state.clone()),
            ),
            (
                "flexible".into(),
                OSD::Map(self.flexible.serialize()?.snapshot()),
            ),
            ("light".into(), OSD::Map(self.light.serialize()?.snapshot())),
            (
                "sculpt".into(),
                OSD::Map(self.sculpt.serialize()?.snapshot()),
            ),
            (
                "particles".into(),
                OSD::Map(self.particles.serialize()?.snapshot()),
            ),
            ("shape".into(), OSD::Map(self.shape.serialize()?.snapshot())),
            (
                "inventory".into(),
                OSD::Map(self.inventory.serialize()?.snapshot()),
            ),
        ]);
        if let Some(textures) = &self.textures {
            values.insert("textures".into(), textures.get_osd()?);
        }
        OSDMap::new_with_dictionary(values)
    }

    pub fn deserialize(&mut self, map: OSDMap) -> Result<(), Error> {
        let values = map.snapshot();
        self.id = required(&values, "id")?.as_uuid()?;
        self.allowed_drop = values
            .get("allowed_drop")
            .map_or(Ok(false), OSD::as_boolean)?;
        self.attachment_position = required(&values, "attachment_position")?.as_vector3()?;
        self.attachment_rotation = required(&values, "attachment_rotation")?.as_quaternion()?;
        self.before_attachment_rotation =
            required(&values, "before_attachment_rotation")?.as_quaternion()?;
        self.name = required(&values, "name")?.as_string()?;
        self.description = required(&values, "description")?.as_string()?;
        self.perms_base = required(&values, "perms_base")?.as_u_integer()?;
        self.perms_owner = required(&values, "perms_owner")?.as_u_integer()?;
        self.perms_group = required(&values, "perms_group")?.as_u_integer()?;
        self.perms_everyone = required(&values, "perms_everyone")?.as_u_integer()?;
        self.perms_next_owner = required(&values, "perms_next_owner")?.as_u_integer()?;
        self.creator_id = required(&values, "creator_identity")?.as_uuid()?;
        self.owner_id = required(&values, "owner_identity")?.as_uuid()?;
        self.last_owner_id = required(&values, "last_owner_identity")?.as_uuid()?;
        self.group_id = required(&values, "group_identity")?.as_uuid()?;
        self.folder_id = required(&values, "folder_id")?.as_uuid()?;
        self.region_handle = required(&values, "region_handle")?.as_u_long()?;
        self.click_action = required(&values, "click_action")?.as_integer()?;
        self.collision_sound = values
            .get("collision_sound")
            .map_or(Ok(UUID::zero()), OSD::as_uuid)?;
        self.collision_sound_volume = values
            .get("collision_sound_volume")
            .map_or(Ok(0.0), |value| value.as_real().map(|v| v as f32))?;
        self.last_attachment_point = required(&values, "last_attachment_point")?.as_integer()?;
        self.link_number = required(&values, "link_number")?.as_integer()?;
        self.local_id = required(&values, "local_id")?.as_u_integer()?;
        self.parent_id = required(&values, "parent_id")?.as_u_integer()?;
        self.position = required(&values, "position")?.as_vector3()?;
        self.rotation = required(&values, "rotation")?.as_quaternion()?;
        self.velocity = required(&values, "velocity")?.as_vector3()?;
        self.angular_velocity = required(&values, "angular_velocity")?.as_vector3()?;
        self.acceleration = required(&values, "acceleration")?.as_vector3()?;
        self.scale = required(&values, "scale")?.as_vector3()?;
        self.sit_offset = required(&values, "sit_offset")?.as_vector3()?;
        self.sit_rotation = required(&values, "sit_rotation")?.as_quaternion()?;
        self.camera_eye_offset = required(&values, "camera_eye_offset")?.as_vector3()?;
        self.camera_at_offset = required(&values, "camera_at_offset")?.as_vector3()?;
        self.state = required(&values, "state")?.as_integer()?;
        self.p_code = required(&values, "prim_code")?.as_integer()?;
        self.material = required(&values, "material")?.as_integer()?;
        self.pass_touches = required(&values, "pass_touches")?.as_boolean()?;
        self.sound_id = required(&values, "sound_id")?.as_uuid()?;
        self.sound_gain = real(&values, "sound_gain")?;
        self.sound_radius = real(&values, "sound_radius")?;
        self.sound_flags = required(&values, "sound_flags")?.as_integer()?;
        self.text_color = required(&values, "text_color")?.as_color4()?;
        self.text = required(&values, "text")?.as_string()?;
        self.sit_name = required(&values, "sit_name")?.as_string()?;
        self.touch_name = required(&values, "touch_name")?.as_string()?;
        self.selected = required(&values, "selected")?.as_boolean()?;
        self.selector_id = required(&values, "selector_id")?.as_uuid()?;
        self.use_physics = required(&values, "use_physics")?.as_boolean()?;
        self.phantom = required(&values, "phantom")?.as_boolean()?;
        self.remote_script_access_pin =
            required(&values, "remote_script_access_pin")?.as_integer()?;
        self.volume_detect = required(&values, "volume_detect")?.as_boolean()?;
        self.die_at_edge = required(&values, "die_at_edge")?.as_boolean()?;
        self.return_at_edge = required(&values, "return_at_edge")?.as_boolean()?;
        self.temporary = required(&values, "temporary")?.as_boolean()?;
        self.sandbox = required(&values, "sandbox")?.as_boolean()?;
        self.creation_date = required(&values, "creation_date")?.as_date()?;
        self.rez_date = required(&values, "rez_date")?.as_date()?;
        self.sale_price = required(&values, "sale_price")?.as_integer()?;
        self.sale_type = required(&values, "sale_type")?.as_integer()?;
        self.script_state = values
            .get("script_state")
            .map_or_else(|| Ok(Vec::new()), OSD::as_binary)?;
        if let Some(map) = nested_map(&values, "flexible")? {
            self.flexible.deserialize(map)?;
        }
        if let Some(map) = nested_map(&values, "light")? {
            self.light.deserialize(map)?;
        }
        if let Some(map) = nested_map(&values, "sculpt")? {
            self.sculpt.deserialize(map)?;
        }
        if let Some(map) = nested_map(&values, "particles")? {
            self.particles.deserialize(map)?;
        }
        if let Some(map) = nested_map(&values, "shape")? {
            self.shape.deserialize(map)?;
        }
        if let Some(map) = nested_map(&values, "inventory")? {
            self.inventory.deserialize(map)?;
        }
        self.textures = values
            .get("textures")
            .cloned()
            .map(PrimitiveTextureEntry::from_osd)
            .transpose()?;
        Ok(())
    }

    pub fn from_primitive(obj: Primitive) -> Result<Self, Error> {
        let mut prim = Self::new()?;
        prim.acceleration = obj.acceleration;
        prim.allowed_drop = has_flag(obj.flags, PrimFlags::ALLOW_INVENTORY_DROP);
        prim.angular_velocity = obj.angular_velocity;
        prim.click_action = i32::from(obj.click_action as u8);
        prim.die_at_edge = has_flag(obj.flags, PrimFlags::DIE_AT_EDGE);
        prim.local_id = obj.local_id;
        prim.material = i32::from(obj.prim_data.material as u8);
        prim.parent_id = obj.parent_id;
        prim.p_code = i32::from(obj.prim_data.p_code as u8);
        prim.phantom = has_flag(obj.flags, PrimFlags::PHANTOM);
        prim.position = obj.position;
        prim.region_handle = obj.region_handle;
        prim.return_at_edge = has_flag(obj.flags, PrimFlags::RETURN_AT_EDGE);
        prim.rotation = obj.rotation;
        prim.sandbox = has_flag(obj.flags, PrimFlags::SANDBOX);
        prim.scale = obj.scale;
        prim.sound_flags = i32::from(obj.sound_flags.0);
        prim.sound_gain = obj.sound_gain;
        prim.sound_id = obj.sound;
        prim.sound_radius = obj.sound_radius;
        prim.state = i32::from(obj.prim_data.state);
        prim.temporary = has_flag(obj.flags, PrimFlags::TEMPORARY);
        prim.text = obj.text.clone();
        prim.text_color = obj.text_color;
        prim.textures = obj.textures.clone();
        prim.use_physics = has_flag(obj.flags, PrimFlags::PHYSICS);
        prim.velocity = obj.velocity;
        if let Some(properties) = &obj.properties {
            prim.creation_date = properties.creation_date;
            prim.creator_id = properties.creator_id;
            prim.description = properties.description.clone();
            prim.folder_id = properties.folder_id;
            prim.group_id = properties.group_id;
            prim.id = properties.object_id;
            prim.last_owner_id = properties.last_owner_id;
            prim.name = properties.name.clone();
            prim.owner_id = properties.owner_id;
            prim.perms_base = properties.permissions.base_mask.0;
            prim.perms_everyone = properties.permissions.everyone_mask.0;
            prim.perms_group = properties.permissions.group_mask.0;
            prim.perms_next_owner = properties.permissions.next_owner_mask.0;
            prim.perms_owner = properties.permissions.owner_mask.0;
            prim.sale_price = properties.sale_price;
            prim.sale_type = i32::from(properties.sale_type as u8);
            prim.sit_name = properties.sit_name.clone();
            prim.touch_name = properties.touch_name.clone();
        } else {
            prim.id = obj.id;
            prim.owner_id = obj.owner_id;
            prim.group_id = obj.group_id;
        }
        if let Some(data) = &obj.flexible {
            prim.flexible = PrimObjectFlexibleBlock {
                drag: data.drag,
                force: data.force,
                gravity: data.gravity,
                softness: data.softness,
                tension: data.tension,
                wind: data.wind,
            };
        }
        if let Some(data) = &obj.light {
            prim.light = PrimObjectLightBlock {
                color: data.color,
                cutoff: data.cutoff,
                falloff: data.falloff,
                intensity: data.intensity,
                radius: data.radius,
            };
        }
        if let Some(data) = &obj.sculpt {
            let OSD::Map(values) = data.get_osd()? else {
                return Err(Error::InvalidOperation);
            };
            prim.sculpt
                .deserialize(OSDMap::new_with_dictionary(values)?)?;
        }
        let particles = &obj.particle_sys;
        prim.particles = PrimObjectParticlesBlock {
            acceleration: particles.part_acceleration,
            angular_velocity: particles.angular_velocity,
            burst_particle_count: i32::from(particles.burst_part_count),
            burst_radius: particles.burst_radius,
            burst_rate: particles.burst_rate,
            burst_speed_max: particles.burst_speed_max,
            burst_speed_min: particles.burst_speed_min,
            data_flags: particles.part_data_flags.0.cast_signed(),
            flags: particles.part_flags.cast_signed(),
            inner_angle: particles.inner_angle,
            max_age: particles.max_age,
            outer_angle: particles.outer_angle,
            particle_end_color: particles.part_end_color,
            particle_end_scale: Vector2 {
                x: particles.part_end_scale_x,
                y: particles.part_end_scale_y,
            },
            particle_max_age: particles.part_max_age,
            particle_start_color: particles.part_start_color,
            particle_start_scale: Vector2 {
                x: particles.part_start_scale_x,
                y: particles.part_start_scale_y,
            },
            pattern: i32::from(particles.pattern.0),
            start_age: particles.start_age,
            target_id: particles.target,
            texture_id: particles.texture,
        };
        prim.shape = PrimObjectShapeBlock {
            path_begin: obj.prim_data.path_begin,
            path_curve: i32::from(obj.prim_data.path_curve as u8),
            path_end: obj.prim_data.path_end,
            path_radius_offset: obj.prim_data.path_radius_offset,
            path_revolutions: obj.prim_data.path_revolutions,
            path_scale_x: obj.prim_data.path_scale_x,
            path_scale_y: obj.prim_data.path_scale_y,
            path_shear_x: obj.prim_data.path_shear_x,
            path_shear_y: obj.prim_data.path_shear_y,
            path_skew: obj.prim_data.path_skew,
            path_taper_x: obj.prim_data.path_taper_x,
            path_taper_y: obj.prim_data.path_taper_y,
            path_twist: obj.prim_data.path_twist,
            path_twist_begin: obj.prim_data.path_twist_begin,
            profile_begin: obj.prim_data.profile_begin,
            profile_curve: i32::from(obj.prim_data.profile_curve_with_field),
            profile_end: obj.prim_data.profile_end,
            profile_hollow: obj.prim_data.profile_hollow,
        };
        Ok(prim)
    }

    pub fn to_primitive(&self) -> Result<Primitive, Error> {
        let mut prim = Primitive::new_with_constructor()?;
        prim.acceleration = self.acceleration;
        prim.angular_velocity = self.angular_velocity;
        prim.click_action = click_action(self.click_action)?;
        prim.group_id = self.group_id;
        prim.id = self.id;
        prim.local_id = self.local_id;
        prim.owner_id = self.owner_id;
        prim.parent_id = self.parent_id;
        prim.position = self.position;
        prim.region_handle = self.region_handle;
        prim.rotation = self.rotation;
        prim.scale = self.scale;
        prim.sound = self.sound_id;
        prim.sound_flags = SoundFlags(u8::try_from(self.sound_flags).map_err(|_| Error::Argument)?);
        prim.sound_gain = self.sound_gain;
        prim.sound_radius = self.sound_radius;
        prim.text = self.text.clone();
        prim.text_color = self.text_color;
        prim.textures = self.textures.clone();
        prim.velocity = self.velocity;
        prim.prim_data.material = crate::object_model::material_from_i32(self.material);
        prim.prim_data.p_code = crate::object_model::pcode_from_i32(self.p_code);
        prim.prim_data.state = u8::try_from(self.state).map_err(|_| Error::Argument)?;
        prim.prim_data.path_begin = self.shape.path_begin;
        prim.prim_data.path_curve = crate::object_model::path_curve_from_i32(self.shape.path_curve);
        prim.prim_data.path_end = self.shape.path_end;
        prim.prim_data.path_radius_offset = self.shape.path_radius_offset;
        prim.prim_data.path_revolutions = self.shape.path_revolutions;
        prim.prim_data.path_scale_x = self.shape.path_scale_x;
        prim.prim_data.path_scale_y = self.shape.path_scale_y;
        prim.prim_data.path_shear_x = self.shape.path_shear_x;
        prim.prim_data.path_shear_y = self.shape.path_shear_y;
        prim.prim_data.path_skew = self.shape.path_skew;
        prim.prim_data.path_taper_x = self.shape.path_taper_x;
        prim.prim_data.path_taper_y = self.shape.path_taper_y;
        prim.prim_data.path_twist = self.shape.path_twist;
        prim.prim_data.path_twist_begin = self.shape.path_twist_begin;
        prim.prim_data.profile_begin = self.shape.profile_begin;
        prim.prim_data.profile_curve_with_field =
            u8::try_from(self.shape.profile_curve).map_err(|_| Error::Argument)?;
        prim.prim_data.profile_end = self.shape.profile_end;
        prim.prim_data.profile_hollow = self.shape.profile_hollow;
        let mut flags = 0_u32;
        for (enabled, flag) in [
            (self.allowed_drop, PrimFlags::ALLOW_INVENTORY_DROP),
            (self.die_at_edge, PrimFlags::DIE_AT_EDGE),
            (self.phantom, PrimFlags::PHANTOM),
            (self.return_at_edge, PrimFlags::RETURN_AT_EDGE),
            (self.sandbox, PrimFlags::SANDBOX),
            (self.temporary, PrimFlags::TEMPORARY),
            (self.use_physics, PrimFlags::PHYSICS),
        ] {
            if enabled {
                flags |= flag.0;
            }
        }
        prim.flags = PrimFlags(flags);
        prim.flexible = Some(PrimitiveFlexibleData {
            drag: self.flexible.drag,
            force: self.flexible.force,
            gravity: self.flexible.gravity,
            softness: self.flexible.softness,
            tension: self.flexible.tension,
            wind: self.flexible.wind,
        });
        prim.light = Some(PrimitiveLightData {
            color: self.light.color,
            cutoff: self.light.cutoff,
            falloff: self.light.falloff,
            intensity: self.light.intensity,
            radius: self.light.radius,
        });
        prim.sculpt = Some(PrimitiveSculptData::from_osd(OSD::Map(
            self.sculpt.serialize()?.snapshot(),
        ))?);
        prim.particle_sys = PrimitiveParticleSystem {
            angular_velocity: self.particles.angular_velocity,
            burst_part_count: u8::try_from(self.particles.burst_particle_count)
                .map_err(|_| Error::Argument)?,
            burst_radius: self.particles.burst_radius,
            burst_rate: self.particles.burst_rate,
            burst_speed_max: self.particles.burst_speed_max,
            burst_speed_min: self.particles.burst_speed_min,
            inner_angle: self.particles.inner_angle,
            max_age: self.particles.max_age,
            outer_angle: self.particles.outer_angle,
            part_acceleration: self.particles.acceleration,
            part_data_flags: PrimitiveParticleSystemParticleDataFlags(
                self.particles.data_flags.cast_unsigned(),
            ),
            part_end_color: self.particles.particle_end_color,
            part_end_scale_x: self.particles.particle_end_scale.x,
            part_end_scale_y: self.particles.particle_end_scale.y,
            part_flags: self.particles.flags.cast_unsigned(),
            part_max_age: self.particles.particle_max_age,
            part_start_color: self.particles.particle_start_color,
            part_start_scale_x: self.particles.particle_start_scale.x,
            part_start_scale_y: self.particles.particle_start_scale.y,
            pattern: PrimitiveParticleSystemSourcePattern(
                u8::try_from(self.particles.pattern).map_err(|_| Error::Argument)?,
            ),
            start_age: self.particles.start_age,
            target: self.particles.target_id,
            texture: self.particles.texture_id,
            ..PrimitiveParticleSystem::default()
        };
        let mut properties = PrimitiveObjectProperties::new()?;
        properties.creation_date = self.creation_date;
        properties.creator_id = self.creator_id;
        properties.description = self.description.clone();
        properties.folder_id = self.folder_id;
        properties.group_id = self.group_id;
        properties.inventory_serial =
            i16::try_from(self.inventory.serial).map_err(|_| Error::Argument)?;
        properties.last_owner_id = self.last_owner_id;
        properties.name = self.name.clone();
        properties.object_id = self.id;
        properties.owner_id = self.owner_id;
        properties.permissions = Permissions::new(
            self.perms_base,
            self.perms_everyone,
            self.perms_group,
            self.perms_next_owner,
            self.perms_owner,
        )?;
        properties.sale_price = self.sale_price;
        properties.sale_type = sale_type(self.sale_type)?;
        properties.sit_name = self.sit_name.clone();
        properties.touch_name = self.touch_name.clone();
        prim.properties = Some(properties);
        Ok(prim)
    }
}

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

    #[test]
    fn prim_object_llsd_round_trip_preserves_nested_and_extended_fields() {
        let mut source = PrimObject::new().unwrap();
        source.name = "OpenSim linkset root".into();
        source.allowed_drop = true;
        source.region_handle = u64::MAX - 7;
        source.local_id = u32::MAX - 3;
        source.script_state = vec![0, 1, 2, 255];
        source.flexible.force = Vector3 {
            x: 1.0,
            y: 2.0,
            z: 3.0,
        };
        source.particles.target_id =
            UUID::new_with_string("82c91f5e-c43b-4936-87e4-03b6e497fa83".into()).unwrap();
        source.inventory.serial = 9;
        source.textures = Some(
            PrimitiveTextureEntry::new_with_uuid(
                UUID::new_with_string("04a14b49-8d45-4b78-bd70-205d74679bfb".into()).unwrap(),
            )
            .unwrap(),
        );
        let mut decoded = PrimObject::new().unwrap();
        decoded.deserialize(source.serialize().unwrap()).unwrap();
        assert_eq!(decoded, source);
    }

    #[test]
    fn primitive_conversion_preserves_representable_state() {
        let mut source = PrimObject::new().unwrap();
        source.name = "native prim".into();
        source.id = UUID::new_with_string("a085d7e9-cf66-472d-86e2-52740783f65f".into()).unwrap();
        source.owner_id =
            UUID::new_with_string("1dc4be58-b125-4a73-8457-bc37e71246fd".into()).unwrap();
        source.click_action = ClickAction::Sit as i32;
        source.p_code = 9;
        source.material = 3;
        source.shape.path_curve = 32;
        source.shape.profile_curve = 1;
        source.sale_type = SaleType::Copy as i32;
        source.allowed_drop = true;
        source.phantom = true;
        source.use_physics = true;
        source.particles.burst_particle_count = 4;
        source.particles.pattern = 8;
        let restored = PrimObject::from_primitive(source.to_primitive().unwrap()).unwrap();
        assert_eq!(restored.id, source.id);
        assert_eq!(restored.name, source.name);
        assert_eq!(restored.owner_id, source.owner_id);
        assert_eq!(restored.click_action, source.click_action);
        assert_eq!(restored.shape, source.shape);
        assert_eq!(restored.particles, source.particles);
        assert!(restored.allowed_drop && restored.phantom && restored.use_physics);
    }
}
