use crossterm::{
    cursor::{Hide, Show},
    event::{DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture},
    execute,
    style::ResetColor,
    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use std::io::{self, Write};

/// Ein Besitzer für Raw Mode und Terminalzustand, auch bei teilweiser Initialisierung.
pub struct TerminalGuard<W: Write> {
    writer: W,
    raw: fn(bool) -> io::Result<()>,
}
impl<W: Write> TerminalGuard<W> {
    pub fn enter(writer: W) -> io::Result<Self> {
        Self::with_raw(writer, |on| {
            if on {
                enable_raw_mode()
            } else {
                disable_raw_mode()
            }
        })
    }
    fn with_raw(writer: W, raw: fn(bool) -> io::Result<()>) -> io::Result<Self> {
        let mut guard = Self { writer, raw };
        guard.resume()?;
        Ok(guard)
    }
    fn resume(&mut self) -> io::Result<()> {
        (self.raw)(true)?;
        execute!(
            self.writer,
            EnterAlternateScreen,
            EnableMouseCapture,
            EnableBracketedPaste,
            Hide
        )
    }
    fn suspend(&mut self) -> io::Result<()> {
        (self.raw)(false)?;
        execute!(
            self.writer,
            ResetColor,
            Show,
            DisableMouseCapture,
            DisableBracketedPaste,
            LeaveAlternateScreen
        )
    }
    /// Foreground child owns cooked terminal input. Always restore the IDE,
    /// including spawn/wait errors and a child interrupted by Ctrl+C.
    pub fn shell(
        &mut self,
        command: &str,
        signals: &crate::signale::Signalquelle,
    ) -> io::Result<i32> {
        let result = self.shell_with_interrupt(command, || signals.abholen().is_some());
        // A child may exit before try_wait observes the terminal signal.
        // It belongs to the handed-off terminal, not the resumed BASIC run.
        while signals.abholen().is_some() {}
        result
    }
    fn shell_with_interrupt(
        &mut self,
        command: &str,
        mut interrupted: impl FnMut() -> bool,
    ) -> io::Result<i32> {
        let result = self.suspend().and_then(|_| {
            let mut child = tb_runtime::host::shell_command(command).spawn()?;
            loop {
                match child.try_wait() {
                    Ok(Some(status)) => return Ok(status.code().unwrap_or(0)),
                    Ok(None) => {}
                    Err(error) => {
                        let _ = child.kill();
                        let _ = child.wait();
                        return Err(error);
                    }
                }
                if interrupted() {
                    let _ = child.kill();
                    return child.wait().map(|status| status.code().unwrap_or(0));
                }
                std::thread::sleep(std::time::Duration::from_millis(10));
            }
        });
        let restored = self.resume();
        restored.and(result)
    }
}
impl<W: Write> Drop for TerminalGuard<W> {
    fn drop(&mut self) {
        let _ = (self.raw)(false);
        let _ = execute!(
            self.writer,
            ResetColor,
            Show,
            DisableMouseCapture,
            DisableBracketedPaste,
            LeaveAlternateScreen
        );
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::{
        cell::RefCell,
        rc::Rc,
        sync::atomic::{AtomicI32, Ordering},
    };
    static RAW: AtomicI32 = AtomicI32::new(0);
    #[derive(Clone)]
    struct Output(Rc<RefCell<Vec<u8>>>);
    impl Write for Output {
        fn write(&mut self, b: &[u8]) -> io::Result<usize> {
            self.0.borrow_mut().extend_from_slice(b);
            Ok(b.len())
        }
        fn flush(&mut self) -> io::Result<()> {
            Ok(())
        }
    }
    fn raw(on: bool) -> io::Result<()> {
        RAW.store(i32::from(on), Ordering::SeqCst);
        Ok(())
    }
    #[test]
    fn terminal_cleanup_on_normal_error_and_partial_initialization() {
        for fail in [false, true] {
            let out = Output(Rc::default());
            let copy = out.clone();
            let result = (|| -> io::Result<()> {
                let _guard = TerminalGuard::with_raw(out, raw)?;
                if fail {
                    return Err(io::Error::other("Renderfehler"));
                }
                Ok(())
            })();
            assert_eq!(result.is_err(), fail);
            assert_eq!(RAW.load(Ordering::SeqCst), 0);
            let s = String::from_utf8(copy.0.borrow().clone()).unwrap();
            assert!(s.contains("?1049h") && s.contains("?1049l") && s.contains("?25h"));
        }
        for (command, abort, expected) in [
            ("exit 0", false, 0),
            ("exit 7", false, 7),
            (
                if cfg!(windows) {
                    "ping -n 30 127.0.0.1 >nul"
                } else {
                    "exec sleep 30"
                },
                true,
                0,
            ),
        ] {
            let out = Output(Rc::default());
            let copy = out.clone();
            let mut guard = TerminalGuard::with_raw(out, raw).unwrap();
            assert_eq!(
                guard.shell_with_interrupt(command, || abort).unwrap(),
                expected
            );
            assert_eq!(RAW.load(Ordering::SeqCst), 1);
            let text = String::from_utf8(copy.0.borrow().clone()).unwrap();
            assert_eq!(text.matches("?1049h").count(), 2);
            assert_eq!(text.matches("?1049l").count(), 1);
            drop(guard);
            assert_eq!(RAW.load(Ordering::SeqCst), 0);
        }
        let out = Output(Rc::default());
        let copy = out.clone();
        assert!(TerminalGuard::with_raw(out, |on| {
            raw(on)?;
            if on {
                Err(io::Error::other("Raw-Fehler"))
            } else {
                Ok(())
            }
        })
        .is_err());
        assert_eq!(RAW.load(Ordering::SeqCst), 0);
        assert!(String::from_utf8(copy.0.borrow().clone())
            .unwrap()
            .contains("?25h"));
    }
}
