//! X11 Clipboard implementation
//!
//! Note that the x11 implementation is really crap right now - we just depend
//! on xclip being on the user's path. If x11 pasting doesn't work, it's
//! probably because xclip is unavailable. There's currently no non-GPL x11
//! clipboard library for Rust. Until then, we have this hack.
//!
//! FIXME: Implement actual X11 clipboard API using the ICCCM reference
//!        https://tronche.com/gui/x/icccm/
use std::ffi::OsStr;
use std::io;
use std::process::{Command, Output};
use std::string::FromUtf8Error;

use super::{Load, Store};

/// The x11 clipboard
pub struct Clipboard;

#[derive(Debug)]
pub enum Error {
    Io(io::Error),
    Xclip(String),
    Utf8(FromUtf8Error),
}

impl ::std::error::Error for Error {
    fn cause(&self) -> Option<&::std::error::Error> {
        match *self {
            Error::Io(ref err) => Some(err),
            Error::Utf8(ref err) => Some(err),
            _ => None,
        }
    }

    fn description(&self) -> &str {
        match *self {
            Error::Io(..) => "Error calling xclip",
            Error::Xclip(..) => "Error reported by xclip",
            Error::Utf8(..) => "Clipboard contents not utf8",
        }
    }
}

impl ::std::fmt::Display for Error {
    fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
        match *self {
            Error::Io(ref err) => match err.kind() {
                io::ErrorKind::NotFound => {
                    write!(f, "Please install `xclip` to enable clipboard support")
                },
                _ => write!(f, "Error calling xclip: {}", err),
            },
            Error::Xclip(ref s) => write!(f, "Error from xclip: {}", s),
            Error::Utf8(ref err) => write!(f, "Error parsing xclip output: {}", err),
        }
    }
}

impl From<io::Error> for Error {
    fn from(val: io::Error) -> Error {
        Error::Io(val)
    }
}

impl From<FromUtf8Error> for Error {
    fn from(val: FromUtf8Error) -> Error {
        Error::Utf8(val)
    }
}

impl Load for Clipboard {
    type Err = Error;

    fn new() -> Result<Self, Error> {
        Ok(Clipboard)
    }

    fn load_primary(&self) -> Result<String, Self::Err> {
        let output = Command::new("xclip").args(&["-o", "-selection", "clipboard"]).output()?;

        Clipboard::process_xclip_output(output)
    }

    fn load_selection(&self) -> Result<String, Self::Err> {
        let output = Command::new("xclip").args(&["-o"]).output()?;

        Clipboard::process_xclip_output(output)
    }
}

impl Store for Clipboard {
    /// Sets the primary clipboard contents
    #[inline]
    fn store_primary<S>(&mut self, contents: S) -> Result<(), Self::Err>
    where
        S: Into<String>,
    {
        self.store(contents, &["-i", "-selection", "clipboard"])
    }

    /// Sets the secondary clipboard contents
    #[inline]
    fn store_selection<S>(&mut self, contents: S) -> Result<(), Self::Err>
    where
        S: Into<String>,
    {
        self.store(contents, &["-i"])
    }
}

impl Clipboard {
    fn process_xclip_output(output: Output) -> Result<String, Error> {
        if output.status.success() {
            String::from_utf8(output.stdout).map_err(::std::convert::From::from)
        } else {
            String::from_utf8(output.stderr).map_err(::std::convert::From::from)
        }
    }

    fn store<C, S>(&mut self, contents: C, args: &[S]) -> Result<(), Error>
    where
        C: Into<String>,
        S: AsRef<OsStr>,
    {
        use std::io::Write;
        use std::process::{Command, Stdio};

        let contents = contents.into();
        let mut child = Command::new("xclip").args(args).stdin(Stdio::piped()).spawn()?;

        if let Some(stdin) = child.stdin.as_mut() {
            stdin.write_all(contents.as_bytes())?;
        }

        // Return error if didn't exit cleanly
        let exit_status = child.wait()?;
        if exit_status.success() {
            Ok(())
        } else {
            Err(Error::Xclip("xclip returned non-zero exit code".into()))
        }
    }
}

#[cfg(test)]
mod tests {
    use super::Clipboard;
    use {Load, Store};

    #[test]
    fn clipboard_works() {
        let mut clipboard = Clipboard::new().expect("create clipboard");
        let arst = "arst";
        let oien = "oien";
        clipboard.store_primary(arst).expect("store selection");
        clipboard.store_selection(oien).expect("store selection");

        let selection = clipboard.load_selection().expect("load selection");
        let primary = clipboard.load_primary().expect("load selection");

        assert_eq!(arst, primary);
        assert_eq!(oien, selection);
    }
}
