//! 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) => {
                #[cfg(target_os = "windows")]
                let path: String = path
                    .chars()
                    .map(|c| match c {
                        '/' => std::path::MAIN_SEPARATOR,
                        c => c,
                    })
                    .collect();
                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::VirtualPath(lhs), VfsPathRepr::VirtualPath(rhs)) => lhs.starts_with(rhs),
            (VfsPathRepr::VirtualPath(_) | VfsPathRepr::PathBuf(_), _) => false,
        }
    }

    pub fn is_virt(&self) -> bool {
        matches!(self.0, VfsPathRepr::VirtualPath(_))
    }

    /// 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 stem_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.stem_and_extension(),
        }
    }

    pub fn name(&self) -> Option<String> {
        match &self.0 {
            VfsPathRepr::PathBuf(p) => Some(p.file_name()?.to_string_lossy().into_owned()),
            VfsPathRepr::VirtualPath(p) => Some(p.name()?.to_owned()),
        }
    }

    ///// **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 std::convert::TryFrom;

//         use super::{AbsPathBuf, VfsPath};
//         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 stem_and_extension(&self) -> Option<(&str, Option<&str>)> {
        let file_name = self.name()?;
        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)),
        }
    }

    fn name(&self) -> 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 {
            Some(file_name)
        }
    }
}

#[cfg(test)]
mod tests;
