use std::ffi::OsStr;
use std::process::Command;
use std::str;

pub struct Executor {}

impl Executor {
    pub fn exec<T: AsRef<OsStr>>(
        command: T,
        composer: impl Fn(&mut Command) -> &mut Command,
    ) -> String {
        let output = composer(&mut Executor::spawn(command))
            .output()
            .expect("command failed to execute");
        if output.status.success() {
            return Executor::unwrap(&output.stdout);
        }
        Executor::unwrap(&output.stderr)
    }

    pub fn exec_with_errors<T: AsRef<OsStr>>(
        command: T,
        composer: impl Fn(&mut Command) -> &mut Command,
    ) -> Option<String> {
        let output = composer(&mut Executor::spawn(command))
            .output()
            .expect("command failed to execute");
        if output.status.success() {
            return None;
        }
        Some(Executor::unwrap(&output.stderr))
    }

    pub fn exec_with_stdout<T: AsRef<OsStr>>(
        command: T,
        composer: impl Fn(&mut Command) -> &mut Command,
    ) -> Option<String> {
        let output = composer(&mut Executor::spawn(command))
            .output()
            .expect("command failed to execute");
        if output.status.success() {
            return Some(Executor::unwrap(&output.stdout));
        }
        None
    }

    pub fn with_stdio<T: AsRef<OsStr>>(
        command: T,
        composer: impl Fn(&mut Command) -> &mut Command,
    ) -> bool {
        let child = composer(&mut Executor::spawn(command))
            .spawn()
            .expect("Failed to execute");
        let output = child
            .wait_with_output()
            .expect("failed to wait on child process");
        output.status.success()
    }

    pub fn spawn<T: AsRef<OsStr>>(program: T) -> Command {
        let mut command = Executor::platform_command();
        command.arg(program);
        command
    }

    fn platform_command() -> Command {
        if cfg!(target_os = "windows") {
            return Executor::windows_command();
        }
        Executor::unix_command()
    }

    fn windows_command() -> Command {
        let mut child_process = Command::new("cmd");
        child_process.arg("/C");
        child_process
    }

    fn unix_command() -> Command {
        let mut child_process = Command::new("sh");
        child_process.arg("-c");
        child_process
    }

    fn unwrap(io: &[u8]) -> String {
        str::from_utf8(io)
            .expect("Invalid output")
            .trim()
            .to_string()
    }
}
