//! Pure type-synthesis and IR-shaping helpers.
//!
//! Free functions extracted from `lib.rs` during the 18g tidy split.
//! None of these touch the `Emitter` state — they map AILang `Type`s
//! to LLVM type strings, mangling descriptors, or built-in op
//! signatures. Submodule access to the parent module's private
//! `Result`, `CodegenError`, and `FnSig` works through normal Rust
//! visibility (a submodule sees its parent's private items).

use ailang_core::ast::*;

use super::{CodegenError, FnSig, Result};

pub(crate) fn llvm_type(t: &Type) -> Result<String> {
    match t {
        Type::Con { name, .. } => match name.as_str() {
            "Int" => Ok("i64".into()),
            "Bool" => Ok("i1".into()),
            "Unit" => Ok("i8".into()),
            "Str" => Ok("ptr".into()),
            "Float" => Ok("double".into()),
            // All other type names are treated as ADT (boxed).
            // If the typechecker didn't reject this earlier, it's
            // intentional — otherwise `ptr` would mask a wrong value.
            _ => Ok("ptr".into()),
        },
        // Function values (Iter 7): all fn-pointers are opaque `ptr`
        // at the LLVM level. The actual signature travels via the
        // emitter's `ssa_fn_sigs` sidetable.
        Type::Fn { .. } => Ok("ptr".into()),
        // Iter 13b: an unresolved rigid `Type::Var` reaching codegen is
        // a substitution bug. Earlier this silently lowered as `ptr`
        // (via the ADT fallback) and produced garbage IR; failing loudly
        // here surfaces the bug in the test suite.
        Type::Var { name } => Err(CodegenError::UnsupportedType(format!(
            "unresolved type var `{name}` in codegen"
        ))),
        other => Err(CodegenError::UnsupportedType(
            ailang_core::pretty::type_to_string(other),
        )),
    }
}

/// Builds an `FnSig` (LLVM types only) from an AILang `Type::Fn`.
/// Returns `None` for non-function types or if any param/ret type fails
/// to lower (e.g. a residual `Type::Var` or `Forall` that the typechecker
/// would reject before us).
pub(crate) fn fn_sig_from_type(t: &Type) -> Option<FnSig> {
    if let Type::Fn { params, ret, .. } = t {
        let p: Result<Vec<String>> = params.iter().map(llvm_type).collect();
        let r = llvm_type(ret);
        if let (Ok(p), Ok(r)) = (p, r) {
            return Some(FnSig { params: p, ret: r });
        }
    }
    None
}

/// Iter 12b: AILang type of a builtin operator. Used by
/// `synth_arg_type` for arg-type inference at polymorphic call sites.
/// Mirrors what the typechecker installs in its env via `builtins`.
pub(crate) fn builtin_ail_type(name: &str) -> Option<Type> {
    // Iter 22-floats.3: same widening as `crates/ailang-check/src/
    // builtins.rs` — `+`/`-`/`*`/`/` and `!=`/`<`/`<=`/`>`/`>=` are
    // polymorphic. `%` stays monomorphic-Int. Codegen lowering for
    // these ops still goes through `builtin_binop` (Int-only) in
    // iter 3; iter 4 converts that to type-dispatched.
    let poly_a_a_to_a = || Type::Forall {
        vars: vec!["a".into()],
        constraints: vec![],
        body: Box::new(Type::Fn {
            params: vec![
                Type::Var { name: "a".into() },
                Type::Var { name: "a".into() },
            ],
            ret: Box::new(Type::Var { name: "a".into() }),
            effects: vec![],
            param_modes: vec![],
            ret_mode: ParamMode::Implicit,
        }),
    };
    let poly_a_a_to_bool = || Type::Forall {
        vars: vec!["a".into()],
        constraints: vec![],
        body: Box::new(Type::Fn {
            params: vec![
                Type::Var { name: "a".into() },
                Type::Var { name: "a".into() },
            ],
            ret: Box::new(Type::bool_()),
            effects: vec![],
            param_modes: vec![],
            ret_mode: ParamMode::Implicit,
        }),
    };
    let int_int_int = || Type::Fn {
        params: vec![Type::int(), Type::int()],
        ret: Box::new(Type::int()),
        effects: vec![],
        param_modes: vec![],
        ret_mode: ParamMode::Implicit,
    };
    Some(match name {
        "+" | "-" | "*" | "/" => poly_a_a_to_a(),
        "%" => int_int_int(),
        "!=" | "<" | "<=" | ">" | ">=" => poly_a_a_to_bool(),
        // Iter 16e: `==` is polymorphic — `forall a. (a, a) -> Bool`.
        // The mono pipeline asks `synth_arg_type` for the actual arg
        // types at the call site; `lower_app` then dispatches to the
        // right LLVM instruction (icmp eq i64 / i1, @strcmp, or
        // constant i1 1) on those resolved types.
        "==" => Type::Forall {
            vars: vec!["a".into()],
            constraints: vec![],
            body: Box::new(Type::Fn {
                params: vec![
                    Type::Var { name: "a".into() },
                    Type::Var { name: "a".into() },
                ],
                ret: Box::new(Type::bool_()),
                effects: vec![],
                param_modes: vec![],
                ret_mode: ParamMode::Implicit,
            }),
        },
        "not" => Type::Fn {
            params: vec![Type::bool_()],
            ret: Box::new(Type::bool_()),
            effects: vec![],
            param_modes: vec![],
            ret_mode: ParamMode::Implicit,
        },
        // Iter 16d: `__unreachable__` is the polymorphic bottom value
        // (`forall a. a`). Mirrors the typechecker's `builtins::install`.
        "__unreachable__" => Type::Forall {
            vars: vec!["a".into()],
            constraints: vec![],
            body: Box::new(Type::Var { name: "a".into() }),
        },
        // Iter 22-floats.3: Float-conversion and inspection builtins.
        // Same lockstep with `crates/ailang-check/src/builtins.rs`.
        "neg" => Type::Forall {
            vars: vec!["a".into()],
            constraints: vec![],
            body: Box::new(Type::Fn {
                params: vec![Type::Var { name: "a".into() }],
                ret: Box::new(Type::Var { name: "a".into() }),
                effects: vec![],
                param_modes: vec![],
                ret_mode: ParamMode::Implicit,
            }),
        },
        "int_to_float" => Type::Fn {
            params: vec![Type::int()],
            ret: Box::new(Type::float()),
            effects: vec![],
            param_modes: vec![],
            ret_mode: ParamMode::Implicit,
        },
        "float_to_int_truncate" => Type::Fn {
            params: vec![Type::float()],
            ret: Box::new(Type::int()),
            effects: vec![],
            param_modes: vec![],
            ret_mode: ParamMode::Implicit,
        },
        "float_to_str" => Type::Fn {
            params: vec![Type::float()],
            ret: Box::new(Type::str_()),
            effects: vec![],
            param_modes: vec![],
            ret_mode: ParamMode::Implicit,
        },
        "is_nan" => Type::Fn {
            params: vec![Type::float()],
            ret: Box::new(Type::bool_()),
            effects: vec![],
            param_modes: vec![],
            ret_mode: ParamMode::Implicit,
        },
        // Iter 22-floats.3: Float bit-pattern constants. Bare values,
        // not fns — referenced via `Term::Var`. Mirrors the
        // typechecker's `builtins::install`. Codegen emits LLVM hex-
        // float literals at the use site in iter 4.
        "nan" | "inf" | "neg_inf" => Type::float(),
        _ => return None,
    })
}

/// Iter 12b: AILang return type of a built-in effect op. The op's
/// param signature is irrelevant here since we only consume the ret.
pub(crate) fn builtin_effect_op_ret(op: &str) -> Option<Type> {
    Some(match op {
        "io/print_int" | "io/print_bool" | "io/print_str" | "io/print_float" => Type::unit(),
        _ => return None,
    })
}

/// Iter 12b: a stable, identifier-safe descriptor for a `Type`.
/// Maps `Int → I`, `Bool → B`, `Unit → U`, `Str → S`, ADT name `Foo →
/// FFoo`, fn → `F<params...>R<ret>` (no recursion guard since types in
/// the MVP are non-recursive at the type level).
pub(crate) fn type_descriptor(t: &Type) -> String {
    match t {
        Type::Con { name, args } => {
            let head = match name.as_str() {
                "Int" => "I".into(),
                "Bool" => "B".into(),
                "Unit" => "U".into(),
                "Str" => "S".into(),
                "Float" => "Fl".into(),
                other => format!("F{other}"),
            };
            if args.is_empty() {
                head
            } else {
                // Iter 13a: parameterised ADTs get their type-arg
                // descriptors appended, e.g. `FBox` of `Int` → `FBox_I`.
                let mut s = head;
                for a in args {
                    s.push('_');
                    s.push_str(&type_descriptor(a));
                }
                s
            }
        }
        Type::Fn { params, ret, .. } => {
            let mut s = String::from("Fn");
            for p in params {
                s.push('_');
                s.push_str(&type_descriptor(p));
            }
            s.push_str("__r_");
            s.push_str(&type_descriptor(ret));
            s
        }
        Type::Var { name } => format!("V{name}"),
        Type::Forall { .. } => "FORALL".into(),
    }
}

/// Floats iter 4.2: arithmetic / comparison ops are type-dispatched
/// over `{Int, Float}`. Caller (`lower_app`) resolves the arg type
/// via `synth_arg_type` and passes it here. Returns the
/// `(instruction, operand_llvm_type, result_llvm_type)` triple to
/// emit. `%` stays Int-only.
///
/// The triple carries operand and result types separately because
/// comparison ops produce `i1` regardless of operand width
/// (e.g. `icmp slt i64 ..., ... -> i1`); arithmetic produces the
/// same type as its operands. Keeping both in the table localises
/// the comparison-vs-arithmetic distinction here — the caller no
/// longer has to second-guess which arm fired.
///
/// Comparison-op Int arms are kept here in iter 4.2 to preserve
/// the no-regression invariant — pre-iter-4 codegen routed
/// `<`/`<=`/`>`/`>=`/`!=` through the same Int-only `builtin_binop`
/// table. Iter 4.3 adds the Float arms (`("fcmp olt", "double", "i1")`
/// and friends).
pub(crate) fn builtin_binop_typed(
    name: &str,
    arg_ty: &Type,
) -> Option<(&'static str, &'static str, &'static str)> {
    let is_int = matches!(arg_ty, Type::Con { name, .. } if name == "Int");
    let is_float = matches!(arg_ty, Type::Con { name, .. } if name == "Float");
    match (name, is_int, is_float) {
        ("+", true, _) => Some(("add", "i64", "i64")),
        ("+", _, true) => Some(("fadd", "double", "double")),
        ("-", true, _) => Some(("sub", "i64", "i64")),
        ("-", _, true) => Some(("fsub", "double", "double")),
        ("*", true, _) => Some(("mul", "i64", "i64")),
        ("*", _, true) => Some(("fmul", "double", "double")),
        ("/", true, _) => Some(("sdiv", "i64", "i64")),
        ("/", _, true) => Some(("fdiv", "double", "double")),
        ("%", true, _) => Some(("srem", "i64", "i64")),
        // Comparison ops: operand types differ, result is always `i1`.
        // Int-arm comparisons preserved from pre-iter-4 `builtin_binop`;
        // Float arms land in iter 4.3.
        ("!=", true, _) => Some(("icmp ne", "i64", "i1")),
        ("<", true, _) => Some(("icmp slt", "i64", "i1")),
        ("<=", true, _) => Some(("icmp sle", "i64", "i1")),
        (">", true, _) => Some(("icmp sgt", "i64", "i1")),
        (">=", true, _) => Some(("icmp sge", "i64", "i1")),
        // Float comparison arms (Floats iter 4.3). Note: `!=` Float
        // uses `fcmp une` ("unordered or not equal") — NOT `one`
        // ("ordered and not equal"), which would return `false` for
        // `nan != nan` and violate IEEE / spec A5.
        ("!=", _, true) => Some(("fcmp une", "double", "i1")),
        ("<",  _, true) => Some(("fcmp olt", "double", "i1")),
        ("<=", _, true) => Some(("fcmp ole", "double", "i1")),
        (">",  _, true) => Some(("fcmp ogt", "double", "i1")),
        (">=", _, true) => Some(("fcmp oge", "double", "i1")),
        _ => None,
    }
}

pub(crate) fn c_byte_len(s: &str) -> usize {
    s.len() + 1 // + NUL terminator
}

/// Escapes a string for LLVM IR `c"..."`. All bytes outside
/// 0x20..0x7E are escaped as `\HH`; `"` and `\` likewise. Ends with `\00`.
pub(crate) fn default_triple() -> &'static str {
    // In the MVP we query the compile host. For cross-compilation this
    // would need to be configurable — not needed now.
    if cfg!(target_os = "linux") && cfg!(target_arch = "x86_64") {
        "x86_64-pc-linux-gnu"
    } else if cfg!(target_os = "macos") && cfg!(target_arch = "aarch64") {
        "arm64-apple-darwin"
    } else if cfg!(target_os = "macos") && cfg!(target_arch = "x86_64") {
        "x86_64-apple-darwin"
    } else if cfg!(target_arch = "aarch64") {
        "aarch64-unknown-linux-gnu"
    } else {
        "x86_64-pc-linux-gnu"
    }
}

pub(crate) fn ll_string_literal(s: &str) -> String {
    let mut out = String::new();
    for &b in s.as_bytes() {
        match b {
            b'"' => out.push_str("\\22"),
            b'\\' => out.push_str("\\5C"),
            0x20..=0x7E => out.push(b as char),
            _ => out.push_str(&format!("\\{:02X}", b)),
        }
    }
    out.push_str("\\00");
    out
}
