use std::{
    fs::File,
    io::{self, BufReader},
    path::{Path, PathBuf},
    process::ExitCode,
    rc::Rc,
    str::FromStr,
};

use clap::Parser;
use lysp::{
    compile::{CompileOptions, syntax::ParseError},
    error::{MachineError, MachineErrorAt},
    read::{InteractiveReader, ModuleReader, read},
    util::Either,
    vm::{
        env::Environment,
        machine::Machine,
        prelude,
        value::{ClosureValue, Value},
    },
};

#[derive(Debug, thiserror::Error)]
#[error("{0}")]
enum Error {
    Machine(#[from] MachineErrorAt),
    Io(#[from] io::Error),
    #[error("Error already printed")]
    Printed,
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Trace {
    Compile,
    Execute,
    Call,
    Return,
    Stack,
    Macro,
}

impl FromStr for Trace {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "compile" => Ok(Self::Compile),
            "execute" => Ok(Self::Execute),
            "call" => Ok(Self::Call),
            "return" => Ok(Self::Return),
            "stack" => Ok(Self::Stack),
            "macro" => Ok(Self::Macro),
            _ => Err(format!("Unknown trace flag: {s:?}")),
        }
    }
}

#[derive(Debug, Parser)]
struct Args {
    #[clap(
        short = 'T',
        long = "trace",
        help = "Enable tracing of execution/compilation steps"
    )]
    trace: Vec<Trace>,
    module: Option<PathBuf>,
    arguments: Vec<String>,
}

fn print_syntax_errors(errors: &[ParseError]) {
    if errors.len() > 1 {
        eprintln!("Syntax errors:");
    } else {
        eprintln!("Syntax error:");
    }
    eprintln!();
    for error in errors {
        let ParseError { input, error } = error;
        eprintln!(" * In expression:");
        eprintln!();
        eprintln!("     {input}");
        eprintln!();
        eprintln!("  :: {error}");
    }
}

fn handle_eval_error(value: Option<&Value>, input: MachineErrorAt) -> Error {
    if let Some(value) = value {
        eprintln!("Error in expression:");
        eprintln!();
        eprintln!("  {value}");
        eprintln!();
    }
    match input.error {
        MachineError::Compile(error) => {
            eprintln!("Compilation error:");
            eprintln!();
            eprintln!(":: {error}");
        }
        MachineError::Read(error) => {
            eprintln!("Syntax error:");
            eprintln!();
            eprintln!(":: {error}");
        }
        error => {
            if let Some(location) = input.location {
                eprintln!("At location {location}:");
                location.disassemble_chunk(location.offset, 5, 1, true);
            }
            eprintln!(":: {error}");
        }
    }
    Error::Printed
}

fn handle_module_error(input: Either<MachineErrorAt, Vec<ParseError>>) -> Error {
    match input {
        Either::Left(error) => handle_eval_error(None, error),
        Either::Right(errors) => {
            print_syntax_errors(&errors);
            Error::Printed
        }
    }
}

fn eval(
    options: &CompileOptions,
    vm: &mut Machine,
    env: &Rc<Environment>,
    value: Value,
) -> Option<Value> {
    let result = vm.evaluate_value(
        options.clone(),
        Some("interactive".into()),
        env,
        value.clone(),
    );
    match result {
        Ok(r) => Some(r),
        Err(error) => {
            handle_eval_error(Some(&value), error);
            None
        }
    }
}

fn run_interactive(
    compile_options: &CompileOptions,
    vm: &mut Machine,
    env: &Rc<Environment>,
) -> Result<(), Error> {
    let mut reader = InteractiveReader::new("> ", ">> ");

    loop {
        let value = match read(&mut reader, vm, env) {
            Ok(Some(value)) => value,
            Ok(None) => break,
            Err(error) => {
                eprintln!("{error}");
                continue;
            }
        };
        if let Some(value) = eval(compile_options, vm, env, value) {
            println!("== {value}");
        } else {
            reader.reset();
        }
    }

    Ok(())
}

fn run_module<P: AsRef<Path>>(
    compile_options: &CompileOptions,
    vm: &mut Machine,
    env: &Rc<Environment>,
    path: P,
) -> Result<(), Error> {
    let path = path.as_ref();
    let name = format!("{}", path.display());
    let reader = BufReader::new(File::open(path)?);
    let module_reader = ModuleReader::new(reader, path, vm.trace_macros);
    let function = match module_reader.compile(Some(name.into()), compile_options, env) {
        Ok(function) => function,
        Err(error) => return Err(handle_module_error(error)),
    };
    let closure = ClosureValue {
        function,
        upvalues: vec![],
    };
    match vm.evaluate_closure(env, closure, 0) {
        Ok(_) => Ok(()),
        Err(error) => Err(handle_eval_error(None, error)),
    }
}

fn main() -> ExitCode {
    let args = Args::parse();
    let mut vm = Machine::default();
    let compile_options = CompileOptions {
        trace_compile: args.trace.contains(&Trace::Compile),
    };
    vm.trace_instructions = args.trace.contains(&Trace::Execute);
    vm.trace_calls = args.trace.contains(&Trace::Call);
    vm.trace_returns = args.trace.contains(&Trace::Return);
    vm.trace_stack = args.trace.contains(&Trace::Stack);
    vm.trace_macros = args.trace.contains(&Trace::Macro);
    let env = Rc::new(Environment::default());
    prelude::load(&env);
    let mut arguments = vec![];
    if let Some(script) = args.module.as_ref() {
        arguments.push(format!("{}", script.display()));
    }
    arguments.extend(args.arguments);
    env.set_global_value(
        "*args*",
        Value::list_or_nil(arguments.into_iter().map(|arg| Value::String(arg.into()))),
    );
    let result = match args.module.as_ref() {
        Some(module) => run_module(&compile_options, &mut vm, &env, module),
        None => run_interactive(&compile_options, &mut vm, &env),
    };
    match result {
        Ok(()) => ExitCode::SUCCESS,
        Err(Error::Printed) => ExitCode::FAILURE,
        Err(error) => {
            eprintln!("{error}");
            ExitCode::FAILURE
        }
    }
}
