use core::ffi::CStr;

use crate::error::Error;

#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(transparent)]
pub struct Path(str);

impl Path {
    pub const SEPARATOR: char = '/';
    pub const SEPARATOR_STR: &'static str = "/";

    /// Parent directory path element
    pub const PARENT_NAME: &'static str = "..";
    /// Path element that refers to itself
    pub const SELF_NAME: &'static str = ".";

    pub fn from_str(s: &str) -> &Self {
        unsafe { core::mem::transmute(s) }
    }

    pub fn from_c_str(s: &CStr) -> Result<&Self, Error> {
        s.to_str()
            .map_err(|_| Error::InvalidArgument)
            .map(Path::from_str)
    }

    pub fn empty() -> &'static Self {
        Self::from_str("")
    }

    pub fn trim_start_separators(&self) -> &Self {
        Self::from_str(self.0.trim_start_matches(Self::SEPARATOR))
    }

    pub fn trim_end_separators(&self) -> &Self {
        Self::from_str(self.0.trim_end_matches(Self::SEPARATOR))
    }

    pub fn display(&self) -> &str {
        &self.0
    }

    pub fn split_right(&self) -> (&Path, &str) {
        if let Some((left, right)) = self
            .0
            .trim_end_matches(Self::SEPARATOR)
            .rsplit_once(Self::SEPARATOR)
        {
            (
                Path::from_str(left.trim_end_matches(Self::SEPARATOR)),
                right,
            )
        } else {
            (Path::empty(), &self.0)
        }
    }

    pub fn split_left(&self) -> (&str, &Path) {
        if let Some((left, right)) = self.0.split_once(Self::SEPARATOR) {
            (
                left,
                Path::from_str(right.trim_start_matches(Self::SEPARATOR)),
            )
        } else {
            (&self.0, Path::empty())
        }
    }

    pub fn split_dirname(&self) -> (&Path, &str) {
        if let Some((left, right)) = self.0.rsplit_once(Self::SEPARATOR) {
            if left.is_empty() {
                // Cases like "/a" or "a"
                let left = if self.is_absolute() { "/" } else { "" }.as_ref();

                (left, right)
            } else {
                (left.as_ref(), right)
            }
        } else {
            ("".as_ref(), self.as_str())
        }
    }

    #[inline]
    pub fn is_absolute(&self) -> bool {
        self.0.starts_with(Self::SEPARATOR)
    }

    #[inline]
    pub fn is_empty(&self) -> bool {
        self.0.is_empty()
    }

    #[inline]
    pub fn len(&self) -> usize {
        self.0.len()
    }

    #[inline]
    pub fn as_ptr(&self) -> *const u8 {
        self.0.as_ptr()
    }

    #[inline]
    pub fn as_str(&self) -> &str {
        &self.0
    }

    #[cfg(feature = "alloc")]
    #[allow(clippy::inherent_to_string)]
    #[inline]
    pub fn to_string(&self) -> alloc::string::String {
        self.0.into()
    }
}

impl AsRef<str> for Path {
    fn as_ref(&self) -> &str {
        &self.0
    }
}

impl AsRef<Path> for Path {
    fn as_ref(&self) -> &Path {
        self
    }
}

impl AsRef<Path> for str {
    fn as_ref(&self) -> &Path {
        Path::from_str(self)
    }
}

#[cfg(feature = "alloc")]
impl AsRef<Path> for alloc::string::String {
    fn as_ref(&self) -> &Path {
        self.as_str().as_ref()
    }
}

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

    #[test]
    fn test_path_split_dirname() {
        let cases = [
            ("/a/b", "/a", "b"),
            ("/a/b/", "/a/b", ""),
            ("/", "/", ""),
            ("", "", ""),
            ("a/b", "a", "b"),
            ("a", "", "a"),
            ("a/b/", "a/b", ""),
        ];

        for (input, out_path, out_name) in cases {
            let path = Path::from_str(input);
            let (left, right) = path.split_dirname();
            assert_eq!(left.as_str(), out_path);
            assert_eq!(right, out_name);
        }
    }
}
