//! Native wearable and attachment state for `AppearanceManager`.

use crate::client_core::ClientWeakHandle;
use crate::packet_catalog::PacketType;
use crate::packets::{
    AgentCachedTexturePacket, AgentCachedTexturePacketWearableDataBlock,
    AgentCachedTextureResponsePacket, AgentIsNowWearingPacket,
    AgentIsNowWearingPacketWearableDataBlock, AgentSetAppearancePacket,
    AgentSetAppearancePacketVisualParamBlock, AgentSetAppearancePacketWearableDataBlock,
    AgentWearablesRequestPacket, AgentWearablesUpdatePacket, DetachAttachmentIntoInvPacket,
    RebakeAvatarTexturesPacket, RezSingleAttachmentFromInvPacket,
};
use crate::{
    AgentCachedBakesReplyEventArgs, AgentWearablesReplyEventArgs, AppearanceManagerTextureData,
    AppearanceManagerWearableData, AvatarTextureIndex, BakeType, GridClient, InventoryAttachment,
    InventoryBase, InventoryFolder, InventoryItem, InventoryObject, InventoryObjectClass,
    InventorySortOrder, InventoryWearable, Permissions, PrimitiveTextureEntry, VisualParams,
};
use futures_util::{StreamExt, stream::FuturesUnordered};
use libremetaverse_structured_data::{OSD, OSDFormat, OSDParser};
use libremetaverse_types::compat::{
    CancellationToken, CancellationTokenSource, EventHandler, Subscription,
};
use libremetaverse_types::{
    AssetType, AttachmentPoint, Color4, Error, FolderType, InventoryType, MultiValueDictionary,
    UUID, Vector3, WearableType,
};
use std::collections::{HashMap, HashSet, VecDeque};
use std::fmt;
use std::hash::{Hash, Hasher};
use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, RwLock};

pub(crate) const ATTACHMENT_ADD: u8 = 0x80;
const MAX_WEARABLE_LAYERS: usize = 60;
const MAX_FOLDER_TRAVERSAL: usize = 256;
const MAX_COF_ENTRIES: usize = 512;

fn mutex<T>(lock: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
    lock.lock()
        .unwrap_or_else(std::sync::PoisonError::into_inner)
}

fn read<T>(lock: &RwLock<T>) -> std::sync::RwLockReadGuard<'_, T> {
    lock.read()
        .unwrap_or_else(std::sync::PoisonError::into_inner)
}

fn write<T>(lock: &RwLock<T>) -> std::sync::RwLockWriteGuard<'_, T> {
    lock.write()
        .unwrap_or_else(std::sync::PoisonError::into_inner)
}

fn check_cancelled(token: Option<&CancellationToken>) -> Result<(), Error> {
    token.map_or(Ok(()), CancellationToken::throw_if_cancellation_requested)
}

struct EventRegistry<T> {
    next_id: AtomicU64,
    handlers: Arc<Mutex<HashMap<u64, EventHandler<T>>>>,
}

impl<T> Default for EventRegistry<T> {
    fn default() -> Self {
        Self {
            next_id: AtomicU64::new(1),
            handlers: Arc::new(Mutex::new(HashMap::new())),
        }
    }
}

impl<T: 'static> EventRegistry<T> {
    fn subscribe(&self, handler: EventHandler<T>) -> Subscription {
        let id = self.next_id.fetch_add(1, Ordering::Relaxed);
        mutex(&self.handlers).insert(id, handler);
        let handlers = Arc::downgrade(&self.handlers);
        Subscription::new(move || {
            if let Some(handlers) = handlers.upgrade() {
                mutex(&handlers).remove(&id);
            }
        })
    }

    fn emit_with(&self, mut event: impl FnMut() -> T) {
        let handlers: Vec<_> = mutex(&self.handlers).values().cloned().collect();
        for handler in handlers {
            handler(event());
        }
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
struct WearableRecord {
    item_id: UUID,
    asset_id: UUID,
    asset_type: AssetType,
    wearable_type: WearableType,
}

impl WearableRecord {
    fn from_item(item: &InventoryItem) -> Option<Self> {
        if !matches!(item.asset_type(), AssetType::Bodypart | AssetType::Clothing) {
            return None;
        }
        let wearable = InventoryWearable { base: item.clone() };
        Some(Self {
            item_id: item.base.uuid(),
            asset_id: item.asset_uuid(),
            asset_type: item.asset_type(),
            wearable_type: wearable.wearable_type(),
        })
    }

    fn public(&self) -> AppearanceManagerWearableData {
        AppearanceManagerWearableData {
            asset: None,
            asset_id: self.asset_id,
            asset_type: self.asset_type,
            item_id: self.item_id,
            wearable_type: self.wearable_type,
        }
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
struct AttachmentRecord {
    item: InventoryItem,
    point: AttachmentPoint,
}

#[derive(Default)]
struct AppearanceEvents {
    agent_wearables_reply: EventRegistry<AgentWearablesReplyEventArgs>,
    appearance_set: EventRegistry<AppearanceSetEventArgs>,
    cached_bakes_reply: EventRegistry<AgentCachedBakesReplyEventArgs>,
    rebake_avatar_requested: EventRegistry<RebakeAvatarTexturesEventArgs>,
}

pub(crate) struct AppearanceManagerInner {
    client: ClientWeakHandle,
    wearables: RwLock<Vec<WearableRecord>>,
    attachments: RwLock<Vec<AttachmentRecord>>,
    last_cof_version: AtomicI32,
    busy: AtomicBool,
    disposed: AtomicBool,
    texture_provider: RwLock<Arc<dyn crate::IBakingTextureProvider>>,
    texture_slots: RwLock<Vec<AppearanceManagerTextureData>>,
    wearable_assets: RwLock<HashMap<UUID, crate::assets::AssetWearable>>,
    visual_parameters: RwLock<Vec<u8>>,
    server_visual_parameters: AtomicBool,
    cache_serial: AtomicI32,
    cache_reply_generation: AtomicU64,
    cache_notify: tokio::sync::Notify,
    wearables_reply_generation: AtomicU64,
    wearables_notify: tokio::sync::Notify,
    appearance_serial: AtomicU64,
    appearance_cancel: Mutex<Option<CancellationTokenSource>>,
    appearance_gate: Arc<tokio::sync::Semaphore>,
    events: AppearanceEvents,
    network_subscriptions: Mutex<Vec<Subscription>>,
}

/// Shared native manager. Public texture fields remain on the compatibility
/// wrapper; wearable and attachment state is shared by every client accessor.
pub struct AppearanceManager {
    pub my_textures: PrimitiveTextureEntry,
    pub my_visual_parameters: Vec<u8>,
    inner: Arc<AppearanceManagerInner>,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct AppearanceSetEventArgs {
    success: bool,
}

impl AppearanceSetEventArgs {
    pub(crate) const fn native_new(success: bool) -> Self {
        Self { success }
    }
    #[allow(clippy::trivially_copy_pass_by_ref)] // Generated property access uses `&self`.
    pub(crate) const fn native_success(&self) -> bool {
        self.success
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct RebakeAvatarTexturesEventArgs {
    texture_id: UUID,
}

impl RebakeAvatarTexturesEventArgs {
    pub(crate) const fn native_new(texture_id: UUID) -> Self {
        Self { texture_id }
    }
    pub(crate) const fn native_texture_id(&self) -> UUID {
        self.texture_id
    }
}

impl Clone for AppearanceManager {
    fn clone(&self) -> Self {
        Self::native_from_inner(Arc::clone(&self.inner))
    }
}

impl fmt::Debug for AppearanceManager {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("AppearanceManager")
            .field("wearables", &read(&self.inner.wearables).len())
            .field("attachments", &read(&self.inner.attachments).len())
            .field("busy", &self.native_manager_busy())
            .finish_non_exhaustive()
    }
}

struct BusyGuard<'a>(&'a AtomicBool);

impl Drop for BusyGuard<'_> {
    fn drop(&mut self) {
        self.0.store(false, Ordering::Release);
    }
}

fn handle_appearance_packet(
    inner: &Arc<AppearanceManagerInner>,
    packet_type: PacketType,
    data: Vec<u8>,
) {
    match packet_type {
        PacketType::AgentWearablesUpdate => handle_wearables_update(inner, data),
        PacketType::AgentCachedTextureResponse => handle_cached_bakes(inner, data),
        PacketType::RebakeAvatarTextures => handle_rebake_request(inner, data),
        _ => {}
    }
}

fn handle_wearables_update(inner: &Arc<AppearanceManagerInner>, data: Vec<u8>) {
    let mut offset = 0;
    let Ok(packet) = AgentWearablesUpdatePacket::new_with_bytes_int32(data, &mut offset) else {
        return;
    };
    let server_baking = inner
        .client
        .upgrade()
        .and_then(|client| client.native_network().ok())
        .and_then(|network| network.native_current_sim())
        .is_some_and(|simulator| {
            simulator.protocols.0 & crate::RegionProtocols::AGENT_APPEARANCE_SERVICE.0 != 0
        });
    let mut received = Vec::new();
    if !server_baking {
        for block in packet.wearable_data {
            let Some(wearable_type) = wearable_type_from_u8(block.wearable_type) else {
                continue;
            };
            if wearable_type == WearableType::Invalid || block.item_id == UUID::zero() {
                continue;
            }
            received.push(WearableRecord {
                item_id: block.item_id,
                asset_id: block.asset_id,
                asset_type: wearable_type_to_asset_type(wearable_type),
                wearable_type,
            });
        }
        *write(&inner.wearables) = received;
        inner
            .server_visual_parameters
            .store(false, Ordering::Release);
    }
    inner
        .wearables_reply_generation
        .fetch_add(1, Ordering::AcqRel);
    inner.wearables_notify.notify_one();
    inner
        .events
        .agent_wearables_reply
        .emit_with(|| AgentWearablesReplyEventArgs);
    if !server_baking && !read(&inner.wearables).is_empty() && !inner.busy.load(Ordering::Acquire) {
        spawn_appearance_request(inner, false, "appearance-wearables");
    }
}

fn handle_cached_bakes(inner: &Arc<AppearanceManagerInner>, data: Vec<u8>) {
    let mut offset = 0;
    let Ok(packet) = AgentCachedTextureResponsePacket::new_with_bytes_int32(data, &mut offset)
    else {
        return;
    };
    let mut slots = write(&inner.texture_slots);
    for bake in packet.wearable_data {
        if let Some(target) = bake_index_to_texture_index()
            .get(usize::from(bake.texture_index))
            .copied()
            && let Some(slot) = slots.get_mut(usize::from(target))
            && bake.texture_id != UUID::zero()
        {
            slot.texture_id = bake.texture_id;
            slot.texture_index = avatar_texture_index_from_i32(i32::from(target))
                .unwrap_or(AvatarTextureIndex::Unknown);
        }
    }
    drop(slots);
    inner.cache_reply_generation.fetch_add(1, Ordering::AcqRel);
    inner.cache_notify.notify_one();
    inner
        .events
        .cached_bakes_reply
        .emit_with(|| AgentCachedBakesReplyEventArgs);
}

fn handle_rebake_request(inner: &Arc<AppearanceManagerInner>, data: Vec<u8>) {
    let mut offset = 0;
    let Ok(packet) = RebakeAvatarTexturesPacket::new_with_bytes_int32(data, &mut offset) else {
        return;
    };
    let texture_id = packet.texture_data.texture_id;
    inner
        .events
        .rebake_avatar_requested
        .emit_with(|| RebakeAvatarTexturesEventArgs { texture_id });
    spawn_appearance_request(inner, true, "appearance-rebake");
}

fn spawn_appearance_request(inner: &Arc<AppearanceManagerInner>, force_rebake: bool, name: &str) {
    let manager = AppearanceManager::native_from_inner(Arc::clone(inner));
    let _ = std::thread::Builder::new()
        .name(name.to_owned())
        .spawn(move || {
            let Ok(runtime) = tokio::runtime::Builder::new_current_thread()
                .enable_all()
                .build()
            else {
                return;
            };
            let _ = runtime.block_on(manager.native_request_set_appearance(force_rebake));
        });
}

impl AppearanceManager {
    pub(crate) fn native_new(client: Option<Arc<GridClient>>) -> Result<Self, Error> {
        let client = client.ok_or(Error::ArgumentNull)?;
        let provider: Arc<dyn crate::IBakingTextureProvider> = Arc::new(
            crate::GridClientBakingTextureProvider::native_new(client.as_ref().clone())?,
        );
        let inner = Arc::new(AppearanceManagerInner {
            client: client.native_weak_handle(),
            wearables: RwLock::new(Vec::new()),
            attachments: RwLock::new(Vec::new()),
            last_cof_version: AtomicI32::new(-1),
            busy: AtomicBool::new(false),
            disposed: AtomicBool::new(false),
            texture_provider: RwLock::new(provider),
            texture_slots: RwLock::new(
                (0..AvatarTextureIndex::NumberOfEntries as usize)
                    .map(|_| texture_data_defaults().expect("texture defaults"))
                    .collect(),
            ),
            wearable_assets: RwLock::new(HashMap::new()),
            visual_parameters: RwLock::new(Vec::new()),
            server_visual_parameters: AtomicBool::new(false),
            cache_serial: AtomicI32::new(0),
            cache_reply_generation: AtomicU64::new(0),
            cache_notify: tokio::sync::Notify::new(),
            wearables_reply_generation: AtomicU64::new(0),
            wearables_notify: tokio::sync::Notify::new(),
            appearance_serial: AtomicU64::new(0),
            appearance_cancel: Mutex::new(None),
            appearance_gate: Arc::new(tokio::sync::Semaphore::new(1)),
            events: AppearanceEvents::default(),
            network_subscriptions: Mutex::new(Vec::new()),
        });
        let weak = Arc::downgrade(&inner);
        let subscription = client
            .native_network()?
            .subscribe_raw_packet(Arc::new(move |event| {
                let Some(inner) = weak.upgrade() else {
                    return;
                };
                handle_appearance_packet(&inner, event.packet_type, event.data);
            }));
        mutex(&inner.network_subscriptions).push(subscription);
        Ok(Self::native_from_inner(inner))
    }

    pub(crate) fn native_from_inner(inner: Arc<AppearanceManagerInner>) -> Self {
        let my_visual_parameters = read(&inner.visual_parameters).clone();
        Self {
            my_textures: PrimitiveTextureEntry {
                default_texture: None,
                face_textures: Vec::new(),
            },
            my_visual_parameters,
            inner,
        }
    }

    pub(crate) fn native_inner(&self) -> Arc<AppearanceManagerInner> {
        Arc::clone(&self.inner)
    }

    fn client(&self) -> Result<GridClient, Error> {
        if self.inner.disposed.load(Ordering::Acquire) {
            return Err(Error::InvalidOperation);
        }
        self.inner.client.upgrade().ok_or(Error::InvalidOperation)
    }

    fn begin(&self) -> Result<BusyGuard<'_>, Error> {
        self.client()?;
        if self
            .inner
            .busy
            .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
            .is_err()
        {
            return Err(Error::InvalidOperation);
        }
        Ok(BusyGuard(&self.inner.busy))
    }

    pub(crate) fn native_subscribe_agent_wearables_reply(
        &self,
        handler: EventHandler<AgentWearablesReplyEventArgs>,
    ) -> Subscription {
        self.inner.events.agent_wearables_reply.subscribe(handler)
    }

    pub(crate) fn native_subscribe_appearance_set(
        &self,
        handler: EventHandler<AppearanceSetEventArgs>,
    ) -> Subscription {
        self.inner.events.appearance_set.subscribe(handler)
    }

    pub(crate) fn native_subscribe_cached_bakes_reply(
        &self,
        handler: EventHandler<AgentCachedBakesReplyEventArgs>,
    ) -> Subscription {
        self.inner.events.cached_bakes_reply.subscribe(handler)
    }

    pub(crate) fn native_subscribe_rebake_avatar_requested(
        &self,
        handler: EventHandler<RebakeAvatarTexturesEventArgs>,
    ) -> Subscription {
        self.inner.events.rebake_avatar_requested.subscribe(handler)
    }

    pub(crate) fn native_texture_provider(&self) -> Arc<dyn crate::IBakingTextureProvider> {
        Arc::clone(&read(&self.inner.texture_provider))
    }

    pub(crate) fn native_set_texture_provider(
        &self,
        provider: Arc<dyn crate::IBakingTextureProvider>,
    ) {
        *write(&self.inner.texture_provider) = provider;
    }

    #[allow(clippy::unnecessary_wraps)] // The mapped Dispose API is fallible.
    pub(crate) fn native_dispose(&self) -> Result<(), Error> {
        self.inner.disposed.store(true, Ordering::Release);
        write(&self.inner.wearables).clear();
        self.inner
            .server_visual_parameters
            .store(false, Ordering::Release);
        write(&self.inner.attachments).clear();
        mutex(&self.inner.network_subscriptions).clear();
        Ok(())
    }

    pub(crate) fn native_manager_busy(&self) -> bool {
        self.inner.busy.load(Ordering::Acquire)
    }

    pub(crate) fn native_last_cof_version(&self) -> i32 {
        self.inner.last_cof_version.load(Ordering::Acquire)
    }

    #[allow(clippy::unnecessary_wraps)] // The mapped method returns Result<(), Error>.
    pub(crate) fn native_update_cof_version(&self, version: i32) -> Result<(), Error> {
        self.inner
            .last_cof_version
            .fetch_max(version, Ordering::AcqRel);
        Ok(())
    }

    pub(crate) fn native_add_to_outfit(
        &self,
        items: Vec<InventoryItem>,
        replace: bool,
    ) -> Result<(), Error> {
        let busy = self.begin()?;
        let mut next = read(&self.inner.wearables).clone();
        apply_wearables(&mut next, items, replace)?;
        self.send_wearables(&next)?;
        *write(&self.inner.wearables) = next;
        self.inner
            .server_visual_parameters
            .store(false, Ordering::Release);
        drop(busy);
        self.inner
            .events
            .appearance_set
            .emit_with(|| AppearanceSetEventArgs { success: true });
        self.inner
            .events
            .agent_wearables_reply
            .emit_with(|| AgentWearablesReplyEventArgs);
        Ok(())
    }

    pub(crate) fn native_remove_from_outfit(&self, items: Vec<InventoryItem>) -> Result<(), Error> {
        let busy = self.begin()?;
        let ids: HashSet<_> = items.into_iter().map(|item| item.base.uuid()).collect();
        let mut next = read(&self.inner.wearables).clone();
        next.retain(|wearable| !ids.contains(&wearable.item_id));
        self.send_wearables(&next)?;
        *write(&self.inner.wearables) = next;
        self.inner
            .server_visual_parameters
            .store(false, Ordering::Release);
        drop(busy);
        self.inner
            .events
            .appearance_set
            .emit_with(|| AppearanceSetEventArgs { success: true });
        self.inner
            .events
            .agent_wearables_reply
            .emit_with(|| AgentWearablesReplyEventArgs);
        Ok(())
    }

    #[allow(clippy::unused_async)] // Preserves the mapped asynchronous API boundary.
    pub(crate) async fn native_replace_outfit(
        &self,
        items: Vec<InventoryItem>,
        safe: bool,
    ) -> Result<(), Error> {
        let busy = self.begin()?;
        let mut next = if safe {
            read(&self.inner.wearables)
                .iter()
                .filter(|wearable| wearable.asset_type == AssetType::Bodypart)
                .cloned()
                .collect()
        } else {
            Vec::new()
        };
        apply_wearables(&mut next, items, false)?;
        self.send_wearables(&next)?;
        *write(&self.inner.wearables) = next;
        self.inner
            .server_visual_parameters
            .store(false, Ordering::Release);
        drop(busy);
        self.inner
            .events
            .appearance_set
            .emit_with(|| AppearanceSetEventArgs { success: true });
        self.inner
            .events
            .agent_wearables_reply
            .emit_with(|| AgentWearablesReplyEventArgs);
        Ok(())
    }

    pub(crate) async fn native_wear_outfit(
        &self,
        values: Vec<InventoryBase>,
        replace: bool,
    ) -> Result<(), Error> {
        let client = self.client()?;
        let store = client
            .native_inventory()?
            .native_store()
            .ok_or(Error::InvalidOperation)?;
        let items: Vec<_> = values
            .into_iter()
            .filter_map(|value| store.native_item_value(value.uuid()))
            .collect();
        if replace {
            self.native_replace_outfit(items, false).await
        } else {
            self.native_add_to_outfit(items, false)
        }
    }

    pub(crate) fn native_get_wearables(
        &self,
    ) -> Result<Box<dyn Iterator<Item = AppearanceManagerWearableData>>, Error> {
        self.client()?;
        let values: Vec<_> = read(&self.inner.wearables)
            .iter()
            .map(|record| {
                let mut value = record.public();
                value.asset = read(&self.inner.wearable_assets)
                    .get(&record.item_id)
                    .cloned();
                value
            })
            .collect();
        Ok(Box::new(values.into_iter()))
    }

    pub(crate) fn native_get_wearable_assets(
        &self,
        wearable_type: WearableType,
    ) -> Result<Box<dyn Iterator<Item = UUID>>, Error> {
        self.client()?;
        let values: Vec<_> = read(&self.inner.wearables)
            .iter()
            .filter(|wearable| wearable.wearable_type == wearable_type)
            .map(|wearable| wearable.asset_id)
            .collect();
        Ok(Box::new(values.into_iter()))
    }

    pub(crate) fn native_get_wearables_by_type(
        &self,
    ) -> Result<MultiValueDictionary<WearableType, AppearanceManagerWearableData>, Error> {
        self.client()?;
        let result = MultiValueDictionary::new_with_constructor()?;
        for wearable in read(&self.inner.wearables).iter() {
            let mut value = wearable.public();
            value.asset = read(&self.inner.wearable_assets)
                .get(&wearable.item_id)
                .cloned();
            result.add(wearable.wearable_type, Some(value))?;
        }
        Ok(result)
    }

    pub(crate) fn native_is_item_worn(&self, item_id: UUID) -> Result<WearableType, Error> {
        self.client()?;
        Ok(read(&self.inner.wearables)
            .iter()
            .find(|wearable| wearable.item_id == item_id)
            .map_or(WearableType::Invalid, |wearable| wearable.wearable_type))
    }

    #[allow(clippy::unused_async)] // Preserves the mapped asynchronous API boundary.
    pub(crate) async fn native_send_outfit(
        &self,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<(), Error> {
        check_cancelled(cancellation_token.as_ref())?;
        let busy = self.begin()?;
        let snapshot = read(&self.inner.wearables).clone();
        check_cancelled(cancellation_token.as_ref())?;
        self.send_wearables(&snapshot)?;
        drop(busy);
        self.inner
            .events
            .appearance_set
            .emit_with(|| AppearanceSetEventArgs { success: true });
        Ok(())
    }

    fn send_wearables(&self, wearables: &[WearableRecord]) -> Result<(), Error> {
        let client = self.client()?;
        let network = client.native_network()?;
        let mut packet = AgentIsNowWearingPacket::new_with_constructor()?;
        packet.agent_data.agent_id = network.native_agent_id();
        packet.agent_data.session_id = network.native_session_id();
        packet.wearable_data = wearables
            .iter()
            .map(|wearable| {
                let mut block = AgentIsNowWearingPacketWearableDataBlock::new_with_constructor()?;
                block.item_id = wearable.item_id;
                block.wearable_type = wearable.wearable_type as u8;
                Ok(block)
            })
            .collect::<Result<Vec<_>, Error>>()?;
        send_generated(
            &network,
            PacketType::AgentIsNowWearing,
            packet.to_bytes_with_method()?,
        )?;
        Ok(())
    }

    pub(crate) fn native_attach(
        &self,
        item: InventoryItem,
        point: AttachmentPoint,
        replace: bool,
    ) -> Result<(), Error> {
        let busy = self.begin()?;
        let name = item.base.name();
        let description = item.description();
        let permissions = item.permissions();
        self.send_attach_packet(
            item.base.uuid(),
            item.base.owner_id(),
            &name,
            &description,
            permissions,
            item.flags(),
            point,
            replace,
        )?;
        self.record_attachment(item, point, replace);
        drop(busy);
        Ok(())
    }

    #[allow(clippy::too_many_arguments)]
    pub(crate) fn native_attach_fields(
        &self,
        item_id: UUID,
        owner_id: UUID,
        name: String,
        description: String,
        permissions: Permissions,
        item_flags: u32,
        point: AttachmentPoint,
        replace: bool,
    ) -> Result<(), Error> {
        let busy = self.begin()?;
        self.send_attach_packet(
            item_id,
            owner_id,
            &name,
            &description,
            permissions,
            item_flags,
            point,
            replace,
        )?;
        let mut item = InventoryItem::new_with_inventory_type_uuid(InventoryType::OBJECT, item_id)?;
        item.base.set_owner_id(owner_id);
        item.base.set_name(name);
        item.set_description(description);
        item.set_permissions(permissions);
        item.set_flags(item_flags);
        item.set_asset_type(AssetType::Object);
        self.record_attachment(item, point, replace);
        drop(busy);
        Ok(())
    }

    #[allow(clippy::too_many_arguments)]
    fn send_attach_packet(
        &self,
        item_id: UUID,
        owner_id: UUID,
        name: &str,
        description: &str,
        permissions: Permissions,
        item_flags: u32,
        point: AttachmentPoint,
        replace: bool,
    ) -> Result<(), Error> {
        let client = self.client()?;
        let network = client.native_network()?;
        send_generated(
            &network,
            PacketType::RezSingleAttachmentFromInv,
            attachment_packet_bytes(
                network.native_agent_id(),
                network.native_session_id(),
                item_id,
                owner_id,
                name,
                description,
                permissions,
                item_flags,
                point,
                replace,
            )?,
        )
    }

    fn record_attachment(&self, item: InventoryItem, point: AttachmentPoint, replace: bool) {
        let mut attachments = write(&self.inner.attachments);
        if replace && point != AttachmentPoint::Default {
            attachments.retain(|existing| existing.point != point);
        }
        attachments.retain(|existing| existing.item.base.uuid() != item.base.uuid());
        attachments.push(AttachmentRecord { item, point });
    }

    pub(crate) fn native_add_attachments(
        &self,
        items: Vec<InventoryItem>,
        remove_existing_first: bool,
        replace: bool,
    ) -> Result<(), Error> {
        let busy = self.begin()?;
        let existing = read(&self.inner.attachments).clone();
        if remove_existing_first {
            for attachment in &existing {
                self.send_detach_packet(attachment.item.base.uuid())?;
                write(&self.inner.attachments)
                    .retain(|candidate| candidate.item.base.uuid() != attachment.item.base.uuid());
            }
        }
        for item in items {
            let object = InventoryObject { base: item.clone() };
            let point = object.attach_point();
            let name = item.base.name();
            let description = item.description();
            self.send_attach_packet(
                item.base.uuid(),
                item.base.owner_id(),
                &name,
                &description,
                item.permissions(),
                item.flags(),
                point,
                replace,
            )?;
            self.record_attachment(item, point, replace);
        }
        drop(busy);
        Ok(())
    }

    pub(crate) fn native_detach(&self, item_id: UUID) -> Result<(), Error> {
        let busy = self.begin()?;
        self.send_detach_packet(item_id)?;
        write(&self.inner.attachments).retain(|attachment| attachment.item.base.uuid() != item_id);
        drop(busy);
        Ok(())
    }

    fn send_detach_packet(&self, item_id: UUID) -> Result<(), Error> {
        let client = self.client()?;
        let network = client.native_network()?;
        let mut packet = DetachAttachmentIntoInvPacket::new_with_constructor()?;
        packet.object_data.agent_id = network.native_agent_id();
        packet.object_data.item_id = item_id;
        send_generated(
            &network,
            PacketType::DetachAttachmentIntoInv,
            packet.to_bytes_with_method()?,
        )
    }

    pub(crate) fn native_get_attachments(
        &self,
    ) -> Result<Box<dyn Iterator<Item = InventoryItem>>, Error> {
        self.client()?;
        let values: Vec<_> = read(&self.inner.attachments)
            .iter()
            .map(|attachment| attachment.item.clone())
            .collect();
        Ok(Box::new(values.into_iter()))
    }

    #[allow(clippy::unused_async)] // Preserves the mapped asynchronous API boundary.
    pub(crate) async fn native_get_attachments_by_point(
        &self,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<MultiValueDictionary<AttachmentPoint, InventoryItem>, Error> {
        check_cancelled(cancellation_token.as_ref())?;
        self.client()?;
        let result = MultiValueDictionary::new_with_constructor()?;
        for attachment in read(&self.inner.attachments).iter() {
            result.add(attachment.point, Some(attachment.item.clone()))?;
        }
        Ok(result)
    }

    pub(crate) fn native_get_attachments_by_item(
        &self,
    ) -> Result<HashMap<InventoryItem, AttachmentPoint>, Error> {
        self.client()?;
        Ok(read(&self.inner.attachments)
            .iter()
            .map(|attachment| (attachment.item.clone(), attachment.point))
            .collect())
    }

    pub(crate) fn native_get_attachments_by_id(
        &self,
    ) -> Result<HashMap<UUID, AttachmentPoint>, Error> {
        self.client()?;
        Ok(read(&self.inner.attachments)
            .iter()
            .map(|attachment| (attachment.item.base.uuid(), attachment.point))
            .collect())
    }

    pub(crate) fn native_is_item_attached(&self, item_id: UUID) -> bool {
        read(&self.inner.attachments)
            .iter()
            .any(|attachment| attachment.item.base.uuid() == item_id)
    }

    pub(crate) fn native_attachments_at(&self, point: AttachmentPoint) -> Vec<InventoryItem> {
        read(&self.inner.attachments)
            .iter()
            .filter(|attachment| attachment.point == point)
            .map(|attachment| attachment.item.clone())
            .collect()
    }

    pub(crate) fn native_attachment_point(&self, item_id: UUID) -> Option<AttachmentPoint> {
        read(&self.inner.attachments)
            .iter()
            .find(|attachment| attachment.item.base.uuid() == item_id)
            .map(|attachment| attachment.point)
    }

    #[allow(clippy::unused_async)] // Preserves the mapped asynchronous API boundary.
    pub(crate) async fn native_get_current_outfit_folder(
        &self,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<Option<InventoryFolder>, Error> {
        check_cancelled(cancellation_token.as_ref())?;
        let client = self.client()?;
        let Some(store) = client.native_inventory()?.native_store() else {
            return Ok(None);
        };
        let Some(id) = store.find_folder_for_type(FolderType::CurrentOutfit) else {
            return Ok(None);
        };
        store.get_value_or_default_with_uuid_a7c63fbe(id)
    }

    #[allow(clippy::unused_async)] // Preserves the mapped asynchronous API boundary.
    pub(crate) async fn native_get_folder_wearables(
        &self,
        folder: UUID,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<(bool, Vec<InventoryWearable>, Vec<InventoryItem>), Error> {
        check_cancelled(cancellation_token.as_ref())?;
        let client = self.client()?;
        let Some(store) = client.native_inventory()?.native_store() else {
            return Ok((false, Vec::new(), Vec::new()));
        };
        let mut queue = VecDeque::from([folder]);
        let mut visited = HashSet::new();
        let mut scheduled = HashSet::from([folder]);
        let mut wearables = Vec::new();
        let mut attachments = Vec::new();
        while let Some(folder_id) = queue.pop_front() {
            check_cancelled(cancellation_token.as_ref())?;
            if !visited.insert(folder_id) {
                continue;
            }
            let Ok(contents) = store.get_contents_with_uuid(folder_id) else {
                return Ok((false, wearables, attachments));
            };
            for value in contents {
                if let Some(child) = value.as_any().downcast_ref::<InventoryFolder>() {
                    let child_id = child.base.uuid();
                    if scheduled.insert(child_id) {
                        if scheduled.len() > MAX_FOLDER_TRAVERSAL {
                            return Ok((false, wearables, attachments));
                        }
                        queue.push_back(child_id);
                    }
                    continue;
                }
                let Some(item) = inventory_item(value.as_ref()) else {
                    continue;
                };
                if matches!(item.asset_type(), AssetType::Bodypart | AssetType::Clothing) {
                    wearables.push(InventoryWearable { base: item });
                } else if matches!(item.asset_type(), AssetType::Object | AssetType::Mesh) {
                    attachments.push(item);
                }
            }
        }
        Ok((true, wearables, attachments))
    }

    pub(crate) async fn native_request_agent_worn(
        &self,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<Vec<InventoryBase>, Error> {
        let client = self.client()?;
        let inventory = client.native_inventory()?;
        let store = inventory.native_store().ok_or(Error::InvalidOperation)?;
        let folder = self
            .native_get_current_outfit_folder(cancellation_token.clone())
            .await?;
        let contents = if let Some(folder) = folder {
            inventory
                .native_folder_contents(
                    folder.base.uuid(),
                    folder.base.owner_id(),
                    true,
                    true,
                    InventorySortOrder::BY_DATE,
                    cancellation_token.clone(),
                    true,
                )
                .await?
        } else {
            Vec::new()
        };
        if contents.len() > MAX_COF_ENTRIES {
            return Err(Error::InvalidOperation);
        }
        check_cancelled(cancellation_token.as_ref())?;
        let mut worn = Vec::new();
        let mut wearables = Vec::new();
        let mut attachments = Vec::new();
        for value in contents {
            check_cancelled(cancellation_token.as_ref())?;
            let Some(link) = inventory_item(value.as_ref()) else {
                continue;
            };
            let target_id = if link.is_link()? {
                link.asset_uuid()
            } else {
                link.base.uuid()
            };
            let Some(item) = store.native_item_value(target_id) else {
                continue;
            };
            worn.push(item.base.clone());
            if let Some(record) = WearableRecord::from_item(&item) {
                wearables.push(record);
            } else if matches!(item.asset_type(), AssetType::Object | AssetType::Mesh) {
                let point = if item.inventory_type() == InventoryType::ATTACHMENT {
                    InventoryAttachment { base: item.clone() }.attachment_point()
                } else {
                    InventoryObject { base: item.clone() }.attach_point()
                };
                attachments.push(AttachmentRecord { item, point });
            }
        }
        *write(&self.inner.wearables) = wearables;
        self.inner
            .server_visual_parameters
            .store(false, Ordering::Release);
        *write(&self.inner.attachments) = attachments;
        self.inner
            .events
            .agent_wearables_reply
            .emit_with(|| AgentWearablesReplyEventArgs);
        Ok(worn)
    }

    #[allow(clippy::unnecessary_wraps)] // Public compatibility methods share the crate error model.
    pub(crate) fn native_get_current_param_values(&self) -> Result<HashMap<i32, f32>, Error> {
        Ok(crate::visual_catalog::decode_visual_params(&read(
            &self.inner.visual_parameters,
        )))
    }

    pub(crate) fn native_server_baking_region(&self) -> Result<bool, Error> {
        let protocols = self
            .client()?
            .native_network()?
            .native_current_sim()
            .map_or(0, |simulator| simulator.protocols.0);
        Ok(protocols & crate::RegionProtocols::AGENT_APPEARANCE_SERVICE.0 != 0)
    }

    fn native_server_baking_available(&self) -> Result<bool, Error> {
        if !self.native_server_baking_region()? {
            return Ok(false);
        }
        let simulator = self
            .client()?
            .native_network()?
            .native_current_sim()
            .ok_or(Error::InvalidOperation)?;
        let Some(caps) = simulator.caps.as_ref() else {
            return Ok(false);
        };
        Ok(caps
            .capability_uri("UpdateAvatarAppearance".to_owned())?
            .is_some())
    }

    fn native_request_cached_bakes_sent(&self) -> Result<bool, Error> {
        let client = self.client()?;
        let network = client.native_network()?;
        let mut packet = AgentCachedTexturePacket::new_with_constructor()?;
        packet.agent_data.agent_id = network.native_agent_id();
        packet.agent_data.session_id = network.native_session_id();
        packet.agent_data.serial_num = self.inner.cache_serial.fetch_add(1, Ordering::AcqRel) + 1;
        let wearables = read(&self.inner.wearables);
        let map = wearable_bake_map();
        let salts = baked_texture_hash();
        for baked_index in 0..11_usize {
            if baked_index == BakeType::Skirt.0 as usize
                && !wearables
                    .iter()
                    .any(|item| item.wearable_type == WearableType::Skirt)
            {
                continue;
            }
            let mut hash = UUID::zero();
            for wearable_type in &map[baked_index] {
                for item in wearables
                    .iter()
                    .filter(|item| item.wearable_type == *wearable_type)
                {
                    hash = UUID::bitxor(hash, item.asset_id);
                }
            }
            if hash != UUID::zero() {
                let mut block = AgentCachedTexturePacketWearableDataBlock::new_with_constructor()?;
                block.id = UUID::bitxor(hash, salts[baked_index]);
                block.texture_index = u8::try_from(baked_index).map_err(|_| Error::Argument)?;
                packet.wearable_data.push(block);
            }
        }
        drop(wearables);
        if packet.wearable_data.is_empty() {
            return Ok(false);
        }
        send_generated(
            &network,
            PacketType::AgentCachedTexture,
            packet.to_bytes_with_method()?,
        )?;
        Ok(true)
    }

    pub(crate) fn native_request_cached_bakes(&self) -> Result<(), Error> {
        self.native_request_cached_bakes_sent().map(|_| ())
    }

    async fn native_gather_legacy_wearables(&self, token: CancellationToken) -> Result<(), Error> {
        token.throw_if_cancellation_requested()?;
        let network = self.client()?.native_network()?;
        let generation = self
            .inner
            .wearables_reply_generation
            .load(Ordering::Acquire);
        let notified = self.inner.wearables_notify.notified();
        let mut packet = AgentWearablesRequestPacket::new_with_constructor()?;
        packet.agent_data.agent_id = network.native_agent_id();
        packet.agent_data.session_id = network.native_session_id();
        send_generated(
            &network,
            PacketType::AgentWearablesRequest,
            packet.to_bytes_with_method()?,
        )?;
        if self
            .inner
            .wearables_reply_generation
            .load(Ordering::Acquire)
            == generation
        {
            tokio::select! {
                () = notified => {},
                () = token.cancelled() => return Err(Error::Cancelled),
                () = tokio::time::sleep(std::time::Duration::from_secs(30)) => {},
            }
        }
        token.throw_if_cancellation_requested()?;
        if read(&self.inner.wearables).is_empty() {
            Err(Error::InvalidOperation)
        } else {
            Ok(())
        }
    }

    async fn native_refresh_worn_for_bake(
        &self,
        use_server_baking: bool,
        token: CancellationToken,
    ) -> Result<(), Error> {
        match self.native_request_agent_worn(Some(token.clone())).await {
            Ok(_) => {}
            Err(Error::Cancelled) => return Err(Error::Cancelled),
            Err(error) if use_server_baking => return Err(error),
            Err(_) => {
                write(&self.inner.wearables).clear();
                write(&self.inner.attachments).clear();
            }
        }
        if !use_server_baking && read(&self.inner.wearables).is_empty() {
            self.native_gather_legacy_wearables(token).await?;
        }
        Ok(())
    }

    pub(crate) fn native_make_appearance_packet(&self) -> Result<AgentSetAppearancePacket, Error> {
        let client = self.client()?;
        let network = client.native_network()?;
        let mut packet = AgentSetAppearancePacket::new_with_constructor()?;
        packet.agent_data.agent_id = network.native_agent_id();
        packet.agent_data.session_id = network.native_session_id();
        packet.agent_data.serial_num =
            u32::try_from(self.inner.appearance_serial.fetch_add(1, Ordering::AcqRel) + 1)
                .unwrap_or(u32::MAX);

        let assets = read(&self.inner.wearable_assets);
        let server_visual = read(&self.inner.visual_parameters).clone();
        let (bytes, param_values) = if self.inner.server_visual_parameters.load(Ordering::Acquire)
            && !server_visual.is_empty()
        {
            let values = crate::visual_catalog::decode_visual_params(&server_visual);
            (server_visual, values)
        } else {
            let params = VisualParams::params();
            let wearing_physics = read(&self.inner.wearables)
                .iter()
                .any(|wearable| wearable.wearable_type == WearableType::Physics);
            let parameter_count = if wearing_physics { 251 } else { 218 };
            let mut values = HashMap::new();
            let bytes = VisualParams::group0_param_ids()
                .into_iter()
                .take(parameter_count)
                .filter_map(|param_id| {
                    let param = params.0.get(&param_id)?;
                    let param_value = assets
                        .values()
                        .find_map(|asset| asset.params.get(&param_id).copied())
                        .unwrap_or(param.default_value);
                    values.insert(param_id, param_value);
                    let range = param.max_value - param.min_value;
                    Some(if range.abs() <= f32::EPSILON {
                        0
                    } else {
                        quantize_visual_param((param_value - param.min_value) / range)
                    })
                })
                .collect();
            (bytes, values)
        };
        drop(assets);
        for value in bytes.iter().copied() {
            let mut block = AgentSetAppearancePacketVisualParamBlock::new_with_constructor()?;
            block.param_value = value;
            packet.visual_param.push(block);
        }
        *write(&self.inner.visual_parameters) = bytes;

        let texture_ids: Vec<_> = read(&self.inner.texture_slots)
            .iter()
            .map(|slot| slot.texture_id)
            .collect();
        packet.object_data.texture_entry = encode_texture_entry(&texture_ids)?;
        let map = wearable_bake_map();
        let salts = baked_texture_hash();
        let wearables = read(&self.inner.wearables);
        for baked_index in 0..11_usize {
            let mut hash = UUID::zero();
            for wearable_type in &map[baked_index] {
                for item in wearables
                    .iter()
                    .filter(|item| item.wearable_type == *wearable_type)
                {
                    hash = UUID::bitxor(hash, item.asset_id);
                }
            }
            if hash != UUID::zero() {
                hash = UUID::bitxor(hash, salts[baked_index]);
            }
            let mut block = AgentSetAppearancePacketWearableDataBlock::new_with_constructor()?;
            block.cache_id = hash;
            block.texture_index = bake_index_to_texture_index()[baked_index];
            packet.wearable_data.push(block);
        }
        let value = |id, default| param_values.get(&id).copied().unwrap_or(default);
        let height = 1.706
            + value(692, 0.0) * 0.1918
            + value(842, 0.0) * 0.0375
            + value(33, 0.0) * 0.12022
            + value(682, 0.5) * 0.01117
            + value(756, 0.0) * 0.038
            + value(198, 0.0) * 0.08
            + value(503, 0.0) * 0.07;
        packet.agent_data.size = Vector3::new_with_single_single_single(0.45, 0.6, height)?;
        Ok(packet)
    }

    pub(crate) async fn native_request_set_appearance(
        &self,
        force_rebake: bool,
    ) -> Result<(), Error> {
        let source = CancellationTokenSource::new();
        if let Some(previous) = mutex(&self.inner.appearance_cancel).replace(source.clone()) {
            previous.cancel();
        }
        let token = source.token();
        let _permit = self
            .inner
            .appearance_gate
            .acquire()
            .await
            .map_err(|_| Error::InvalidOperation)?;
        token.throw_if_cancellation_requested()?;
        self.inner.busy.store(true, Ordering::Release);
        let _busy = BusyGuard(&self.inner.busy);
        let old_slots = clone_texture_slots(&read(&self.inner.texture_slots))?;
        let old_assets = read(&self.inner.wearable_assets).clone();
        let old_visual_parameters = read(&self.inner.visual_parameters).clone();
        let old_server_visual = self.inner.server_visual_parameters.load(Ordering::Acquire);
        let old_cof_version = self.inner.last_cof_version.load(Ordering::Acquire);
        let result = async {
            let start_simulator = self
                .client()?
                .native_network()?
                .native_current_sim()
                .ok_or(Error::InvalidOperation)?;
            let use_server_baking = self.native_server_baking_available()?;
            self.native_refresh_worn_for_bake(use_server_baking, token.clone())
                .await?;
            if force_rebake {
                let mut slots = write(&self.inner.texture_slots);
                for target in bake_index_to_texture_index() {
                    if let Some(slot) = slots.get_mut(usize::from(target)) {
                        slot.texture_id = UUID::zero();
                        slot.texture = None;
                    }
                }
            }
            if use_server_baking {
                self.native_server_bake(token.clone()).await?;
            } else {
                if !force_rebake && !read(&self.inner.wearables).is_empty() {
                    let generation = self.inner.cache_reply_generation.load(Ordering::Acquire);
                    let notified = self.inner.cache_notify.notified();
                    let request_sent = self.native_request_cached_bakes_sent()?;
                    if request_sent
                        && self.inner.cache_reply_generation.load(Ordering::Acquire) == generation
                    {
                        tokio::select! {
                            () = notified => {},
                            () = token.cancelled() => return Err(Error::Cancelled),
                            () = tokio::time::sleep(std::time::Duration::from_secs(30)) => {},
                        }
                    }
                    let _cache_reply_received =
                        self.inner.cache_reply_generation.load(Ordering::Acquire) != generation;
                }
                self.native_gather_baking_inputs(token.clone()).await?;
                self.native_local_bake(Some(token.clone()), force_rebake)
                    .await?;
            }
            token.throw_if_cancellation_requested()?;
            let packet = self.native_make_appearance_packet()?;
            let network = self.client()?.native_network()?;
            if network.native_current_sim().as_ref() != Some(&start_simulator) {
                return Err(Error::InvalidOperation);
            }
            send_generated(
                &network,
                PacketType::AgentSetAppearance,
                packet.to_bytes_with_method()?,
            )
        }
        .await;
        if result.is_err() {
            *write(&self.inner.texture_slots) = old_slots;
            *write(&self.inner.wearable_assets) = old_assets;
            *write(&self.inner.visual_parameters) = old_visual_parameters;
            self.inner
                .server_visual_parameters
                .store(old_server_visual, Ordering::Release);
            self.inner
                .last_cof_version
                .store(old_cof_version, Ordering::Release);
        }
        // Never invoke callbacks while the manager's state locks are held.
        let success = result.is_ok();
        self.inner
            .events
            .appearance_set
            .emit_with(|| AppearanceSetEventArgs { success });
        result
    }

    async fn native_server_bake(&self, token: CancellationToken) -> Result<(), Error> {
        let cof = self
            .native_get_current_outfit_folder(Some(token.clone()))
            .await?
            .ok_or(Error::InvalidOperation)?;
        if cof.version() < 0 {
            return Err(Error::InvalidOperation);
        }
        let simulator = self
            .client()?
            .native_network()?
            .native_current_sim()
            .ok_or(Error::InvalidOperation)?;
        let capability = simulator
            .caps
            .as_ref()
            .ok_or(Error::InvalidOperation)?
            .capability_uri("UpdateAvatarAppearance".to_owned())?
            .ok_or(Error::InvalidOperation)?;
        for retry in 0..5_u32 {
            token.throw_if_cancellation_requested()?;
            if retry > 0 {
                let delay = (1_u64 << retry).saturating_sub(1);
                tokio::select! {
                    () = tokio::time::sleep(std::time::Duration::from_secs(delay)) => {},
                    () = token.cancelled() => return Err(Error::Cancelled),
                }
            }
            let payload = OSD::Map(HashMap::from([(
                "cof_version".to_owned(),
                OSD::Integer(cof.version()),
            )]));
            let (_, bytes) = self
                .client()?
                .native_http_caps_client()
                .post_with_uri_osd_format_osd_cancellation_token_i_progress(
                    capability.clone(),
                    OSDFormat::Xml,
                    payload,
                    token.clone(),
                    None,
                )
                .await?;
            let OSD::Map(result) = OSDParser::deserialize_with_bytes(bytes)? else {
                continue;
            };
            if !result
                .get("success")
                .is_some_and(|value| value.as_boolean().unwrap_or(false))
            {
                continue;
            }
            let visual = result
                .get("visual_params")
                .map(OSD::as_binary)
                .transpose()?
                .unwrap_or_default();
            let textures = match result.get("textures") {
                Some(OSD::Array(values)) => values
                    .iter()
                    .map(OSD::as_uuid)
                    .collect::<Result<Vec<_>, _>>()?,
                _ => Vec::new(),
            };
            let required = [8_usize, 9, 10, 11];
            if required.iter().any(|&index| {
                textures
                    .get(index)
                    .is_none_or(|id| *id == UUID::zero() || *id == default_avatar_texture())
            }) {
                continue;
            }
            let mut slots: Vec<_> = (0..AvatarTextureIndex::NumberOfEntries as usize)
                .map(|_| texture_data_defaults().expect("texture defaults"))
                .collect();
            for (index, id) in textures.into_iter().take(slots.len()).enumerate() {
                slots[index].texture_id = id;
                slots[index].texture_index = i32::try_from(index)
                    .ok()
                    .and_then(avatar_texture_index_from_i32)
                    .unwrap_or(AvatarTextureIndex::Unknown);
            }
            *write(&self.inner.texture_slots) = slots;
            *write(&self.inner.visual_parameters) = visual;
            self.inner
                .server_visual_parameters
                .store(true, Ordering::Release);
            if let Some(version) = result
                .get("cof_version")
                .and_then(|value| value.as_integer().ok())
            {
                self.inner
                    .last_cof_version
                    .fetch_max(version, Ordering::AcqRel);
            }
            return Ok(());
        }
        Err(Error::InvalidOperation)
    }

    async fn native_gather_baking_inputs(&self, token: CancellationToken) -> Result<(), Error> {
        let records = read(&self.inner.wearables).clone();
        let assets = self.client()?.native_assets()?;
        let mut decoded = HashMap::new();
        let mut slots: Vec<_> = (0..AvatarTextureIndex::NumberOfEntries as usize)
            .map(|_| texture_data_defaults().expect("texture defaults"))
            .collect();
        {
            let current = read(&self.inner.texture_slots);
            for target in bake_index_to_texture_index() {
                if let (Some(source), Some(destination)) = (
                    current.get(usize::from(target)),
                    slots.get_mut(usize::from(target)),
                ) {
                    destination.texture_id = source.texture_id;
                    destination.texture_index = source.texture_index;
                }
            }
        }
        for record in records {
            token.throw_if_cancellation_requested()?;
            let Some(asset) = assets
                .request_asset_with_uuid_asset_type_boolean_cancellation_token(
                    record.asset_id,
                    record.asset_type,
                    true,
                    Some(token.clone()),
                )
                .await?
            else {
                return Err(Error::InvalidOperation);
            };
            let wearable_asset =
                crate::assets::AssetWearable::native_from_asset(&asset, record.wearable_type)?;
            let public = AppearanceManagerWearableData {
                asset: Some(wearable_asset.clone()),
                asset_id: record.asset_id,
                asset_type: record.asset_type,
                item_id: record.item_id,
                wearable_type: record.wearable_type,
            };
            decode_wearable_params(public, &mut slots)?;
            decoded.insert(record.item_id, wearable_asset);
        }

        let provider = self.native_texture_provider();
        let semaphore = Arc::new(tokio::sync::Semaphore::new(5));
        let mut requests = FuturesUnordered::new();
        for (index, slot) in slots.iter().enumerate() {
            if slot.texture_id == UUID::zero() {
                continue;
            }
            let provider = Arc::clone(&provider);
            let semaphore = Arc::clone(&semaphore);
            let request_token = token.clone();
            let texture_id = slot.texture_id;
            requests.push(async move {
                let _permit = semaphore
                    .acquire_owned()
                    .await
                    .map_err(|_| Error::InvalidOperation)?;
                request_token.throw_if_cancellation_requested()?;
                let texture = provider
                    .request_texture(texture_id, Some(request_token))
                    .await?
                    .ok_or(Error::InvalidOperation)?;
                Ok::<_, Error>((index, texture))
            });
        }
        while let Some(result) = requests.next().await {
            let (index, texture) = result?;
            slots[index].texture = Some(texture);
        }
        token.throw_if_cancellation_requested()?;
        *write(&self.inner.texture_slots) = slots;
        *write(&self.inner.wearable_assets) = decoded;
        self.inner
            .server_visual_parameters
            .store(false, Ordering::Release);
        Ok(())
    }

    async fn native_local_bake(
        &self,
        cancellation_token: Option<CancellationToken>,
        force_rebake: bool,
    ) -> Result<(), Error> {
        check_cancelled(cancellation_token.as_ref())?;
        // Texture decoding populates these slots. Baking an empty outfit is a
        // valid recovery path and simply retains the previous uploaded bakes.
        let indices: Vec<_> = read(&self.inner.texture_slots)
            .iter()
            .enumerate()
            .filter_map(|(index, slot)| slot.texture.as_ref().map(|_| index))
            .collect();
        if indices.is_empty() {
            return Ok(());
        }
        let mut pending_uploads = Vec::new();
        for bake_index in 0..11_i32 {
            check_cancelled(cancellation_token.as_ref())?;
            let bake_type = BakeType(bake_index);
            let target_index = bake_type_to_agent_texture_index(bake_type);
            if !force_rebake
                && read(&self.inner.texture_slots)
                    .get(target_index as usize)
                    .is_some_and(|slot| slot.texture_id != UUID::zero())
            {
                continue;
            }
            let required = bake_type_to_textures(bake_type);
            let mut compositor = crate::appearance_baker::Baker::native_new(bake_type)?;
            let mut layer_count = 0;
            for index in required {
                if let Some(slot) = read(&self.inner.texture_slots).get(index as usize)
                    && slot.texture.is_some()
                    && let Some(copy) = clone_texture_slot(slot)?
                {
                    compositor.native_add_texture(copy)?;
                    layer_count += 1;
                }
            }
            if layer_count == 0 {
                continue;
            }
            compositor.native_bake()?;
            let Some(baked_texture) = compositor.native_baked_texture() else {
                continue;
            };
            pending_uploads.push((bake_type_to_agent_texture_index(bake_type), baked_texture));
        }
        let assets = self.client()?.native_assets()?;
        let upload_gate = Arc::new(tokio::sync::Semaphore::new(6));
        let mut uploads = FuturesUnordered::new();
        for (target, baked_texture) in pending_uploads {
            let assets = assets.clone();
            let gate = Arc::clone(&upload_gate);
            let token = cancellation_token.clone();
            uploads.push(async move {
                let _permit = gate
                    .acquire_owned()
                    .await
                    .map_err(|_| Error::InvalidOperation)?;
                check_cancelled(token.as_ref())?;
                let uploaded = assets
                    .request_upload_baked_texture(baked_texture.native_bytes(), token)
                    .await?;
                Ok::<_, Error>((target, uploaded, baked_texture))
            });
        }
        while let Some(result) = uploads.next().await {
            let (target, uploaded, baked_texture) = result?;
            if uploaded == UUID::zero() {
                return Err(Error::InvalidOperation);
            }
            if let Some(slot) = write(&self.inner.texture_slots).get_mut(target as usize) {
                slot.texture_id = uploaded;
                slot.texture = Some(baked_texture);
                slot.texture_index = target;
            }
        }
        Ok(())
    }
}

fn clone_texture_slot(
    slot: &AppearanceManagerTextureData,
) -> Result<Option<AppearanceManagerTextureData>, Error> {
    Ok(Some(AppearanceManagerTextureData {
        alpha_masks: slot.alpha_masks.clone(),
        color: slot.color,
        texture: slot
            .texture
            .as_ref()
            .map(crate::assets::AssetTexture::native_deep_clone)
            .transpose()?,
        texture_id: slot.texture_id,
        texture_index: slot.texture_index,
    }))
}

fn clone_texture_slots(
    slots: &[AppearanceManagerTextureData],
) -> Result<Vec<AppearanceManagerTextureData>, Error> {
    slots
        .iter()
        .map(|slot| clone_texture_slot(slot)?.ok_or(Error::InvalidOperation))
        .collect()
}

fn encode_texture_entry(texture_ids: &[UUID]) -> Result<Vec<u8>, Error> {
    let default = default_avatar_texture();
    let mut output = default.get_bytes()?;
    for (face, id) in texture_ids.iter().copied().enumerate() {
        if id == UUID::zero() || id == default {
            continue;
        }
        let mask = 1_u64
            .checked_shl(u32::try_from(face).map_err(|_| Error::Argument)?)
            .ok_or(Error::Argument)?;
        output.extend_from_slice(&texture_entry_face_bitfield(mask));
        output.extend_from_slice(&id.get_bytes()?);
    }
    output.push(0); // Texture exception terminator.

    // Remaining default face attributes and their zero exception terminators.
    // BinaryWriter in the reference uses little-endian numeric values.
    output.extend_from_slice(&[0, 0, 0, 0, 0]); // inverted white RGBA + terminator
    output.extend_from_slice(&1.0_f32.to_le_bytes());
    output.push(0); // RepeatU
    output.extend_from_slice(&1.0_f32.to_le_bytes());
    output.push(0); // RepeatV
    for _ in 0..3 {
        output.extend_from_slice(&0_i16.to_le_bytes());
        output.push(0); // OffsetU, OffsetV, Rotation
    }
    output.extend_from_slice(&[0, 0]); // material + terminator
    output.extend_from_slice(&[0, 0]); // media + terminator
    output.extend_from_slice(&[0, 0]); // glow + terminator
    output.extend_from_slice(&UUID::zero().get_bytes()?);
    output.push(0); // material ID terminator
    output.extend_from_slice(&UUID::zero().get_bytes()?); // render material ID
    Ok(output)
}

fn texture_entry_face_bitfield(mask: u64) -> Vec<u8> {
    let byte_count = usize::try_from((u64::BITS - mask.leading_zeros()).div_ceil(7)).unwrap_or(1);
    (0..byte_count)
        .map(|index| {
            let shift = 7 * (byte_count - index - 1);
            let byte = u8::try_from((mask >> shift) & 0x7f).unwrap_or(0);
            if index + 1 < byte_count {
                byte | 0x80
            } else {
                byte
            }
        })
        .collect()
}

impl PartialEq for AppearanceManagerWearableData {
    fn eq(&self, other: &Self) -> bool {
        self.asset_id == other.asset_id
            && self.asset_type == other.asset_type
            && self.item_id == other.item_id
            && self.wearable_type == other.wearable_type
    }
}

impl Eq for AppearanceManagerWearableData {}

#[allow(clippy::unnecessary_wraps)] // The generated constructor is fallible across the API.
pub(crate) fn texture_data_defaults() -> Result<AppearanceManagerTextureData, Error> {
    Ok(AppearanceManagerTextureData {
        alpha_masks: HashMap::new(),
        color: Color4::white(),
        texture: None,
        texture_id: UUID::zero(),
        texture_index: AvatarTextureIndex::Unknown,
    })
}

pub(crate) fn texture_data_to_string(value: &AppearanceManagerTextureData) -> String {
    format!("{:?}: {}", value.texture_index, value.texture_id)
}

#[allow(clippy::unnecessary_wraps)] // The generated constructor is fallible across the API.
pub(crate) fn wearable_data_defaults() -> Result<AppearanceManagerWearableData, Error> {
    Ok(AppearanceManagerWearableData {
        asset: None,
        asset_id: UUID::zero(),
        asset_type: AssetType::Unknown,
        item_id: UUID::zero(),
        wearable_type: WearableType::Invalid,
    })
}

pub(crate) fn wearable_data_to_string(value: &AppearanceManagerWearableData) -> String {
    format!(
        "{:?}: item {}, asset {}",
        value.wearable_type, value.item_id, value.asset_id
    )
}

impl Eq for crate::VisualAlphaParam {}
impl Hash for crate::VisualAlphaParam {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.domain.to_bits().hash(state);
        self.multiply_blend.hash(state);
        self.skip_if_zero.hash(state);
        self.tga_file.hash(state);
    }
}

#[allow(clippy::needless_pass_by_value)] // Mirrors the by-value compatibility signature.
pub(crate) fn decode_wearable_params(
    wearable: AppearanceManagerWearableData,
    textures: &mut Vec<AppearanceManagerTextureData>,
) -> Result<(), Error> {
    let Some(asset) = wearable.asset.as_ref() else {
        return Ok(());
    };
    if textures.len() < AvatarTextureIndex::NumberOfEntries as usize {
        textures.resize_with(AvatarTextureIndex::NumberOfEntries as usize, || {
            texture_data_defaults().expect("texture defaults")
        });
    }
    let params = VisualParams::params();
    let mut masks = HashMap::new();
    let mut colors = Vec::new();
    for (&param_id, &value) in &asset.params {
        let Some(param) = params.0.get(&param_id) else {
            continue;
        };
        if let Some(Some(color)) = &param.color_params {
            let accepted = match wearable.wearable_type {
                WearableType::Tattoo => matches!(param_id, 1062..=1064),
                WearableType::Jacket => matches!(param_id, 809..=811),
                WearableType::Hair => matches!(param_id, 112..=115),
                WearableType::Skin => matches!(param_id, 108 | 110 | 111),
                _ => true,
            };
            if accepted {
                colors.push(crate::AppearanceManagerColorParamInfo {
                    value,
                    visual_color_param: color.clone(),
                    visual_param: param.clone(),
                    wearable_type: wearable.wearable_type,
                });
            }
        }
        if let Some(Some(alpha)) = &param.alpha_params
            && !alpha.tga_file.is_empty()
            && !param.is_bump_attribute
        {
            masks
                .entry(alpha.clone())
                .or_insert(if value.abs() < f32::EPSILON {
                    0.01
                } else {
                    value
                });
        }
        if let Some(drivers) = &param.drivers {
            for driver_id in drivers {
                if let Some(driver) = params.0.get(driver_id)
                    && let Some(Some(alpha)) = &driver.alpha_params
                    && !alpha.tga_file.is_empty()
                    && !driver.is_bump_attribute
                {
                    masks
                        .entry(alpha.clone())
                        .or_insert(if value.abs() < f32::EPSILON {
                            0.01
                        } else {
                            value
                        });
                }
            }
        }
    }
    let color = if colors.is_empty() {
        Color4::white()
    } else {
        color_from_params(colors)?
    };
    for (&index, &texture_id) in &asset.textures {
        let slot_index = index as usize;
        let Some(slot) = textures.get_mut(slot_index) else {
            continue;
        };
        slot.texture_index = index;
        slot.alpha_masks.clone_from(&masks);
        slot.color = color;
        let id = if texture_id == default_avatar_texture() {
            UUID::zero()
        } else {
            texture_id
        };
        if slot.texture_id != id {
            slot.texture_id = id;
            slot.texture = None;
        }
    }
    Ok(())
}

#[allow(
    clippy::cast_possible_truncation,
    clippy::cast_precision_loss,
    clippy::cast_sign_loss
)] // Reference color ramps select adjacent entries using normalized f32 positions.
pub(crate) fn color_from_params(
    params: Vec<crate::AppearanceManagerColorParamInfo>,
) -> Result<Color4, Error> {
    let mut result = Color4 {
        r: 0.0,
        g: 0.0,
        b: 0.0,
        a: 0.0,
    };
    for param in params {
        let colors = &param.visual_color_param.colors;
        if colors.is_empty() {
            continue;
        }
        let selected = if colors.len() == 1 {
            colors[0]
        } else {
            let range = param.visual_param.max_value - param.visual_param.min_value;
            let normalized = if range.abs() <= f32::EPSILON {
                0.0
            } else {
                ((param.value - param.visual_param.min_value) / range).clamp(0.0, 1.0)
            };
            let position = normalized * (colors.len() - 1) as f32;
            let lower = position.floor() as usize;
            let upper = (lower + 1).min(colors.len() - 1);
            Color4::lerp(colors[lower], colors[upper], position - lower as f32)?
        };
        result = match param.visual_color_param.operation {
            crate::VisualColorOperation::Add => Color4::add(result, selected),
            crate::VisualColorOperation::Multiply => Color4::mul(result, selected),
            crate::VisualColorOperation::Blend => Color4::lerp(result, selected, param.value)?,
        };
    }
    Ok(result)
}

pub(crate) fn bake_index_to_texture_index() -> Vec<u8> {
    vec![8, 9, 10, 11, 19, 20, 40, 41, 42, 43, 44]
}

pub(crate) fn avatar_texture_index_from_i32(value: i32) -> Option<AvatarTextureIndex> {
    use AvatarTextureIndex as T;
    let values = [
        T::HeadBodypaint,
        T::UpperShirt,
        T::LowerPants,
        T::EyesIris,
        T::Hair,
        T::UpperBodypaint,
        T::LowerBodypaint,
        T::LowerShoes,
        T::HeadBaked,
        T::UpperBaked,
        T::LowerBaked,
        T::EyesBaked,
        T::LowerSocks,
        T::UpperJacket,
        T::LowerJacket,
        T::UpperGloves,
        T::UpperUndershirt,
        T::LowerUnderpants,
        T::Skirt,
        T::SkirtBaked,
        T::HairBaked,
        T::LowerAlpha,
        T::UpperAlpha,
        T::HeadAlpha,
        T::EyesAlpha,
        T::HairAlpha,
        T::HeadTattoo,
        T::UpperTattoo,
        T::LowerTattoo,
        T::HeadUniversalTattoo,
        T::UpperUniversalTattoo,
        T::LowerUniversalTattoo,
        T::SkirtTattoo,
        T::HairTattoo,
        T::EyesTattoo,
        T::LeftArmTattoo,
        T::LeftLegTattoo,
        T::Aux1Tattoo,
        T::Aux2Tattoo,
        T::Aux3Tattoo,
        T::LeftArmBaked,
        T::LegLegBaked,
        T::Aux1Baked,
        T::Aux2Baked,
        T::Aux3Baked,
    ];
    usize::try_from(value)
        .ok()
        .and_then(|index| values.get(index).copied())
}

fn uuid(value: &str) -> UUID {
    UUID::new_with_string(value.to_owned()).expect("fixed appearance UUID")
}

#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn quantize_visual_param(value: f32) -> u8 {
    (value.clamp(0.0, 1.0) * 255.0).round() as u8
}

pub(crate) fn baked_texture_hash() -> Vec<UUID> {
    [
        "18ded8d6-bcfc-e415-8539-944c0f5ea7a6",
        "338c29e3-3024-4dbb-998d-7c04cf4fa88f",
        "91b4a2c7-1b1a-ba16-9a16-1f8f8dcc1c3f",
        "b2cf28af-b840-1071-3c6a-78085d8128b5",
        "ea800387-ea1a-14e0-56cb-24f2022f969a",
        "0af1ef7c-ad24-11dd-8790-001f5bf833e8",
        "9d762b57-ffe3-2e34-d897-0c44c8e07c72",
        "e12f6f01-8b0e-e00a-03c7-bc7e56a6cbdc",
        "3e2984a2-f03c-71d5-3e97-75fb8c7e1e2f",
        "29bbb16c-4b0c-4809-8de5-4b4df7cca8ef",
        "e0f8b768-e68d-a0cc-d1d8-c2e7f3b51b6e",
    ]
    .into_iter()
    .map(uuid)
    .collect()
}

pub(crate) fn default_avatar_texture() -> UUID {
    uuid("c228d1cf-4b5d-4ba8-84f4-899a0796aa97")
}

pub(crate) fn img_use_baked(bake: BakeType) -> UUID {
    let value = match bake {
        BakeType::Head => "5a9f4a74-30f2-821c-b88d-70499d3e7183",
        BakeType::UpperBody => "ae2de45c-d252-50b8-5c6e-19f39ce79317",
        BakeType::LowerBody => "24daea5f-0539-cfcf-047f-fbc40b2786ba",
        BakeType::Eyes => "52cc6bb6-2ee5-e632-d3ad-50197b1dcb8a",
        BakeType::Skirt => "43529ce8-7faa-ad92-165a-bc4078371687",
        BakeType::Hair => "09aac1fb-6bce-0bee-7d44-caac6dbb6c63",
        BakeType::BakedLeftArm => "ff62763f-d60a-9855-890b-0c96f8f8cd98",
        BakeType::BakedLeftLeg => "8e915e25-31d1-cc95-ae08-d58a47488251",
        BakeType::BakedAux1 => "9742065b-19b5-297c-858a-29711d539043",
        BakeType::BakedAux2 => "03642e83-2bd1-4eb9-34b4-4c47ed586d2d",
        BakeType::BakedAux3 => "edd51b77-fc10-ce7a-4b3d-011dfc349e4f",
        _ => return UUID::zero(),
    };
    uuid(value)
}

pub(crate) fn img_use_baked_indices()
-> libremetaverse_types::compat::FrozenDictionary<UUID, AvatarTextureIndex> {
    libremetaverse_types::compat::FrozenDictionary(HashMap::from([
        (img_use_baked(BakeType::Head), AvatarTextureIndex::HeadBaked),
        (
            img_use_baked(BakeType::UpperBody),
            AvatarTextureIndex::UpperBaked,
        ),
        (
            img_use_baked(BakeType::LowerBody),
            AvatarTextureIndex::LowerBaked,
        ),
        (img_use_baked(BakeType::Eyes), AvatarTextureIndex::EyesBaked),
        (
            img_use_baked(BakeType::Skirt),
            AvatarTextureIndex::SkirtBaked,
        ),
        (img_use_baked(BakeType::Hair), AvatarTextureIndex::HairBaked),
        (
            img_use_baked(BakeType::BakedLeftArm),
            AvatarTextureIndex::LeftArmBaked,
        ),
        (
            img_use_baked(BakeType::BakedLeftLeg),
            AvatarTextureIndex::LegLegBaked,
        ),
        (
            img_use_baked(BakeType::BakedAux1),
            AvatarTextureIndex::Aux1Baked,
        ),
        (
            img_use_baked(BakeType::BakedAux2),
            AvatarTextureIndex::Aux2Baked,
        ),
        (
            img_use_baked(BakeType::BakedAux3),
            AvatarTextureIndex::Aux3Baked,
        ),
    ]))
}

pub(crate) fn wearable_bake_map() -> Vec<Vec<WearableType>> {
    use WearableType::{
        Alpha, Eyes, Gloves, Hair, Invalid, Jacket, Pants, Shape, Shirt, Shoes, Skin, Skirt, Socks,
        Tattoo, Underpants, Undershirt,
    };
    vec![
        vec![
            Shape, Skin, Tattoo, Hair, Alpha, Invalid, Invalid, Invalid, Invalid,
        ],
        vec![
            Shape, Skin, Tattoo, Shirt, Jacket, Gloves, Undershirt, Alpha, Invalid,
        ],
        vec![
            Shape, Skin, Tattoo, Pants, Shoes, Socks, Jacket, Underpants, Alpha,
        ],
        vec![
            Eyes, Invalid, Invalid, Invalid, Invalid, Invalid, Invalid, Invalid, Invalid,
        ],
        vec![
            Skirt, Invalid, Invalid, Invalid, Invalid, Invalid, Invalid, Invalid, Invalid,
        ],
        vec![
            Hair, Invalid, Invalid, Invalid, Invalid, Invalid, Invalid, Invalid, Invalid,
        ],
        vec![Invalid; 9],
        vec![Invalid; 9],
        vec![Invalid; 9],
        vec![Invalid; 9],
        vec![Invalid; 9],
    ]
}

pub(crate) const fn bake_type_to_agent_texture_index(bake: BakeType) -> AvatarTextureIndex {
    match bake {
        BakeType::Head => AvatarTextureIndex::HeadBaked,
        BakeType::UpperBody => AvatarTextureIndex::UpperBaked,
        BakeType::LowerBody => AvatarTextureIndex::LowerBaked,
        BakeType::Eyes => AvatarTextureIndex::EyesBaked,
        BakeType::Skirt => AvatarTextureIndex::SkirtBaked,
        BakeType::Hair => AvatarTextureIndex::HairBaked,
        BakeType::BakedLeftArm => AvatarTextureIndex::LeftArmBaked,
        BakeType::BakedLeftLeg => AvatarTextureIndex::LegLegBaked,
        BakeType::BakedAux1 => AvatarTextureIndex::Aux1Baked,
        BakeType::BakedAux2 => AvatarTextureIndex::Aux2Baked,
        BakeType::BakedAux3 => AvatarTextureIndex::Aux3Baked,
        _ => AvatarTextureIndex::Unknown,
    }
}

pub(crate) fn bake_type_to_textures(bake: BakeType) -> Vec<AvatarTextureIndex> {
    use AvatarTextureIndex as T;
    match bake {
        BakeType::Head => vec![
            T::HeadBodypaint,
            T::HeadTattoo,
            T::HeadUniversalTattoo,
            T::Hair,
            T::HeadAlpha,
        ],
        BakeType::UpperBody => vec![
            T::UpperBodypaint,
            T::UpperTattoo,
            T::UpperUniversalTattoo,
            T::UpperGloves,
            T::UpperUndershirt,
            T::UpperShirt,
            T::UpperJacket,
            T::UpperAlpha,
        ],
        BakeType::LowerBody => vec![
            T::LowerBodypaint,
            T::LowerTattoo,
            T::LowerUniversalTattoo,
            T::LowerUnderpants,
            T::LowerSocks,
            T::LowerShoes,
            T::LowerPants,
            T::LowerJacket,
            T::LowerAlpha,
        ],
        BakeType::Eyes => vec![T::EyesIris, T::EyesTattoo, T::EyesAlpha],
        BakeType::Skirt => vec![T::Skirt, T::SkirtTattoo],
        BakeType::Hair => vec![T::Hair, T::HairTattoo, T::HairAlpha],
        BakeType::BakedLeftArm => vec![T::LeftArmTattoo],
        BakeType::BakedLeftLeg => vec![T::LeftLegTattoo],
        BakeType::BakedAux1 => vec![T::Aux1Tattoo],
        BakeType::BakedAux2 => vec![T::Aux2Tattoo],
        BakeType::BakedAux3 => vec![T::Aux3Tattoo],
        _ => Vec::new(),
    }
}

pub(crate) const fn morph_layer_for_bake_type(bake: BakeType) -> AvatarTextureIndex {
    match bake {
        BakeType::Head | BakeType::Hair => AvatarTextureIndex::Hair,
        BakeType::UpperBody => AvatarTextureIndex::UpperShirt,
        BakeType::LowerBody => AvatarTextureIndex::LowerPants,
        BakeType::Skirt => AvatarTextureIndex::Skirt,
        BakeType::BakedLeftArm => AvatarTextureIndex::LeftArmTattoo,
        BakeType::BakedLeftLeg => AvatarTextureIndex::LeftLegTattoo,
        BakeType::BakedAux1 => AvatarTextureIndex::Aux1Tattoo,
        BakeType::BakedAux2 => AvatarTextureIndex::Aux2Tattoo,
        BakeType::BakedAux3 => AvatarTextureIndex::Aux3Tattoo,
        _ => AvatarTextureIndex::Unknown,
    }
}

pub(crate) const fn wearable_type_to_asset_type(wearable_type: WearableType) -> AssetType {
    match wearable_type {
        WearableType::Shape | WearableType::Skin | WearableType::Hair | WearableType::Eyes => {
            AssetType::Bodypart
        }
        WearableType::Invalid => AssetType::Unknown,
        _ => AssetType::Clothing,
    }
}

fn wearable_type_from_u8(value: u8) -> Option<WearableType> {
    use WearableType as W;
    [
        W::Shape,
        W::Skin,
        W::Hair,
        W::Eyes,
        W::Shirt,
        W::Pants,
        W::Shoes,
        W::Socks,
        W::Jacket,
        W::Gloves,
        W::Undershirt,
        W::Underpants,
        W::Skirt,
        W::Alpha,
        W::Tattoo,
        W::Physics,
        W::Universal,
    ]
    .get(usize::from(value))
    .copied()
    .or((value == u8::MAX).then_some(W::Invalid))
}

fn apply_wearables(
    current: &mut Vec<WearableRecord>,
    items: Vec<InventoryItem>,
    replace: bool,
) -> Result<(), Error> {
    let mut seen = HashSet::new();
    for item in items {
        let wearable = WearableRecord::from_item(&item).ok_or(Error::Argument)?;
        if !seen.insert(wearable.item_id)
            || current
                .iter()
                .any(|existing| existing.item_id == wearable.item_id)
        {
            continue;
        }
        if replace || wearable.asset_type == AssetType::Bodypart {
            current.retain(|existing| existing.wearable_type != wearable.wearable_type);
        } else if current
            .iter()
            .filter(|existing| existing.wearable_type == wearable.wearable_type)
            .count()
            >= MAX_WEARABLE_LAYERS
        {
            return Err(Error::InvalidOperation);
        }
        current.push(wearable);
    }
    Ok(())
}

#[allow(clippy::too_many_arguments)]
fn attachment_packet_bytes(
    agent_id: UUID,
    session_id: UUID,
    item_id: UUID,
    owner_id: UUID,
    name: &str,
    description: &str,
    permissions: Permissions,
    item_flags: u32,
    point: AttachmentPoint,
    replace: bool,
) -> Result<Vec<u8>, Error> {
    let mut packet = RezSingleAttachmentFromInvPacket::new_with_constructor()?;
    packet.agent_data.agent_id = agent_id;
    packet.agent_data.session_id = session_id;
    packet.object_data.item_id = item_id;
    packet.object_data.owner_id = owner_id;
    packet.object_data.name = name.as_bytes().to_vec();
    packet.object_data.description = description.as_bytes().to_vec();
    packet.object_data.everyone_mask = permissions.everyone_mask.0;
    packet.object_data.group_mask = permissions.group_mask.0;
    packet.object_data.next_owner_mask = permissions.next_owner_mask.0;
    packet.object_data.item_flags = item_flags;
    packet.object_data.attachment_pt = (point as u8) | if replace { 0 } else { ATTACHMENT_ADD };
    packet.to_bytes_with_method()
}

fn send_generated(
    network: &crate::NetworkManager,
    packet_type: PacketType,
    bytes: Vec<u8>,
) -> Result<(), Error> {
    let simulator = network
        .native_current_sim()
        .ok_or(Error::InvalidOperation)?;
    let length = i32::try_from(bytes.len()).map_err(|_| Error::Argument)?;
    simulator.native_send_packet_data(bytes, length, packet_type, false)
}

fn inventory_item(value: &dyn InventoryObjectClass) -> Option<InventoryItem> {
    if let Some(item) = value.as_any().downcast_ref::<InventoryItem>() {
        return Some(item.clone());
    }
    macro_rules! item {
        ($kind:ty) => {
            if let Some(item) = value.as_any().downcast_ref::<$kind>() {
                return Some(item.base.clone());
            }
        };
    }
    item!(crate::InventoryAnimation);
    item!(crate::InventoryAttachment);
    item!(crate::InventoryCallingCard);
    item!(crate::InventoryCategory);
    item!(crate::InventoryGesture);
    item!(crate::InventoryLSL);
    item!(crate::InventoryLandmark);
    item!(crate::InventoryMaterial);
    item!(crate::InventoryNotecard);
    item!(crate::InventoryObject);
    item!(crate::InventorySettings);
    item!(crate::InventorySnapshot);
    item!(crate::InventorySound);
    item!(crate::InventoryTexture);
    item!(crate::InventoryWearable);
    None
}

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

    fn wearable(id: u64, wearable_type: WearableType, asset_type: AssetType) -> InventoryItem {
        let mut wearable = InventoryWearable::new(UUID::new_with_u_int64(id).unwrap()).unwrap();
        wearable.base.set_asset_type(asset_type);
        wearable.base.set_inventory_type(InventoryType::WEARABLE);
        wearable
            .base
            .set_asset_uuid(UUID::new_with_u_int64(id + 1_000).unwrap());
        wearable.set_wearable_type(wearable_type);
        wearable.base
    }

    #[test]
    fn wearable_layers_are_bounded_and_bodyparts_replace_by_slot() {
        let mut clothing = Vec::new();
        apply_wearables(
            &mut clothing,
            (1..=60)
                .map(|id| wearable(id, WearableType::Shirt, AssetType::Clothing))
                .collect(),
            false,
        )
        .unwrap();
        assert_eq!(clothing.len(), MAX_WEARABLE_LAYERS);
        assert_eq!(
            apply_wearables(
                &mut clothing,
                vec![wearable(61, WearableType::Shirt, AssetType::Clothing)],
                false,
            ),
            Err(Error::InvalidOperation)
        );

        let mut bodyparts = Vec::new();
        apply_wearables(
            &mut bodyparts,
            vec![wearable(100, WearableType::Shape, AssetType::Bodypart)],
            false,
        )
        .unwrap();
        apply_wearables(
            &mut bodyparts,
            vec![wearable(101, WearableType::Shape, AssetType::Bodypart)],
            false,
        )
        .unwrap();
        assert_eq!(bodyparts.len(), 1);
        assert_eq!(bodyparts[0].item_id, UUID::new_with_u_int64(101).unwrap());
    }

    #[test]
    fn attachment_packet_and_replacement_state_preserve_inventory_fields() {
        let client = GridClient::new().unwrap();
        let item_id = UUID::new_with_u_int64(500).unwrap();
        let owner_id = UUID::new_with_u_int64(501).unwrap();
        let permissions = Permissions::new(1, 2, 3, 4, 5).unwrap();
        let mut item =
            InventoryItem::new_with_inventory_type_uuid(InventoryType::OBJECT, item_id).unwrap();
        item.base.set_owner_id(owner_id);
        item.base.set_name("recording hat".into());
        item.set_description("packet fields".into());
        item.set_permissions(permissions);
        item.set_flags(0x1234_5678);
        item.set_asset_type(AssetType::Object);

        let bytes = attachment_packet_bytes(
            UUID::zero(),
            UUID::zero(),
            item_id,
            owner_id,
            "recording hat",
            "packet fields",
            permissions,
            0x1234_5678,
            AttachmentPoint::Skull,
            false,
        )
        .unwrap();
        let mut position = 0;
        let packet =
            RezSingleAttachmentFromInvPacket::new_with_bytes_int32(bytes, &mut position).unwrap();
        assert_eq!(packet.object_data.item_id, item_id);
        assert_eq!(packet.object_data.owner_id, owner_id);
        assert_eq!(packet.object_data.name, b"recording hat");
        assert_eq!(packet.object_data.description, b"packet fields");
        assert_eq!(packet.object_data.everyone_mask, 2);
        assert_eq!(packet.object_data.group_mask, 3);
        assert_eq!(packet.object_data.next_owner_mask, 4);
        assert_eq!(packet.object_data.item_flags, 0x1234_5678);
        assert_eq!(
            packet.object_data.attachment_pt,
            (AttachmentPoint::Skull as u8) | ATTACHMENT_ADD
        );
        let manager = client.appearance();
        manager.record_attachment(item, AttachmentPoint::Skull, false);
        let replacement_id = UUID::new_with_u_int64(502).unwrap();
        let mut replacement =
            InventoryItem::new_with_inventory_type_uuid(InventoryType::OBJECT, replacement_id)
                .unwrap();
        replacement.set_permissions(permissions);
        manager.record_attachment(replacement, AttachmentPoint::Skull, true);
        let state = manager.native_get_attachments_by_id().unwrap();
        assert_eq!(state.len(), 1);
        assert_eq!(state.get(&replacement_id), Some(&AttachmentPoint::Skull));
    }

    #[test]
    fn appearance_protocol_constants_and_event_payloads_preserve_reference_values() {
        let expected = [
            "18ded8d6-bcfc-e415-8539-944c0f5ea7a6",
            "338c29e3-3024-4dbb-998d-7c04cf4fa88f",
            "91b4a2c7-1b1a-ba16-9a16-1f8f8dcc1c3f",
            "b2cf28af-b840-1071-3c6a-78085d8128b5",
            "ea800387-ea1a-14e0-56cb-24f2022f969a",
            "0af1ef7c-ad24-11dd-8790-001f5bf833e8",
            "9d762b57-ffe3-2e34-d897-0c44c8e07c72",
            "e12f6f01-8b0e-e00a-03c7-bc7e56a6cbdc",
            "3e2984a2-f03c-71d5-3e97-75fb8c7e1e2f",
            "29bbb16c-4b0c-4809-8de5-4b4df7cca8ef",
            "e0f8b768-e68d-a0cc-d1d8-c2e7f3b51b6e",
        ];
        assert_eq!(
            baked_texture_hash(),
            expected
                .into_iter()
                .map(|value| UUID::new_with_string(value.to_owned()).unwrap())
                .collect::<Vec<_>>()
        );
        assert!(AppearanceSetEventArgs::native_new(true).native_success());
        assert!(!AppearanceSetEventArgs::native_new(false).native_success());
        let texture_id = UUID::new_with_u_int64(700).unwrap();
        assert_eq!(
            RebakeAvatarTexturesEventArgs::native_new(texture_id).native_texture_id(),
            texture_id
        );
        assert_eq!(wearable_type_from_u8(0), Some(WearableType::Shape));
        assert_eq!(wearable_type_from_u8(16), Some(WearableType::Universal));
        assert_eq!(wearable_type_from_u8(u8::MAX), Some(WearableType::Invalid));
        assert_eq!(wearable_type_from_u8(17), None);
    }

    #[test]
    fn appearance_packet_contains_all_bakes_visual_parameters_and_texture_entry() {
        let client = GridClient::new().unwrap();
        let packet = client.appearance().native_make_appearance_packet().unwrap();
        assert_eq!(packet.wearable_data.len(), 11);
        assert_eq!(packet.visual_param.len(), 218);
        assert_eq!(packet.object_data.texture_entry.len(), 80);
        assert_eq!(
            &packet.object_data.texture_entry[..16],
            default_avatar_texture().get_bytes().unwrap().as_slice()
        );
        assert_eq!(packet.object_data.texture_entry[16], 0);
        let snapshot = client.appearance();
        assert_eq!(
            snapshot.my_visual_parameters.len(),
            packet.visual_param.len()
        );
        assert_eq!(
            snapshot.my_visual_parameters,
            packet
                .visual_param
                .iter()
                .map(|block| block.param_value)
                .collect::<Vec<_>>()
        );

        let physics_client = GridClient::new().unwrap();
        let physics_manager = physics_client.appearance();
        write(&physics_manager.inner.wearables).push(WearableRecord {
            item_id: UUID::new_with_u_int64(901).unwrap(),
            asset_id: UUID::new_with_u_int64(902).unwrap(),
            asset_type: AssetType::Clothing,
            wearable_type: WearableType::Physics,
        });
        let physics_packet = physics_manager.native_make_appearance_packet().unwrap();
        assert_eq!(physics_packet.visual_param.len(), 251);
    }

    #[test]
    fn texture_entry_encodes_face_bitfields_before_texture_ids() {
        let texture_id = UUID::new_with_u_int64(808).unwrap();
        let mut textures = vec![UUID::zero(); 9];
        textures[8] = texture_id;
        let encoded = encode_texture_entry(&textures).unwrap();
        assert_eq!(
            &encoded[..16],
            default_avatar_texture().get_bytes().unwrap().as_slice()
        );
        assert_eq!(&encoded[16..18], &[0x82, 0x00]);
        assert_eq!(&encoded[18..34], texture_id.get_bytes().unwrap().as_slice());
        assert_eq!(encoded[34], 0);
        assert_eq!(encoded.len(), 98);
    }

    #[test]
    fn legacy_wearables_packet_preserves_valid_item_asset_and_type_fields() {
        let client = GridClient::new().unwrap();
        let manager = client.appearance();
        manager.inner.busy.store(true, Ordering::Release);
        let item_id = UUID::new_with_u_int64(1_001).unwrap();
        let asset_id = UUID::new_with_u_int64(1_002).unwrap();
        let mut packet = AgentWearablesUpdatePacket::new_with_constructor().unwrap();
        let mut shirt =
            crate::packets::AgentWearablesUpdatePacketWearableDataBlock::new_with_constructor()
                .unwrap();
        shirt.item_id = item_id;
        shirt.asset_id = asset_id;
        shirt.wearable_type = WearableType::Shirt as u8;
        packet.wearable_data.push(shirt);
        let mut empty =
            crate::packets::AgentWearablesUpdatePacketWearableDataBlock::new_with_constructor()
                .unwrap();
        empty.wearable_type = WearableType::Pants as u8;
        packet.wearable_data.push(empty);

        handle_appearance_packet(
            &manager.inner,
            PacketType::AgentWearablesUpdate,
            packet.to_bytes_with_method().unwrap(),
        );

        let wearables = read(&manager.inner.wearables);
        assert_eq!(wearables.len(), 1);
        assert_eq!(wearables[0].item_id, item_id);
        assert_eq!(wearables[0].asset_id, asset_id);
        assert_eq!(wearables[0].asset_type, AssetType::Clothing);
        assert_eq!(wearables[0].wearable_type, WearableType::Shirt);
        assert_eq!(
            manager
                .inner
                .wearables_reply_generation
                .load(Ordering::Acquire),
            1
        );
    }

    #[tokio::test]
    async fn failed_appearance_request_emits_failure_and_empty_cache_request_does_not_wait() {
        let client = GridClient::new().unwrap();
        let manager = client.appearance();
        assert!(!manager.native_request_cached_bakes_sent().unwrap());

        let observed = Arc::new(Mutex::new(Vec::new()));
        let callback_observed = Arc::clone(&observed);
        let _subscription = manager.native_subscribe_appearance_set(Arc::new(move |event| {
            mutex(&callback_observed).push(event.native_success());
        }));

        assert_eq!(
            manager.native_request_set_appearance(false).await,
            Err(Error::InvalidOperation)
        );
        assert_eq!(*mutex(&observed), vec![false]);
        assert!(!manager.native_manager_busy());
    }
}
