//! Gemeinsamer CLI-/Standalone-Runner; der Aufrufer bestimmt das Ladeverfahren.
use std::path::Path;
use std::process::ExitCode;
use tb_runtime::host::{Ereignis, Host};
use tb_ui::host::TerminalHost;
use tb_vm::bytecode::CompiledModule;
use tb_vm::interp::{RunEvent, Vm};
use tb_vm::project_io::{new_execution, run_target};

pub fn run(
    first: &Path,
    command: &str,
    mut load: impl FnMut(&Path) -> Result<CompiledModule, ExitCode>,
) -> ExitCode {
    let result = match TerminalHost::new() {
        Ok(mut host) => {
            let size = host.groesse().ok();
            let result = run_chain(first, command, &mut load, &mut host, size);
            drop(host);
            result
        }
        Err(_) => run_chain(first, command, &mut load, &mut PipeHost::new(), None),
    };
    finish(result)
}

fn finish(result: Result<(RunEvent, Vm), ExitCode>) -> ExitCode {
    let (ereignis, vm) = match result {
        Ok(result) => result,
        Err(code) => return code,
    };
    print!("{}", tb_runtime::snapshot::text(&vm.rt.screen));
    // `LPRINT` sammelt im Druckerpuffer; am Programmende geht er in die
    // Datei LPT1.TXT im aktuellen Verzeichnis (dokumentierte Abweichung —
    // einen Druckerkanal gibt es plattformübergreifend nicht).
    if !vm.rt.print.drucker.is_empty() {
        if let Err(e) = std::fs::write("LPT1.TXT", &vm.rt.print.drucker) {
            eprintln!("Druckerausgabe nicht schreibbar: {e}");
        }
    }

    match ereignis {
        RunEvent::Ended => ExitCode::SUCCESS,
        RunEvent::Stopped { line } => {
            // STOP außerhalb der IDE: Meldung + Exit-Code ≠ 0 (D6).
            eprintln!("{}:{line}: STOP in line {line}", vm.current_file());
            ExitCode::from(3)
        }
        RunEvent::Error {
            code,
            line,
            message,
        } => {
            eprintln!(
                "{}:{line}:{}: Runtime error {code}: {message} in line {line}",
                vm.current_file(),
                vm.current_source_pos().column
            );
            ExitCode::from(2)
        }
        // Ohne Debugger-Flags treten diese Ereignisse nicht auf.
        RunEvent::Interrupted { line } => {
            eprintln!("Abgebrochen in Zeile {line}");
            ExitCode::from(3)
        }
        // Ohne Debugger-Flags treten diese Ereignisse nicht auf.
        RunEvent::Breakpoint { .. } | RunEvent::Stepped { .. } => ExitCode::from(2),
        RunEvent::Restart { .. } => unreachable!("RUN wird vom Runner aufgelöst"),
    }
}

pub fn run_chain(
    first: &Path,
    command: &str,
    mut load: impl FnMut(&Path) -> Result<CompiledModule, ExitCode>,
    host: &mut dyn Host,
    size: Option<(usize, usize)>,
) -> Result<(RunEvent, Vm), ExitCode> {
    let mut current = first.to_path_buf();
    let mut start_line = None;
    loop {
        let module = load(&current)?;
        let mut vm = new_execution(module, command, size, start_line.take()).map_err(|error| {
            eprintln!("Runtime error {}: {}", error.0, error);
            ExitCode::from(2)
        })?;
        if !vm.rt.zeitpunkt().1 {
            eprintln!("Zeitzone nicht ermittelbar — Zeitfunktionen rechnen in UTC.");
        }
        let event = vm.run(host);
        let event =
            if event == RunEvent::Ended && vm.forms.has_visible_forms() && !vm.is_terminated() {
                vm.run_visible_forms(host)
            } else {
                event
            };
        match event {
            RunEvent::Restart { program, line } => {
                if let Some(program) = program {
                    current = run_target(&current, &program).map_err(|error| {
                        eprintln!("{error}");
                        ExitCode::from(1)
                    })?;
                }
                start_line = line;
            }
            event => return Ok((event, vm)),
        }
    }
}

/// Host ohne Terminal: für Pipes und Skripte (`tbc run x.bas < eingabe.txt`).
/// Zeigt während des Laufs nichts an; die Ausgabe entsteht am Ende aus dem
/// Bildschirm-Snapshot. Tastendrücke kommen zeilenweise von stdin.
struct PipeHost {
    puffer: std::collections::VecDeque<Ereignis>,
    eof: bool,
    start: std::time::Instant,
}

impl PipeHost {
    fn new() -> Self {
        let mut host = PipeHost {
            puffer: std::collections::VecDeque::new(),
            eof: false,
            start: std::time::Instant::now(),
        };
        use std::io::{IsTerminal, Read};
        let mut stdin = std::io::stdin();
        if !stdin.is_terminal() {
            let mut input = String::new();
            let _ = stdin.read_to_string(&mut input);
            for line in input.split_inclusive('\n') {
                for character in line.trim_end_matches(['\r', '\n']).chars() {
                    host.puffer
                        .push_back(Ereignis::Taste(character.to_string(), 0));
                }
                host.puffer.push_back(Ereignis::Taste(
                    tb_runtime::host::taste::ENTER.to_string(),
                    0,
                ));
            }
            host.puffer.push_back(Ereignis::Ende);
            host.eof = true;
        }
        host
    }

    /// Eine Zeile von stdin in Tastendrücke zerlegen.
    fn nachfuellen(&mut self) {
        use std::io::BufRead;
        if self.eof {
            return;
        }
        let mut zeile = String::new();
        match std::io::stdin().lock().read_line(&mut zeile) {
            Ok(0) | Err(_) => {
                self.eof = true;
                self.puffer.push_back(Ereignis::Ende);
            }
            Ok(_) => {
                while zeile.ends_with('\n') || zeile.ends_with('\r') {
                    zeile.pop();
                }
                for c in zeile.chars() {
                    self.puffer.push_back(Ereignis::Taste(c.to_string(), 0));
                }
                self.puffer.push_back(Ereignis::Taste(
                    tb_runtime::host::taste::ENTER.to_string(),
                    0,
                ));
            }
        }
    }
}

impl Host for PipeHost {
    fn present(&mut self, _screen: &tb_runtime::screen::TextScreen) {}

    fn next_event(&mut self, blockierend: bool) -> Option<Ereignis> {
        if self.puffer.is_empty() && blockierend {
            self.nachfuellen();
        }
        self.puffer.pop_front()
    }

    fn warten(&mut self, deadline_ms: Option<u64>) -> Option<Ereignis> {
        if let Some(event) = self.next_event(false) {
            return Some(event);
        }
        if let Some(deadline) = deadline_ms {
            std::thread::sleep(std::time::Duration::from_millis(
                deadline.saturating_sub(self.jetzt_ms()),
            ));
            None
        } else {
            self.next_event(true).or(Some(Ereignis::Ende))
        }
    }

    fn jetzt_ms(&mut self) -> u64 {
        self.start.elapsed().as_millis() as u64
    }
}
