//! `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_vm::project_io::{module_name, SourceLoader};

mod export_cancel;

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("link") => cmd_link(&args[1..]),
        Some("check") => cmd_check(&args[1..]),
        Some("convert-frm") => cmd_convert_frm(&args[1..]),
        _ => {
            eprintln!(
                "Aufruf: tbc run|build|link|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|link|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 {
    if args.len() != 1 {
        eprintln!("Aufruf: tbc check <Projekt>");
        return ExitCode::from(1);
    }
    match compile(args.first()) {
        Ok(_) => ExitCode::SUCCESS,
        Err(code) => code,
    }
}

fn cmd_build(args: &[String]) -> ExitCode {
    cmd_output(args, false)
}
fn cmd_link(args: &[String]) -> ExitCode {
    cmd_output(args, true)
}
fn cmd_output(args: &[String], link: bool) -> ExitCode {
    match build(args, link) {
        Ok(path) => {
            println!("{}", path.display());
            ExitCode::SUCCESS
        }
        Err(error) => {
            eprintln!("{error:#}");
            ExitCode::from(1)
        }
    }
}

fn build(args: &[String], link: bool) -> anyhow::Result<PathBuf> {
    use anyhow::{bail, ensure, Context};
    let mut inputs = Vec::new();
    let mut library = false;
    let mut exe = false;
    let mut force = false;
    let mut output = None;
    let mut target = None;
    let mut template = None;
    let mut args = args.iter();
    while let Some(arg) = args.next() {
        match arg.as_str() {
            "--library" => {
                ensure!(
                    !library && !link,
                    "--library ist nur bei build zulässig und darf nicht doppelt sein"
                );
                library = true;
            }
            "--exe" => {
                ensure!(!exe, "Doppeltes --exe");
                exe = true;
            }
            "--force" => {
                ensure!(!force, "Doppeltes --force");
                force = true;
            }
            "-o" | "--output" | "--target" | "--template" => {
                let value = args
                    .next()
                    .filter(|v| !v.starts_with('-'))
                    .with_context(|| format!("Wert fehlt für {arg}"))?;
                let slot = match arg.as_str() {
                    "--target" => &mut target,
                    "--template" => &mut template,
                    _ => &mut output,
                };
                ensure!(
                    slot.replace(value.clone()).is_none(),
                    "Doppelte Option {arg}"
                );
            }
            "--" => inputs.extend(args.by_ref().cloned()),
            value if value.starts_with('-') => bail!("Unbekannte Build-Option: {value}"),
            _ => inputs.push(arg.clone()),
        }
    }
    ensure!(!inputs.is_empty(), "Aufruf: tbc build|link <Quelle> [Bibliothek.tbl ...] [--library | --exe] [-o Ziel] [--force]");
    ensure!(!(exe && library), "--library und --exe schließen sich aus");
    ensure!(
        exe || (target.is_none() && template.is_none()),
        "--target/--template benötigen --exe"
    );
    ensure!(
        exe || library || link || output.is_none(),
        "Ausgabeoptionen benötigen --exe, --library oder link"
    );
    let source = &inputs[0];
    let path = PathBuf::from(source);
    let tbc = tb_vm::project_io::has_extension(&path, "tbc");
    ensure!(!tbc || (!library && inputs.len()==1 && (!link || exe)), "Fertiges TBC kann nur ohne zusätzliche TBL mit link --exe verpackt werden; es ist kein Library-Produkt");
    let mut protected = vec![path.clone()];
    let mut compiler = tb_vm::project::ProjectCompiler::default();
    let loaded = if tbc {
        None
    } else {
        let mut project = SourceLoader::default()
            .load(&path)
            .map_err(anyhow::Error::msg)?;
        for input in &inputs[1..] {
            project
                .add_library(Path::new(input))
                .map_err(anyhow::Error::msg)?;
        }
        protected.extend(project.protected_inputs());
        Some(project)
    };
    let has_libraries = loaded.as_ref().is_some_and(|p| !p.libraries.is_empty());
    let diags = |errors: Vec<tb_frontend::Diagnostic>| {
        anyhow::anyhow!(errors
            .iter()
            .map(ToString::to_string)
            .collect::<Vec<_>>()
            .join("\n"))
    };
    let cancel = export_cancel::ExportCancel::new()?;
    if library {
        let product = loaded
            .as_ref()
            .unwrap()
            .compile_library(&mut compiler)
            .map_err(diags)?;
        let bytes = product.to_tbl().map_err(anyhow::Error::msg)?;
        let out = output
            .map(PathBuf::from)
            .unwrap_or_else(|| path.with_extension("tbl"));
        tb_export::publish(
            &out,
            force,
            &protected,
            &|| cancel.cancelled(),
            |temporary| {
                std::fs::write(temporary, &bytes)?;
                tb_vm::library::Library::from_tbl(&std::fs::read(temporary)?)
                    .map_err(anyhow::Error::msg)?;
                Ok(())
            },
        )?;
        return Ok(out);
    }
    let selected = if exe {
        Some(match target {
            Some(s) => tb_export::Target::parse(&s)?,
            None => tb_export::Target::host()?,
        })
    } else {
        None
    };
    let module = if let Some(project) = loaded {
        project
            .compile(&mut compiler, &module_name(&path))
            .map_err(diags)?
    } else {
        tb_vm::bytecode::CompiledModule::from_tbc(&std::fs::read(&path)?)
            .map_err(|e| anyhow::anyhow!("{e}"))?
    };
    if let Some(target) = selected {
        let out = output.map(PathBuf::from).unwrap_or_else(|| {
            path.with_extension(if target == tb_export::Target::WindowsAmd64 {
                "exe"
            } else {
                ""
            })
        });
        let template = match template {
            Some(p) => PathBuf::from(p),
            None => std::env::current_exe()?
                .parent()
                .context("Compilerpfad ohne Verzeichnis")?
                .join("runtimes")
                .join(target.triple())
                .join(target.runtime_name()),
        };
        tb_export::export(
            tb_export::Export {
                module: &module,
                target,
                template: &template,
                output: &out,
                overwrite: force,
                protected: &protected,
            },
            &|| cancel.cancelled(),
        )?;
        Ok(out)
    } else {
        let legacy = !link && !has_libraries && output.is_none() && !force && inputs.len() == 1;
        let out = output
            .map(PathBuf::from)
            .unwrap_or_else(|| path.with_extension("tbc"));
        if legacy
            && !tbc
            && protected.iter().all(|p| {
                tb_vm::project_io::identity(p).ok() != tb_vm::project_io::identity(&out).ok()
            })
        {
            std::fs::write(&out, module.to_tbc())?;
        } else {
            tb_export::publish(
                &out,
                force,
                &protected,
                &|| cancel.cancelled(),
                |temporary| {
                    std::fs::write(temporary, module.to_tbc())?;
                    tb_vm::bytecode::CompiledModule::from_tbc(&std::fs::read(temporary)?)
                        .map_err(|e| anyhow::anyhow!("{e}"))?;
                    Ok(())
                },
            )?;
        }
        Ok(out)
    }
}

fn cmd_run(args: &[String]) -> ExitCode {
    let Some(first) = args.first() else {
        eprintln!("Aufruf: tbc run <datei.bas|datei.frm|projekt.mak|datei.tbc>");
        return ExitCode::from(1);
    };
    tb_runner::run(Path::new(first), &args[1..].join(" "), |path| {
        compile(Some(&path.display().to_string())).map(|(_, module)| module)
    })
}

#[cfg(test)]
fn run_chain(
    args: &[String],
    host: &mut dyn tb_runtime::host::Host,
    size: Option<(usize, usize)>,
) -> Result<(tb_vm::interp::RunEvent, tb_vm::interp::Vm), ExitCode> {
    let first = args.first().ok_or(ExitCode::from(1))?;
    tb_runner::run_chain(
        Path::new(first),
        &args[1..].join(" "),
        |path| compile(Some(&path.display().to_string())).map(|(_, module)| module),
        host,
        size,
    )
}

#[cfg(test)]
mod ide_acceptance;
