// This codeparts are taken from the Helix editor repository and are therefore
// licensed under the Mozilla Public License. Edits are mine but still licensed under
// the MPL as the original code.

// Original code link: https://github.com/helix-editor/helix/blob/d015eff4aa0ca294d007cb61683b5fa815ce0640/helix-view/src/clipboard.rs

use serde::{Deserialize, Serialize};
use std::{borrow::Cow, ffi::OsStr};
use thiserror::Error;

#[derive(Debug, Error)]
pub enum ClipboardError {
    #[error(transparent)]
    IoError(#[from] std::io::Error),
    #[error("could not convert terminal output to UTF-8: {0}")]
    FromUtf8Error(#[from] std::string::FromUtf8Error),
    #[error("clipboard provider command failed")]
    CommandFailed,
    #[error("failed to write to clipboard provider's stdin")]
    StdinWriteFailed,
    #[error("clipboard provider did not return any contents")]
    MissingStdout,
    #[error("This clipboard provider does not support reading")]
    ReadingNotSupported,
}

type Result<T> = std::result::Result<T, ClipboardError>;

pub use external::ClipboardProvider;

pub(crate) mod external {
    use super::*;

    #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
    pub struct Command {
        command: Cow<'static, str>,
        #[serde(default)]
        args: Cow<'static, [Cow<'static, str>]>,
    }

    #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
    #[serde(rename_all = "kebab-case")]
    pub struct CommandProvider {
        paste: Command,
    }

    #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
    #[serde(rename_all = "kebab-case")]
    #[allow(clippy::large_enum_variant)]
    pub enum ClipboardProvider {
        Pasteboard,
        Wayland,
        XClip,
        XSel,
        Win32Yank,
        Tmux,
        Custom(CommandProvider),
        None,
    }

    impl Default for ClipboardProvider {
        #[cfg(windows)]
        fn default() -> Self {
            if binary_exists("win32yank.exe") {
                Self::Win32Yank
            } else {
                Self::None
            }
        }

        #[cfg(target_os = "macos")]
        fn default() -> Self {
            if env_var_is_set("TMUX") && binary_exists("tmux") {
                Self::Tmux
            } else if binary_exists("pbcopy") && binary_exists("pbpaste") {
                Self::Pasteboard
            } else {
                Self::None
            }
        }

        #[cfg(not(any(windows, target_os = "macos")))]
        fn default() -> Self {
            fn is_exit_success(program: &str, args: &[&str]) -> bool {
                std::process::Command::new(program)
                    .args(args)
                    .output()
                    .ok()
                    .and_then(|out| out.status.success().then_some(()))
                    .is_some()
            }

            if env_var_is_set("WAYLAND_DISPLAY")
                && binary_exists("wl-copy")
                && binary_exists("wl-paste")
            {
                Self::Wayland
            } else if env_var_is_set("DISPLAY") && binary_exists("xclip") {
                Self::XClip
            } else if env_var_is_set("DISPLAY")
                && binary_exists("xsel")
                // FIXME: check performance of is_exit_success
                && is_exit_success("xsel", &["-o", "-b"])
            {
                Self::XSel
            } else if env_var_is_set("TMUX") && binary_exists("tmux") {
                Self::Tmux
            } else if binary_exists("win32yank.exe") {
                Self::Win32Yank
            } else {
                Self::None
            }
        }
    }

    impl ClipboardProvider {
        pub fn set_contents(&self, content: &str) -> Result<()> {
            fn paste_to_builtin(provider: CommandProvider, content: &str) -> Result<()> {
                execute_command(&provider.paste, Some(content), false).map(|_| ())
            }

            match self {
                Self::Pasteboard => paste_to_builtin(PASTEBOARD, content),
                Self::Wayland => paste_to_builtin(WL_CLIPBOARD, content),
                Self::XClip => paste_to_builtin(XCLIP, content),
                Self::XSel => paste_to_builtin(XSEL, content),
                Self::Win32Yank => paste_to_builtin(WIN32, content),
                Self::Tmux => paste_to_builtin(TMUX, content),
                Self::Custom(command_provider) => {
                    execute_command(&command_provider.paste, Some(content), false).map(|_| ())
                }
                Self::None => Ok(()),
            }
        }
    }

    macro_rules! command_provider {
        ($name:ident,
         paste => $paste_cmd:literal $( , $paste_arg:literal )* ; ) => {
            const $name: CommandProvider = CommandProvider {
                paste: Command {
                    command: Cow::Borrowed($paste_cmd),
                    args: Cow::Borrowed(&[ $( Cow::Borrowed($paste_arg) ),* ])
                },
            };
        };
    }

    command_provider! {
        TMUX,
        paste => "tmux", "load-buffer", "-w", "-";
    }
    command_provider! {
        PASTEBOARD,
        paste => "pbcopy";
    }
    command_provider! {
        WL_CLIPBOARD,
        paste => "wl-copy", "--type", "text/plain";
    }
    command_provider! {
        XCLIP,
        paste => "xclip", "-i", "-selection", "clipboard";
    }
    command_provider! {
        XSEL,
        paste => "xsel", "-i", "-b";
    }
    command_provider! {
        WIN32,
        paste => "win32yank.exe", "-i", "--crlf";
    }

    fn execute_command(
        cmd: &Command,
        input: Option<&str>,
        pipe_output: bool,
    ) -> Result<Option<String>> {
        use std::io::Write;
        use std::process::{Command, Stdio};

        let stdin = input.map(|_| Stdio::piped()).unwrap_or_else(Stdio::null);
        let stdout = pipe_output.then(Stdio::piped).unwrap_or_else(Stdio::null);

        let mut command: Command = Command::new(cmd.command.as_ref());

        #[allow(unused_mut)]
        let mut command_mut: &mut Command = command
            .args(cmd.args.iter().map(AsRef::as_ref))
            .stdin(stdin)
            .stdout(stdout)
            .stderr(Stdio::null());

        let mut child = command_mut.spawn()?;

        if let Some(input) = input {
            let mut stdin = child.stdin.take().ok_or(ClipboardError::StdinWriteFailed)?;
            stdin
                .write_all(input.as_bytes())
                .map_err(|_| ClipboardError::StdinWriteFailed)?;
        }

        // TODO: add timer?
        let output = child.wait_with_output()?;

        if !output.status.success() {
            log::error!(
                "clipboard provider {} failed with stderr: \"{}\"",
                cmd.command,
                String::from_utf8_lossy(&output.stderr)
            );
            return Err(ClipboardError::CommandFailed);
        }

        if pipe_output {
            Ok(Some(String::from_utf8(output.stdout)?))
        } else {
            Ok(None)
        }
    }
}

// Those parts are from a different file in Helix repo:
// https://github.com/helix-editor/helix/blob/ca7479ca8840595272deaaa8e823b23c26a8d3c9/helix-stdx/src/env.rs

/// Checks if the given environment variable is set.
pub(crate) fn env_var_is_set(env_var_name: &str) -> bool {
    std::env::var_os(env_var_name).is_some()
}

/// Checks if a binary with the given name exists.
pub(crate) fn binary_exists<T: AsRef<OsStr>>(binary_name: T) -> bool {
    which::which(binary_name).is_ok()
}
