//! Bounded OpenSim/Second Life event consumption and persistent world state.

#![allow(clippy::missing_errors_doc)]

use libremetaverse::{Avatar, GridClient, Primitive, Simulator};
use libremetaverse_types::{UUID, compat::Subscription};
use std::{
    collections::{BTreeMap, BTreeSet, HashMap},
    sync::{
        Arc,
        atomic::{AtomicBool, AtomicU64, Ordering},
        mpsc::{self, Receiver, RecvError, SyncSender, TryRecvError},
    },
    thread,
    time::Duration,
};

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RegionDescriptor {
    pub id: UUID,
    pub handle: u64,
    pub name: String,
    pub size: (u32, u32),
}

impl RegionDescriptor {
    #[must_use]
    pub fn from_simulator(simulator: &Simulator) -> Self {
        Self {
            id: simulator.region_id,
            handle: simulator.handle,
            name: simulator.name.clone(),
            size: simulator.region_size(),
        }
    }
}

#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum PrimitiveChange {
    Full,
    Movement,
}

pub enum WorldEvent {
    RegionChanged(Option<RegionDescriptor>),
    PrimitiveUpdated {
        region: RegionDescriptor,
        primitive: Arc<Primitive>,
        change: PrimitiveChange,
    },
    AvatarUpdated {
        region: RegionDescriptor,
        avatar: Arc<Avatar>,
    },
    AvatarMoved {
        region: RegionDescriptor,
        local_id: u32,
        position: libremetaverse_types::Vector3,
        rotation: libremetaverse_types::Quaternion,
        velocity: libremetaverse_types::Vector3,
    },
    ObjectsRemoved {
        region: RegionDescriptor,
        local_ids: Vec<u32>,
    },
    TerrainUpdated {
        region: RegionDescriptor,
        x: i32,
        y: i32,
        patch_size: i32,
        height_map: Vec<f32>,
    },
    VisibilitySnapshot {
        region: RegionDescriptor,
        primitives: Vec<Arc<Primitive>>,
        avatars: Vec<Arc<Avatar>>,
        terrain: Vec<((i32, i32), TerrainPatch)>,
    },
}

#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum DirtyWorldItem {
    Region,
    Primitive(u32, PrimitiveChange),
    Avatar(u32),
    Terrain(i32, i32),
}

#[derive(Clone, Debug, PartialEq)]
pub struct TerrainPatch {
    pub patch_size: i32,
    pub height_map: Arc<[f32]>,
}

#[derive(Clone)]
pub struct WorldSnapshot {
    pub observed_unix_millis: u64,
    pub region: Option<RegionDescriptor>,
    pub primitives: Arc<HashMap<u32, Arc<Primitive>>>,
    pub avatars: Arc<HashMap<u32, Arc<Avatar>>>,
    pub terrain: Arc<BTreeMap<(i32, i32), TerrainPatch>>,
}

#[derive(Default)]
pub struct WorldState {
    region: Option<RegionDescriptor>,
    primitives: HashMap<u32, Arc<Primitive>>,
    avatars: HashMap<u32, Arc<Avatar>>,
    terrain: BTreeMap<(i32, i32), TerrainPatch>,
    dirty: BTreeSet<DirtyWorldItem>,
}

impl WorldState {
    pub fn apply(&mut self, event: WorldEvent) {
        match event {
            WorldEvent::RegionChanged(region) => self.change_region(region),
            WorldEvent::PrimitiveUpdated {
                region,
                primitive,
                change,
            } => {
                self.ensure_region(region);
                let local_id = primitive.local_id;
                self.primitives.insert(local_id, primitive);
                self.dirty.retain(
                    |item| !matches!(item, DirtyWorldItem::Primitive(id, _) if *id == local_id),
                );
                self.dirty
                    .insert(DirtyWorldItem::Primitive(local_id, change));
            }
            WorldEvent::AvatarUpdated { region, avatar } => {
                self.ensure_region(region);
                let local_id = avatar.local_id;
                self.avatars.insert(local_id, avatar);
                self.dirty.insert(DirtyWorldItem::Avatar(local_id));
            }
            WorldEvent::AvatarMoved {
                region,
                local_id,
                position,
                rotation,
                velocity,
            } => {
                self.ensure_region(region);
                if let Some(avatar) = self.avatars.get_mut(&local_id) {
                    let avatar = Arc::make_mut(avatar);
                    avatar.position = position;
                    avatar.rotation = rotation;
                    avatar.velocity = velocity;
                    self.dirty.insert(DirtyWorldItem::Avatar(local_id));
                }
            }
            WorldEvent::ObjectsRemoved { region, local_ids } => {
                self.ensure_region(region);
                for local_id in local_ids {
                    self.primitives.remove(&local_id);
                    self.avatars.remove(&local_id);
                    self.dirty
                        .insert(DirtyWorldItem::Primitive(local_id, PrimitiveChange::Full));
                }
            }
            WorldEvent::TerrainUpdated {
                region,
                x,
                y,
                patch_size,
                height_map,
            } => {
                self.ensure_region(region);
                self.terrain.insert(
                    (x, y),
                    TerrainPatch {
                        patch_size,
                        height_map: height_map.into(),
                    },
                );
                self.dirty.insert(DirtyWorldItem::Terrain(x, y));
            }
            WorldEvent::VisibilitySnapshot {
                region,
                primitives,
                avatars,
                terrain,
            } => {
                self.region = Some(region);
                self.primitives = primitives
                    .into_iter()
                    .map(|primitive| (primitive.local_id, primitive))
                    .collect();
                self.avatars = avatars
                    .into_iter()
                    .map(|avatar| (avatar.local_id, avatar))
                    .collect();
                self.terrain = terrain.into_iter().collect();
                self.dirty.clear();
                self.dirty.insert(DirtyWorldItem::Region);
            }
        }
    }

    fn ensure_region(&mut self, region: RegionDescriptor) {
        if self.region.as_ref().map(|value| value.handle) != Some(region.handle) {
            self.change_region(Some(region));
        }
    }

    fn change_region(&mut self, region: Option<RegionDescriptor>) {
        self.region = region;
        self.primitives.clear();
        self.avatars.clear();
        self.terrain.clear();
        self.dirty.clear();
        self.dirty.insert(DirtyWorldItem::Region);
    }

    #[must_use]
    pub fn region(&self) -> Option<&RegionDescriptor> {
        self.region.as_ref()
    }

    #[must_use]
    pub fn primitive(&self, local_id: u32) -> Option<Arc<Primitive>> {
        self.primitives.get(&local_id).cloned()
    }

    #[must_use]
    pub fn avatar(&self, local_id: u32) -> Option<Arc<Avatar>> {
        self.avatars.get(&local_id).cloned()
    }

    #[must_use]
    pub fn terrain(&self, x: i32, y: i32) -> Option<&TerrainPatch> {
        self.terrain.get(&(x, y))
    }

    pub fn take_dirty(&mut self) -> Vec<DirtyWorldItem> {
        std::mem::take(&mut self.dirty).into_iter().collect()
    }

    #[must_use]
    pub fn snapshot(&self) -> WorldSnapshot {
        WorldSnapshot {
            observed_unix_millis: unix_millis_now(),
            region: self.region.clone(),
            primitives: Arc::new(self.primitives.clone()),
            avatars: Arc::new(self.avatars.clone()),
            terrain: Arc::new(self.terrain.clone()),
        }
    }
}

fn unix_millis_now() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map_or(0, |duration| {
            duration.as_millis().try_into().unwrap_or(u64::MAX)
        })
}

#[derive(Clone)]
pub struct WorldEventSender {
    sender: SyncSender<WorldEvent>,
    dropped: Arc<AtomicU64>,
}

impl WorldEventSender {
    pub fn send(&self, event: WorldEvent) {
        if self.sender.try_send(event).is_err() {
            self.dropped.fetch_add(1, Ordering::Relaxed);
        }
    }

    #[must_use]
    pub fn dropped(&self) -> u64 {
        self.dropped.load(Ordering::Relaxed)
    }
}

pub struct WorldEventReceiver {
    receiver: Receiver<WorldEvent>,
    dropped: Arc<AtomicU64>,
}

#[derive(Clone)]
pub struct WorldEventMetrics {
    dropped: Arc<AtomicU64>,
}

impl WorldEventMetrics {
    #[must_use]
    pub fn dropped(&self) -> u64 {
        self.dropped.load(Ordering::Relaxed)
    }
}

#[must_use]
pub fn world_event_channel(capacity: usize) -> Option<(WorldEventSender, WorldEventReceiver)> {
    if capacity == 0 {
        return None;
    }
    let (sender, receiver) = mpsc::sync_channel(capacity);
    let dropped = Arc::new(AtomicU64::new(0));
    Some((
        WorldEventSender {
            sender,
            dropped: Arc::clone(&dropped),
        },
        WorldEventReceiver { receiver, dropped },
    ))
}

impl WorldEventReceiver {
    pub fn recv(&self) -> Result<WorldEvent, RecvError> {
        self.receiver.recv()
    }

    pub fn try_recv(&self) -> Result<WorldEvent, TryRecvError> {
        self.receiver.try_recv()
    }

    #[must_use]
    pub fn dropped(&self) -> u64 {
        self.dropped.load(Ordering::Relaxed)
    }

    #[must_use]
    pub fn metrics(&self) -> WorldEventMetrics {
        WorldEventMetrics {
            dropped: Arc::clone(&self.dropped),
        }
    }
}

pub struct LibreMetaverseWorldEvents {
    _subscriptions: Vec<Subscription>,
    stopped: Arc<AtomicBool>,
    monitor: Option<thread::JoinHandle<()>>,
}

impl Drop for LibreMetaverseWorldEvents {
    fn drop(&mut self) {
        self.stopped.store(true, Ordering::Release);
        if let Some(monitor) = self.monitor.take() {
            monitor.thread().unpark();
            let _ = monitor.join();
        }
    }
}

impl LibreMetaverseWorldEvents {
    #[must_use]
    pub fn subscribe(client: &GridClient, capacity: usize) -> Option<(Self, WorldEventReceiver)> {
        Self::subscribe_inner(client, None, capacity)
    }

    #[must_use]
    pub fn subscribe_visible(
        client: &GridClient,
        agent: Arc<libremetaverse::AgentManager>,
        max_distance_meters: u32,
        capacity: usize,
    ) -> Option<(Self, WorldEventReceiver)> {
        let maximum = f32::from(u16::try_from(max_distance_meters).ok()?);
        (maximum > 0.0).then_some(())?;
        Self::subscribe_inner(client, Some((agent, maximum)), capacity)
    }

    #[allow(clippy::too_many_lines)] // One subscription owner keeps all native event lifetimes together.
    fn subscribe_inner(
        client: &GridClient,
        view: Option<(Arc<libremetaverse::AgentManager>, f32)>,
        capacity: usize,
    ) -> Option<(Self, WorldEventReceiver)> {
        if capacity == 0 {
            return None;
        }
        let (ingress, receiver) = world_event_channel(capacity)?;
        let objects = client.objects();
        let terrain = client.terrain();
        let network = client.network();
        let monitor_view = view.clone();
        let monitor_ingress = ingress.clone();
        let mut subscriptions = Vec::with_capacity(6);

        let event_ingress = ingress.clone();
        let event_view = view.clone();
        subscriptions.push(objects.subscribe_object_update(Arc::new(move |event| {
            let simulator = event.simulator();
            let primitive = event.prim();
            let Some(primitive) = visible_primitive(&simulator, primitive, event_view.as_ref())
            else {
                return;
            };
            event_ingress.send(WorldEvent::PrimitiveUpdated {
                region: RegionDescriptor::from_simulator(&simulator),
                primitive: Arc::new(primitive),
                change: PrimitiveChange::Full,
            });
        })));
        let event_ingress = ingress.clone();
        let event_view = view.clone();
        subscriptions.push(
            objects.subscribe_terse_object_update(Arc::new(move |event| {
                let simulator = event.simulator();
                let update = event.update();
                if update.avatar {
                    if visible_position(update.position, event_view.as_ref()) {
                        event_ingress.send(WorldEvent::AvatarMoved {
                            region: RegionDescriptor::from_simulator(&simulator),
                            local_id: update.local_id,
                            position: update.position,
                            rotation: update.rotation,
                            velocity: update.velocity,
                        });
                    } else {
                        event_ingress.send(WorldEvent::ObjectsRemoved {
                            region: RegionDescriptor::from_simulator(&simulator),
                            local_ids: vec![update.local_id],
                        });
                    }
                    return;
                }
                let mut primitive = event.prim();
                primitive.position = update.position;
                primitive.rotation = update.rotation;
                primitive.velocity = update.velocity;
                primitive.acceleration = update.acceleration;
                primitive.angular_velocity = update.angular_velocity;
                let local_id = primitive.local_id;
                let Some(primitive) = visible_primitive(&simulator, primitive, event_view.as_ref())
                else {
                    event_ingress.send(WorldEvent::ObjectsRemoved {
                        region: RegionDescriptor::from_simulator(&simulator),
                        local_ids: vec![local_id],
                    });
                    return;
                };
                event_ingress.send(WorldEvent::PrimitiveUpdated {
                    region: RegionDescriptor::from_simulator(&simulator),
                    primitive: Arc::new(primitive),
                    change: PrimitiveChange::Movement,
                });
            })),
        );
        let event_ingress = ingress.clone();
        let event_view = view.clone();
        subscriptions.push(objects.subscribe_avatar_update(Arc::new(move |event| {
            let simulator = event.simulator();
            let avatar = event.avatar();
            if !visible_position(avatar.position, event_view.as_ref()) {
                return;
            }
            event_ingress.send(WorldEvent::AvatarUpdated {
                region: RegionDescriptor::from_simulator(&simulator),
                avatar: Arc::new(avatar),
            });
        })));
        let event_ingress = ingress.clone();
        subscriptions.push(objects.subscribe_kill_object(Arc::new(move |event| {
            let simulator = event.simulator();
            event_ingress.send(WorldEvent::ObjectsRemoved {
                region: RegionDescriptor::from_simulator(&simulator),
                local_ids: vec![event.object_local_id()],
            });
        })));
        let event_ingress = ingress.clone();
        let event_view = view;
        subscriptions.push(
            terrain.subscribe_land_patch_received(Arc::new(move |event| {
                let simulator = event.simulator();
                if !visible_terrain_patch(
                    event.x(),
                    event.y(),
                    event.patch_size(),
                    event_view.as_ref(),
                ) {
                    return;
                }
                event_ingress.send(WorldEvent::TerrainUpdated {
                    region: RegionDescriptor::from_simulator(&simulator),
                    x: event.x(),
                    y: event.y(),
                    patch_size: event.patch_size(),
                    height_map: event.height_map(),
                });
            })),
        );
        let event_ingress = ingress;
        let callback_client = client.clone();
        subscriptions.push(network.subscribe_sim_changed(Arc::new(move |_| {
            event_ingress.send(WorldEvent::RegionChanged(
                callback_client
                    .network()
                    .current_sim()
                    .as_ref()
                    .map(RegionDescriptor::from_simulator),
            ));
        })));

        let stopped = Arc::new(AtomicBool::new(false));
        let monitor = if let Some((agent, maximum)) = monitor_view {
            let monitor_client = client.clone();
            let monitor_stopped = Arc::clone(&stopped);
            Some(
                thread::Builder::new()
                    .name("metacrate-world-visibility".into())
                    .spawn(move || {
                        let mut last = None;
                        while !monitor_stopped.load(Ordering::Acquire) {
                            if let Some(simulator) = monitor_client.network().current_sim() {
                                let position = agent.sim_position();
                                let changed = last.is_none_or(
                                    |(handle, previous): (u64, libremetaverse_types::Vector3)| {
                                        let dx = position.x - previous.x;
                                        let dy = position.y - previous.y;
                                        let dz = position.z - previous.z;
                                        handle != simulator.handle
                                            || dx * dx + dy * dy + dz * dz >= 16.0
                                    },
                                );
                                if changed {
                                    monitor_ingress
                                        .send(visibility_snapshot(&simulator, &agent, maximum));
                                    last = Some((simulator.handle, position));
                                }
                            }
                            thread::park_timeout(Duration::from_millis(250));
                        }
                    })
                    .ok()?,
            )
        } else {
            None
        };

        Some((
            Self {
                _subscriptions: subscriptions,
                stopped,
                monitor,
            },
            receiver,
        ))
    }
}

fn visibility_snapshot(
    simulator: &Simulator,
    agent: &libremetaverse::AgentManager,
    maximum: f32,
) -> WorldEvent {
    let center = agent.sim_position();
    let primitives = simulator
        .objects_primitives
        .read()
        .unwrap_or_else(std::sync::PoisonError::into_inner);
    let avatars = simulator
        .objects_avatars
        .read()
        .unwrap_or_else(std::sync::PoisonError::into_inner);
    let visible_primitives = primitives
        .values()
        .filter_map(|primitive| resolved_primitive(primitive.clone(), &primitives, &avatars))
        .filter(|primitive| position_in_radius(primitive.position, center, maximum))
        .map(Arc::new)
        .collect();
    let visible_avatars = avatars
        .values()
        .filter(|avatar| position_in_radius(avatar.position, center, maximum))
        .cloned()
        .map(Arc::new)
        .collect();
    drop(primitives);
    drop(avatars);
    let terrain = simulator
        .terrain
        .read()
        .unwrap_or_else(std::sync::PoisonError::into_inner)
        .iter()
        .filter(|patch| terrain_patch_in_radius(patch.x, patch.y, 16, center, maximum))
        .map(|patch| {
            (
                (patch.x, patch.y),
                TerrainPatch {
                    patch_size: 16,
                    height_map: Arc::from(patch.data.clone()),
                },
            )
        })
        .collect();
    WorldEvent::VisibilitySnapshot {
        region: RegionDescriptor::from_simulator(simulator),
        primitives: visible_primitives,
        avatars: visible_avatars,
        terrain,
    }
}

fn position_in_radius(
    position: libremetaverse_types::Vector3,
    center: libremetaverse_types::Vector3,
    maximum: f32,
) -> bool {
    let dx = position.x - center.x;
    let dy = position.y - center.y;
    let dz = position.z - center.z;
    dx * dx + dy * dy + dz * dz <= maximum * maximum
}

fn visible_position(
    position: libremetaverse_types::Vector3,
    view: Option<&(Arc<libremetaverse::AgentManager>, f32)>,
) -> bool {
    view.is_none_or(|(agent, maximum)| {
        let center = agent.sim_position();
        position_in_radius(position, center, *maximum)
    })
}

fn visible_terrain_patch(
    x: i32,
    y: i32,
    patch_size: i32,
    view: Option<&(Arc<libremetaverse::AgentManager>, f32)>,
) -> bool {
    let Some((agent, maximum)) = view else {
        return true;
    };
    terrain_patch_in_radius(x, y, patch_size, agent.sim_position(), *maximum)
}

fn terrain_patch_in_radius(
    x: i32,
    y: i32,
    patch_size: i32,
    center: libremetaverse_types::Vector3,
    maximum: f32,
) -> bool {
    let (Ok(x), Ok(y), Ok(patch_size)) = (
        i16::try_from(x),
        i16::try_from(y),
        i16::try_from(patch_size),
    ) else {
        return false;
    };
    let (Some(patch_x), Some(patch_y)) = (x.checked_mul(patch_size), y.checked_mul(patch_size))
    else {
        return false;
    };
    let patch_x = f32::from(patch_x) + f32::from(patch_size) * 0.5;
    let patch_y = f32::from(patch_y) + f32::from(patch_size) * 0.5;
    let dx = patch_x - center.x;
    let dy = patch_y - center.y;
    dx * dx + dy * dy <= maximum * maximum
}

fn visible_primitive(
    simulator: &Simulator,
    mut primitive: Primitive,
    view: Option<&(Arc<libremetaverse::AgentManager>, f32)>,
) -> Option<Primitive> {
    if view.is_none() {
        return Some(primitive);
    }
    let primitives = simulator
        .objects_primitives
        .read()
        .unwrap_or_else(std::sync::PoisonError::into_inner);
    let avatars = simulator
        .objects_avatars
        .read()
        .unwrap_or_else(std::sync::PoisonError::into_inner);
    primitive = resolved_primitive(primitive, &primitives, &avatars)?;
    visible_position(primitive.position, view).then_some(primitive)
}

fn resolved_primitive(
    mut primitive: Primitive,
    primitives: &HashMap<u32, Primitive>,
    avatars: &HashMap<u32, Avatar>,
) -> Option<Primitive> {
    let mut parent_id = primitive.parent_id;
    for _ in 0..32 {
        if parent_id == 0 {
            primitive.parent_id = 0;
            return Some(primitive);
        }
        if let Some(parent) = primitives.get(&parent_id) {
            primitive.position = libremetaverse_types::Vector3::add_with_vector3_vector3(
                parent.position,
                libremetaverse_types::Vector3::mul_with_vector3_quaternion(
                    primitive.position,
                    parent.rotation,
                ),
            )
            .ok()?;
            primitive.rotation = libremetaverse_types::Quaternion::mul_with_quaternion_quaternion(
                parent.rotation,
                primitive.rotation,
            );
            parent_id = parent.parent_id;
            continue;
        }
        let avatar = avatars.get(&parent_id)?;
        primitive.position = libremetaverse_types::Vector3::add_with_vector3_vector3(
            avatar.position,
            libremetaverse_types::Vector3::mul_with_vector3_quaternion(
                primitive.position,
                avatar.rotation,
            ),
        )
        .ok()?;
        primitive.rotation = libremetaverse_types::Quaternion::mul_with_quaternion_quaternion(
            avatar.rotation,
            primitive.rotation,
        );
        primitive.parent_id = 0;
        primitive.is_attachment = true;
        return Some(primitive);
    }
    None
}

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

    #[test]
    fn region_changes_clear_state_and_dirty_changes_coalesce() {
        let first = RegionDescriptor {
            id: UUID::new_with_string("00000000-0000-0000-0000-000000000001".into()).unwrap(),
            handle: 1,
            name: "First".into(),
            size: (256, 256),
        };
        let second = RegionDescriptor {
            id: UUID::new_with_string("00000000-0000-0000-0000-000000000002".into()).unwrap(),
            handle: 2,
            name: "Second".into(),
            size: (1024, 1024),
        };
        let mut world = WorldState::default();
        world.apply(WorldEvent::RegionChanged(Some(first)));
        world.apply(WorldEvent::TerrainUpdated {
            region: second.clone(),
            x: 0,
            y: 0,
            patch_size: 1,
            height_map: vec![3.0],
        });
        assert_eq!(world.region(), Some(&second));
        assert_eq!(world.terrain(0, 0).unwrap().height_map.as_ref(), &[3.0]);
        assert_eq!(
            world.take_dirty(),
            vec![DirtyWorldItem::Region, DirtyWorldItem::Terrain(0, 0)]
        );
        assert!(world.take_dirty().is_empty());
        world.apply(WorldEvent::VisibilitySnapshot {
            region: second,
            primitives: Vec::new(),
            avatars: Vec::new(),
            terrain: Vec::new(),
        });
        assert!(world.terrain(0, 0).is_none());
        assert_eq!(world.take_dirty(), vec![DirtyWorldItem::Region]);
    }
}
