// Copyright (C) 2026 alto555
// SPDX-License-Identifier: GPL-2.0-only

//! Synthetic input driver, for testing the viewer end to end.
//!
//! No xdotool on this machine, but the server has XTEST.  This connects,
//! looks up the XTEST opcode, and injects pointer and key events so the
//! interactive paths can actually be exercised and screenshotted rather than
//! assumed to work.
//!
//!   xdrive move <x> <y>
//!   xdrive click <button>
//!   xdrive key <keysym-hex>
//!   xdrive sleep <ms>
//! Several commands may be chained in one invocation.

use std::io::{Read, Write};
use std::os::unix::net::UnixStream;

fn pad4(n: usize) -> usize {
    (4 - (n % 4)) % 4
}

fn auth_cookie(display: &str) -> Option<Vec<u8>> {
    let path = std::env::var("XAUTHORITY")
        .ok()
        .or_else(|| std::env::var("HOME").ok().map(|h| format!("{}/.Xauthority", h)))?;
    let d = std::fs::read(path).ok()?;
    let mut i = 0usize;
    while i + 2 <= d.len() {
        i += 2;
        let mut f: Vec<Vec<u8>> = Vec::new();
        for _ in 0..4 {
            if i + 2 > d.len() {
                return None;
            }
            let n = ((d[i] as usize) << 8) | d[i + 1] as usize;
            i += 2;
            if i + n > d.len() {
                return None;
            }
            f.push(d[i..i + n].to_vec());
            i += n;
        }
        if f[1] == display.as_bytes() && f[2] == b"MIT-MAGIC-COOKIE-1" {
            return Some(f[3].clone());
        }
    }
    None
}

struct Conn {
    s: UnixStream,
    xtest: u8,
    min_keycode: u8,
    keymap: Vec<Vec<u32>>,
}

impl Conn {
    fn open() -> std::io::Result<Conn> {
        let disp = std::env::var("DISPLAY").unwrap_or_else(|_| ":0".into());
        let num = disp.trim_start_matches(':').split('.').next().unwrap_or("0").to_string();
        let mut s = UnixStream::connect(format!("/tmp/.X11-unix/X{}", num))?;
        let cookie = auth_cookie(&num).unwrap_or_default();
        let name: &[u8] = b"MIT-MAGIC-COOKIE-1";
        let mut r = vec![b'l', 0];
        r.extend_from_slice(&11u16.to_le_bytes());
        r.extend_from_slice(&0u16.to_le_bytes());
        r.extend_from_slice(&(name.len() as u16).to_le_bytes());
        r.extend_from_slice(&(cookie.len() as u16).to_le_bytes());
        r.extend_from_slice(&0u16.to_le_bytes());
        r.extend_from_slice(name);
        r.resize(r.len() + pad4(name.len()), 0);
        r.extend_from_slice(&cookie);
        r.resize(r.len() + pad4(cookie.len()), 0);
        s.write_all(&r)?;
        let mut head = [0u8; 8];
        s.read_exact(&mut head)?;
        let extra = u16::from_le_bytes([head[6], head[7]]) as usize * 4;
        let mut body = vec![0u8; extra];
        s.read_exact(&mut body)?;
        if head[0] != 1 {
            return Err(std::io::Error::new(
                std::io::ErrorKind::PermissionDenied,
                "X setup refused",
            ));
        }
        let min_keycode = body[26];
        let max_keycode = body[27];
        let mut c = Conn {
            s,
            xtest: 0,
            min_keycode,
            keymap: Vec::new(),
        };
        c.xtest = c.query_extension("XTEST")?;
        c.load_keymap(min_keycode, max_keycode)?;
        Ok(c)
    }

    fn reply(&mut self) -> std::io::Result<Vec<u8>> {
        loop {
            let mut b = [0u8; 32];
            self.s.read_exact(&mut b)?;
            match b[0] {
                0 => {
                    return Err(std::io::Error::new(
                        std::io::ErrorKind::Other,
                        format!("X error {} opcode {}", b[1], b[10]),
                    ))
                }
                1 => {
                    let extra = u32::from_le_bytes([b[4], b[5], b[6], b[7]]) as usize * 4;
                    let mut out = b.to_vec();
                    if extra > 0 {
                        let mut rest = vec![0u8; extra];
                        self.s.read_exact(&mut rest)?;
                        out.extend_from_slice(&rest);
                    }
                    return Ok(out);
                }
                _ => {}
            }
        }
    }

    fn query_extension(&mut self, name: &str) -> std::io::Result<u8> {
        let n = name.as_bytes();
        let mut r = vec![98u8, 0];
        r.extend_from_slice(&(2 + ((n.len() + pad4(n.len())) / 4) as u16).to_le_bytes());
        r.extend_from_slice(&(n.len() as u16).to_le_bytes());
        r.extend_from_slice(&0u16.to_le_bytes());
        r.extend_from_slice(n);
        r.resize(r.len() + pad4(n.len()), 0);
        self.s.write_all(&r)?;
        let rep = self.reply()?;
        if rep[8] == 0 {
            return Err(std::io::Error::new(
                std::io::ErrorKind::Unsupported,
                format!("{} not present", name),
            ));
        }
        Ok(rep[9])
    }

    fn load_keymap(&mut self, min: u8, max: u8) -> std::io::Result<()> {
        let count = max - min + 1;
        let mut r = vec![101u8, 0];
        r.extend_from_slice(&2u16.to_le_bytes());
        r.push(min);
        r.push(count);
        r.extend_from_slice(&0u16.to_le_bytes());
        self.s.write_all(&r)?;
        let rep = self.reply()?;
        let per = rep[1] as usize;
        let extra = u32::from_le_bytes([rep[4], rep[5], rep[6], rep[7]]) as usize * 4;
        let body = &rep[32..32 + extra];
        self.keymap = (0..count as usize)
            .map(|k| {
                (0..per)
                    .map(|j| {
                        let o = (k * per + j) * 4;
                        if o + 4 <= body.len() {
                            u32::from_le_bytes([body[o], body[o + 1], body[o + 2], body[o + 3]])
                        } else {
                            0
                        }
                    })
                    .collect()
            })
            .collect();
        Ok(())
    }

    fn keycode_for(&self, keysym: u32) -> Option<u8> {
        for (i, row) in self.keymap.iter().enumerate() {
            if row.iter().any(|k| *k == keysym) {
                return Some(self.min_keycode + i as u8);
            }
        }
        None
    }

    fn fake(&mut self, kind: u8, detail: u8, x: i16, y: i16) -> std::io::Result<()> {
        let mut r = vec![self.xtest, 2];
        r.extend_from_slice(&9u16.to_le_bytes());
        r.push(kind);
        r.push(detail);
        r.extend_from_slice(&0u16.to_le_bytes());
        r.extend_from_slice(&0u32.to_le_bytes()); // delay
        r.extend_from_slice(&0u32.to_le_bytes()); // root = default
        r.extend_from_slice(&[0u8; 8]);
        r.extend_from_slice(&x.to_le_bytes());
        r.extend_from_slice(&y.to_le_bytes());
        r.extend_from_slice(&[0u8; 8]);
        self.s.write_all(&r)?;
        self.s.flush()
    }
}

fn main() -> std::io::Result<()> {
    let a: Vec<String> = std::env::args().skip(1).collect();
    let mut c = Conn::open()?;
    let mut i = 0usize;
    while i < a.len() {
        match a[i].as_str() {
            "move" => {
                let x: i16 = a[i + 1].parse().unwrap();
                let y: i16 = a[i + 2].parse().unwrap();
                c.fake(6, 0, x, y)?;
                i += 2;
            }
            "sclick" => {
                // click with Shift_L held
                let b: u8 = a[i + 1].parse().unwrap();
                let sh = c.keycode_for(0xffe1).unwrap_or(50);
                c.fake(2, sh, 0, 0)?;
                std::thread::sleep(std::time::Duration::from_millis(20));
                c.fake(4, b, 0, 0)?;
                std::thread::sleep(std::time::Duration::from_millis(30));
                c.fake(5, b, 0, 0)?;
                std::thread::sleep(std::time::Duration::from_millis(20));
                c.fake(3, sh, 0, 0)?;
                i += 1;
            }
            "down" => {
                let b: u8 = a[i + 1].parse().unwrap();
                c.fake(4, b, 0, 0)?;
                i += 1;
            }
            "up" => {
                let b: u8 = a[i + 1].parse().unwrap();
                c.fake(5, b, 0, 0)?;
                i += 1;
            }
            "click" => {
                let b: u8 = a[i + 1].parse().unwrap();
                c.fake(4, b, 0, 0)?;
                std::thread::sleep(std::time::Duration::from_millis(30));
                c.fake(5, b, 0, 0)?;
                i += 1;
            }
            "skey" => {
                // hold Shift_L across the keypress
                let ks = u32::from_str_radix(a[i + 1].trim_start_matches("0x"), 16).unwrap();
                let sh = c.keycode_for(0xffe1).unwrap_or(50);
                c.fake(2, sh, 0, 0)?;
                if let Some(kc) = c.keycode_for(ks) {
                    c.fake(2, kc, 0, 0)?;
                    std::thread::sleep(std::time::Duration::from_millis(30));
                    c.fake(3, kc, 0, 0)?;
                }
                c.fake(3, sh, 0, 0)?;
                i += 1;
            }
            "key" => {
                let ks = u32::from_str_radix(a[i + 1].trim_start_matches("0x"), 16).unwrap();
                match c.keycode_for(ks) {
                    Some(kc) => {
                        c.fake(2, kc, 0, 0)?;
                        std::thread::sleep(std::time::Duration::from_millis(30));
                        c.fake(3, kc, 0, 0)?;
                    }
                    None => eprintln!("no keycode for keysym 0x{:x}", ks),
                }
                i += 1;
            }
            "sleep" => {
                let ms: u64 = a[i + 1].parse().unwrap();
                std::thread::sleep(std::time::Duration::from_millis(ms));
                i += 1;
            }
            o => eprintln!("unknown command {}", o),
        }
        i += 1;
    }
    Ok(())
}
