//! `tbc` — Standalone-Compiler von Terminal Basic.
//!
//! Unterbefehle (Phase 2):
//! - `tbc run <datei.bas|datei.frm|projekt.mak|datei.tbc>`    Kompilieren und ausführen
//! - `tbc build <datei.bas|datei.frm|projekt.mak>`  Zu `.tbc` kompilieren
//! - `tbc check <datei.bas|datei.frm|projekt.mak>`  Syntax/Semantik prüfen
//! - `tbc convert-frm <quelle.frm> <ziel.frm>`  Binärformular in Text wandeln
//!
//! Exit-Codes von `run` (Entscheidung D6, docs/tbvm-design.md):
//! 0 = END/SYSTEM/Programmende · 3 = STOP · 2 = Laufzeitfehler ·
//! 1 = Compile-Fehler/Bedienfehler.

use std::path::{Path, PathBuf};
use std::process::ExitCode;
use tb_runtime::host::{Ereignis, Host};
use tb_ui::host::TerminalHost;
use tb_vm::interp::{RunEvent, Vm};
use tb_vm::project_io::{module_name, new_execution, run_target, SourceLoader};

fn main() -> ExitCode {
    let args: Vec<String> = std::env::args().skip(1).collect();
    match args.first().map(String::as_str) {
        Some("run") => cmd_run(&args[1..]),
        Some("build") => cmd_build(&args[1..]),
        Some("check") => cmd_check(&args[1..]),
        Some("convert-frm") => cmd_convert_frm(&args[1..]),
        _ => {
            eprintln!(
                "Aufruf: tbc run|build|check <datei.bas|datei.frm|projekt.mak|datei.tbc> | tbc convert-frm <quelle.frm> <ziel.frm>"
            );
            ExitCode::from(1)
        }
    }
}

fn cmd_convert_frm(args: &[String]) -> ExitCode {
    if args.len() != 2 {
        eprintln!("Aufruf: tbc convert-frm <quelle.frm> <ziel.frm>");
        return ExitCode::from(1);
    }
    let input = Path::new(&args[0]);
    let output = Path::new(&args[1]);
    let bytes = match std::fs::read(input) {
        Ok(bytes) => bytes,
        Err(error) => {
            eprintln!("{}: {error}", input.display());
            return ExitCode::from(1);
        }
    };
    let converted = match tb_ui::frm::read_binary(&input.display().to_string(), &bytes) {
        Ok(converted) => converted,
        Err(error) => {
            eprintln!("{error}");
            return ExitCode::from(1);
        }
    };
    let text = tb_ui::frm::write_text(&converted.form);
    if let Err(error) = std::fs::write(output, text) {
        eprintln!("{}: {error}", output.display());
        return ExitCode::from(1);
    }
    for warning in &converted.skipped {
        eprintln!(
            "{}: Byte 0x{:04x}: {} nicht übernommen",
            input.display(),
            warning.offset,
            warning.name
        );
    }
    eprintln!(
        "{}: {} nicht übernommene Binärangaben",
        input.display(),
        converted.skipped.len()
    );
    println!("{}", output.display());
    ExitCode::SUCCESS
}

fn compile(
    path_arg: Option<&String>,
) -> Result<(PathBuf, tb_vm::bytecode::CompiledModule), ExitCode> {
    let Some(path) = path_arg else {
        eprintln!("Aufruf: tbc run|build|check <datei.bas|datei.frm|projekt.mak|datei.tbc>");
        return Err(ExitCode::from(1));
    };
    let path = PathBuf::from(path);
    if path
        .extension()
        .is_some_and(|ext| ext.eq_ignore_ascii_case("tbc"))
    {
        let bytes = std::fs::read(&path).map_err(|e| {
            eprintln!("{}: {e}", path.display());
            ExitCode::from(1)
        })?;
        let module = tb_vm::bytecode::CompiledModule::from_tbc(&bytes).map_err(|e| {
            eprintln!("{}: {e}", path.display());
            ExitCode::from(1)
        })?;
        return Ok((path, module));
    }
    let input = match SourceLoader::default().load(&path) {
        Ok(input) => input,
        Err(error) => {
            eprintln!("{error}");
            return Err(ExitCode::from(1));
        }
    };
    let compiled = input.compile(&mut Default::default(), &module_name(&path));
    match compiled {
        Ok(m) => Ok((path, m)),
        Err(diags) => {
            for d in &diags {
                eprintln!("{d}");
            }
            eprintln!("{} Fehler.", diags.len());
            Err(ExitCode::from(1))
        }
    }
}

fn cmd_check(args: &[String]) -> ExitCode {
    match compile(args.first()) {
        Ok(_) => ExitCode::SUCCESS,
        Err(code) => code,
    }
}

fn cmd_build(args: &[String]) -> ExitCode {
    let (path, module) = match compile(args.first()) {
        Ok(x) => x,
        Err(code) => return code,
    };
    let out = path.with_extension("tbc");
    match std::fs::write(&out, module.to_tbc()) {
        Ok(()) => {
            println!("{}", out.display());
            ExitCode::SUCCESS
        }
        Err(e) => {
            eprintln!("{}: {e}", out.display());
            ExitCode::from(1)
        }
    }
}

fn cmd_run(args: &[String]) -> ExitCode {
    // Ohne Terminal (Pipe, Skript, CI) läuft das Programm im PipeHost:
    // Eingabe zeilenweise von stdin, Ausgabe am Ende als Snapshot.
    let result = match TerminalHost::new() {
        Ok(mut host) => {
            let size = host.groesse().ok();
            let result = run_chain(args, &mut host, size);
            drop(host); // Alternativschirm verlassen, bevor gedruckt wird
            result
        }
        Err(_) => run_chain(args, &mut PipeHost::new(), None),
    };
    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"),
    }
}

fn run_chain(
    args: &[String],
    host: &mut dyn Host,
    size: Option<(usize, usize)>,
) -> Result<(RunEvent, Vm), ExitCode> {
    let Some(first) = args.first() else {
        eprintln!("Aufruf: tbc run <datei.bas|datei.frm|projekt.mak|datei.tbc>");
        return Err(ExitCode::from(1));
    };
    let mut current = PathBuf::from(first);
    let mut start_line = None;
    let command = args[1..].join(" ");
    loop {
        let current_arg = current.display().to_string();
        let (path, module) = compile(Some(&current_arg))?;
        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(&path, &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
    }
}
