use crate::formats::*;

use bedrock_asm::*;
use log::{info, fatal};
use switchboard::*;

use std::io::{Read, Write};
use std::path::Path;


pub fn main(mut args: Switchboard) {
    args.positional("source");
    args.positional("destination");
    args.named("extension").default("brc");

    args.named("no-libs");
    args.named("no-project-libs");
    args.named("no-env-libs");

    args.named("format").default("binary");
    args.named("dry-run").short('n');
    args.named("tree");
    args.named("with-symbols");
    args.raise_errors();

    let source_path        = args.get("source").as_path_opt().map(
        |p| p.canonicalize().unwrap_or_else(|e| fatal!("{p:?}: {e:?}")));
    let destination_path   = args.get("destination").as_path_opt();
    let extension          = args.get("extension").as_string();
    let opt_extension      = Some(extension.as_str());

    let no_libs            = args.get("no-libs").as_bool();
    let no_project_libs    = args.get("no-project-libs").as_bool();
    let no_env_libs        = args.get("no-env-libs").as_bool();

    let format             = Format::from_str(args.get("format").as_str());
    let dry_run            = args.get("dry-run").as_bool();
    let print_tree         = args.get("tree").as_bool();
    let export_symbols     = args.get("with-symbols").as_bool();

    // -----------------------------------------------------------------------

    let mut compiler = new_compiler();

    if let Some(path) = &source_path {
        info!("Reading program source from {path:?}");
        compiler.root_from_path(path).unwrap_or_else(|err| fatal!("{err:?}: {path:?}"));
    } else {
        let mut source_code = String::new();
        info!("Reading program source from standard input");
        if let Err(err) = std::io::stdin().read_to_string(&mut source_code) {
            fatal!("Could not read from standard input\n{err:?}");
        }
        compiler.root_from_string(source_code, "<standard input>")
    };
    if compiler.error().is_some() && !no_libs && !no_project_libs {
        compiler.include_libs_from_parent(opt_extension);
    }
    if compiler.error().is_some() && !no_libs && !no_env_libs {
        compiler.include_libs_from_path_variable("BEDROCK_LIBS", opt_extension);
    }

    if print_tree {
        compiler.hierarchy().report()
    }
    if let Some(error) = compiler.error() {
        error.report();
        std::process::exit(1);
    }

    let merged_source = compiler.get_compiled_source().unwrap_or_else(|error| {
        error.report();
        std::process::exit(1);
    });

    if !dry_run && format == Format::Source {
        write_bytes_and_exit(merged_source.as_bytes(), destination_path.as_ref());
    }

    // -----------------------------------------------------------------------

    let path = Some("<merged source>");
    let syntactic = match parse_syntactic(&merged_source, path) {
        Ok(tokens) => tokens,
        Err(errors) => {
            report_syntactic_errors(&errors, &merged_source);
            std::process::exit(1);
        }
    };

    let semantic = match parse_semantic(syntactic) {
        Ok(tokens) => tokens,
        Err(errors) => {
            report_semantic_errors(&errors, &merged_source);
            std::process::exit(1);
        }
    };

    let AssembledProgram { mut bytecode, symbols } = generate_bytecode(&semantic);
    // Remove null bytes from end of bytecode.
    while let Some(0) = bytecode.last() {
        bytecode.pop();
    }

    let length = bytecode.len();
    let percentage = (length as f32 / 65536.0 * 100.0).round() as u16;
    info!("Assembled program in {length} bytes ({percentage}% of maximum)");

    if !dry_run {
        if export_symbols {
            if let Some(path) = &destination_path {
                let mut symbols_path = path.to_path_buf();
                symbols_path.add_extension("sym");
                let mut symbols_string = String::new();
                for symbol in &symbols {
                    let address = &symbol.address;
                    let name = &symbol.name;
                    let location = &symbol.source.location();
                    symbols_string.push_str(&format!(
                        "{address:04x} {name} {location}\n"
                    ));
                }
                match std::fs::write(&symbols_path, symbols_string) {
                    Ok(_) => info!("Saved symbols to {symbols_path:?}"),
                    Err(err) => info!("Could not write symbols to {symbols_path:?}\n{err:?}"),
                }
            }
        }

        let bytes = match format {
            Format::Binary => bytecode,
            Format::Clang => format_clang(&bytecode),
            Format::Source => unreachable!("Source output is handled before full assembly"),
        };
        write_bytes_and_exit(&bytes, destination_path.as_ref());
    }
}


fn write_bytes_and_exit<P: AsRef<Path>>(bytes: &[u8], path: Option<&P>) -> ! {
    match path {
        Some(path) => match std::fs::write(path, bytes) {
            Ok(_) => info!("Wrote output to {:?}", path.as_ref()),
            Err(err) => fatal!("Could not write to {:?}\n{err:?}", path.as_ref()),
        }
        None => match std::io::stdout().write_all(bytes) {
            Ok(_) => info!("Wrote output to standard output"),
            Err(err) => fatal!("Could not write to standard output\n{err:?}"),
        }
    }
    std::process::exit(0);
}