//! Misc utilities for the ABI crate

use crate::error::Error;
use core::{fmt, str::FromStr};

/// Helper struct to hold a fixed-size string of UTF-8 characters
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct FixedString<const N: usize> {
    data: [u8; N],
    len: usize,
}

impl<const N: usize> FixedString<N> {
    /// Returns an empty [FixedString]
    pub const fn empty() -> Self {
        Self {
            data: [0; N],
            len: 0,
        }
    }

    /// Copies character codepoints, encoding them in UTF-8, into this [FixedString]. Returns
    /// false if the result cannot fit into the string.
    pub fn append_from_chars<I: Iterator<Item = char>>(&mut self, it: I) -> Result<usize, usize> {
        let start = self.len;
        for ch in it {
            let ch_len = ch.len_utf8();
            if self.len + ch_len > N {
                return Err(self.len - start);
            }
            ch.encode_utf8(&mut self.data[self.len..]);
            self.len += ch_len;
        }
        Ok(self.len - start)
    }

    /// Appends bytes to the string. Returns false and does not change the string if there is not
    /// enough space to fit the whole `bytes` slice there.
    pub fn append_from_bytes(&mut self, bytes: &[u8]) -> bool {
        if self.len + bytes.len() > N {
            return false;
        }
        self.data[self.len..self.len + bytes.len()].copy_from_slice(bytes);
        self.len += bytes.len();
        true
    }

    /// Returns the length of the string
    pub const fn len(&self) -> usize {
        self.len
    }

    /// Returns `true` if the string is empty
    pub const fn is_empty(&self) -> bool {
        self.len == 0
    }

    /// Returns the underlying byte data of the string
    pub fn as_bytes(&self) -> &[u8] {
        &self.data[..self.len]
    }
}

impl<const N: usize> fmt::Debug for FixedString<N> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        fmt::Debug::fmt(self.as_ref(), f)
    }
}

impl<const N: usize> AsRef<str> for FixedString<N> {
    fn as_ref(&self) -> &str {
        // Safety: totally safe, the FixedString can only be constructed from a &str or be empty
        unsafe { core::str::from_utf8_unchecked(&self.data[..self.len]) }
    }
}

impl<const N: usize> FromStr for FixedString<N> {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        if s.len() >= N {
            return Err(Error::InvalidArgument);
        }
        let len = s.len();
        let mut data = [0; N];
        data[..len].copy_from_slice(s.as_bytes());
        Ok(Self { data, len })
    }
}

impl<const N: usize> PartialEq<&str> for FixedString<N> {
    fn eq(&self, other: &&str) -> bool {
        &self.data[..self.len] == other.as_bytes()
    }
}

/// Reinterprets a `T` structure as a slice of its bytes
#[cfg(feature = "bytemuck")]
pub fn bytes_of<T: bytemuck::Pod>(data: &T) -> &[u8] {
    bytemuck::bytes_of(data)
}

/// Reinterprets a slice of bytes as a `T` structure reference
#[cfg(feature = "bytemuck")]
pub fn from_bytes<T: bytemuck::Pod>(bytes: &[u8]) -> &T {
    bytemuck::from_bytes(bytes)
}

/// Reinterprets a `T` structure as a slice of its bytes
#[cfg(not(feature = "bytemuck"))]
pub fn bytes_of<T>(data: &T) -> &[u8] {
    unsafe { core::slice::from_raw_parts(data as *const T as *const u8, core::mem::size_of::<T>()) }
}

/// Reinterprets a slice of bytes as a `T` structure reference
#[cfg(not(feature = "bytemuck"))]
pub fn from_bytes<T>(bytes: &[u8]) -> &T {
    // TODO align check
    assert_eq!(bytes.len(), core::mem::size_of::<T>());
    unsafe { &*(&bytes[0] as *const u8 as *const T) }
}

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

    #[test]
    fn test_fixed_string_append() {
        let mut fixed = FixedString::<16>::empty();
        assert!(fixed.append_from_bytes(b"test"));
        assert!(fixed.append_from_bytes(b"2test"));
        assert!(fixed.append_from_bytes(b"3test"));
        assert_eq!(fixed, "test2test3test");
        assert!(!fixed.append_from_bytes(b"4test"));
        assert_eq!(fixed, "test2test3test");

        let mut fixed = FixedString::<256>::empty();
        assert_eq!(fixed.append_from_chars("test".chars()), Ok(4));
        assert_eq!(fixed, "test");
        assert_eq!(
            fixed.append_from_chars("very0very1very2very3very4longfilename.txt".chars()),
            Ok(41)
        );
        assert_eq!(fixed, "testvery0very1very2very3very4longfilename.txt");
        assert_eq!(fixed.append_from_chars("юнікод".chars()), Ok(12));
        assert_eq!(fixed, "testvery0very1very2very3very4longfilename.txtюнікод");
    }
}
