use hase::{assemble, debug, run, ExecutionResult};
use is_terminal::is_terminal;
use std::{
    fs,
    io::{self, stdin, BufRead, Write},
    iter::Peekable,
    path::PathBuf,
};

enum RunMode {
    Normal,
    ToEnd,
    Debug,
}

fn main() {
    if let Err(err) = handle_args() {
        eprintln!("{err}");
        std::process::exit(1)
    }
}

/// Run a program:
/// $ hase debug program.hase
/// $ hase to-end program.hase
/// $ hase program.hase
/// $ hase program.rom
/// $ echo '=1' | hase
/// $ hase <<< '=1'
/// $ hase -
///
/// Assemble a program:
/// $ hase asm program.hase
/// $ hase asm program.hase program.rom
/// $ echo '=1' | hase asm program.rom
/// $ hase asm program.rom<<< '=1'
/// $ hase asm -
/// $ hase asm - program.rom
/// $ hase asm program.rom
fn handle_args() -> Result<(), String> {
    let mut args = std::env::args().peekable();
    let path = args.next();
    let mode = loop {
        if args.peek().map(|v| v == "debug").unwrap_or_default() {
            args.next();
            break RunMode::Debug;
        } else if args.peek().map(|v| v == "to-end").unwrap_or_default() {
            args.next();
            break RunMode::ToEnd;
        } else {
            break RunMode::Normal;
        }
    };
    if let Some(arg) = args.next() {
        if arg == "-" {
            run_stdin(mode)
        } else if arg == "asm" {
            handle_assembly(args)
        } else {
            handle_running(arg, mode)
        }
    } else if !is_terminal(&stdin()) {
        run_stdin(mode)
    } else {
        return Err(format!(
            "Hase, a simple paper computing language.\nUsage: {} [debug|asm|to-end] (<file.hase> | <file.o> | -)\n\nRun a program:\t\thase file.hase\nCompile a program:\thase asm file.hase > file.o\nDebug a program:\thase debug file.hase\nRun stdin:\t\thase -",
            path.to_owned().unwrap()
        ));
    }
}

fn read_file(path: &PathBuf) -> Result<String, String> {
    fs::read_to_string(path).map_err(|e| {
        format!(
            "Couldn't read {}: {e}",
            path.as_os_str().to_str().unwrap_or_default()
        )
    })
}

fn handle_running(arg: String, mode: RunMode) -> Result<(), String> {
    let path = PathBuf::from(arg);
    match path.extension().map(|e| e.to_str().unwrap()) {
        Some("hase") => handle_running_source(
            path.file_name().unwrap().to_str().unwrap(),
            read_file(&path)?,
            mode,
        ),
        Some("o") => match mode {
            RunMode::Debug => Err("Can't debug compiled code, did you mean to-end?".to_owned()),
            other => run_bytecode(
                fs::read(&path).map_err(|e| {
                    format!(
                        "Couldn't read {}: {e}",
                        path.as_os_str().to_str().unwrap_or_default()
                    )
                })?,
                matches!(other, RunMode::ToEnd),
            ),
        },
        _ => Err("Expected .hase or .o file".to_string()),
    }
}

fn handle_running_source(name: &str, source: String, mode: RunMode) -> Result<(), String> {
    match mode {
        RunMode::Debug => debug(&source),
        other => run_bytecode(
            assemble(&source).map_err(|e| format!("{}:{e}", name))?,
            matches!(other, RunMode::ToEnd),
        ),
    }
}

fn handle_assembly(mut args: Peekable<std::env::Args>) -> Result<(), String> {
    let Some(source) = args.next() else {
        return print_bytecode(assemble_stdin()?);
    };
    let path = PathBuf::from(&source);
    if source == "-" {
        return print_or_write(&mut args, assemble_stdin()?);
    }
    match path.extension().map(|e| e.to_str().unwrap()) {
        Some("hase") => print_or_write(
            &mut args,
            assemble(
                &fs::read_to_string(&source)
                    .map_err(|e| format!("Couldn't read file {source}: {e}"))?,
            )
            .map_err(|e| format!("{source}:{e}"))?,
        ),
        Some("o") => {
            todo!()
        }
        _ => Err("Expected .hase or .rom file or stdin".to_string()),
    }
}

fn print_or_write(args: &mut Peekable<std::env::Args>, bytecode: Vec<u8>) -> Result<(), String> {
    match args.next() {
        Some(out) => {
            fs::write(&out, &bytecode).map_err(|e| format!("Couldn't write to file {out}: {e}"))
        }
        None => print_bytecode(bytecode),
    }
}

fn print_bytecode(bytecode: Vec<u8>) -> Result<(), String> {
    io::stdout()
        .lock()
        .write(&bytecode)
        .map_err(|e| format!("error writing to stdout: {e}"))
        .map(|_| ())
}

fn run_stdin(mode: RunMode) -> Result<(), String> {
    match io::stdin()
        .lock()
        .lines()
        .collect::<Result<Vec<String>, _>>()
    {
        Ok(content) => handle_running_source("stdin", content.join("\n"), mode),
        Err(err) => Err(format!("Couldn't read stdin: {err}")),
    }
}

fn run_bytecode(bytecode: Vec<u8>, debug: bool) -> Result<(), String> {
    match run(bytecode, debug) {
        Ok(ExecutionResult::Number(val)) => println!("{val}"),
        Ok(ExecutionResult::Full(registers)) => println!("{registers:?}"),
        Ok(ExecutionResult::List(list)) => println!("{list:?}"),
        Err(e) => return Err(e),
    };
    Ok(())
}

fn read_stdin() -> Result<String, String> {
    Ok(io::stdin()
        .lock()
        .lines()
        .collect::<Result<Vec<String>, _>>()
        .map_err(|e| format!("Couldn't read stdin: {e}"))?
        .join("\n"))
}

fn assemble_stdin() -> Result<Vec<u8>, String> {
    assemble(&read_stdin()?).map_err(|e| format!("stdin:{e}"))
}