mod bytecoding;
mod check;
mod diagnostic;
mod discover;
mod format;
mod intermediate;
mod parser;
mod project;
mod resolver;
mod sql;
mod utils;

type Result<T, E = diagnostic::Diagnostic> = core::result::Result<T, E>;

pub mod codespan;
pub mod error;
pub mod pr;
pub mod printer;

pub use bytecoding::compile_program as bytecode_program;
pub use check::{CheckParams, check, check_overlay};
pub use codespan::Span;
pub use discover::{DiscoverParams, discover};
pub use format::format;
pub use intermediate::inline;
pub use lutra_bin::{ir, rr};
pub use project::{Project, SourceTree};

#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
pub enum ProgramFormat {
    SqlPg,
    SqlDuckdb,
    BytecodeLt,
}

pub fn compile(
    project: &Project,
    program: &str,
    name_hint: Option<&str>,
    format: ProgramFormat,
) -> Result<(rr::Program, rr::ProgramType), error::Error> {
    // resolve
    let program_pr = self::check::check_overlay(project, program, name_hint)?;

    // lower
    let program_ir = intermediate::lowerer::lower_expr(project, &program_pr);
    tracing::debug!("ir:\n{}\n", lutra_bin::ir::print(&program_ir));

    // intermediate optimizations
    let program_ir = intermediate::inline(program_ir);
    tracing::debug!("ir (inlined):\n{}\n", lutra_bin::ir::print(&program_ir));
    let program_ir = intermediate::layouter::on_program(program_ir);

    // backend (sql or bytecode)
    let program = match format {
        ProgramFormat::SqlPg => rr::Program::SqlPostgres(Box::new(sql::compile_ir(
            &program_ir,
            sql::Dialect::Postgres,
        ))),
        ProgramFormat::SqlDuckdb => {
            rr::Program::SqlDuckDB(Box::new(sql::compile_ir(&program_ir, sql::Dialect::DuckDB)))
        }
        ProgramFormat::BytecodeLt => {
            rr::Program::BytecodeLt(bytecoding::compile_program(program_ir.clone()))
        }
    };

    // construct the program ty
    let ir_func_ty = program_ir.main.ty.kind.into_function().unwrap();
    let ty = rr::ProgramType {
        input: ir_func_ty.params.into_iter().next().unwrap(),
        output: ir_func_ty.body,
        defs: program_ir.defs,
    };

    Ok((program, ty))
}

pub fn project_to_types(project: &Project) -> ir::Module {
    let module = intermediate::lowerer::lower_type_defs(project);

    intermediate::layouter::on_root_module(module)
}

/// Internal implementation detail, do not use.
// Only exposed because I don't want to maintain a separate lexer for IR parser.
pub mod _lexer {
    pub use crate::diagnostic::Diagnostic;
    pub use crate::parser::lexer::{Token, TokenKind};

    pub use crate::parser::lexer::lex_source_recovery as lex;
}

#[track_caller]
pub fn _test_compile_ty(ty_source: &str) -> ir::Ty {
    let source = format!("type t: {ty_source}");

    let source = SourceTree::single("".into(), source);
    let project = check(source, CheckParams {}).unwrap_or_else(|e| panic!("{e}"));

    let module = project_to_types(&project);

    let item = module.decls.into_iter().next().unwrap();
    assert_eq!(item.name, "t");
    let ir::Decl::Type(mut ty) = item.decl else {
        panic!()
    };

    ty.name = None;
    ty
}

pub fn _test_compile_main(source: &str) -> Result<ir::Program, error::Error> {
    let source = SourceTree::single("".into(), source.to_string());
    let project = check(source, CheckParams {})?;

    let main = check_overlay(&project, "main", None)?;
    let program = intermediate::lowerer::lower_expr(&project, &main);
    Ok(intermediate::layouter::on_program(program))
}
