use bedrock_asm::*;

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


static mut VERBOSE: bool = false;

macro_rules! verbose {
    ($($tokens:tt)*) => { if unsafe { VERBOSE } {
            eprint!("[INFO] "); eprintln!($($tokens)*);
    } };
}
macro_rules! error {
    ($($tokens:tt)*) => {{
        eprint!("[ERROR] "); eprintln!($($tokens)*); std::process::exit(1);
    }};
}


fn main() {
    let args = Arguments::from_env_or_exit();

    // -----------------------------------------------------------------------
    // RESOLVE syntactic symbols
    let ext = args.ext.unwrap_or(String::from("brc"));
    let mut resolver = if let Some(path) = &args.source {
        match SourceUnit::from_path(&path, &ext) {
            Ok(source_unit) => SymbolResolver::from_source_unit(source_unit),
            Err(err) => match err {
                ParseError::InvalidExtension => error!(
                    "File {path:?} has invalid extension, must be '.{ext}'"),
                ParseError::NotFound => error!(
                    "File {path:?} was not found"),
                ParseError::InvalidUtf8 => error!(
                    "File {path:?} does not contain valid UTF-8 text"),
                ParseError::NotReadable => error!(
                    "File {path:?} is not readable"),
                ParseError::IsADirectory => error!(
                    "File {path:?} is a directory"),
                ParseError::Unknown => error!(
                    "Unknown error while attempting to read from {path:?}")
            }
        }
    } else {
        let mut source_code = String::new();
        verbose!("Reading program source from standard input");
        if let Err(err) = std::io::stdin().read_to_string(&mut source_code) {
            eprintln!("Could not read from standard input, exiting.");
            eprintln!("({err:?})");
            std::process::exit(1);
        }
        let path = "<standard input>";
        let source_unit = SourceUnit::from_source_code(source_code, path);
        SymbolResolver::from_source_unit(source_unit)
    };
    // Load project libraries.
    if let Some(path) = &args.source {
        if !args.no_libs && !args.no_project_libs {
            let project_library = gather_project_libraries(path, &ext);
            resolver.add_library_units(project_library);
        }
    }
    // Load environment libraries.
    if !args.no_libs && !args.no_env_libs {
        for env_library in gather_environment_libraries(&ext) {
            resolver.add_library_units(env_library);
        }
    }
    resolver.resolve();

    // -----------------------------------------------------------------------
    // PRINT information, generate merged source code
    if args.tree {
        print_source_tree(&resolver);
    }
    if print_resolver_errors(&resolver) {
        std::process::exit(1);
    };
    let merged_source = resolver.get_merged_source_code();
    if args.resolve {
        write_bytes_and_exit(merged_source.as_bytes(), args.output.as_ref());
    }

    // -----------------------------------------------------------------------
    // PARSE semantic tokens from merged source code
    let path = Some("<merged source>");
    let mut semantic_tokens = generate_semantic_tokens(&merged_source, path);
    if print_semantic_errors(&semantic_tokens, &merged_source) {
        std::process::exit(1);
    };

    // -----------------------------------------------------------------------
    // GENERATE symbols file and bytecode
    let bytecode = generate_bytecode(&mut semantic_tokens);
    // let symbols = generate_symbols_file(&semantic_tokens);
    write_bytes_and_exit(&bytecode, args.output.as_ref());
}


fn write_bytes_and_exit<P: AsRef<Path>>(bytes: &[u8], path: Option<&P>) -> ! {
    if let Some(path) = path {
        if let Err(err) = std::fs::write(path, bytes) {
            eprintln!("Could not write to path {:?}, exiting.", path.as_ref());
            eprintln!("({err:?})");
            std::process::exit(1);
        }
    } else {
        if let Err(err) = std::io::stdout().write_all(bytes) {
            eprintln!("Could not write to standard output, exiting.");
            eprintln!("({err:?})");
            std::process::exit(1);
        }
    }
    std::process::exit(0);
}


xflags::xflags! {
    cmd arguments {
        /// Print additional debug information
        optional --verbose
        /// Print the assembler version and exit
        optional --version


        /// Bedrock source code file to assemble.
        optional source: PathBuf
        /// Destination path for assembler output.
        optional output: PathBuf
        /// File extension to identify source files.
        optional ext: String

        /// Don't include libraries or resolve references.
        optional --no-libs
        /// Don't include project libraries
        optional --no-project-libs
        /// Don't include environment libraries.
        optional --no-env-libs

        /// Show the resolved source file heirarchy
        optional --tree
        /// Assemble the program without saving any output
        optional --check
        /// Only return resolved source code.
        optional --resolve
    }
}