use std::io::Read;
use std::ops::Div;
use std::path::{Path, PathBuf};

pub struct AbsoluteFile {
    inner: PathBuf,
}

impl AbsoluteFile {
    pub fn new(path: impl AsRef<Path>) -> AbsoluteFile {
        let path = path.as_ref();

        if !path.is_absolute() {
            panic!(
                "AbsoluteFile::new must take an absolute path :: {}",
                path.display()
            )
        } else if path.is_dir() {
            // At the moment, this is not an invariant, but rather a way to catch bugs
            // in tests.
            panic!(
                "AbsoluteFile::new must not take a directory :: {}",
                path.display()
            )
        } else {
            AbsoluteFile {
                inner: path.to_path_buf(),
            }
        }
    }

    pub fn dir(&self) -> AbsolutePath {
        AbsolutePath::new(if let Some(parent) = self.inner.parent() {
            parent
        } else {
            unreachable!("Internal error: could not get parent in dir")
        })
    }
}

impl From<AbsoluteFile> for PathBuf {
    fn from(file: AbsoluteFile) -> Self {
        file.inner
    }
}

pub struct AbsolutePath {
    inner: PathBuf,
}

impl AbsolutePath {
    pub fn new(path: impl AsRef<Path>) -> AbsolutePath {
        let path = path.as_ref();

        if path.is_absolute() {
            AbsolutePath {
                inner: path.to_path_buf(),
            }
        } else {
            panic!("AbsolutePath::new must take an absolute path")
        }
    }
}

impl Div<&str> for &AbsolutePath {
    type Output = AbsolutePath;

    fn div(self, rhs: &str) -> Self::Output {
        let parts = rhs.split('/');
        let mut result = self.inner.clone();

        for part in parts {
            result = result.join(part);
        }

        AbsolutePath::new(result)
    }
}

impl AsRef<Path> for AbsolutePath {
    fn as_ref(&self) -> &Path {
        self.inner.as_path()
    }
}

pub struct RelativePath {
    inner: PathBuf,
}

impl RelativePath {
    pub fn new(path: impl Into<PathBuf>) -> RelativePath {
        let path = path.into();

        if path.is_relative() {
            RelativePath { inner: path }
        } else {
            panic!("RelativePath::new must take a relative path")
        }
    }
}

impl<T: AsRef<str>> Div<T> for &RelativePath {
    type Output = RelativePath;

    fn div(self, rhs: T) -> Self::Output {
        let parts = rhs.as_ref().split('/');
        let mut result = self.inner.clone();

        for part in parts {
            result = result.join(part);
        }

        RelativePath::new(result)
    }
}
pub trait DisplayPath {
    fn display_path(&self) -> String;
}

impl DisplayPath for AbsolutePath {
    fn display_path(&self) -> String {
        self.inner.display().to_string()
    }
}

impl DisplayPath for PathBuf {
    fn display_path(&self) -> String {
        self.display().to_string()
    }
}

impl DisplayPath for str {
    fn display_path(&self) -> String {
        self.to_string()
    }
}

impl DisplayPath for &str {
    fn display_path(&self) -> String {
        (*self).to_string()
    }
}

impl DisplayPath for String {
    fn display_path(&self) -> String {
        self.clone()
    }
}

impl DisplayPath for &String {
    fn display_path(&self) -> String {
        (*self).to_string()
    }
}
pub enum Stub<'a> {
    FileWithContent(&'a str, &'a str),
    FileWithContentToBeTrimmed(&'a str, &'a str),
    EmptyFile(&'a str),
}

pub fn file_contents(full_path: impl AsRef<Path>) -> String {
    let mut file = std::fs::File::open(full_path.as_ref()).expect("can not open file");
    let mut contents = String::new();
    file.read_to_string(&mut contents)
        .expect("can not read file");
    contents
}

pub fn file_contents_binary(full_path: impl AsRef<Path>) -> Vec<u8> {
    let mut file = std::fs::File::open(full_path.as_ref()).expect("can not open file");
    let mut contents = Vec::new();
    file.read_to_end(&mut contents).expect("can not read file");
    contents
}

pub fn line_ending() -> String {
    #[cfg(windows)]
    {
        String::from("\r\n")
    }

    #[cfg(not(windows))]
    {
        String::from("\n")
    }
}

pub fn delete_file_at(full_path: impl AsRef<Path>) {
    let full_path = full_path.as_ref();

    if full_path.exists() {
        std::fs::remove_file(full_path).expect("can not delete file");
    }
}

pub fn create_file_at(full_path: impl AsRef<Path>) -> Result<(), std::io::Error> {
    let full_path = full_path.as_ref();

    if let Some(parent) = full_path.parent() {
        panic!(format!("{:?} exists", parent.display()));
    }

    std::fs::write(full_path, b"fake data")
}

pub fn copy_file_to(source: &str, destination: &str) {
    std::fs::copy(source, destination).expect("can not copy file");
}

pub fn files_exist_at(files: Vec<impl AsRef<Path>>, path: impl AsRef<Path>) -> bool {
    files.iter().all(|f| {
        let mut loc = PathBuf::from(path.as_ref());
        loc.push(f);
        loc.exists()
    })
}

pub fn delete_directory_at(full_path: &str) {
    std::fs::remove_dir_all(PathBuf::from(full_path)).expect("can not remove directory");
}

pub fn executable_path() -> PathBuf {
    let mut path = binaries();
    path.push("nu");
    path
}

pub fn root() -> PathBuf {
    let manifest_dir = if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") {
        PathBuf::from(manifest_dir)
    } else {
        PathBuf::from(env!("CARGO_MANIFEST_DIR"))
    };

    let test_path = manifest_dir.join("Cargo.lock");
    if test_path.exists() {
        manifest_dir
    } else {
        manifest_dir
            .parent()
            .expect("Couldn't find the debug binaries directory")
            .parent()
            .expect("Couldn't find the debug binaries directory")
            .to_path_buf()
    }
}

pub fn binaries() -> PathBuf {
    std::env::var("CARGO_TARGET_DIR")
        .ok()
        .map(|target_dir| PathBuf::from(target_dir).join("debug"))
        .unwrap_or_else(|| root().join("target/debug"))
}

pub fn fixtures() -> PathBuf {
    root().join("tests/fixtures")
}

pub fn in_directory(str: impl AsRef<Path>) -> String {
    let path = str.as_ref();
    let path = if path.is_relative() {
        root().join(path)
    } else {
        path.to_path_buf()
    };

    path.display().to_string()
}
