use std::{fs::read_to_string, path::PathBuf, process::exit};

use codespan_reporting::diagnostic::{Diagnostic, Label};
use codespan_reporting::files::SimpleFile;
use codespan_reporting::term;
use codespan_reporting::term::termcolor::{ColorChoice, StandardStream};

use crate::frontend::parser::Parser;
use crate::frontend::sema::Sema;
use crate::middle::builder::MirBuilder;
use crate::middle::dce::eliminate_dead_code;
use crate::middle::fold::fold_constants;
use clap::Parser as ClapParser;

pub mod backend;
pub mod frontend;
pub mod middle;

use crate::backend::cranelift::CraneliftBackend;

#[derive(ClapParser)]
#[command(version, about, long_about = None)]
struct Cli {
    /// The input source file to compile
    input: String,

    /// The output file path
    #[arg(short, long)]
    output: Option<String>,

    /// Emit Cranelift IR instead of an object file
    #[arg(long)]
    emit_ir: bool,
}

fn main() {
    let cli = Cli::parse();

    let content = read_to_string(&cli.input).unwrap_or_else(|err| {
        eprintln!("error: failed to read source file ({:?})", err);
        exit(1);
    });

    let writer = StandardStream::stderr(ColorChoice::Always);
    let config = term::Config::default();
    let file = SimpleFile::new(&cli.input, &content);

    let mut parser = Parser::new(&content);
    let module = parser.parse_module();

    if let Some(errors) = parser.errors() {
        for error in errors {
            let diagnostic = Diagnostic::error()
                .with_message(error.message)
                .with_label(Label::primary((), error.span));

            term::emit_to_write_style(&mut writer.lock(), &config, &file, &diagnostic).unwrap();
        }

        exit(1);
    }

    let mut sema = Sema::new();
    let typed_module = sema.analyze_module(&module);

    if let Some(errors) = sema.errors() {
        for error in errors {
            let diagnostic = Diagnostic::error()
                .with_message(error.message)
                .with_label(Label::primary((), error.span));

            term::emit_to_write_style(&mut writer.lock(), &config, &file, &diagnostic).unwrap();
        }

        exit(1);
    }

    let mut mir_module = MirBuilder::build(&typed_module);
    fold_constants(&mut mir_module);
    let warnings = eliminate_dead_code(&mut mir_module);

    for warning in warnings {
        let diagnostic = Diagnostic::warning()
            .with_message(warning.message)
            .with_label(Label::primary((), warning.span));

        term::emit_to_write_style(&mut writer.lock(), &config, &file, &diagnostic).unwrap();
    }

    let backend = CraneliftBackend::new();
    let (ir, obj_bytes) = backend.compile_module(&mir_module);

    if cli.emit_ir {
        println!("{}", ir);
    } else {
        let output_path = cli.output.unwrap_or_else(|| {
            PathBuf::from(&cli.input)
                .with_extension("o")
                .to_string_lossy()
                .into_owned()
        });

        std::fs::write(&output_path, obj_bytes).unwrap_or_else(|err| {
            eprintln!("error: failed to write object file: {:?}", err);
            exit(1);
        });

        println!("Generated object file: {}", output_path);
    }
}
