//! Abstract-ish representation of paths for VFS.
use std::fmt;

use paths::{AbsPath, AbsPathBuf};

/// Path in [`Vfs`].
///
/// Long-term, we want to support files which do not reside in the file-system,
/// so we treat `VfsPath`s as opaque identifiers.
///
/// [`Vfs`]: crate::Vfs
#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
pub struct VfsPath(VfsPathRepr);

impl VfsPath {
    /// Creates an "in-memory" path from `/`-separated string.
    ///
    /// This is most useful for testing, to avoid windows/linux differences
    ///
    /// # Panics
    ///
    /// Panics if `path` does not start with `'/'`.
    pub fn new_virtual_path(path: String) -> VfsPath {
        assert!(path.starts_with('/'));
        VfsPath(VfsPathRepr::VirtualPath(VirtualPath(path)))
    }

    /// Returns the `AbsPath` representation of `self` if `self` is on the file system.
    pub fn as_path(&self) -> Option<&AbsPath> {
        match &self.0 {
            VfsPathRepr::PathBuf(it) => Some(it.as_path()),
            VfsPathRepr::VirtualPath(_) => None,
        }
    }

    /// Creates a new `VfsPath` with `path` adjoined to `self`.
    pub fn join(&self, path: &str) -> Option<VfsPath> {
        match &self.0 {
            VfsPathRepr::PathBuf(it) => {
                let res = it.join(path).normalize();
                Some(VfsPath(VfsPathRepr::PathBuf(res)))
            }
            VfsPathRepr::VirtualPath(it) => {
                let res = it.join(path)?;
                Some(VfsPath(VfsPathRepr::VirtualPath(res)))
            }
        }
    }

    /// Remove the last component of `self` if there is one.
    ///
    /// If `self` has no component, returns `false`; else returns `true`.
    ///
    /// # Example
    ///
    /// ```
    /// # use vfs::{AbsPathBuf, VfsPath};
    /// let mut path = VfsPath::from(AbsPathBuf::assert("/foo/bar".into()));
    /// assert!(path.pop());
    /// assert_eq!(path, VfsPath::from(AbsPathBuf::assert("/foo".into())));
    /// assert!(path.pop());
    /// assert_eq!(path, VfsPath::from(AbsPathBuf::assert("/".into())));
    /// assert!(!path.pop());
    /// ```
    pub fn pop(&mut self) -> bool {
        match &mut self.0 {
            VfsPathRepr::PathBuf(it) => it.pop(),
            VfsPathRepr::VirtualPath(it) => it.pop(),
        }
    }

    /// Returns `true` if `other` is a prefix of `self`.
    pub fn starts_with(&self, other: &VfsPath) -> bool {
        match (&self.0, &other.0) {
            (VfsPathRepr::PathBuf(lhs), VfsPathRepr::PathBuf(rhs)) => lhs.starts_with(rhs),
            (VfsPathRepr::PathBuf(_), _) => false,
            (VfsPathRepr::VirtualPath(lhs), VfsPathRepr::VirtualPath(rhs)) => lhs.starts_with(rhs),
            (VfsPathRepr::VirtualPath(_), _) => false,
        }
    }

    /// Returns the `VfsPath` without its final component, if there is one.
    ///
    /// Returns [`None`] if the path is a root or prefix.
    pub fn parent(&self) -> Option<VfsPath> {
        let mut parent = self.clone();
        if parent.pop() {
            Some(parent)
        } else {
            None
        }
    }

    /// Returns `self`'s base name and file extension.
    pub fn name_and_extension(&self) -> Option<(&str, Option<&str>)> {
        match &self.0 {
            VfsPathRepr::PathBuf(p) => Some((
                p.file_stem()?.to_str()?,
                p.extension().and_then(|extension| extension.to_str()),
            )),
            VfsPathRepr::VirtualPath(p) => p.name_and_extension(),
        }
    }

    /// **Don't make this `pub`**
    ///
    /// Encode the path in the given buffer.
    ///
    /// The encoding will be `0` if [`AbsPathBuf`], `1` if [`VirtualPath`], followed
    /// by `self`'s representation.
    ///
    /// Note that this encoding is dependent on the operating system.
    pub(crate) fn encode(&self, buf: &mut Vec<u8>) {
        let tag = match &self.0 {
            VfsPathRepr::PathBuf(_) => 0,
            VfsPathRepr::VirtualPath(_) => 1,
        };
        buf.push(tag);
        match &self.0 {
            VfsPathRepr::PathBuf(path) => {
                #[cfg(windows)]
                {
                    use windows_paths::Encode;
                    let path: &std::path::Path = path.as_ref();
                    let components = path.components();
                    let mut add_sep = false;
                    for component in components {
                        if add_sep {
                            windows_paths::SEP.encode(buf);
                        }
                        let len_before = buf.len();
                        match component {
                            std::path::Component::Prefix(prefix) => {
                                // kind() returns a normalized and comparable path prefix.
                                prefix.kind().encode(buf);
                            }
                            std::path::Component::RootDir => {
                                if !add_sep {
                                    component.as_os_str().encode(buf);
                                }
                            }
                            _ => component.as_os_str().encode(buf),
                        }

                        // some components may be encoded empty
                        add_sep = len_before != buf.len();
                    }
                }
                #[cfg(unix)]
                {
                    use std::os::unix::ffi::OsStrExt;
                    buf.extend(path.as_os_str().as_bytes());
                }
                #[cfg(not(any(windows, unix)))]
                {
                    buf.extend(path.as_os_str().to_string_lossy().as_bytes());
                }
            }
            VfsPathRepr::VirtualPath(VirtualPath(s)) => buf.extend(s.as_bytes()),
        }
    }
}

#[cfg(windows)]
mod windows_paths {
    pub(crate) trait Encode {
        fn encode(&self, buf: &mut Vec<u8>);
    }

    impl Encode for std::ffi::OsStr {
        fn encode(&self, buf: &mut Vec<u8>) {
            use std::os::windows::ffi::OsStrExt;
            for wchar in self.encode_wide() {
                buf.extend(wchar.to_le_bytes().iter().copied());
            }
        }
    }

    impl Encode for u8 {
        fn encode(&self, buf: &mut Vec<u8>) {
            let wide = *self as u16;
            buf.extend(wide.to_le_bytes().iter().copied())
        }
    }

    impl Encode for &str {
        fn encode(&self, buf: &mut Vec<u8>) {
            debug_assert!(self.is_ascii());
            for b in self.as_bytes() {
                b.encode(buf)
            }
        }
    }

    pub(crate) const SEP: &str = "\\";
    const VERBATIM: &str = "\\\\?\\";
    const UNC: &str = "UNC";
    const DEVICE: &str = "\\\\.\\";
    const COLON: &str = ":";

    impl Encode for std::path::Prefix<'_> {
        fn encode(&self, buf: &mut Vec<u8>) {
            match self {
                std::path::Prefix::Verbatim(c) => {
                    VERBATIM.encode(buf);
                    c.encode(buf);
                }
                std::path::Prefix::VerbatimUNC(server, share) => {
                    VERBATIM.encode(buf);
                    UNC.encode(buf);
                    SEP.encode(buf);
                    server.encode(buf);
                    SEP.encode(buf);
                    share.encode(buf);
                }
                std::path::Prefix::VerbatimDisk(d) => {
                    VERBATIM.encode(buf);
                    d.encode(buf);
                    COLON.encode(buf);
                }
                std::path::Prefix::DeviceNS(device) => {
                    DEVICE.encode(buf);
                    device.encode(buf);
                }
                std::path::Prefix::UNC(server, share) => {
                    SEP.encode(buf);
                    SEP.encode(buf);
                    server.encode(buf);
                    SEP.encode(buf);
                    share.encode(buf);
                }
                std::path::Prefix::Disk(d) => {
                    d.encode(buf);
                    COLON.encode(buf);
                }
            }
        }
    }
    #[test]
    fn paths_encoding() {
        // drive letter casing agnostic
        test_eq("C:/x.rs", "c:/x.rs");
        // separator agnostic
        test_eq("C:/x/y.rs", "C:\\x\\y.rs");

        fn test_eq(a: &str, b: &str) {
            let mut b1 = Vec::new();
            let mut b2 = Vec::new();
            vfs(a).encode(&mut b1);
            vfs(b).encode(&mut b2);
            assert_eq!(b1, b2);
        }
    }

    #[test]
    fn test_sep_root_dir_encoding() {
        let mut buf = Vec::new();
        vfs("C:/x/y").encode(&mut buf);
        assert_eq!(&buf, &[0, 67, 0, 58, 0, 92, 0, 120, 0, 92, 0, 121, 0])
    }

    #[cfg(test)]
    fn vfs(str: &str) -> super::VfsPath {
        use super::{AbsPathBuf, VfsPath};
        use std::convert::TryFrom;
        VfsPath::from(AbsPathBuf::try_from(str).unwrap())
    }
}

/// Internal, private representation of [`VfsPath`].
#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
enum VfsPathRepr {
    PathBuf(AbsPathBuf),
    VirtualPath(VirtualPath),
}

impl From<AbsPathBuf> for VfsPath {
    fn from(v: AbsPathBuf) -> Self {
        VfsPath(VfsPathRepr::PathBuf(v.normalize()))
    }
}

impl fmt::Display for VfsPath {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match &self.0 {
            VfsPathRepr::PathBuf(it) => fmt::Display::fmt(&it.display(), f),
            VfsPathRepr::VirtualPath(VirtualPath(it)) => fmt::Display::fmt(it, f),
        }
    }
}

impl fmt::Debug for VfsPath {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        fmt::Debug::fmt(&self.0, f)
    }
}

impl fmt::Debug for VfsPathRepr {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match &self {
            VfsPathRepr::PathBuf(it) => fmt::Debug::fmt(&it.display(), f),
            VfsPathRepr::VirtualPath(VirtualPath(it)) => fmt::Debug::fmt(&it, f),
        }
    }
}

/// `/`-separated virtual path.
///
/// This is used to describe files that do not reside on the file system.
#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
struct VirtualPath(String);

impl VirtualPath {
    /// Returns `true` if `other` is a prefix of `self` (as strings).
    fn starts_with(&self, other: &VirtualPath) -> bool {
        self.0.starts_with(&other.0)
    }

    /// Remove the last component of `self`.
    ///
    /// This will find the last `'/'` in `self`, and remove everything after it,
    /// including the `'/'`.
    ///
    /// If `self` contains no `'/'`, returns `false`; else returns `true`.
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// let mut path = VirtualPath("/foo/bar".to_string());
    /// path.pop();
    /// assert_eq!(path.0, "/foo");
    /// path.pop();
    /// assert_eq!(path.0, "");
    /// ```
    fn pop(&mut self) -> bool {
        let pos = match self.0.rfind('/') {
            Some(pos) => pos,
            None => return false,
        };
        self.0 = self.0[..pos].to_string();
        true
    }

    /// Append the given *relative* path `path` to `self`.
    ///
    /// This will resolve any leading `"../"` in `path` before appending it.
    ///
    /// Returns [`None`] if `path` has more leading `"../"` than the number of
    /// components in `self`.
    ///
    /// # Notes
    ///
    /// In practice, appending here means `self/path` as strings.
    fn join(&self, mut path: &str) -> Option<VirtualPath> {
        let mut res = self.clone();
        while path.starts_with("../") {
            if !res.pop() {
                return None;
            }
            path = &path["../".len()..]
        }
        res.0 = format!("{}/{}", res.0, path);
        Some(res)
    }

    /// Returns `self`'s base name and file extension.
    ///
    /// # Returns
    /// - `None` if `self` ends with `"//"`.
    /// - `Some((name, None))` if `self`'s base contains no `.`, or only one `.` at
    /// the start.
    /// - `Some((name, Some(extension))` else.
    ///
    /// # Note
    /// The extension will not contains `.`. This means `"/foo/bar.baz.rs"` will
    /// return `Some(("bar.baz", Some("rs"))`.
    fn name_and_extension(&self) -> Option<(&str, Option<&str>)> {
        let file_path = if self.0.ends_with('/') { &self.0[..&self.0.len() - 1] } else { &self.0 };
        let file_name = match file_path.rfind('/') {
            Some(position) => &file_path[position + 1..],
            None => file_path,
        };

        if file_name.is_empty() {
            None
        } else {
            let mut file_stem_and_extension = file_name.rsplitn(2, '.');
            let extension = file_stem_and_extension.next();
            let file_stem = file_stem_and_extension.next();

            match (file_stem, extension) {
                (None, None) => None,
                (None | Some(""), Some(_)) => Some((file_name, None)),
                (Some(file_stem), extension) => Some((file_stem, extension)),
            }
        }
    }
}

#[cfg(test)]
mod tests;
