use torque_asm::*;

use log::*;
use switchboard::*;

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


fn main() {
    let mut args = Switchboard::from_env();
    args.positional("source");
    args.positional("destination");
    args.named("extension").default("tq");
    args.named("no-libs");
    args.named("no-project-libs");
    args.named("no-env-libs");
    args.named("format").default("debug");
    args.named("width");
    args.named("dry-run").short('n');
    args.named("tree");
    args.named("help").short('h');
    args.named("version");
    args.named("verbose").short('v');
    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     = 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 width           = args.get("width").as_u32_opt();
    let dry_run         = args.get("dry-run").as_bool();
    let print_tree      = args.get("tree").as_bool();
    let print_help      = args.get("help").as_bool();
    let print_version   = args.get("version").as_bool();
    let verbose         = args.get("verbose").as_bool();

    if verbose { log::set_log_level(log::LogLevel::Info) }
    if print_version {
        let version = env!("CARGO_PKG_VERSION");
        eprintln!("torque assembler, version {version}");
        eprintln!("written by ben bridle");
        std::process::exit(0);
    }
    if print_help {
        eprintln!("\
Usage: tq [source] [destination]

Torque multi-assembler, see http://benbridle.com/torque for documentation.

Arguments:
  [source]               Path to a source file to assemble
  [destination]          Path to which output will be written

Switches:
  --extension=<ext>      File extension to identify library files (default is 'tq')
  --format=<fmt>         Format to apply to assembled bytecode (default is 'debug')
  --width=<width>        Force a fixed width for all assembled words
  --no-project-libs      Don't search for libraries in the source parent folder
  --no-env-libs          Don't search for libraries in the TORQUE_LIBS path variable
  --no-libs              Combination of --no-project-libs and --no-env-libs
  --tree                 Display a tree visualisation of all included library files
  --dry-run        (-n)  Assemble and show errors only, don't write any output
  --help           (-h)  Print this help information
  --verbose,       (-v)  Print additional information
  --version              Print the assembler version and exit

Environment variables:
  TORQUE_LIBS
    A list of colon-separated paths which will be searched to find
    Torque source code files to use as libraries when assembling a
    Torque program. If a library file resolves an unresolved symbol
    in the program being assembled, the library file will be merged
    into the program.

Output formats:
  cmd
    CMD module load format used by the CP/M operating system.
  debug
    Print assembled words as human-readable binary literals.
  inhx
    Original 8-bit Intel hex format.
  inhx32
    Modified 16-bit Intel hex format used by Microchip.
  raw
    Assembled words are converted to big-endian bytestrings and concatenated.
    Each word is padded to the nearest byte. Words must all be the same width.
  source
    Print the source file before assembly, with symbols resolved.

Created by Ben Bridle.
        ");
        std::process::exit(0);
    }

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

    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("TORQUE_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.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 intermediate = match parse_intermediate(semantic) {
        Ok(tokens) => tokens,
        Err(errors) => {
            report_intermediate_errors(&errors, &merged_source);
            std::process::exit(1);
        }
    };

    let segments = match parse_bytecode(intermediate, width) {
        Ok(segments) => segments,
        Err(errors) => {
            report_bytecode_errors(&errors, &merged_source);
            std::process::exit(1);
        }
    };


    if !dry_run {
        let result = match format {
            Format::Cmd => format_cmd(&segments),
            Format::Debug => format_debug(&segments),
            Format::Inhx => format_inhx(&segments),
            Format::Inhx32 => format_inhx32(&segments),
            Format::Raw => format_raw(&segments, width),
            Format::Source => unreachable!("Source output is handled before full assembly"),
        };
        match result {
            Ok(bytes) => write_bytes_and_exit(&bytes, destination.as_ref()),
            Err(error) => report_format_error(&error, format, &merged_source),
        }
    }
}


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 {:?}", path.as_ref()),
            Err(err) => fatal!("Could not write to path {:?}\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);
}