//! Region/local coordinate conversions and parent-relative object positions.

use crate::{Avatar, Error, Primitive, Simulator};
use libremetaverse_types::{Vector3, Vector3d};

pub struct PositionHelper;

impl PositionHelper {
    pub fn to_global_position(sim_handle: u64, local_pos: Vector3) -> Result<Vector3d, Error> {
        let global_x = (sim_handle >> 32) as u32;
        let global_y = sim_handle as u32;
        Vector3d::new_with_double_double_double(
            f64::from(global_x) + f64::from(local_pos.x),
            f64::from(global_y) + f64::from(local_pos.y),
            f64::from(local_pos.z),
        )
    }

    pub fn to_local_position(sim_handle: u64, global_pos: Vector3d) -> Result<Vector3, Error> {
        let global_x = (sim_handle >> 32) as u32;
        let global_y = sim_handle as u32;
        Vector3::new_with_single_single_single(
            (global_pos.x - f64::from(global_x)) as f32,
            (global_pos.y - f64::from(global_y)) as f32,
            global_pos.z as f32,
        )
    }

    pub fn global_position_with_simulator_vector3(
        sim: Simulator,
        local_pos: Vector3,
    ) -> Result<Vector3d, Error> {
        Self::to_global_position(sim.handle, local_pos)
    }

    pub fn global_position_with_primitive_simulator(
        prim: Primitive,
        current_sim: Simulator,
    ) -> Result<Vector3d, Error> {
        Self::global_position_with_simulator_vector3(current_sim, prim.position)
    }

    pub fn region_handle_from_global(global_x: u32, global_y: u32) -> Result<u64, Error> {
        let x = global_x / Simulator::DEFAULT_REGION_SIZE_X * Simulator::DEFAULT_REGION_SIZE_X;
        let y = global_y / Simulator::DEFAULT_REGION_SIZE_Y * Simulator::DEFAULT_REGION_SIZE_Y;
        Ok((u64::from(x) << 32) | u64::from(y))
    }

    pub fn region_handle_from_global_position(global_pos: Vector3d) -> Result<u64, Error> {
        Self::region_handle_from_global(global_pos.x as u32, global_pos.y as u32)
    }

    pub fn get_avatar_position(sim: Simulator, avatar: Avatar) -> Result<Vector3, Error> {
        if avatar.parent_id == 0 {
            return Ok(avatar.position);
        }
        let objects = sim
            .objects_primitives
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        Ok(objects
            .get(&avatar.parent_id)
            .map_or(avatar.position, |parent| {
                Vector3::add_with_vector3_vector3_87e25eaf(
                    parent.position,
                    Vector3::mul_with_vector3_quaternion(avatar.position, parent.rotation),
                )
            }))
    }

    pub fn get_prim_position(sim: Simulator, prim: Primitive) -> Result<Vector3, Error> {
        if prim.parent_id == 0 {
            return Ok(prim.position);
        }
        let objects = sim
            .objects_primitives
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        Ok(objects
            .get(&prim.parent_id)
            .map_or(prim.position, |parent| {
                Vector3::add_with_vector3_vector3_87e25eaf(
                    parent.position,
                    Vector3::mul_with_vector3_quaternion(prim.position, parent.rotation),
                )
            }))
    }

    pub fn format_coordinates(
        x: Option<Option<i32>>,
        y: Option<Option<i32>>,
        z: Option<Option<i32>>,
    ) -> Result<String, Error> {
        let values: Vec<_> = [x.flatten(), y.flatten(), z.flatten()]
            .into_iter()
            .flatten()
            .map(|value| value.to_string())
            .collect();
        Ok(if values.is_empty() {
            String::new()
        } else {
            format!(" ({})", values.join(","))
        })
    }

    pub fn format_region_coordinates(
        region_name: String,
        x: Option<Option<i32>>,
        y: Option<Option<i32>>,
        z: Option<Option<i32>>,
    ) -> Result<String, Error> {
        Ok(region_name + &Self::format_coordinates(x, y, z)?)
    }
}

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

    #[test]
    fn local_global_round_trip_and_region_alignment() {
        let handle = ((512_u64 * 256) << 32) | (1024_u64 * 256);
        let local = Vector3::new_with_single_single_single(10.5, 20.25, 5.0).unwrap();
        let global = PositionHelper::to_global_position(handle, local).unwrap();
        assert_eq!(
            PositionHelper::to_local_position(handle, global).unwrap(),
            local
        );
        assert_eq!(
            PositionHelper::region_handle_from_global(300, 700).unwrap(),
            (256_u64 << 32) | 512
        );
    }

    #[test]
    fn formats_only_present_coordinates() {
        assert_eq!(
            PositionHelper::format_coordinates(None, None, None).unwrap(),
            ""
        );
        assert_eq!(
            PositionHelper::format_coordinates(Some(Some(10)), Some(None), Some(Some(30))).unwrap(),
            " (10,30)"
        );
    }
}
