use atty;
use failure::{Error, ResultExt};

use std::ffi::OsStr;
use std::fmt;
use std::fs::create_dir_all;
use std::fs::{File, OpenOptions};
use std::io::{self, stdin, stdout};
use std::path::PathBuf;

#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum StreamOrPath {
    Stream,
    Path(PathBuf),
}

impl StreamOrPath {
    pub fn is_stream(&self) -> bool {
        match *self {
            StreamOrPath::Stream => true,
            StreamOrPath::Path(_) => false,
        }
    }

    pub fn name(&self) -> &str {
        match *self {
            StreamOrPath::Stream => "stream",
            StreamOrPath::Path(ref p) => p.to_str().unwrap_or("<file-path-is-not-unicode>"),
        }
    }

    pub fn short_name(&self) -> &str {
        match *self {
            StreamOrPath::Stream => "stream",
            StreamOrPath::Path(ref p) => p.file_stem().map_or("<invalid-file-stem>", |s| {
                s.to_str().unwrap_or("<file-stem-is-not-unicode>")
            }),
        }
    }

    pub fn open_as_output(&self, append: bool) -> Result<Box<dyn io::Write>, Error> {
        Ok(match *self {
            StreamOrPath::Stream => Box::new(stdout()),
            StreamOrPath::Path(ref p) => {
                if let Some(dir) = p.parent() {
                    create_dir_all(dir)
                        .context(format!("Could not create directory leading towards '{}'", p.display(),))?;
                }
                Box::new(
                    OpenOptions::new()
                        .create(true)
                        .truncate(!append)
                        .write(true)
                        .append(append)
                        .open(p)
                        .context(format!("Could not open '{}' for writing", p.display()))?,
                )
            }
        })
    }

    pub fn open_as_input(&self) -> Result<Box<dyn io::Read>, Error> {
        Ok(match *self {
            StreamOrPath::Stream => {
                if atty::is(atty::Stream::Stdin) {
                    bail!("Cannot read from standard input while a terminal is connected")
                } else {
                    Box::new(stdin())
                }
            }
            StreamOrPath::Path(ref p) => {
                Box::new(File::open(p).context(format!("Could not open '{}' for reading", p.display()))?)
            }
        })
    }
}

impl<'a> From<&'a OsStr> for StreamOrPath {
    fn from(p: &'a OsStr) -> Self {
        StreamOrPath::Path(PathBuf::from(p))
    }
}

impl<'a> From<&'a str> for StreamOrPath {
    fn from(s: &str) -> Self {
        use self::StreamOrPath::*;
        if s.is_empty() {
            Stream
        } else {
            Path(PathBuf::from(s))
        }
    }
}

impl fmt::Display for StreamOrPath {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        use self::StreamOrPath::*;
        match *self {
            Stream => Ok(()),
            Path(ref p) => p.display().fmt(f),
        }
    }
}

#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Spec {
    pub src: StreamOrPath,
    pub dst: StreamOrPath,
}

impl Spec {
    pub fn sep() -> char {
        ':'
    }
}

impl fmt::Display for Spec {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        use self::fmt::Write;
        use self::StreamOrPath::*;
        match (&self.src, &self.dst) {
            (&Stream, &Stream) => f.write_char(Spec::sep()),
            (&Path(ref p), &Stream) => p.display().fmt(f),
            (&Stream, &Path(ref p)) => f.write_char(Spec::sep()).and(p.display().fmt(f)),
            (&Path(ref p1), &Path(ref p2)) => p1
                .display()
                .fmt(f)
                .and(f.write_char(Spec::sep()))
                .and(p2.display().fmt(f)),
        }
    }
}

impl<'a> From<&'a str> for Spec {
    fn from(src: &'a str) -> Self {
        use self::StreamOrPath::*;
        let mut it = src.splitn(2, Spec::sep());
        match (it.next(), it.next()) {
            (None, Some(_)) | (None, None) => unreachable!(),
            (Some(p), None) => Spec {
                src: StreamOrPath::from(p),
                dst: Stream,
            },
            (Some(p1), Some(p2)) => Spec {
                src: StreamOrPath::from(p1),
                dst: StreamOrPath::from(p2),
            },
        }
    }
}
