//! Bit-level packing compatible with `LibreMetaverse`'s `BitPack` wire helper.

#![allow(clippy::missing_errors_doc)] // Public signatures are fixed by the C# compatibility map.
#![allow(clippy::cast_precision_loss)] // Fixed-point conversion intentionally mirrors C# casts.
#![allow(clippy::cast_possible_truncation)] // Fixed-point packing truncates exactly like C#.
#![allow(clippy::cast_sign_loss)] // Signed fixed-point values are biased before conversion.

use crate::Error;
use libremetaverse_types::{Color4, UUID};

const BITS_PER_BYTE: i32 = 8;

/// A cursor over a caller-owned byte buffer that reads and writes from the
/// most-significant bit to the least-significant bit of each byte.
pub struct BitPack {
    pub data: Vec<u8>,
    byte_pos: usize,
    bit_pos: i32,
}

impl BitPack {
    pub fn new(data: Vec<u8>, pos: i32) -> Result<Self, Error> {
        let byte_pos = usize::try_from(pos).map_err(|_| Error::IndexOutOfRange)?;
        if byte_pos > data.len() {
            return Err(Error::IndexOutOfRange);
        }
        Ok(Self {
            data,
            byte_pos,
            bit_pos: 0,
        })
    }

    pub fn pack_bit(&mut self, bit: bool) -> Result<(), Error> {
        self.pack_bit_array(&[u8::from(bit)], 1)
    }

    pub fn pack_bits_with_int32_int32(&mut self, data: i32, total_count: i32) -> Result<(), Error> {
        self.pack_bit_array(&data.to_le_bytes(), total_count)
    }

    pub fn pack_bits_with_u_int32_int32(
        &mut self,
        data: u32,
        total_count: i32,
    ) -> Result<(), Error> {
        self.pack_bit_array(&data.to_le_bytes(), total_count)
    }

    pub fn pack_color(&mut self, data: Color4) -> Result<(), Error> {
        self.pack_bit_array(&data.get_bytes_with_method()?, 32)
    }

    pub fn pack_fixed(
        &mut self,
        data: f32,
        is_signed: bool,
        int_bits: i32,
        frac_bits: i32,
    ) -> Result<(), Error> {
        let unsigned_bits = int_bits.checked_add(frac_bits).ok_or(Error::Argument)?;
        let total_bits = unsigned_bits
            .checked_add(i32::from(is_signed))
            .ok_or(Error::Argument)?;
        let max = checked_power_of_two(int_bits)?;
        let min = if is_signed { -max } else { 0 };
        let scale = checked_power_of_two(frac_bits)?;
        let mut fixed = data.clamp(min as f32, max as f32);
        if is_signed {
            fixed += max as f32;
        }
        fixed *= scale as f32;

        let storage_bits = match total_bits {
            ..=8 => 8,
            9..=16 => 16,
            17..=31 => 32,
            _ => return Err(Error::Argument),
        };
        self.pack_bits_with_u_int32_int32(fixed as u32, storage_bits)
    }

    pub fn pack_float(&mut self, data: f32) -> Result<(), Error> {
        self.pack_bit_array(&data.to_le_bytes(), 32)
    }

    pub fn pack_uuid(&mut self, data: UUID) -> Result<(), Error> {
        for byte in data.get_bytes()? {
            self.pack_bit_array(&[byte], 8)?;
        }
        Ok(())
    }

    pub fn unpack_bits(&mut self, total_count: i32) -> Result<i32, Error> {
        Ok(self.unpack_bits_to_u32(total_count)?.cast_signed())
    }

    pub fn unpack_byte(&mut self) -> Result<u8, Error> {
        u8::try_from(self.unpack_bits_to_u32(8)?).map_err(|_| Error::Argument)
    }

    pub fn unpack_fixed(
        &mut self,
        signed: bool,
        int_bits: i32,
        frac_bits: i32,
    ) -> Result<f32, Error> {
        let unsigned_bits = int_bits.checked_add(frac_bits).ok_or(Error::Argument)?;
        let total_bits = unsigned_bits
            .checked_add(i32::from(signed))
            .ok_or(Error::Argument)?;
        let max = checked_power_of_two(int_bits)?;
        let scale = checked_power_of_two(frac_bits)?;
        let packed = match total_bits {
            ..=8 => u32::from(self.unpack_byte()?),
            9..=16 => self.unpack_bits_to_u32(16)?,
            17..=31 => self.unpack_bits_to_u32(32)?,
            _ => return Ok(0.0),
        };
        let mut fixed = packed as f32 / scale as f32;
        if signed {
            fixed -= max as f32;
        }
        Ok(fixed)
    }

    pub fn unpack_float(&mut self) -> Result<f32, Error> {
        Ok(f32::from_bits(self.unpack_bits_to_u32(32)?))
    }

    pub fn unpack_int(&mut self) -> Result<i32, Error> {
        self.unpack_bits(32)
    }

    pub fn unpack_short(&mut self) -> Result<i16, Error> {
        Ok(u16::try_from(self.unpack_bits_to_u32(16)?)
            .map_err(|_| Error::Argument)?
            .cast_signed())
    }

    pub fn unpack_string(&mut self, size: i32) -> Result<String, Error> {
        if self.bit_pos != 0 {
            return Err(Error::IndexOutOfRange);
        }
        let size = usize::try_from(size).map_err(|_| Error::IndexOutOfRange)?;
        let end = self
            .byte_pos
            .checked_add(size)
            .ok_or(Error::IndexOutOfRange)?;
        let bytes = self
            .data
            .get(self.byte_pos..end)
            .ok_or(Error::IndexOutOfRange)?;
        let value = String::from_utf8_lossy(bytes).into_owned();
        self.byte_pos = end;
        Ok(value)
    }

    pub fn unpack_u_bits(&mut self, total_count: i32) -> Result<u32, Error> {
        self.unpack_bits_to_u32(total_count)
    }

    pub fn unpack_u_int(&mut self) -> Result<u32, Error> {
        self.unpack_bits_to_u32(32)
    }

    pub fn unpack_u_short(&mut self) -> Result<u16, Error> {
        u16::try_from(self.unpack_bits_to_u32(16)?).map_err(|_| Error::Argument)
    }

    pub fn unpack_uuid(&mut self) -> Result<UUID, Error> {
        if self.bit_pos != 0 {
            return Err(Error::IndexOutOfRange);
        }
        let end = self
            .byte_pos
            .checked_add(16)
            .ok_or(Error::IndexOutOfRange)?;
        let bytes = self
            .data
            .get(self.byte_pos..end)
            .ok_or(Error::IndexOutOfRange)?;
        let value = UUID::new_with_bytes_int32(bytes.to_vec(), 0)?;
        self.byte_pos = end;
        Ok(value)
    }

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

    pub fn set_bit_pos(&mut self, value: i32) {
        self.bit_pos = value;
    }

    #[must_use]
    pub fn byte_pos(&self) -> i32 {
        let position = if self.byte_pos != 0 && self.bit_pos == 0 {
            self.byte_pos - 1
        } else {
            self.byte_pos
        };
        i32::try_from(position).unwrap_or(i32::MAX)
    }

    fn pack_bit_array(&mut self, source: &[u8], mut total_count: i32) -> Result<(), Error> {
        if total_count <= 0 {
            return Ok(());
        }
        let required = usize::try_from((total_count + 7) / 8).map_err(|_| Error::Argument)?;
        if required > source.len() {
            return Err(Error::Argument);
        }
        let mut source_byte = 0_usize;
        while total_count > 0 {
            let mut count = total_count.min(BITS_PER_BYTE);
            total_count -= count;
            while count > 0 {
                let target = self
                    .data
                    .get_mut(self.byte_pos)
                    .ok_or(Error::IndexOutOfRange)?;
                let target_bit = 0x80_u8 >> self.bit_pos;
                let source_bit = 1_u8 << (count - 1);
                if source[source_byte] & source_bit != 0 {
                    *target |= target_bit;
                } else {
                    *target &= !target_bit;
                }
                count -= 1;
                self.bit_pos += 1;
                if self.bit_pos >= BITS_PER_BYTE {
                    self.bit_pos = 0;
                    self.byte_pos += 1;
                }
            }
            source_byte += 1;
        }
        Ok(())
    }

    fn unpack_bits_to_u32(&mut self, mut total_count: i32) -> Result<u32, Error> {
        if total_count <= 0 {
            return Ok(0);
        }
        if total_count > 32 {
            return Err(Error::Argument);
        }
        let mut output = [0_u8; 4];
        let mut output_byte = 0_usize;
        while total_count > 0 {
            let mut count = total_count.min(BITS_PER_BYTE);
            total_count -= count;
            while count > 0 {
                output[output_byte] <<= 1;
                let source = *self.data.get(self.byte_pos).ok_or(Error::IndexOutOfRange)?;
                if source & (0x80_u8 >> self.bit_pos) != 0 {
                    output[output_byte] += 1;
                }
                self.bit_pos += 1;
                count -= 1;
                if self.bit_pos >= BITS_PER_BYTE {
                    self.bit_pos = 0;
                    self.byte_pos += 1;
                }
            }
            output_byte += 1;
        }
        Ok(u32::from_le_bytes(output))
    }
}

fn checked_power_of_two(bits: i32) -> Result<i32, Error> {
    let bits = u32::try_from(bits).map_err(|_| Error::Argument)?;
    1_i32.checked_shl(bits).ok_or(Error::Argument)
}

#[cfg(test)]
mod tests {
    use super::BitPack;
    use crate::Error;

    #[test]
    fn reports_bounds_errors_without_panicking() {
        let mut writer = BitPack::new(vec![], 0).unwrap();
        assert_eq!(writer.pack_bit(true), Err(Error::IndexOutOfRange));

        let mut reader = BitPack::new(vec![0], 0).unwrap();
        assert_eq!(reader.unpack_u_bits(33), Err(Error::Argument));
        assert_eq!(reader.unpack_u_bits(16), Err(Error::IndexOutOfRange));
    }

    #[test]
    fn byte_position_matches_completed_byte_semantics() {
        let mut bits = BitPack::new(vec![0; 2], 0).unwrap();
        bits.pack_bit(true).unwrap();
        assert_eq!(bits.byte_pos(), 0);
        for _ in 1..8 {
            bits.pack_bit(false).unwrap();
        }
        assert_eq!(bits.byte_pos(), 0);
        assert_eq!(bits.bit_pos(), 0);
    }
}
