//! Avatar profile and simulator-avatar state manager.

#![allow(
    clippy::needless_pass_by_value,
    clippy::too_many_lines,
    clippy::unnecessary_wraps,
    clippy::unused_self
)] // Public compatibility signatures and profile wire schemas are fixed.

use crate::client_core::ClientWeakHandle;
use crate::packet_catalog::PacketType;
use crate::packets::{
    AvatarAnimationPacket, AvatarInterestsReplyPacket, AvatarPickerRequestPacket,
    AvatarPropertiesRequestPacket, ClassifiedInfoRequestPacket, GenericMessagePacket,
    GenericMessagePacketParamListBlock, TrackAgentPacket, UUIDNameRequestPacket,
    UUIDNameRequestPacketUUIDNameBlockBlock,
};
use crate::{Error, GridClient};
use libremetaverse_structured_data::{OSD, OSDMap, OSDParser};
use libremetaverse_types::UUID;
use libremetaverse_types::compat::{CancellationToken, EventHandler, Subscription, Uri};
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex};

struct EventSlot<T> {
    next: AtomicU64,
    handlers: Mutex<HashMap<u64, EventHandler<T>>>,
}

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

impl<T: 'static> EventSlot<T> {
    fn subscribe(self: &Arc<Self>, handler: EventHandler<T>) -> Subscription {
        let id = self.next.fetch_add(1, Ordering::Relaxed);
        self.handlers
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .insert(id, handler);
        let weak = Arc::downgrade(self);
        Subscription::new(move || {
            if let Some(slot) = weak.upgrade() {
                slot.handlers
                    .lock()
                    .unwrap_or_else(std::sync::PoisonError::into_inner)
                    .remove(&id);
            }
        })
    }

    fn clear(&self) {
        self.handlers
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .clear();
    }
}

impl<T: Clone + 'static> EventSlot<T> {
    fn emit(&self, value: T) {
        let handlers = self
            .handlers
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .values()
            .cloned()
            .collect::<Vec<_>>();
        for handler in handlers {
            let value = value.clone();
            let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| handler(value)));
        }
    }
}

#[derive(Clone, Debug)]
pub struct Animation {
    pub animation_id: UUID,
    pub animation_sequence: i32,
    pub animation_source_object_id: UUID,
}

#[derive(Clone, Debug)]
pub struct AvatarAnimationEventArgs {
    animations: Vec<Animation>,
    avatar_id: UUID,
}

fn handle_avatar_animation(inner: &AvatarManagerInner, data: Vec<u8>) {
    if inner.disposed.load(Ordering::Acquire) {
        return;
    }
    let mut offset = 0;
    let Ok(packet) = AvatarAnimationPacket::new_with_bytes_int32(data, &mut offset) else {
        return;
    };
    if packet.animation_list.len() > 256 {
        return;
    }
    let animations = packet
        .animation_list
        .into_iter()
        .enumerate()
        .map(|(index, animation)| Animation {
            animation_id: animation.anim_id,
            animation_sequence: animation.anim_sequence_id,
            animation_source_object_id: packet
                .animation_source_list
                .get(index)
                .map_or_else(UUID::zero, |source| source.object_id),
        })
        .collect();
    inner.avatar_animation.emit(AvatarAnimationEventArgs {
        animations,
        avatar_id: packet.sender.id,
    });
}

fn decode_avatar_interests(data: Vec<u8>, avatar_id: UUID) -> Option<crate::AvatarInterests> {
    let mut offset = 0;
    let packet = AvatarInterestsReplyPacket::new_with_bytes_int32(data, &mut offset).ok()?;
    if packet.agent_data.avatar_id != avatar_id {
        return None;
    }
    Some(crate::AvatarInterests {
        languages_text: String::from_utf8_lossy(&packet.properties_data.languages_text)
            .trim_end_matches('\0')
            .to_owned(),
        skills_mask: packet.properties_data.skills_mask,
        skills_text: String::from_utf8_lossy(&packet.properties_data.skills_text)
            .trim_end_matches('\0')
            .to_owned(),
        want_to_mask: packet.properties_data.want_to_mask,
        want_to_text: String::from_utf8_lossy(&packet.properties_data.want_to_text)
            .trim_end_matches('\0')
            .to_owned(),
    })
}

impl AvatarAnimationEventArgs {
    pub(crate) fn native_new(avatar_id: UUID, animations: Vec<Animation>) -> Result<Self, Error> {
        if animations.len() > 256 {
            return Err(Error::Argument);
        }
        Ok(Self {
            animations,
            avatar_id,
        })
    }

    pub(crate) fn native_animations(&self) -> Vec<Animation> {
        self.animations.clone()
    }

    pub(crate) fn native_avatar_id(&self) -> UUID {
        self.avatar_id
    }
}

pub(crate) struct AvatarManagerInner {
    client: ClientWeakHandle,
    disposed: AtomicBool,
    avatar_animation: Arc<EventSlot<crate::AvatarAnimationEventArgs>>,
    avatar_appearance: Arc<EventSlot<crate::AvatarAppearanceEventArgs>>,
    avatar_classified_reply: Arc<EventSlot<crate::AvatarClassifiedReplyEventArgs>>,
    avatar_groups_reply: Arc<EventSlot<crate::AvatarGroupsReplyEventArgs>>,
    avatar_interests_reply: Arc<EventSlot<crate::AvatarInterestsReplyEventArgs>>,
    avatar_notes_reply: Arc<EventSlot<crate::AvatarNotesReplyEventArgs>>,
    avatar_picker_reply: Arc<EventSlot<crate::AvatarPickerReplyEventArgs>>,
    avatar_picks_reply: Arc<EventSlot<crate::AvatarPicksReplyEventArgs>>,
    avatar_properties_reply: Arc<EventSlot<crate::AvatarPropertiesReplyEventArgs>>,
    classified_info_reply: Arc<EventSlot<crate::ClassifiedInfoReplyEventArgs>>,
    display_name_update: Arc<EventSlot<crate::DisplayNameUpdateEventArgs>>,
    pick_info_reply: Arc<EventSlot<crate::PickInfoReplyEventArgs>>,
    uuid_name_reply: Arc<EventSlot<crate::UUIDNameReplyEventArgs>>,
    viewer_effect: Arc<EventSlot<crate::ViewerEffectEventArgs>>,
    viewer_effect_look_at: Arc<EventSlot<crate::ViewerEffectLookAtEventArgs>>,
    viewer_effect_point_at: Arc<EventSlot<crate::ViewerEffectPointAtEventArgs>>,
    subscriptions: Mutex<Vec<Subscription>>,
}

#[derive(Clone)]
pub struct AvatarManager(Arc<AvatarManagerInner>);

impl AvatarManager {
    pub(crate) fn native_new(client: Option<Arc<GridClient>>) -> Result<Self, Error> {
        let client = client.ok_or(Error::ArgumentNull)?;
        let inner = Arc::new(AvatarManagerInner {
            client: client.native_weak_handle(),
            disposed: AtomicBool::new(false),
            avatar_animation: Arc::default(),
            avatar_appearance: Arc::default(),
            avatar_classified_reply: Arc::default(),
            avatar_groups_reply: Arc::default(),
            avatar_interests_reply: Arc::default(),
            avatar_notes_reply: Arc::default(),
            avatar_picker_reply: Arc::default(),
            avatar_picks_reply: Arc::default(),
            avatar_properties_reply: Arc::default(),
            classified_info_reply: Arc::default(),
            display_name_update: Arc::default(),
            pick_info_reply: Arc::default(),
            uuid_name_reply: Arc::default(),
            viewer_effect: Arc::default(),
            viewer_effect_look_at: Arc::default(),
            viewer_effect_point_at: Arc::default(),
            subscriptions: Mutex::new(Vec::new()),
        });
        let weak = Arc::downgrade(&inner);
        let subscription = client
            .native_network()?
            .subscribe_raw_packet(Arc::new(move |event| {
                if event.packet_type != PacketType::AvatarAnimation {
                    return;
                }
                let Some(inner) = weak.upgrade() else { return };
                handle_avatar_animation(&inner, event.data);
            }));
        inner
            .subscriptions
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .push(subscription);
        Ok(Self(inner))
    }

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

    pub(crate) fn native_from_inner(inner: Arc<AvatarManagerInner>) -> Self {
        Self(inner)
    }

    pub(crate) fn native_dispose(&self) -> Result<(), Error> {
        self.0.disposed.store(true, Ordering::Release);
        self.0
            .subscriptions
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .clear();
        self.0.avatar_animation.clear();
        self.0.avatar_appearance.clear();
        self.0.avatar_classified_reply.clear();
        self.0.avatar_groups_reply.clear();
        self.0.avatar_interests_reply.clear();
        self.0.avatar_notes_reply.clear();
        self.0.avatar_picker_reply.clear();
        self.0.avatar_picks_reply.clear();
        self.0.avatar_properties_reply.clear();
        self.0.classified_info_reply.clear();
        self.0.display_name_update.clear();
        self.0.pick_info_reply.clear();
        self.0.uuid_name_reply.clear();
        self.0.viewer_effect.clear();
        self.0.viewer_effect_look_at.clear();
        self.0.viewer_effect_point_at.clear();
        Ok(())
    }

    pub(crate) fn native_subscribe_avatar_animation(
        &self,
        handler: EventHandler<crate::AvatarAnimationEventArgs>,
    ) -> Subscription {
        self.0.avatar_animation.subscribe(handler)
    }
    pub(crate) fn native_subscribe_avatar_appearance(
        &self,
        handler: EventHandler<crate::AvatarAppearanceEventArgs>,
    ) -> Subscription {
        self.0.avatar_appearance.subscribe(handler)
    }
    pub(crate) fn native_subscribe_avatar_classified_reply(
        &self,
        handler: EventHandler<crate::AvatarClassifiedReplyEventArgs>,
    ) -> Subscription {
        self.0.avatar_classified_reply.subscribe(handler)
    }
    pub(crate) fn native_subscribe_avatar_groups_reply(
        &self,
        handler: EventHandler<crate::AvatarGroupsReplyEventArgs>,
    ) -> Subscription {
        self.0.avatar_groups_reply.subscribe(handler)
    }
    pub(crate) fn native_subscribe_avatar_interests_reply(
        &self,
        handler: EventHandler<crate::AvatarInterestsReplyEventArgs>,
    ) -> Subscription {
        self.0.avatar_interests_reply.subscribe(handler)
    }
    pub(crate) fn native_subscribe_avatar_notes_reply(
        &self,
        handler: EventHandler<crate::AvatarNotesReplyEventArgs>,
    ) -> Subscription {
        self.0.avatar_notes_reply.subscribe(handler)
    }
    pub(crate) fn native_subscribe_avatar_picker_reply(
        &self,
        handler: EventHandler<crate::AvatarPickerReplyEventArgs>,
    ) -> Subscription {
        self.0.avatar_picker_reply.subscribe(handler)
    }
    pub(crate) fn native_subscribe_avatar_picks_reply(
        &self,
        handler: EventHandler<crate::AvatarPicksReplyEventArgs>,
    ) -> Subscription {
        self.0.avatar_picks_reply.subscribe(handler)
    }
    pub(crate) fn native_subscribe_avatar_properties_reply(
        &self,
        handler: EventHandler<crate::AvatarPropertiesReplyEventArgs>,
    ) -> Subscription {
        self.0.avatar_properties_reply.subscribe(handler)
    }
    pub(crate) fn native_subscribe_classified_info_reply(
        &self,
        handler: EventHandler<crate::ClassifiedInfoReplyEventArgs>,
    ) -> Subscription {
        self.0.classified_info_reply.subscribe(handler)
    }
    pub(crate) fn native_subscribe_display_name_update(
        &self,
        handler: EventHandler<crate::DisplayNameUpdateEventArgs>,
    ) -> Subscription {
        self.0.display_name_update.subscribe(handler)
    }
    pub(crate) fn native_subscribe_pick_info_reply(
        &self,
        handler: EventHandler<crate::PickInfoReplyEventArgs>,
    ) -> Subscription {
        self.0.pick_info_reply.subscribe(handler)
    }
    pub(crate) fn native_subscribe_uuid_name_reply(
        &self,
        handler: EventHandler<crate::UUIDNameReplyEventArgs>,
    ) -> Subscription {
        self.0.uuid_name_reply.subscribe(handler)
    }
    pub(crate) fn native_subscribe_viewer_effect(
        &self,
        handler: EventHandler<crate::ViewerEffectEventArgs>,
    ) -> Subscription {
        self.0.viewer_effect.subscribe(handler)
    }
    pub(crate) fn native_subscribe_viewer_effect_look_at(
        &self,
        handler: EventHandler<crate::ViewerEffectLookAtEventArgs>,
    ) -> Subscription {
        self.0.viewer_effect_look_at.subscribe(handler)
    }
    pub(crate) fn native_subscribe_viewer_effect_point_at(
        &self,
        handler: EventHandler<crate::ViewerEffectPointAtEventArgs>,
    ) -> Subscription {
        self.0.viewer_effect_point_at.subscribe(handler)
    }

    pub(crate) fn native_capability_available(&self, name: &str) -> Result<bool, Error> {
        if self.0.disposed.load(Ordering::Acquire) {
            return Ok(false);
        }
        let Some(client) = self.0.client.upgrade() else {
            return Ok(false);
        };
        let network = client.native_network()?;
        let Some(simulator) = network.native_current_sim() else {
            return Ok(false);
        };
        let Some(caps) = simulator.native_caps() else {
            return Ok(false);
        };
        Ok(caps.capability_uri(name.to_owned())?.is_some())
    }

    pub(crate) fn native_request_own_avatar_textures(&self) -> Result<(), Error> {
        if self.0.disposed.load(Ordering::Acquire) {
            return Err(Error::InvalidOperation);
        }
        let Some(client) = self.0.client.upgrade() else {
            return Ok(());
        };
        let network = client.native_network()?;
        if network.native_current_sim().is_none() {
            return Ok(());
        }
        self.send_generic(
            &network,
            "avatartexturesrequest",
            &[network.native_agent_id()],
        )
    }

    fn live_network(&self) -> Result<crate::NetworkManager, Error> {
        if self.0.disposed.load(Ordering::Acquire) {
            return Err(Error::InvalidOperation);
        }
        self.0
            .client
            .upgrade()
            .ok_or(Error::InvalidOperation)?
            .native_network()
    }

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

    fn send_generic(
        &self,
        network: &crate::NetworkManager,
        method: &str,
        parameters: &[UUID],
    ) -> Result<(), Error> {
        let mut packet = GenericMessagePacket::new_with_constructor()?;
        let agent_id = network.native_agent_id();
        packet.agent_data.agent_id = agent_id;
        packet.agent_data.session_id = network.native_session_id();
        packet.agent_data.transaction_id = UUID::zero();
        packet.method_data.method = method.as_bytes().to_vec();
        packet.method_data.invoice = UUID::zero();
        packet.param_list = parameters
            .iter()
            .map(|value| GenericMessagePacketParamListBlock {
                parameter: value.to_string().into_bytes(),
            })
            .collect();
        self.send_bytes(
            network,
            PacketType::GenericMessage,
            packet.to_bytes_with_method()?,
        )
    }

    pub(crate) fn native_request_track_agent(&self, target: UUID) -> Result<(), Error> {
        let network = self.live_network()?;
        let mut packet = TrackAgentPacket::new_with_constructor()?;
        packet.agent_data.agent_id = network.native_agent_id();
        packet.agent_data.session_id = network.native_session_id();
        packet.target_data.prey_id = target;
        self.send_bytes(
            &network,
            PacketType::TrackAgent,
            packet.to_bytes_with_method()?,
        )
    }

    pub(crate) fn native_request_avatar_names(&self, ids: &[UUID]) -> Result<(), Error> {
        const MAX_UUIDS_PER_PACKET: usize = 100;
        if ids.len() > 100_000 {
            return Err(Error::Argument);
        }
        let network = self.live_network()?;
        for chunk in ids.chunks(MAX_UUIDS_PER_PACKET) {
            let mut packet = UUIDNameRequestPacket::new_with_constructor()?;
            packet.uuid_name_block = chunk
                .iter()
                .copied()
                .map(|id| UUIDNameRequestPacketUUIDNameBlockBlock { id })
                .collect();
            self.send_bytes(
                &network,
                PacketType::UUIDNameRequest,
                packet.to_bytes_with_method()?,
            )?;
        }
        Ok(())
    }

    pub(crate) fn native_request_avatar_properties(&self, avatar_id: UUID) -> Result<(), Error> {
        let network = self.live_network()?;
        let mut packet = AvatarPropertiesRequestPacket::new_with_constructor()?;
        packet.agent_data.agent_id = network.native_agent_id();
        packet.agent_data.session_id = network.native_session_id();
        packet.agent_data.avatar_id = avatar_id;
        self.send_bytes(
            &network,
            PacketType::AvatarPropertiesRequest,
            packet.to_bytes_with_method()?,
        )
    }

    /// Requests the legacy profile-interest block and waits for the matching
    /// simulator reply.
    ///
    /// The modern `AgentProfile` capability includes groups and picks but does
    /// not include the five interest fields. Consumers that need a complete
    /// editable profile can pair this request with [`Self::request_agent_profile`].
    ///
    /// # Errors
    ///
    /// Returns an error if there is no active simulator, the request cannot be
    /// encoded or sent, the matching reply channel closes, or cancellation is
    /// requested.
    pub async fn request_avatar_interests(
        &self,
        avatar_id: UUID,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<crate::AvatarInterests, Error> {
        let network = self.live_network()?;
        let token = cancellation_token.unwrap_or_default();
        token.throw_if_cancellation_requested()?;
        let (sender, receiver) = tokio::sync::oneshot::channel();
        let sender = Arc::new(Mutex::new(Some(sender)));
        let callback = Arc::clone(&sender);
        let subscription = network.subscribe_raw_packet(Arc::new(move |event| {
            if event.packet_type != PacketType::AvatarInterestsReply {
                return;
            }
            let Some(interests) = decode_avatar_interests(event.data, avatar_id) else {
                return;
            };
            if let Some(sender) = callback
                .lock()
                .unwrap_or_else(std::sync::PoisonError::into_inner)
                .take()
            {
                let _ = sender.send(interests);
            }
        }));
        self.native_request_avatar_properties(avatar_id)?;
        let result = tokio::select! {
            value = receiver => value.map_err(|_| Error::InvalidOperation),
            () = token.cancelled() => Err(Error::Cancelled),
        };
        drop(subscription);
        result
    }

    pub(crate) fn native_request_avatar_name_search(
        &self,
        name: String,
        query_id: UUID,
    ) -> Result<(), Error> {
        if name.len() > 255 {
            return Err(Error::Argument);
        }
        let network = self.live_network()?;
        let mut packet = AvatarPickerRequestPacket::new_with_constructor()?;
        packet.agent_data.agent_id = network.native_agent_id();
        packet.agent_data.session_id = network.native_session_id();
        packet.agent_data.query_id = query_id;
        packet.data.name = name.into_bytes();
        self.send_bytes(
            &network,
            PacketType::AvatarPickerRequest,
            packet.to_bytes_with_method()?,
        )
    }

    pub(crate) fn native_request_classified_info(&self, classified_id: UUID) -> Result<(), Error> {
        let network = self.live_network()?;
        let mut packet = ClassifiedInfoRequestPacket::new_with_constructor()?;
        packet.agent_data.agent_id = network.native_agent_id();
        packet.agent_data.session_id = network.native_session_id();
        packet.data.classified_id = classified_id;
        self.send_bytes(
            &network,
            PacketType::ClassifiedInfoRequest,
            packet.to_bytes_with_method()?,
        )
    }

    pub(crate) fn native_request_generic(
        &self,
        method: &str,
        parameters: &[UUID],
    ) -> Result<(), Error> {
        let network = self.live_network()?;
        self.send_generic(&network, method, parameters)
    }

    pub(crate) async fn native_get_display_names(
        &self,
        ids: Vec<UUID>,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<
        (
            bool,
            Option<Vec<crate::AgentDisplayName>>,
            Option<Vec<UUID>>,
        ),
        Error,
    > {
        if ids.is_empty() || ids.len() > 10_000 {
            return Ok((false, None, None));
        }
        let Some(client) = self.0.client.upgrade() else {
            return Ok((false, None, None));
        };
        let token = cancellation_token.unwrap_or_else(|| client.cancellation_token());
        token.throw_if_cancellation_requested()?;
        let network = client.native_network()?;
        let Some(simulator) = network.native_current_sim() else {
            return Ok((false, None, None));
        };
        let Some(caps) = simulator.native_caps() else {
            return Ok((false, None, None));
        };
        let Some(base) = caps.capability_uri("GetDisplayNames".to_owned())? else {
            return Ok((false, None, None));
        };
        let http = client.native_http_caps_client();
        let mut names = Vec::new();
        let mut bad_ids = Vec::new();
        for chunk in ids.chunks(90) {
            token.throw_if_cancellation_requested()?;
            let query = chunk
                .iter()
                .map(ToString::to_string)
                .collect::<Vec<_>>()
                .join("&ids=");
            let separator = if base.0.contains('?') { '&' } else { '?' };
            let uri = Uri(format!("{}{separator}ids={query}", base.0));
            let (response, bytes) = http.get(uri, token.clone(), None).await?;
            if !response.is_success_status_code() {
                return Ok((false, None, None));
            }
            let OSD::Map(map) = OSDParser::deserialize_with_bytes(bytes)? else {
                return Ok((false, None, None));
            };
            let mut message = crate::messages::linden::GetDisplayNamesMessage::new()?;
            message.deserialize(OSDMap::new_with_dictionary(map)?)?;
            names.extend(message.agents);
            bad_ids.extend(message.bad_i_ds);
        }
        Ok((true, Some(names), Some(bad_ids)))
    }

    pub(crate) async fn native_request_agent_profile(
        &self,
        avatar_id: UUID,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<(bool, Option<crate::messages::linden::AgentProfileMessage>), Error> {
        let Some(client) = self.0.client.upgrade() else {
            return Ok((false, None));
        };
        let token = cancellation_token.unwrap_or_else(|| client.cancellation_token());
        token.throw_if_cancellation_requested()?;
        let network = client.native_network()?;
        let Some(simulator) = network.native_current_sim() else {
            return Ok((false, None));
        };
        let Some(caps) = simulator.native_caps() else {
            return Ok((false, None));
        };
        let Some(base) = caps.capability_uri("AgentProfile".to_owned())? else {
            return Ok((false, None));
        };
        let uri = Uri(format!("{}/{}", base.0.trim_end_matches('/'), avatar_id));
        let (response, bytes) = client
            .native_http_caps_client()
            .get(uri, token, None)
            .await?;
        if !response.is_success_status_code() {
            return Ok((false, None));
        }
        let OSD::Map(map) = OSDParser::deserialize_with_bytes(bytes)? else {
            return Ok((false, None));
        };
        let mut profile = crate::messages::linden::AgentProfileMessage::new()?;
        profile.deserialize(OSDMap::new_with_dictionary(map)?)?;
        Ok((true, Some(profile)))
    }
}

pub(crate) fn agent_profile_group_defaults() -> crate::messages::linden::AgentProfileMessageGroupData
{
    crate::messages::linden::AgentProfileMessageGroupData {
        description: String::new(),
        enabled: false,
        founder_id: UUID::zero(),
        id: UUID::zero(),
        image_id: UUID::zero(),
        is_mature_publish: false,
        is_open_enrollment: false,
        is_shown_in_search: false,
        name: String::new(),
    }
}

fn agent_profile_pick_defaults() -> crate::messages::linden::AgentProfileMessagePickData {
    crate::messages::linden::AgentProfileMessagePickData {
        description: String::new(),
        enabled: false,
        grid_x: 0.0,
        grid_y: 0.0,
        id: UUID::zero(),
        name: String::new(),
        parcel_id: UUID::zero(),
        parcel_name: String::new(),
        region_name: String::new(),
        region_x: 0.0,
        region_y: 0.0,
        region_z: 0.0,
        slurl: Uri("about:blank".to_owned()),
        snapshot_id: UUID::zero(),
    }
}

pub(crate) fn agent_profile_defaults() -> crate::messages::linden::AgentProfileMessage {
    crate::messages::linden::AgentProfileMessage {
        allowed_maturity: crate::SimAccess(0),
        avatar_id: UUID::zero(),
        caption_index: None,
        caption_text: None,
        customer_type: String::new(),
        display_name: String::new(),
        display_name_next_update: std::time::UNIX_EPOCH,
        first_life_about_text: String::new(),
        first_life_image_id: UUID::zero(),
        flags: crate::ProfileFlags(0),
        groups: Vec::new(),
        hide_age: None,
        home_page: String::new(),
        is_display_name_default: false,
        is_mature_profile: false,
        legacy_first_name: String::new(),
        legacy_last_name: String::new(),
        member_since: std::time::UNIX_EPOCH,
        notes: String::new(),
        partner_id: UUID::zero(),
        picks: Vec::new(),
        preferred_maturity: crate::SimAccess(0),
        second_life_about_text: String::new(),
        second_life_image_id: UUID::zero(),
        title: String::new(),
        username: String::new(),
    }
}

fn value(map: &OSDMap, key: &str) -> OSD {
    map.get(key).unwrap_or(OSD::Undefined)
}

pub(crate) fn deserialize_agent_profile(
    profile: &mut crate::messages::linden::AgentProfileMessage,
    map: &OSDMap,
) -> Result<(), Error> {
    profile.avatar_id = value(map, "id").as_uuid()?;
    profile.second_life_image_id = value(map, "sl_image_id").as_uuid()?;
    profile.first_life_image_id = value(map, "fl_image_id").as_uuid()?;
    profile.partner_id = value(map, "partner_id").as_uuid()?;
    profile.second_life_about_text = value(map, "sl_about_text").as_string()?;
    profile.first_life_about_text = value(map, "fl_about_text").as_string()?;
    profile.member_since = value(map, "member_since").as_date()?;
    profile.customer_type = value(map, "customer_type").as_string()?;
    profile.notes = value(map, "notes").as_string()?;
    profile.allowed_maturity = crate::SimAccess(
        u8::try_from(value(map, "allowed_maturity").as_integer()?).map_err(|_| Error::Argument)?,
    );
    profile.preferred_maturity = crate::SimAccess(
        u8::try_from(value(map, "preferred_maturity").as_integer()?)
            .map_err(|_| Error::Argument)?,
    );
    profile.display_name = value(map, "display_name").as_string()?;
    profile.display_name_next_update = value(map, "display_name_next_update").as_date()?;
    profile.is_display_name_default = value(map, "is_display_name_default").as_boolean()?;
    profile.username = value(map, "username").as_string()?;
    profile.legacy_first_name = value(map, "legacy_first_name").as_string()?;
    profile.legacy_last_name = value(map, "legacy_last_name").as_string()?;
    profile.is_mature_profile = value(map, "mature_profile").as_boolean()?;
    profile.title = value(map, "title").as_string()?;
    profile.home_page = value(map, "home_page").as_string()?;
    profile.hide_age = map
        .get("hide_age")
        .map(|entry| entry.as_boolean().map(Some))
        .transpose()?;
    profile.caption_index = map
        .get("charter_member")
        .map(|entry| entry.as_integer().map(Some))
        .transpose()?;
    profile.caption_text = map
        .get("caption")
        .map(|entry| entry.as_string())
        .transpose()?;
    let mut flags = 0;
    for (key, flag) in [
        ("online", crate::ProfileFlags::ONLINE.0),
        ("allow_publish", crate::ProfileFlags::ALLOW_PUBLISH.0),
        ("identified", crate::ProfileFlags::IDENTIFIED.0),
        ("transacted", crate::ProfileFlags::TRANSACTED.0),
    ] {
        if value(map, key).as_boolean()? {
            flags |= flag;
        }
    }
    profile.flags = crate::ProfileFlags(flags);
    profile.groups.clear();
    if let OSD::Array(groups) = value(map, "groups") {
        if groups.len() > 512 {
            return Err(Error::Argument);
        }
        for group in groups {
            let OSD::Map(group) = group else { continue };
            let group = OSDMap::new_with_dictionary(group)?;
            profile
                .groups
                .push(crate::messages::linden::AgentProfileMessageGroupData {
                    description: value(&group, "description").as_string()?,
                    enabled: value(&group, "enabled").as_boolean()?,
                    founder_id: value(&group, "founder_id").as_uuid()?,
                    id: value(&group, "id").as_uuid()?,
                    image_id: value(&group, "image_id").as_uuid()?,
                    is_mature_publish: value(&group, "mature_publish").as_boolean()?,
                    is_open_enrollment: value(&group, "open_enrollment").as_boolean()?,
                    is_shown_in_search: value(&group, "show_in_search").as_boolean()?,
                    name: value(&group, "name").as_string()?,
                });
        }
    }
    profile.picks.clear();
    if let OSD::Array(picks) = value(map, "picks") {
        if picks.len() > 512 {
            return Err(Error::Argument);
        }
        for pick in picks {
            let OSD::Map(pick) = pick else { continue };
            let pick = OSDMap::new_with_dictionary(pick)?;
            let mut result = agent_profile_pick_defaults();
            result.description = value(&pick, "description").as_string()?;
            result.enabled = value(&pick, "enabled").as_boolean()?;
            result.grid_x = value(&pick, "grid_x").as_real()?;
            result.grid_y = value(&pick, "grid_y").as_real()?;
            result.id = value(&pick, "id").as_uuid()?;
            result.name = value(&pick, "name").as_string()?;
            result.parcel_id = value(&pick, "parcel_id").as_uuid()?;
            result.parcel_name = value(&pick, "parcel_name").as_string()?;
            result.region_name = value(&pick, "region_name").as_string()?;
            result.region_x = value(&pick, "region_x").as_real()?;
            result.region_y = value(&pick, "region_y").as_real()?;
            result.region_z = value(&pick, "region_z").as_real()?;
            result.slurl = value(&pick, "slurl")
                .as_uri()?
                .unwrap_or_else(|| Uri("about:blank".to_owned()));
            result.snapshot_id = value(&pick, "snapshot_id").as_uuid()?;
            profile.picks.push(result);
        }
    }
    Ok(())
}

pub(crate) fn serialize_agent_profile(
    profile: &crate::messages::linden::AgentProfileMessage,
) -> Result<OSDMap, Error> {
    let mut map = std::collections::HashMap::from([
        ("id".to_owned(), OSD::UUID(profile.avatar_id)),
        (
            "sl_image_id".to_owned(),
            OSD::UUID(profile.second_life_image_id),
        ),
        (
            "fl_image_id".to_owned(),
            OSD::UUID(profile.first_life_image_id),
        ),
        ("partner_id".to_owned(), OSD::UUID(profile.partner_id)),
        (
            "sl_about_text".to_owned(),
            OSD::String(profile.second_life_about_text.clone()),
        ),
        (
            "fl_about_text".to_owned(),
            OSD::String(profile.first_life_about_text.clone()),
        ),
        ("member_since".to_owned(), OSD::Date(profile.member_since)),
        (
            "customer_type".to_owned(),
            OSD::String(profile.customer_type.clone()),
        ),
        ("notes".to_owned(), OSD::String(profile.notes.clone())),
        (
            "allowed_maturity".to_owned(),
            OSD::Integer(i32::from(profile.allowed_maturity.0)),
        ),
        (
            "preferred_maturity".to_owned(),
            OSD::Integer(i32::from(profile.preferred_maturity.0)),
        ),
        (
            "display_name".to_owned(),
            OSD::String(profile.display_name.clone()),
        ),
        (
            "display_name_next_update".to_owned(),
            OSD::Date(profile.display_name_next_update),
        ),
        (
            "is_display_name_default".to_owned(),
            OSD::Boolean(profile.is_display_name_default),
        ),
        ("username".to_owned(), OSD::String(profile.username.clone())),
        (
            "legacy_first_name".to_owned(),
            OSD::String(profile.legacy_first_name.clone()),
        ),
        (
            "legacy_last_name".to_owned(),
            OSD::String(profile.legacy_last_name.clone()),
        ),
        (
            "mature_profile".to_owned(),
            OSD::Boolean(profile.is_mature_profile),
        ),
        ("title".to_owned(), OSD::String(profile.title.clone())),
        (
            "home_page".to_owned(),
            OSD::String(profile.home_page.clone()),
        ),
    ]);
    for (key, flag) in [
        ("online", crate::ProfileFlags::ONLINE.0),
        ("allow_publish", crate::ProfileFlags::ALLOW_PUBLISH.0),
        ("identified", crate::ProfileFlags::IDENTIFIED.0),
        ("transacted", crate::ProfileFlags::TRANSACTED.0),
    ] {
        if profile.flags.0 & flag != 0 {
            map.insert(key.to_owned(), OSD::Boolean(true));
        }
    }
    if let Some(Some(value)) = profile.hide_age {
        map.insert("hide_age".to_owned(), OSD::Boolean(value));
    }
    if let Some(Some(value)) = profile.caption_index {
        map.insert("charter_member".to_owned(), OSD::Integer(value));
    } else if let Some(value) = &profile.caption_text {
        map.insert("caption".to_owned(), OSD::String(value.clone()));
    }
    if profile.groups.len() > 512 || profile.picks.len() > 512 {
        return Err(Error::Argument);
    }
    map.insert(
        "groups".to_owned(),
        OSD::Array(
            profile
                .groups
                .iter()
                .map(|group| {
                    OSD::Map(std::collections::HashMap::from([
                        (
                            "description".to_owned(),
                            OSD::String(group.description.clone()),
                        ),
                        ("enabled".to_owned(), OSD::Boolean(group.enabled)),
                        ("founder_id".to_owned(), OSD::UUID(group.founder_id)),
                        ("id".to_owned(), OSD::UUID(group.id)),
                        ("image_id".to_owned(), OSD::UUID(group.image_id)),
                        (
                            "mature_publish".to_owned(),
                            OSD::Boolean(group.is_mature_publish),
                        ),
                        ("name".to_owned(), OSD::String(group.name.clone())),
                        (
                            "open_enrollment".to_owned(),
                            OSD::Boolean(group.is_open_enrollment),
                        ),
                        (
                            "show_in_search".to_owned(),
                            OSD::Boolean(group.is_shown_in_search),
                        ),
                    ]))
                })
                .collect(),
        ),
    );
    map.insert(
        "picks".to_owned(),
        OSD::Array(
            profile
                .picks
                .iter()
                .map(|pick| {
                    OSD::Map(std::collections::HashMap::from([
                        (
                            "description".to_owned(),
                            OSD::String(pick.description.clone()),
                        ),
                        ("enabled".to_owned(), OSD::Boolean(pick.enabled)),
                        ("grid_x".to_owned(), OSD::Real(pick.grid_x)),
                        ("grid_y".to_owned(), OSD::Real(pick.grid_y)),
                        ("id".to_owned(), OSD::UUID(pick.id)),
                        ("name".to_owned(), OSD::String(pick.name.clone())),
                        ("parcel_id".to_owned(), OSD::UUID(pick.parcel_id)),
                        (
                            "parcel_name".to_owned(),
                            OSD::String(pick.parcel_name.clone()),
                        ),
                        (
                            "region_name".to_owned(),
                            OSD::String(pick.region_name.clone()),
                        ),
                        ("region_x".to_owned(), OSD::Real(pick.region_x)),
                        ("region_y".to_owned(), OSD::Real(pick.region_y)),
                        ("region_z".to_owned(), OSD::Real(pick.region_z)),
                        ("slurl".to_owned(), OSD::Uri(pick.slurl.clone())),
                        ("snapshot_id".to_owned(), OSD::UUID(pick.snapshot_id)),
                    ]))
                })
                .collect(),
        ),
    );
    OSDMap::new_with_dictionary(map)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::packets::{
        AvatarAnimationPacketAnimationListBlock, AvatarAnimationPacketAnimationSourceListBlock,
    };

    #[test]
    fn avatar_animation_packet_emits_complete_active_animation_list() {
        let client = GridClient::new().expect("client");
        let manager = client.avatars();
        let received = Arc::new(Mutex::new(Vec::new()));
        let values = Arc::clone(&received);
        let _subscription = manager.native_subscribe_avatar_animation(Arc::new(move |event| {
            values.lock().expect("event lock").push(event);
        }));
        let avatar_id = UUID::random().expect("avatar id");
        let animation_id = UUID::random().expect("animation id");
        let source_id = UUID::random().expect("source id");
        let mut packet = AvatarAnimationPacket::new_with_constructor().expect("packet");
        packet.sender.id = avatar_id;
        let mut animation =
            AvatarAnimationPacketAnimationListBlock::new_with_constructor().expect("animation");
        animation.anim_id = animation_id;
        animation.anim_sequence_id = 42;
        packet.animation_list.push(animation);
        let mut source =
            AvatarAnimationPacketAnimationSourceListBlock::new_with_constructor().expect("source");
        source.object_id = source_id;
        packet.animation_source_list.push(source);

        handle_avatar_animation(
            &manager.0,
            packet.to_bytes_with_method().expect("packet bytes"),
        );

        let events = received.lock().expect("event lock");
        assert_eq!(events.len(), 1);
        assert_eq!(events[0].native_avatar_id(), avatar_id);
        let animations = events[0].native_animations();
        assert_eq!(animations.len(), 1);
        assert_eq!(animations[0].animation_id, animation_id);
        assert_eq!(animations[0].animation_sequence, 42);
        assert_eq!(animations[0].animation_source_object_id, source_id);
    }

    #[test]
    fn avatar_animation_empty_list_signals_stopped_animations() {
        let client = GridClient::new().expect("client");
        let manager = client.avatars();
        let received = Arc::new(Mutex::new(Vec::new()));
        let values = Arc::clone(&received);
        let _subscription = manager.native_subscribe_avatar_animation(Arc::new(move |event| {
            values
                .lock()
                .expect("event lock")
                .push(event.native_animations().len());
        }));
        let mut packet = AvatarAnimationPacket::new_with_constructor().expect("packet");
        packet.sender.id = UUID::random().expect("avatar id");

        handle_avatar_animation(
            &manager.0,
            packet.to_bytes_with_method().expect("packet bytes"),
        );

        assert_eq!(*received.lock().expect("event lock"), [0]);
    }

    #[test]
    fn avatar_interests_reply_is_correlated_and_decoded() {
        let avatar_id = UUID::random().expect("avatar id");
        let mut packet = AvatarInterestsReplyPacket::new_with_constructor().expect("packet");
        packet.agent_data.avatar_id = avatar_id;
        packet.properties_data.languages_text = b"Rust, LSL\0".to_vec();
        packet.properties_data.skills_mask = 0x12;
        packet.properties_data.skills_text = b"Protocol testing\0".to_vec();
        packet.properties_data.want_to_mask = 0x34;
        packet.properties_data.want_to_text = b"Build things\0".to_vec();
        let bytes = packet.to_bytes_with_method().expect("packet bytes");

        assert!(decode_avatar_interests(bytes.clone(), UUID::zero()).is_none());
        let interests = decode_avatar_interests(bytes, avatar_id).expect("matching reply");
        assert_eq!(interests.languages_text, "Rust, LSL");
        assert_eq!(interests.skills_mask, 0x12);
        assert_eq!(interests.skills_text, "Protocol testing");
        assert_eq!(interests.want_to_mask, 0x34);
        assert_eq!(interests.want_to_text, "Build things");
    }
}
