// Copyright 2026 California Institute of Technology
//
// You should have received a copy of the licensing terms for this
// software included in the file “LICENSE” located in the top-level
// directory of this package. If you did not, you can view a copy at
// http://dcc.ligo.org/M1500244/LICENSE.txt

//! System-clipboard access, two ways at once:
//!
//! 1. Best-effort local helper (`wl-copy` on Wayland, `xclip`/`xsel` on
//!    X11) — covers terminals that ignore OSC 52 (the VTE family: GNOME
//!    Terminal etc.).  Skipped silently if no helper is installed.
//! 2. The OSC 52 terminal escape sequence, unconditionally — asks the
//!    terminal emulator itself to set the clipboard, so it works over SSH
//!    and needs no helpers (tmux needs `set-clipboard on`).

use base64::Engine as _;
use base64::engine::general_purpose::STANDARD;
use std::io::Write;
use std::process::{Command, Stdio};

/// The OSC 52 escape sequence that places `text` on the system clipboard:
/// `ESC ] 52 ; c ; <base64 of text> BEL`.
pub fn osc52_sequence(text: &str) -> Vec<u8> {
    let mut seq = b"\x1b]52;c;".to_vec();
    seq.extend_from_slice(STANDARD.encode(text).as_bytes());
    seq.push(b'\x07');
    seq
}

/// Copy `text` to the system clipboard: local helper first (best effort),
/// then OSC 52 through the terminal on stdout.
pub fn copy(text: &str) -> std::io::Result<()> {
    copy_via_local_helper(text);
    let mut out = std::io::stdout();
    out.write_all(&osc52_sequence(text))?;
    out.flush()
}

/// Try the local clipboard helpers for this session type until one succeeds.
/// `false` if none did (not installed, no display, or they failed).
fn copy_via_local_helper(text: &str) -> bool {
    let wayland = env_is_set("WAYLAND_DISPLAY");
    let x11 = env_is_set("DISPLAY");
    helper_commands(wayland, x11)
        .iter()
        .any(|argv| run_helper(argv, text))
}

fn env_is_set(name: &str) -> bool {
    std::env::var_os(name).is_some_and(|v| !v.is_empty())
}

/// The helper command lines to try, in order.  Wayland sessions usually also
/// have an X11 `DISPLAY` (XWayland), so `wl-copy` goes first and the X11
/// tools stay as fallbacks.
fn helper_commands(wayland: bool, x11: bool) -> Vec<&'static [&'static str]> {
    const WL_COPY: &[&str] = &["wl-copy"];
    const XCLIP: &[&str] = &["xclip", "-selection", "clipboard"];
    const XSEL: &[&str] = &["xsel", "--clipboard", "--input"];
    let mut commands: Vec<&'static [&'static str]> = Vec::new();
    if wayland {
        commands.push(WL_COPY);
    }
    if x11 {
        commands.push(XCLIP);
        commands.push(XSEL);
    }
    commands
}

/// Pipe `text` to `argv`'s stdin; `true` if it ran and exited successfully.
fn run_helper(argv: &[&str], text: &str) -> bool {
    let Ok(mut child) = Command::new(argv[0])
        .args(&argv[1..])
        .stdin(Stdio::piped())
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .spawn()
    else {
        return false; // helper not installed
    };
    if let Some(mut stdin) = child.stdin.take() {
        // The helper may exit without draining stdin; a broken pipe is fine —
        // the exit status below is what decides success.
        let _ = stdin.write_all(text.as_bytes());
    }
    child.wait().is_ok_and(|status| status.success())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn sequence_wraps_base64_payload() {
        assert_eq!(osc52_sequence("hello"), b"\x1b]52;c;aGVsbG8=\x07");
    }

    #[test]
    fn empty_text_is_an_empty_payload() {
        assert_eq!(osc52_sequence(""), b"\x1b]52;c;\x07");
    }

    #[test]
    fn channel_names_encode_cleanly() {
        // ':' and '_' are typical channel-name characters.
        assert_eq!(osc52_sequence("X1:SINE_1"), b"\x1b]52;c;WDE6U0lORV8x\x07");
    }

    #[test]
    fn helper_order_matches_session_type() {
        let names = |wayland, x11| -> Vec<&str> {
            helper_commands(wayland, x11)
                .iter()
                .map(|argv| argv[0])
                .collect()
        };
        // XWayland sessions have both displays set; wl-copy wins.
        assert_eq!(names(true, true), ["wl-copy", "xclip", "xsel"]);
        assert_eq!(names(false, true), ["xclip", "xsel"]);
        assert_eq!(names(true, false), ["wl-copy"]);
        assert_eq!(names(false, false), Vec::<&str>::new());
    }

    // run_helper is exercised with stand-in commands so the tests neither
    // need the real clipboard tools nor touch the real clipboard.

    #[test]
    fn run_helper_pipes_text_and_reports_exit_status() {
        // grep -q exits 0 only if the piped text contains the pattern.
        assert!(run_helper(&["grep", "-q", "hello"], "hello world"));
        assert!(!run_helper(&["grep", "-q", "hello"], "goodbye"));
    }

    #[test]
    fn run_helper_survives_early_stdin_close() {
        // `true` exits without reading stdin: the broken pipe must not
        // panic or count as failure.
        assert!(run_helper(&["true"], "some text"));
    }

    #[test]
    fn missing_helper_is_a_silent_failure() {
        assert!(!run_helper(&["definitely-not-a-real-clipboard-tool"], "x"));
    }
}
