use crate::v4::error::Result;
use std::cmp::Ordering;
use std::ffi::{CStr, CString};

/// A null-terminated string that always maps to the 128 bytes assigned to it in [Message](crate::v4::Message).
///
/// You can get the string value back out with `to_string()`:
/// ```
/// # use toe_beans::v4::File;
/// # use std::ffi::CString;
/// # let file = File::new(c"/example").unwrap();
/// let path: CString = file.to_string();
/// ```
#[derive(Debug, PartialEq)]
pub struct File([u8; 128]);

impl File {
    /// Construct a valid file field.
    /// `path` must not be more than 128 bytes.
    /// The last byte of which must be `0x00` (null-terminated).
    pub fn new(path: &CStr) -> Result<Self> {
        let bytes = path.to_bytes_with_nul();
        match bytes.len().cmp(&128) {
            Ordering::Less => {
                let mut padding = [0u8; 128];
                bytes
                    .iter()
                    .enumerate()
                    .for_each(|(i, byte)| padding[i] = *byte);
                Ok(Self(padding))
            }
            Ordering::Equal => Ok(Self(bytes.try_into().unwrap())),
            Ordering::Greater => Err("File path does not fit in a 128 byte array"),
        }
    }

    /// Converts an array slice of exactly 128 byte length
    /// into a File without performing any checks (such as length or null-termination).
    ///
    /// Panics if slice is not 128 bytes.
    ///
    /// Useful if you are handling the return of `parse_file`.
    pub fn from_slice_unchecked(path: &[u8]) -> Self {
        Self(path.try_into().unwrap())
    }

    /// Create a File by passing the inner type directly.
    pub fn from_array(name: [u8; 128]) -> Self {
        Self(name)
    }

    /// A File that contains all zero bytes.
    ///
    /// This is more performant than `File::new(c"")`
    pub const EMPTY: Self = Self([0; 128]);

    /// Convert the internal array to a null-terminated
    /// string representation with padding removed.
    pub fn to_string(&self) -> CString {
        CStr::from_bytes_until_nul(&self.0).unwrap().to_owned()
    }

    /// Get the length of File.
    ///
    /// Note that this returns the length of the inner array
    /// which is always 128. To get the length of its string
    /// representation call `file.to_string().len()`.
    pub fn len(&self) -> usize {
        128
    }
}

impl From<&File> for [u8; 128] {
    fn from(file: &File) -> Self {
        file.0
    }
}

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

    #[test]
    fn test_new_file_below_length() {
        let file = File::new(c"12345");
        assert!(file.is_ok());
    }

    #[test]
    fn test_new_file_at_length() {
        // 127 bytes plus the NUL byte
        let file = File::new(c"1234512345123451234512345123451234512345123451234512345123451234512345123451234512345123451234512345123451234512345123451234512");
        assert!(file.is_ok());
    }

    #[test]
    fn test_new_file_above_length() {
        let file = File::new(c"1234512345123451234512345123451234512345123451234512345123451234512345123451234512345123451234512345123451234512345123451234512345");
        assert!(file.is_err());
    }

    #[test]
    fn test_empty() {
        let file = File::EMPTY;
        assert_eq!(file.0, [0; 128]);
    }
}
