//! Native implementations of the pinned static helper surface.

use crate::{Error, Primitive, Simulator};
use flate2::Compression;
use flate2::read::ZlibDecoder;
use flate2::write::ZlibEncoder;
use libremetaverse_structured_data::{OSD, OSDParser};
use libremetaverse_types::UUID;
use libremetaverse_types::compat::{ExternalError, Object, ReadWrite};
use std::collections::HashMap;
use std::io::{Read, Write};
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::path::PathBuf;

const MAX_DECOMPRESSED_OSD: u64 = 128 * 1024 * 1024;

pub struct Helpers;

impl Helpers {
    pub const MSG_APPENDED_ACKS: u8 = 0x10;
    pub const MSG_RESENT: u8 = 0x20;
    pub const MSG_RELIABLE: u8 = 0x40;
    pub const MSG_ZEROCODED: u8 = 0x80;

    pub fn copy_stream(
        mut input: Box<dyn ReadWrite + Send>,
        mut output: Box<dyn ReadWrite + Send>,
    ) -> Result<(), Error> {
        std::io::copy(&mut input, &mut output).map_err(|_| Error::InvalidOperation)?;
        output.flush().map_err(|_| Error::InvalidOperation)
    }

    pub fn decompress_osd(mesh_bytes: Vec<u8>) -> Result<OSD, Error> {
        Self::decode_zlib(&mesh_bytes)
    }

    pub fn float_to_terse_string(val: f32) -> Result<String, Error> {
        if val == 0.0 {
            return Ok(".00".to_owned());
        }
        if val.is_nan() {
            return Ok("NaN".to_owned());
        }
        if val == f32::INFINITY {
            return Ok("Infinity".to_owned());
        }
        if val == f32::NEG_INFINITY {
            return Ok("-Infinity".to_owned());
        }
        let mut value = format!("{val:.2}");
        while value.ends_with('0') {
            value.pop();
        }
        if value.ends_with('.') {
            value.pop();
        } else if value.starts_with("-0") {
            value.remove(1);
        } else if value.starts_with('0') {
            value.remove(0);
        }
        Ok(value)
    }

    pub fn get_resource_stream_with_string(
        resource_name: String,
    ) -> Result<Option<Box<dyn ReadWrite + Send>>, Error> {
        Self::get_resource_stream_with_string_string(resource_name, "openmetaverse_data".to_owned())
    }

    pub fn get_resource_stream_with_string_string(
        resource_name: String,
        search_path: String,
    ) -> Result<Option<Box<dyn ReadWrite + Send>>, Error> {
        let base = std::env::current_exe()
            .ok()
            .and_then(|path| path.parent().map(std::path::Path::to_path_buf))
            .unwrap_or_else(|| PathBuf::from("."));
        let filename = base.join(search_path).join(resource_name);
        match std::fs::OpenOptions::new().read(true).open(filename) {
            Ok(file) => Ok(Some(Box::new(file))),
            Err(_) => Ok(None),
        }
    }

    pub fn global_pos_to_region_handle(
        global_x: f32,
        global_y: f32,
        local_x: &mut f32,
        local_y: &mut f32,
    ) -> Result<u64, Error> {
        let x =
            (global_x as u32 / Simulator::DEFAULT_REGION_SIZE_X) * Simulator::DEFAULT_REGION_SIZE_X;
        let y =
            (global_y as u32 / Simulator::DEFAULT_REGION_SIZE_Y) * Simulator::DEFAULT_REGION_SIZE_Y;
        *local_x = global_x - x as f32;
        *local_y = global_y - y as f32;
        Ok((u64::from(x) << 32) | u64::from(y))
    }

    #[allow(clippy::too_many_arguments)]
    pub fn inventory_crc(
        creation_date: i32,
        sale_type: u8,
        inv_type: i8,
        type_: i8,
        mut asset_id: UUID,
        mut group_id: UUID,
        sale_price: i32,
        mut owner_id: UUID,
        mut creator_id: UUID,
        mut item_id: UUID,
        mut folder_id: UUID,
        everyone_mask: u32,
        flags: u32,
        next_owner_mask: u32,
        group_mask: u32,
        owner_mask: u32,
    ) -> Result<u32, Error> {
        let values = [
            asset_id.crc()?,
            folder_id.crc()?,
            item_id.crc()?,
            creator_id.crc()?,
            owner_id.crc()?,
            group_id.crc()?,
            owner_mask,
            next_owner_mask,
            everyone_mask,
            group_mask,
            flags,
            inv_type as u32,
            type_ as u32,
            creation_date as u32,
            sale_price as u32,
            u32::from(sale_type).wrapping_mul(0x0707_3096),
        ];
        Ok(values.into_iter().fold(0_u32, u32::wrapping_add))
    }

    pub fn osd_to_prim_list(osd: OSD) -> Result<Vec<Primitive>, Error> {
        let OSD::Map(map) = osd else {
            return Err(Error::Argument);
        };
        map.into_iter()
            .map(|(local_id, value)| {
                let mut primitive = Primitive::from_osd(value)?;
                primitive.local_id = local_id.parse().map_err(|_| Error::Argument)?;
                Ok(primitive)
            })
            .collect()
    }

    pub fn prim_list_to_osd(prims: Vec<Primitive>) -> Result<OSD, Error> {
        let mut map = HashMap::with_capacity(prims.len());
        for primitive in prims {
            map.insert(primitive.local_id.to_string(), primitive.get_osd()?);
        }
        Ok(OSD::Map(map))
    }

    pub fn safe_action(
        action: Box<dyn Fn() + Send + Sync>,
        action_name: Option<String>,
        logger: Option<Box<dyn Fn(String, Option<ExternalError>) + Send + Sync>>,
    ) -> Result<(), Error> {
        if let Err(payload) = catch_unwind(AssertUnwindSafe(action)) {
            if let Some(logger) = logger {
                let label = action_name.filter(|name| !name.is_empty());
                let message = label.map_or_else(
                    || "Error executing action".to_owned(),
                    |name| format!("Error executing {name}"),
                );
                let detail = payload.downcast_ref::<&str>().map_or_else(
                    || {
                        payload
                            .downcast_ref::<String>()
                            .cloned()
                            .unwrap_or_else(|| "panic".to_owned())
                    },
                    |value| (*value).to_owned(),
                );
                logger(message, Some(ExternalError(detail)));
            }
        }
        Ok(())
    }

    pub fn split_by(
        str: String,
        chunk_length: i32,
    ) -> Result<Box<dyn Iterator<Item = String>>, Error> {
        if str.is_empty() || chunk_length < 1 {
            return Err(Error::Argument);
        }
        let chunk_length = usize::try_from(chunk_length).map_err(|_| Error::Argument)?;
        let units: Vec<u16> = str.encode_utf16().collect();
        let chunks: Vec<String> = units
            .chunks(chunk_length)
            .map(String::from_utf16_lossy)
            .collect();
        Ok(Box::new(chunks.into_iter()))
    }

    pub fn struct_to_string(value: Object) -> Result<String, Error> {
        if let Object::Map(fields) = value {
            let mut fields: Vec<_> = fields.into_iter().collect();
            fields.sort_unstable_by(|left, right| left.0.cmp(&right.0));
            Ok(fields
                .into_iter()
                .map(|(name, value)| format!("{name}: {value:?}"))
                .collect::<Vec<_>>()
                .join(" "))
        } else {
            Ok(format!("{value:?}"))
        }
    }

    pub fn te_glow_byte(glow: f32) -> Result<u8, Error> {
        Ok((glow * 255.0) as u8)
    }

    pub fn te_glow_float(bytes: Vec<u8>, pos: i32) -> Result<f32, Error> {
        Ok(f32::from(*bytes.get(index(pos)?).ok_or(Error::IndexOutOfRange)?) / 255.0)
    }

    pub fn te_offset_float(bytes: Vec<u8>, pos: i32) -> Result<f32, Error> {
        let pos = index(pos)?;
        let pair: [u8; 2] = bytes
            .get(pos..pos.checked_add(2).ok_or(Error::IndexOutOfRange)?)
            .ok_or(Error::IndexOutOfRange)?
            .try_into()
            .map_err(|_| Error::IndexOutOfRange)?;
        Ok(f32::from(i16::from_le_bytes(pair)) / 32767.0)
    }

    pub fn te_offset_short(offset: f32) -> Result<i16, Error> {
        Ok((offset.clamp(-1.0, 1.0) * 32767.0).round_ties_even() as i16)
    }

    pub fn te_rotation_float(bytes: Vec<u8>, pos: i32) -> Result<f32, Error> {
        let pos = index(pos)?;
        let pair: [u8; 2] = bytes
            .get(pos..pos.checked_add(2).ok_or(Error::IndexOutOfRange)?)
            .ok_or(Error::IndexOutOfRange)?
            .try_into()
            .map_err(|_| Error::IndexOutOfRange)?;
        Ok(f32::from(u16::from_le_bytes(pair)) / 32768.0 * std::f32::consts::TAU)
    }

    pub fn te_rotation_short(rotation: f32) -> Result<i16, Error> {
        let remainder =
            rotation - (rotation / std::f32::consts::TAU).round_ties_even() * std::f32::consts::TAU;
        Ok((remainder / std::f32::consts::TAU * 32768.0 + 0.5).round_ties_even() as i16)
    }

    pub fn z_compress_osd(data: OSD) -> Result<Vec<u8>, Error> {
        let binary = OSDParser::serialize_llsd_binary_with_osd_boolean(data, false)?;
        let mut encoder = ZlibEncoder::new(Vec::new(), Compression::best());
        encoder
            .write_all(&binary)
            .map_err(|_| Error::InvalidOperation)?;
        encoder.finish().map_err(|_| Error::InvalidOperation)
    }

    pub fn z_decompress_osd(data: Vec<u8>) -> Result<OSD, Error> {
        Self::decode_zlib(&data)
    }

    pub fn zero_decode(
        src: Option<&[u8]>,
        srclen: i32,
        dest: Option<&mut [u8]>,
    ) -> Result<i32, Error> {
        crate::packet_wire::zero_decode(src, srclen, dest)
    }

    pub fn zero_encode(
        src: Option<&[u8]>,
        srclen: i32,
        dest: Option<&mut [u8]>,
    ) -> Result<i32, Error> {
        crate::packet_wire::zero_encode(src, srclen, dest)
    }

    fn decode_zlib(data: &[u8]) -> Result<OSD, Error> {
        let decoder = ZlibDecoder::new(data);
        let mut limited = decoder.take(MAX_DECOMPRESSED_OSD + 1);
        let mut decoded = Vec::new();
        limited
            .read_to_end(&mut decoded)
            .map_err(|_| Error::Parse {
                position: 0,
                context: "invalid zlib-compressed LLSD",
            })?;
        if decoded.len() as u64 > MAX_DECOMPRESSED_OSD {
            return Err(Error::Argument);
        }
        OSDParser::deserialize_llsd_binary_with_bytes(decoded)
    }
}

fn index(value: i32) -> Result<usize, Error> {
    usize::try_from(value).map_err(|_| Error::IndexOutOfRange)
}

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

    #[test]
    fn terse_float_and_texture_entry_conversions_match_pinned_examples() {
        assert_eq!(Helpers::float_to_terse_string(0.0).unwrap(), ".00");
        assert_eq!(Helpers::float_to_terse_string(0.5).unwrap(), ".5");
        assert_eq!(Helpers::float_to_terse_string(-0.25).unwrap(), "-.25");
        for offset in [-1.0, -0.25, 0.0, 0.75, 1.0] {
            let encoded = Helpers::te_offset_short(offset).unwrap();
            let decoded = Helpers::te_offset_float(encoded.to_le_bytes().to_vec(), 0).unwrap();
            assert!((decoded - offset).abs() <= 1.0 / 32767.0);
        }
    }

    #[test]
    fn compressed_llsd_round_trips_and_rejects_non_zlib_input() {
        let value = OSD::Map(HashMap::from([
            ("grid".to_owned(), OSD::String("OpenSim".to_owned())),
            ("count".to_owned(), OSD::Integer(3)),
        ]));
        let compressed = Helpers::z_compress_osd(value.clone()).unwrap();
        assert_eq!(Helpers::z_decompress_osd(compressed).unwrap(), value);
        assert!(Helpers::z_decompress_osd(vec![1, 2, 3]).is_err());
    }

    #[test]
    fn split_by_counts_utf16_units() {
        let chunks: Vec<_> = Helpers::split_by("ab🙂cd".to_owned(), 2).unwrap().collect();
        assert_eq!(chunks, ["ab", "🙂", "cd"]);
    }

    #[test]
    fn region_handle_returns_local_coordinates() {
        let mut local_x = 0.0;
        let mut local_y = 0.0;
        let handle =
            Helpers::global_pos_to_region_handle(300.5, 600.25, &mut local_x, &mut local_y)
                .unwrap();
        assert_eq!(handle, (256_u64 << 32) | 512);
        assert_eq!(local_x, 44.5);
        assert_eq!(local_y, 88.25);
    }
}
