use std::{
    fs::File,
    io::Read,
    path::PathBuf,
    process::{Child, Command, Output},
};

fn signal(pid: u32, sig: &str) {
    Command::new("kill")
        .arg("-s")
        .arg(sig)
        .arg(format!("{}", pid))
        .status()
        .expect("failed to execute 'kill'");
}

pub struct Sway {
    pub sock: PathBuf,
    process: Child,
}

impl Sway {
    pub fn start() -> Sway {
        let tmp = std::env::temp_dir()
            .join("swaysome_tests")
            .join(std::thread::current().name().unwrap());
        std::fs::create_dir_all(&tmp).expect("Unable to create temporary working directory");

        let pwd = std::env::current_dir().expect("Unable to get current dir");
        let conf_path = pwd.join("tests/sway.conf");
        let swaysock_path = tmp.join("swaysock");
        let sway_log =
            File::create(tmp.join("sway.log")).expect("Unable to create sway's log file");

        let sway = Command::new("sway")
            .arg("-c")
            .arg(conf_path.clone())
            .env_clear()
            .env("WLR_BACKENDS", "headless")
            .env("WLR_LIBINPUT_NO_DEVICES", "1")
            .env("XDG_RUNTIME_DIR", &tmp)
            .env("SWAYSOCK", &swaysock_path)
            .stderr(sway_log)
            .spawn()
            .expect("failed to execute sway");

        // check that sway works correctly without using swaysome
        let sway = Sway {
            sock: swaysock_path,
            process: sway,
        };
        match sway.check_connection("loaded_config_file_name") {
            Ok(()) => {
                // Let's do some common initialization of the desktop
                sway.send_command(["create_output"].as_slice());
                sway.send_command(["create_output"].as_slice());
                return sway;
            }
            Err(()) => {
                eprintln!("Failed to start 'sway', aborting the tests");
                eprintln!("---- sway stderr ----");
                let mut buffer = String::new();
                let mut sway_stderr =
                    File::open(tmp.join("sway.log")).expect("Unable to open sway's log file");
                sway_stderr.read_to_string(&mut buffer).unwrap();
                eprintln!("{}", buffer);
                eprintln!("---------------------");
                panic!();
            }
        }
    }

    pub fn send_command(&self, commands: &[&str]) -> Output {
        Command::new("swaymsg")
            .args(commands)
            .env_clear()
            .env("SWAYSOCK", self.sock.clone())
            .output()
            .expect("Couldn't run swaymsg")
    }

    // work around https://github.com/rust-lang/rust/issues/46379
    // TODO: maybe implement that: https://momori.dev/posts/organize-rust-integration-tests-without-dead-code-warning/
    #[allow(dead_code)]
    pub fn spawn_some_apps(&self) {
        self.send_command(["exec", "foot -T TERM1"].as_slice());
        // Make sure the app are created in the right order.
        // 200ms would still sometimes be racy on my Ryzen 5 PRO 4650U, so let's
        // take a safe bet and give plenty of time for shared CI runners.
        std::thread::sleep(std::time::Duration::from_millis(500));
        self.send_command(["exec", "foot -T TERM2"].as_slice());
        std::thread::sleep(std::time::Duration::from_millis(500));
        self.send_command(["exec", "foot -T TERM3"].as_slice());
        std::thread::sleep(std::time::Duration::from_millis(500));
    }

    fn check_connection(&self, flag: &str) -> Result<(), ()> {
        let mut retries = 100; // wait for max 10s
        while retries > 0 {
            let version = self.send_command(["-t", "get_version"].as_slice());
            if String::from_utf8(version.stdout).unwrap().contains(flag)
                || String::from_utf8(version.stderr).unwrap().contains(flag)
            {
                return Ok(());
            }
            std::thread::sleep(std::time::Duration::from_millis(100));
            retries -= 1;
        }
        return Err(());
    }

    fn stop(&mut self) {
        // in case some apps were spawned, kill them, and give them some time to be killed
        self.send_command(["[all] kill"].as_slice());
        std::thread::sleep(std::time::Duration::from_millis(500));
        // now terminate sway
        signal(self.process.id(), "TERM");
        match self.check_connection("Unable to connect to") {
            Ok(()) => eprintln!("Sway terminated correctly on its own"),
            Err(_) => {
                self.process.kill().expect("Failed to kill sway");
                eprintln!("Sway had to be killed");
            }
        }
    }
}

impl Drop for Sway {
    fn drop(&mut self) {
        self.stop();
    }
}
