//! Vivox session and participant state models.

use crate::{Error, VoiceGateway, VoicePosition};
use base64::Engine as _;
use libremetaverse_types::UUID;
use libremetaverse_types::compat::{EventHandler, Subscription};
use std::collections::HashMap;
use std::sync::{
    Arc, Mutex, Weak,
    atomic::{AtomicU64, Ordering},
};

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

#[derive(Default)]
struct UnitEvent {
    next_id: AtomicU64,
    handlers: Arc<Mutex<HashMap<u64, EventHandler<()>>>>,
}

impl UnitEvent {
    fn subscribe(&self, handler: Option<EventHandler<()>>) -> Subscription {
        let Some(handler) = handler else {
            return Subscription::detached();
        };
        let id = self.next_id.fetch_add(1, Ordering::Relaxed);
        lock(&self.handlers).insert(id, handler);
        let handlers: Weak<Mutex<HashMap<u64, EventHandler<()>>>> = Arc::downgrade(&self.handlers);
        Subscription::new(move || {
            if let Some(handlers) = handlers.upgrade() {
                lock(&handlers).remove(&id);
            }
        })
    }

    fn emit(&self) {
        let handlers: Vec<_> = lock(&self.handlers).values().cloned().collect();
        for handler in handlers {
            handler(());
        }
    }
}

struct VoiceSessionInner {
    connector: VoiceGateway,
    handle: String,
    is_spatial: bool,
    participants: Mutex<HashMap<String, VoiceParticipant>>,
    participant_added: UnitEvent,
    participant_updated: UnitEvent,
    participant_removed: UnitEvent,
}

#[derive(Clone)]
pub struct VoiceSession {
    pub region_name: String,
    inner: Arc<VoiceSessionInner>,
}

impl VoiceSession {
    pub fn new(conn: VoiceGateway, handle: String) -> Result<Self, Error> {
        if handle.len() > 16 * 1024 || handle.chars().any(char::is_control) {
            return Err(Error::Argument);
        }
        Ok(Self {
            region_name: String::new(),
            inner: Arc::new(VoiceSessionInner {
                connector: conn,
                handle,
                is_spatial: true,
                participants: Mutex::new(HashMap::new()),
                participant_added: UnitEvent::default(),
                participant_updated: UnitEvent::default(),
                participant_removed: UnitEvent::default(),
            }),
        })
    }

    #[must_use]
    pub fn subscribe_on_participant_added(
        &self,
        handler: Option<EventHandler<()>>,
    ) -> Subscription {
        self.inner.participant_added.subscribe(handler)
    }

    #[must_use]
    pub fn subscribe_on_participant_update(
        &self,
        handler: Option<EventHandler<()>>,
    ) -> Subscription {
        self.inner.participant_updated.subscribe(handler)
    }

    #[must_use]
    pub fn subscribe_on_participant_removed(
        &self,
        handler: Option<EventHandler<()>>,
    ) -> Subscription {
        self.inner.participant_removed.subscribe(handler)
    }

    pub fn set3_d_position(
        &self,
        speaker_position: VoicePosition,
        listener_position: VoicePosition,
    ) -> Result<(), Error> {
        self.inner.connector.session_set3_d_position(
            self.inner.handle.clone(),
            speaker_position,
            listener_position,
        )?;
        Ok(())
    }

    #[must_use]
    pub fn connector(&self) -> VoiceGateway {
        self.inner.connector.clone()
    }

    #[must_use]
    pub fn handle(&self) -> String {
        self.inner.handle.clone()
    }

    #[must_use]
    pub fn is_spatial(&self) -> bool {
        self.inner.is_spatial
    }

    pub(crate) fn close(&self) {
        lock(&self.inner.participants).clear();
    }

    pub(crate) fn add_participant(&self, uri: String) -> Result<(), Error> {
        let mut participants = lock(&self.inner.participants);
        if participants.contains_key(&uri) {
            return Ok(());
        }
        participants.insert(uri.clone(), VoiceParticipant::new(uri, self.clone())?);
        drop(participants);
        self.inner.participant_added.emit();
        Ok(())
    }

    pub(crate) fn remove_participant(&self, uri: &str) {
        if lock(&self.inner.participants).remove(uri).is_some() {
            self.inner.participant_removed.emit();
        }
    }

    pub(crate) fn update_participant(
        &self,
        uri: &str,
        muted: bool,
        speaking: bool,
        volume: i32,
        energy: f32,
    ) {
        let mut participants = lock(&self.inner.participants);
        let Some(participant) = participants.get_mut(uri) else {
            return;
        };
        participant.is_muted = muted;
        participant.is_speaking = speaking;
        participant.volume = volume;
        participant.energy = energy;
        drop(participants);
        self.inner.participant_updated.emit();
    }
}

#[derive(Clone)]
pub struct VoiceParticipant {
    avatar_name: String,
    energy: f32,
    id: UUID,
    is_muted: bool,
    is_speaking: bool,
    uri: String,
    volume: i32,
    session: VoiceSession,
}

impl VoiceParticipant {
    pub fn new(puri: String, s: VoiceSession) -> Result<Self, Error> {
        if puri.len() > 16 * 1024 || puri.chars().any(char::is_control) {
            return Err(Error::Argument);
        }
        Ok(Self {
            id: Self::id_from_name(puri.clone())?,
            avatar_name: String::new(),
            energy: 0.0,
            is_muted: false,
            is_speaking: false,
            uri: puri,
            volume: 0,
            session: s,
        })
    }

    pub fn id_from_name(in_name: String) -> Result<UUID, Error> {
        let bare = in_name
            .strip_prefix("sip:")
            .and_then(|value| value.split_once('@').map(|(name, _)| name))
            .unwrap_or(&in_name);
        if bare.len() != 25 || !bare.starts_with('x') || !bare.ends_with("==") {
            return Ok(UUID::zero());
        }
        let encoded = bare[1..].replace('-', "+").replace('_', "/");
        let bytes = base64::engine::general_purpose::STANDARD
            .decode(encoded)
            .map_err(|_| Error::Argument)?;
        if bytes.len() != 16 {
            return Err(Error::Argument);
        }
        UUID::new_with_bytes_int32(bytes, 0)
    }

    #[must_use]
    pub const fn energy(&self) -> f32 {
        self.energy
    }

    pub fn set_energy(&mut self, value: f32) {
        self.energy = value;
    }

    #[must_use]
    pub const fn id(&self) -> UUID {
        self.id
    }

    #[must_use]
    pub const fn is_muted(&self) -> bool {
        self.is_muted
    }

    pub fn set_is_muted(&mut self, value: bool) {
        self.is_muted = value;
        let _ = self
            .session
            .connector()
            .session_set_participant_mute_for_me(self.session.handle(), self.uri.clone(), value);
    }

    #[must_use]
    pub const fn is_speaking(&self) -> bool {
        self.is_speaking
    }

    pub fn set_is_speaking(&mut self, value: bool) {
        self.is_speaking = value;
    }

    #[must_use]
    pub fn name(&self) -> String {
        self.avatar_name.clone()
    }

    pub fn set_name(&mut self, value: String) {
        self.avatar_name = value;
    }

    #[must_use]
    pub const fn volume(&self) -> i32 {
        self.volume
    }

    pub fn set_volume(&mut self, value: i32) {
        self.volume = value;
        let _ = self
            .session
            .connector()
            .session_set_participant_volume_for_me(self.session.handle(), self.uri.clone(), value);
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::atomic::{AtomicU64, Ordering};

    #[test]
    fn participant_uuid_round_trips_gateway_sip_name() {
        let client = libremetaverse::GridClient::new().unwrap();
        let gateway = VoiceGateway::new(client).unwrap();
        let id = UUID::new_with_string("1673cfd3-8229-4445-8d92-ec3570e5e587".into()).unwrap();
        let sip = gateway.sip_from_uuid(id).unwrap();
        assert_eq!(VoiceParticipant::id_from_name(sip).unwrap(), id);
        assert_eq!(
            VoiceParticipant::id_from_name("ordinary-name".into()).unwrap(),
            UUID::zero()
        );
    }

    #[test]
    fn session_participant_lifecycle_emits_only_real_changes() {
        let client = libremetaverse::GridClient::new().unwrap();
        let gateway = VoiceGateway::new(client).unwrap();
        let session = VoiceSession::new(gateway, "session".into()).unwrap();
        let count = Arc::new(AtomicU64::new(0));
        let observed = Arc::clone(&count);
        let _subscription = session.subscribe_on_participant_added(Some(Arc::new(move |()| {
            observed.fetch_add(1, Ordering::SeqCst);
        })));
        session
            .add_participant("sip:person@example.test".into())
            .unwrap();
        session
            .add_participant("sip:person@example.test".into())
            .unwrap();
        assert_eq!(count.load(Ordering::SeqCst), 1);
        session.update_participant("sip:person@example.test", true, true, 12, 0.5);
        session.remove_participant("sip:person@example.test");
        session.close();
    }
}
