/*!

Cross-platform file path handling

*/

// NB: transitionary, de-mode-ing.
#[forbid(deprecated_mode)];
#[forbid(deprecated_pattern)];

use cmp::Eq;

pub struct WindowsPath {
    host: Option<~str>,
    device: Option<~str>,
    is_absolute: bool,
    components: ~[~str],
}

pub struct PosixPath {
    is_absolute: bool,
    components: ~[~str],
}

pub trait GenericPath {

    static pure fn from_str((&str)) -> self;

    pure fn dirname() -> ~str;
    pure fn filename() -> Option<~str>;
    pure fn filestem() -> Option<~str>;
    pure fn filetype() -> Option<~str>;

    pure fn with_dirname((&str)) -> self;
    pure fn with_filename((&str)) -> self;
    pure fn with_filestem((&str)) -> self;
    pure fn with_filetype((&str)) -> self;

    pure fn push((&str)) -> self;
    pure fn push_rel((&self)) -> self;
    pure fn push_many((&[~str])) -> self;
    pure fn pop() -> self;

    pure fn normalize() -> self;
}

#[cfg(windows)]
pub type Path = WindowsPath;

#[cfg(windows)]
pub pure fn Path(s: &str) -> Path {
    from_str::<WindowsPath>(s)
}

#[cfg(unix)]
pub type Path = PosixPath;

#[cfg(unix)]
pub pure fn Path(s: &str) -> Path {
    from_str::<PosixPath>(s)
}

impl PosixPath : ToStr {
    pure fn to_str() -> ~str {
        let mut s = ~"";
        if self.is_absolute {
            s += "/";
        }
        s + str::connect(self.components, "/")
    }
}

impl PosixPath : Eq {
    pure fn eq(other: &PosixPath) -> bool {
        return self.is_absolute == (*other).is_absolute &&
            self.components == (*other).components;
    }
    pure fn ne(other: &PosixPath) -> bool { !self.eq(other) }
}

impl WindowsPath : Eq {
    pure fn eq(other: &WindowsPath) -> bool {
        return self.host == (*other).host &&
            self.device == (*other).device &&
            self.is_absolute == (*other).is_absolute &&
            self.components == (*other).components;
    }
    pure fn ne(other: &WindowsPath) -> bool { !self.eq(other) }
}

// FIXME (#3227): when default methods in traits are working, de-duplicate
// PosixPath and WindowsPath, most of their methods are common.
impl PosixPath : GenericPath {

    static pure fn from_str(s: &str) -> PosixPath {
        let mut components = str::split_nonempty(s, |c| c == '/');
        let is_absolute = (s.len() != 0 && s[0] == '/' as u8);
        return PosixPath { is_absolute: is_absolute,
                           components: components }
    }

    pure fn dirname() -> ~str {
        unsafe {
            let s = self.dir_path().to_str();
            if s.len() == 0 {
                ~"."
            } else {
                move s
            }
        }
    }

    pure fn filename() -> Option<~str> {
        match self.components.len() {
          0 => None,
          n => Some(copy self.components[n - 1])
        }
    }

    pure fn filestem() -> Option<~str> {
        match self.filename() {
          None => None,
          Some(ref f) => {
            match str::rfind_char(*f, '.') {
              Some(p) => Some(f.slice(0, p)),
              None => Some(copy *f)
            }
          }
        }
    }

    pure fn filetype() -> Option<~str> {
        match self.filename() {
          None => None,
          Some(ref f) => {
            match str::rfind_char(*f, '.') {
              Some(p) if p < f.len() => Some(f.slice(p, f.len())),
              _ => None
            }
          }
        }
    }

    pure fn with_dirname(d: &str) -> PosixPath {
        let dpath = from_str::<PosixPath>(d);
        match self.filename() {
          Some(ref f) => dpath.push(*f),
          None => move dpath
        }
    }

    pure fn with_filename(f: &str) -> PosixPath {
        unsafe {
            assert ! str::any(f, |c| windows::is_sep(c as u8));
            self.dir_path().push(f)
        }
    }

    pure fn with_filestem(s: &str) -> PosixPath {
        match self.filetype() {
          None => self.with_filename(s),
          Some(ref t) => self.with_filename(str::from_slice(s) + *t)
        }
    }

    pure fn with_filetype(t: &str) -> PosixPath {
        if t.len() == 0 {
            match self.filestem() {
              None => copy self,
              Some(ref s) => self.with_filename(*s)
            }
        } else {
            let t = ~"." + str::from_slice(t);
            match self.filestem() {
              None => self.with_filename(t),
              Some(ref s) => self.with_filename(*s + t)
            }
        }
    }

    pure fn dir_path() -> PosixPath {
        if self.components.len() != 0 {
            self.pop()
        } else {
            copy self
        }
    }

    pure fn file_path() -> PosixPath {
        let cs = match self.filename() {
          None => ~[],
          Some(ref f) => ~[copy *f]
        };
        return PosixPath { is_absolute: false,
                           components: cs }
    }

    pure fn push_rel(other: &PosixPath) -> PosixPath {
        assert !other.is_absolute;
        self.push_many(other.components)
    }

    pure fn push_many(cs: &[~str]) -> PosixPath {
        let mut v = copy self.components;
        for cs.each |e| {
            let mut ss = str::split_nonempty(
                *e,
                |c| windows::is_sep(c as u8));
            unsafe { v.push_all_move(move ss); }
        }
        PosixPath { components: move v, ..self }
    }

    pure fn push(s: &str) -> PosixPath {
        let mut v = copy self.components;
        let mut ss = str::split_nonempty(s, |c| windows::is_sep(c as u8));
        unsafe { v.push_all_move(move ss); }
        PosixPath { components: move v, ..self }
    }

    pure fn pop() -> PosixPath {
        let mut cs = copy self.components;
        if cs.len() != 0 {
            unsafe { cs.pop(); }
        }
        return PosixPath { components: move cs, ..self }
    }

    pure fn normalize() -> PosixPath {
        return PosixPath {
            components: normalize(self.components),
            ..self
        }
    }
}


impl WindowsPath : ToStr {
    pure fn to_str() -> ~str {
        let mut s = ~"";
        match self.host {
          Some(ref h) => { s += "\\\\"; s += *h; }
          None => { }
        }
        match self.device {
          Some(ref d) => { s += *d; s += ":"; }
          None => { }
        }
        if self.is_absolute {
            s += "\\";
        }
        s + str::connect(self.components, "\\")
    }
}


impl WindowsPath : GenericPath {

    static pure fn from_str(s: &str) -> WindowsPath {
        let host;
        let device;
        let rest;

        match windows::extract_drive_prefix(s) {
          Some((ref d, ref r)) => {
            host = None;
            device = Some(copy *d);
            rest = copy *r;
          }
          None => {
            match windows::extract_unc_prefix(s) {
              Some((ref h, ref r)) => {
                host = Some(copy *h);
                device = None;
                rest = copy *r;
              }
              None => {
                host = None;
                device = None;
                rest = str::from_slice(s);
              }
            }
          }
        }

        let mut components =
            str::split_nonempty(rest, |c| windows::is_sep(c as u8));
        let is_absolute = (rest.len() != 0 && windows::is_sep(rest[0]));
        return WindowsPath { host: host,
                             device: device,
                             is_absolute: is_absolute,
                             components: components }
    }

    pure fn dirname() -> ~str {
        unsafe {
            let s = self.dir_path().to_str();
            if s.len() == 0 {
                ~"."
            } else {
                move s
            }
        }
    }

    pure fn filename() -> Option<~str> {
        match self.components.len() {
          0 => None,
          n => Some(copy self.components[n - 1])
        }
    }

    pure fn filestem() -> Option<~str> {
        match self.filename() {
          None => None,
          Some(ref f) => {
            match str::rfind_char(*f, '.') {
              Some(p) => Some(f.slice(0, p)),
              None => Some(copy *f)
            }
          }
        }
    }

    pure fn filetype() -> Option<~str> {
        match self.filename() {
          None => None,
          Some(ref f) => {
            match str::rfind_char(*f, '.') {
              Some(p) if p < f.len() => Some(f.slice(p, f.len())),
              _ => None
            }
          }
        }
    }

    pure fn with_dirname(d: &str) -> WindowsPath {
        let dpath = from_str::<WindowsPath>(d);
        match self.filename() {
          Some(ref f) => dpath.push(*f),
          None => move dpath
        }
    }

    pure fn with_filename(f: &str) -> WindowsPath {
        assert ! str::any(f, |c| windows::is_sep(c as u8));
        self.dir_path().push(f)
    }

    pure fn with_filestem(s: &str) -> WindowsPath {
        match self.filetype() {
          None => self.with_filename(s),
          Some(ref t) => self.with_filename(str::from_slice(s) + *t)
        }
    }

    pure fn with_filetype(t: &str) -> WindowsPath {
        if t.len() == 0 {
            match self.filestem() {
              None => copy self,
              Some(ref s) => self.with_filename(*s)
            }
        } else {
            let t = ~"." + str::from_slice(t);
            match self.filestem() {
              None => self.with_filename(t),
              Some(ref s) =>
              self.with_filename(*s + t)
            }
        }
    }

    pure fn dir_path() -> WindowsPath {
        if self.components.len() != 0 {
            self.pop()
        } else {
            copy self
        }
    }

    pure fn file_path() -> WindowsPath {
        let cs = match self.filename() {
          None => ~[],
          Some(ref f) => ~[copy *f]
        };
        return WindowsPath { host: None,
                             device: None,
                             is_absolute: false,
                             components: cs }
    }

    pure fn push_rel(other: &WindowsPath) -> WindowsPath {
        assert !other.is_absolute;
        self.push_many(other.components)
    }

    pure fn push_many(cs: &[~str]) -> WindowsPath {
        let mut v = copy self.components;
        for cs.each |e| {
            let mut ss = str::split_nonempty(
                *e,
                |c| windows::is_sep(c as u8));
            unsafe { v.push_all_move(move ss); }
        }
        return WindowsPath { components: move v, ..self }
    }

    pure fn push(s: &str) -> WindowsPath {
        let mut v = copy self.components;
        let mut ss = str::split_nonempty(s, |c| windows::is_sep(c as u8));
        unsafe { v.push_all_move(move ss); }
        return WindowsPath { components: move v, ..self }
    }

    pure fn pop() -> WindowsPath {
        let mut cs = copy self.components;
        if cs.len() != 0 {
            unsafe { cs.pop(); }
        }
        return WindowsPath { components: move cs, ..self }
    }

    pure fn normalize() -> WindowsPath {
        return WindowsPath {
            components: normalize(self.components),
            ..self
        }
    }
}


pub pure fn normalize(components: &[~str]) -> ~[~str] {
    let mut cs = ~[];
    unsafe {
        for components.each |c| {
            unsafe {
                if *c == ~"." && components.len() > 1 { loop; }
                if *c == ~"" { loop; }
                if *c == ~".." && cs.len() != 0 {
                    cs.pop();
                    loop;
                }
                cs.push(copy *c);
            }
        }
    }
    move cs
}

#[test]
fn test_double_slash_collapsing()
{
    let path = from_str::<PosixPath>("tmp/");
    let path = path.push("/hmm");
    let path = path.normalize();
    assert ~"tmp/hmm" == path.to_str();

    let path = from_str::<WindowsPath>("tmp/");
    let path = path.push("/hmm");
    let path = path.normalize();
    assert ~"tmp\\hmm" == path.to_str();
}

mod posix {

    #[cfg(test)]
    fn mk(s: &str) -> PosixPath { from_str::<PosixPath>(s) }

    #[cfg(test)]
    fn t(wp: &PosixPath, s: &str) {
        let ss = wp.to_str();
        let sss = str::from_slice(s);
        if (ss != sss) {
            debug!("got %s", ss);
            debug!("expected %s", sss);
            assert ss == sss;
        }
    }

    #[test]
    fn test_filetype_foo_bar() {
        let wp = mk("foo.bar");
        assert wp.filetype() == Some(~".bar");
    }

    #[test]
    fn test_filetype_foo() {
        let wp = mk("foo");
        assert wp.filetype() == None;
    }

    #[test]
    fn test_posix_paths() {
        t(&(mk("hi")), "hi");
        t(&(mk("/lib")), "/lib");
        t(&(mk("hi/there")), "hi/there");
        t(&(mk("hi/there.txt")), "hi/there.txt");

        t(&(mk("hi/there.txt")), "hi/there.txt");
        t(&(mk("hi/there.txt")
           .with_filetype("")), "hi/there");

        t(&(mk("/a/b/c/there.txt")
            .with_dirname("hi")), "hi/there.txt");

        t(&(mk("hi/there.txt")
            .with_dirname(".")), "./there.txt");

        t(&(mk("a/b/c")
            .push("..")), "a/b/c/..");

        t(&(mk("there.txt")
            .with_filetype("o")), "there.o");

        t(&(mk("hi/there.txt")
            .with_filetype("o")), "hi/there.o");

        t(&(mk("hi/there.txt")
            .with_filetype("o")
            .with_dirname("/usr/lib")),
          "/usr/lib/there.o");

        t(&(mk("hi/there.txt")
            .with_filetype("o")
            .with_dirname("/usr/lib/")),
          "/usr/lib/there.o");

        t(&(mk("hi/there.txt")
            .with_filetype("o")
            .with_dirname("/usr//lib//")),
            "/usr/lib/there.o");

        t(&(mk("/usr/bin/rust")
            .push_many([~"lib", ~"thingy.so"])
            .with_filestem("librustc")),
          "/usr/bin/rust/lib/librustc.so");

    }

    #[test]
    fn test_normalize() {
        t(&(mk("hi/there.txt")
            .with_dirname(".").normalize()), "there.txt");

        t(&(mk("a/b/../c/././/../foo.txt/").normalize()),
          "a/foo.txt");

        t(&(mk("a/b/c")
            .push("..").normalize()), "a/b");
    }
}

// Various windows helpers, and tests for the impl.
mod windows {

    #[inline(always)]
    pub pure fn is_sep(u: u8) -> bool {
        u == '/' as u8 || u == '\\' as u8
    }

    pub pure fn extract_unc_prefix(s: &str) -> Option<(~str,~str)> {
        if (s.len() > 1 &&
            s[0] == '\\' as u8 &&
            s[1] == '\\' as u8) {
            let mut i = 2;
            while i < s.len() {
                if s[i] == '\\' as u8 {
                    let pre = s.slice(2, i);
                    let rest = s.slice(i, s.len());
                    return Some((move pre, move rest));
                }
                i += 1;
            }
        }
        None
    }

    pub pure fn extract_drive_prefix(s: &str) -> Option<(~str,~str)> {
        unsafe {
            if (s.len() > 1 &&
                libc::isalpha(s[0] as libc::c_int) != 0 &&
                s[1] == ':' as u8) {
                let rest = if s.len() == 2 {
                    ~""
                } else {
                    s.slice(2, s.len())
                };
                return Some((s.slice(0,1), move rest));
            }
            None
        }
    }

    #[test]
    fn test_extract_unc_prefixes() {
        assert extract_unc_prefix("\\\\").is_none();
        assert extract_unc_prefix("\\\\hi").is_none();
        assert extract_unc_prefix("\\\\hi\\") == Some((~"hi", ~"\\"));
        assert extract_unc_prefix("\\\\hi\\there") ==
            Some((~"hi", ~"\\there"));
        assert extract_unc_prefix("\\\\hi\\there\\friends.txt") ==
            Some((~"hi", ~"\\there\\friends.txt"));
    }

    #[test]
    fn test_extract_drive_prefixes() {
        assert extract_drive_prefix("c").is_none();
        assert extract_drive_prefix("c:") == Some((~"c", ~""));
        assert extract_drive_prefix("d:") == Some((~"d", ~""));
        assert extract_drive_prefix("z:") == Some((~"z", ~""));
        assert extract_drive_prefix("c:\\hi") == Some((~"c", ~"\\hi"));
        assert extract_drive_prefix("d:hi") == Some((~"d", ~"hi"));
        assert extract_drive_prefix("c:hi\\there.txt") ==
            Some((~"c", ~"hi\\there.txt"));
        assert extract_drive_prefix("c:\\hi\\there.txt") ==
            Some((~"c", ~"\\hi\\there.txt"));
    }

    #[test]
    fn test_windows_paths() {
        fn mk(s: &str) -> WindowsPath { from_str::<WindowsPath>(s) }
        fn t(wp: &WindowsPath, s: &str) {
            let ss = wp.to_str();
            let sss = str::from_slice(s);
            if (ss != sss) {
                debug!("got %s", ss);
                debug!("expected %s", sss);
                assert ss == sss;
            }
        }

        t(&(mk("hi")), "hi");
        t(&(mk("hi/there")), "hi\\there");
        t(&(mk("hi/there.txt")), "hi\\there.txt");

        t(&(mk("there.txt")
            .with_filetype("o")), "there.o");

        t(&(mk("hi/there.txt")
            .with_filetype("o")), "hi\\there.o");

        t(&(mk("hi/there.txt")
            .with_filetype("o")
            .with_dirname("c:\\program files A")),
          "c:\\program files A\\there.o");

        t(&(mk("hi/there.txt")
            .with_filetype("o")
            .with_dirname("c:\\program files B\\")),
          "c:\\program files B\\there.o");

        t(&(mk("hi/there.txt")
            .with_filetype("o")
            .with_dirname("c:\\program files C\\/")),
            "c:\\program files C\\there.o");

        t(&(mk("c:\\program files (x86)\\rust")
            .push_many([~"lib", ~"thingy.dll"])
            .with_filename("librustc.dll")),
          "c:\\program files (x86)\\rust\\lib\\librustc.dll");

    }

    #[cfg(test)]
    fn mk(s: &str) -> PosixPath { from_str::<PosixPath>(s) }

    #[test]
    fn test_filetype_foo_bar() {
        let wp = mk("foo.bar");
        assert wp.filetype() == Some(~".bar");
    }

    #[test]
    fn test_filetype_foo() {
        let wp = mk("foo");
        assert wp.filetype() == None;
    }

}
