use bedrock_asm::*;
use bedrock_pc::*;
use phosphor::*;

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();
    if args.version {
        let name = env!("CARGO_PKG_NAME");
        let version = env!("CARGO_PKG_VERSION");
        eprintln!("{name} v{version}");
        eprintln!("Written by Ben Bridle.");
        std::process::exit(0);
    }
    if args.verbose {
        unsafe { VERBOSE = true; }
    }
    match args.subcommand {
        ArgumentsCmd::Run(run) => main_run(run),
        ArgumentsCmd::Asm(asm) => main_asm(asm),
    }
}

fn main_run(args: Run) {
    if args.debug {
        unsafe { VERBOSE = true; }
    }

    let bytecode = load_bytecode(args.program.as_ref().map(|p| p.as_path()));
    let metadata = parse_metadata(&bytecode);
    if metadata.is_none() {
        verbose!("Could not read program metadata");
    }
    // TODO: Load and parse symbols file into debug state, use nearest symbol
    //       path when debugging.

    let config = EmulatorConfig {
        dimensions: args.dimensions().unwrap_or(ScreenDimensions::ZERO),
        fullscreen: args.fullscreen,
        scale: args.scale(),
        debug_palette: args.palette(),
        initial_transmission: None,
        encode_stdin: args.encode_stdin,
        encode_stdout: args.encode_stdout,
    };
    let phosphor = Phosphor::new();

    if phosphor.is_ok() && args.dimensions().is_some() {
        verbose!("Starting graphical emulator");
        let mut phosphor = phosphor.unwrap();
        let mut graphical = GraphicalEmulator::new(&config, args.debug);
        graphical.load_program(&bytecode);
        if let EmulatorSignal::Promote = graphical.run() {
            let program_name = match &metadata {
                Some(metadata) => match &metadata.name {
                    Some(name) => name.to_string(),
                    None => String::from("Bedrock"),
                }
                None => String::from("Bedrock"),
            };
            let window = WindowBuilder {
                dimensions: Some(graphical.dimensions()),
                size_bounds: Some(graphical.size_bounds()),
                fullscreen: graphical.fullscreen,
                scale: graphical.scale,
                title: Some(program_name),
                icon: None,
                cursor: None,
                program: Box::new(graphical),
            };

            phosphor.create_window(window);
            phosphor.run().unwrap();
        }
    } else {
        verbose!("Starting headless emulator");
        let mut headless = HeadlessEmulator::new(&config, args.debug);
        headless.load_program(&bytecode);
        headless.run(args.debug);
    };

    std::process::exit(0);
}

fn load_bytecode(path: Option<&Path>) -> Vec<u8> {
    // TODO: Etch file location into bytecode.
    if let Some(path) = path {
        if let Ok(bytecode) = load_bytecode_from_file(path) {
            verbose!("Loaded program from {path:?} ({} bytes)", bytecode.len());
            return bytecode;
        } else if let Some((bytecode, path)) = load_bytecode_from_bedrock_path(path) {
            verbose!("Loaded program from {path:?} ({} bytes)", bytecode.len());
            return bytecode;
        } else {
            eprintln!("Could not read program from {path:?}, exiting");
            std::process::exit(1);
        }
    } else {
        verbose!("Reading program from standard input...");
        if let Ok(bytecode) = load_bytecode_from_stdin() {
            verbose!("Loaded program from standard input ({} bytes)", bytecode.len());
            return bytecode;
        } else {
            eprintln!("Could not read program from standard input, exiting");
            std::process::exit(1);
        }
    }
}

/// Attempt to load bytecode from a file path.
fn load_bytecode_from_file(path: &Path) -> Result<Vec<u8>, std::io::Error> {
    load_bytecode_from_readable_source(std::fs::File::open(path)?)
}

/// Attempt to load bytecode from a directory in the BEDROCK_PATH environment variable.
fn load_bytecode_from_bedrock_path(path: &Path) -> Option<(Vec<u8>, PathBuf)> {
    if path.is_relative() && path.components().count() == 1 {
        for base_path in std::env::var("BEDROCK_PATH").ok()?.split(':') {
            let mut base_path = PathBuf::from(base_path);
            if !base_path.is_absolute() { continue; }
            base_path.push(path);
            verbose!("Attempting to load program from {base_path:?}");
            if let Ok(bytecode) = load_bytecode_from_file(&base_path) {
                return Some((bytecode, base_path));
            }
            if path.extension().is_some() { continue; }
            base_path.set_extension("br");
            verbose!("Attempting to load program from {base_path:?}");
            if let Ok(bytecode) = load_bytecode_from_file(&base_path) {
                return Some((bytecode, base_path));
            }
        }
    }
    return None;
}

/// Attempt to load bytecode from standard input.
fn load_bytecode_from_stdin() -> Result<Vec<u8>, std::io::Error> {
    load_bytecode_from_readable_source(std::io::stdin())
}

/// Attempt to load bytecode from a source that implements std::io::Read.
fn load_bytecode_from_readable_source(source: impl Read) -> Result<Vec<u8>, std::io::Error> {
    let mut bytecode = Vec::<u8>::new();
    source.take(65536).read_to_end(&mut bytecode)?;
    return Ok(bytecode);
}


fn main_asm(args: Asm) {
    // -----------------------------------------------------------------------
    // 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 && !args.check {
        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);

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

    if !args.check {
        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! {
    /// Integrated Bedrock assembler and emulator.
    ///
    /// The arguments and options shown are for running a Bedrock program.
    /// To see the documentation for the assembler, run `br asm --help`
    ///
    /// Usage:
    ///   To load a Bedrock program from a file, run `br [path]`, where
    ///   [path] is the path of an assembled Bedrock program.
    ///
    ///   To load a Bedrock program from piped input, run `[command] | br`,
    ///   where [command] is a command that generates Bedrock bytecode.
    ///
    ///   To assemble a Bedrock program from a source file and write to an
    ///   output file, run `br asm [source] [output]`, where [source] is the
    ///   path of the source file and [output] is the path to write to.
    ///
    ///   To assemble and run a Bedrock program without saving to a file,
    ///   run `br asm [source] | br`, where [source] is the path of the
    ///   source file.
    ///
    /// Environment variables:
    ///   BEDROCK_PATH
    ///     A list of colon-separated paths which will be searched to find
    ///     a Bedrock program when the program doesn't exist in the current
    ///     directory. This allows the user to run frequently-used programs
    ///     from any directory.
    ///   BEDROCK_LIBS
    ///     A list of colon-separated paths which will be searched to find
    ///     Bedrock source code files to use as libraries when assembling a
    ///     Bedrock program. If a library file resolves an unresolved symbol
    ///     in the program being assembled, the library file will be merged
    ///     into the program.
    cmd arguments {
        /// Print additional debug information
        optional -v, --verbose
        /// Print the assembler version and exit
        optional --version

        /// Run a Bedrock program (this is the default command)
        default cmd run {
            /// Path to a Bedrock program to run
            optional program: PathBuf

            /// Show debug information while the program is running
            optional -d, --debug
            /// Start the program in fullscreen mode (toggle with F11)
            optional -f, --fullscreen
            /// Set the initial window size in the format <width>x<height>
            optional -s, --screen size: ParsedDimensions
            /// Set the pixel size for the screen (change with F5/F6)
            optional -z, --zoom factor: u32
            /// Set a debug colour palette in the format <rgb>,... (toggle with F2)
            optional --palette colours: ParsedPalette
            /// Encode standard input
            optional -i, --encode-stdin
            /// Encode standard output
            optional -o, --encode-stdout
        }

        /// Assemble a Bedrock program from source code
        cmd asm {
            /// 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
        }
    }
}


impl Run {
    pub fn dimensions(&self) -> Option<geometry::Dimensions<u16>> {
        let size = match &self.screen {
            Some(parsed) => geometry::Dimensions::new(parsed.width, parsed.height),
            None => DEFAULT_SCREEN_SIZE,
        };
        match size.is_zero() {
            true => None,
            false => Some(size),
        }
    }

    pub fn scale(&self) -> u32 {
        std::cmp::max(1, self.zoom.unwrap_or(1))
    }

    pub fn palette(&self) -> Option<[Colour; 16]> {
        self.palette.as_ref().map(|p| p.palette)
    }
}


#[derive(Debug)]
struct ParsedDimensions { width: u16, height: u16 }
impl std::str::FromStr for ParsedDimensions {
    type Err = ParsedDimensionsError;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        if s == "none" {
            Ok( Self { width: 0, height: 0 } )
        } else {
            let (w_str, h_str) = s.split_once('x').ok_or(ParsedDimensionsError)?;
            Ok( Self {
                width:  u16::from_str(w_str).or(Err(ParsedDimensionsError))?,
                height: u16::from_str(h_str).or(Err(ParsedDimensionsError))?,
            } )
        }
    }
}

pub struct ParsedDimensionsError;
impl std::fmt::Display for ParsedDimensionsError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
        f.write_str("try giving a value like 800x600")
    }
}


#[derive(Debug)]
pub struct ParsedPalette { palette: [Colour; 16] }
impl std::str::FromStr for ParsedPalette {
    type Err = ParsedPaletteError;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        fn decode_ascii_hex_digit(ascii: u8) -> Result<u8, ParsedPaletteError> {
            match ascii {
                b'0'..=b'9' => Ok(ascii - b'0'),
                b'a'..=b'f' => Ok(ascii - b'a' + 10),
                b'A'..=b'F' => Ok(ascii - b'A' + 10),
                _ => { Err(ParsedPaletteError)}
            }
        }
        let mut colours = Vec::new();
        for token in s.split(',') {
            let mut bytes = token.bytes();
            if bytes.len() != 3 { return Err(ParsedPaletteError); }
            let r = decode_ascii_hex_digit(bytes.next().unwrap())?;
            let g = decode_ascii_hex_digit(bytes.next().unwrap())?;
            let b = decode_ascii_hex_digit(bytes.next().unwrap())?;
            colours.push(Colour::from_rgb(r*17, g*17, b*17));
        }
        let c = &colours;
        let palette = match colours.len() {
            2 => [ c[0],  c[1],  c[0],  c[1],  c[0],  c[1],  c[0],  c[1],
                   c[0],  c[1],  c[0],  c[1],  c[0],  c[1],  c[0],  c[1] ],
            3 => [ c[0],  c[1],  c[0],  c[2],  c[0],  c[1],  c[0],  c[2],
                   c[0],  c[1],  c[0],  c[2],  c[0],  c[1],  c[0],  c[2] ],
            4 => [ c[0],  c[1],  c[2],  c[3],  c[0],  c[1],  c[2],  c[3],
                   c[0],  c[1],  c[2],  c[3],  c[0],  c[1],  c[2],  c[3] ],
            8 => [ c[0],  c[1],  c[2],  c[3],  c[4],  c[5],  c[6],  c[7],
                   c[0],  c[1],  c[2],  c[3],  c[4],  c[5],  c[6],  c[7] ],
           16 => [ c[0],  c[1],  c[2],  c[3],  c[4],  c[5],  c[6],  c[7],
                   c[8],  c[9], c[10], c[11], c[12], c[13], c[14], c[15] ],
            _ => return Err(ParsedPaletteError),
        };
        Ok( Self { palette })
    }
}

pub struct ParsedPaletteError;
impl std::fmt::Display for ParsedPaletteError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
        f.write_str("try giving a value like 000,fff,880,808")
    }
}