use torque_asm::*;

use assembler::FileError;
use log::{info, fatal};
use switchboard::*;

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


fn main() {
    let mut args = Switchboard::from_env();
    args.positional("source");
    args.positional("destination");
    args.positional("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 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_string());
    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
  [extension]            File extension to identify library files (default is 'tq')

Switches:
  --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)  Prints help
  --verbose,       (-v)  Print additional debug 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:
  <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 = if let Some(path) = &source_path {
        info!("Reading program source from {path:?}");
        Compiler::from_path(path).unwrap_or_else(|err| match err {
            FileError::InvalidExtension => fatal!(
                "File {path:?} has invalid extension, must be '.{extension}'"),
            FileError::NotFound => fatal!(
                "File {path:?} was not found"),
            FileError::InvalidUtf8 => fatal!(
                "File {path:?} does not contain valid UTF-8 text"),
            FileError::NotReadable => fatal!(
                "File {path:?} is not readable"),
            FileError::IsADirectory => fatal!(
                "File {path:?} is a directory"),
            FileError::Unknown => fatal!(
                "Unknown error while attempting to read from {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::from_string(source_code, "<standard input>")
    };
    if compiler.error().is_some() && !no_libs && !no_project_libs {
        compiler.include_libs_from_parent(&extension);
    }
    if compiler.error().is_some() && !no_libs && !no_env_libs {
        compiler.include_libs_from_path_variable("TORQUE_LIBS", &extension);
    }

    if print_tree {
        compiler.resolver.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::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 merged 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);
}