//! Code generation from HIR to JavaScript.
//!
//! # Design
//!
//! The codegen is deterministic: the same HIR always produces the same output.
//! This is critical for immutability verification via hashing.
//!
//! # References
//! - ECMA-262 (ES2022) - JavaScript language specification
//! - Lattner & Adve (2004) "LLVM: A Compilation Framework"

use crate::hir::*;
use std::fmt::Write;

/// Generate JavaScript code from a module.
///
/// # Panics
///
/// This function does not panic. All errors are handled gracefully.
pub fn generate(module: &JsModule) -> String {
    let mut output = String::new();

    // Write generation metadata as header comment
    if let Some(ref meta) = module.metadata {
        write_metadata_header(&mut output, meta);
    }

    // Write statements
    for stmt in &module.statements {
        write_stmt(&mut output, stmt, 0);
        output.push('\n');
    }

    output
}

fn write_metadata_header(out: &mut String, meta: &GenerationMetadata) {
    out.push_str("/**\n");
    out.push_str(" * @generated - This file is auto-generated.\n");
    out.push_str(" * Do not edit manually. To update, run:\n");
    out.push_str(" *\n");
    let _ = writeln!(out, " *   {}", meta.regenerate_cmd);
    out.push_str(" *\n");
    let _ = writeln!(out, " * Generated by: {} v{}", meta.tool, meta.version);
    let _ = writeln!(out, " * Timestamp: {}", meta.timestamp);
    let _ = writeln!(out, " * Input hash: {}", meta.input_hash);
    out.push_str(" */\n\n");
}

fn write_stmt(out: &mut String, stmt: &Stmt, indent: usize) {
    let pad = "    ".repeat(indent);

    match stmt {
        Stmt::Let { name, value } => {
            out.push_str(&pad);
            out.push_str("let ");
            out.push_str(name.as_str());
            out.push_str(" = ");
            write_expr(out, value);
            out.push(';');
        }
        Stmt::Const { name, value } => {
            out.push_str(&pad);
            out.push_str("const ");
            out.push_str(name.as_str());
            out.push_str(" = ");
            write_expr(out, value);
            out.push(';');
        }
        Stmt::Assign { name, value } => {
            out.push_str(&pad);
            out.push_str(name.as_str());
            out.push_str(" = ");
            write_expr(out, value);
            out.push(';');
        }
        Stmt::MemberAssign {
            object,
            member,
            value,
        } => {
            out.push_str(&pad);
            write_expr(out, object);
            out.push('.');
            out.push_str(member.as_str());
            out.push_str(" = ");
            write_expr(out, value);
            out.push(';');
        }
        Stmt::AddAssign { target, value } => {
            out.push_str(&pad);
            write_expr(out, target);
            out.push_str(" += ");
            write_expr(out, value);
            out.push(';');
        }
        Stmt::PostIncrement(expr) => {
            out.push_str(&pad);
            write_expr(out, expr);
            out.push_str("++;");
        }
        Stmt::Expr(expr) => {
            out.push_str(&pad);
            write_expr(out, expr);
            out.push(';');
        }
        Stmt::Return(None) => {
            out.push_str(&pad);
            out.push_str("return;");
        }
        Stmt::Return(Some(expr)) => {
            out.push_str(&pad);
            out.push_str("return ");
            write_expr(out, expr);
            out.push(';');
        }
        Stmt::If {
            condition,
            then_branch,
            else_branch,
        } => {
            out.push_str(&pad);
            out.push_str("if (");
            write_expr(out, condition);
            out.push_str(") {\n");
            for s in then_branch {
                write_stmt(out, s, indent + 1);
                out.push('\n');
            }
            out.push_str(&pad);
            out.push('}');
            if let Some(else_stmts) = else_branch {
                out.push_str(" else {\n");
                for s in else_stmts {
                    write_stmt(out, s, indent + 1);
                    out.push('\n');
                }
                out.push_str(&pad);
                out.push('}');
            }
        }
        Stmt::For {
            var,
            start,
            end,
            body,
        } => {
            out.push_str(&pad);
            out.push_str("for (let ");
            out.push_str(var.as_str());
            out.push_str(" = ");
            write_expr(out, start);
            out.push_str("; ");
            out.push_str(var.as_str());
            out.push_str(" < ");
            write_expr(out, end);
            out.push_str("; ");
            out.push_str(var.as_str());
            out.push_str("++) {\n");
            for s in body {
                write_stmt(out, s, indent + 1);
                out.push('\n');
            }
            out.push_str(&pad);
            out.push('}');
        }
        Stmt::While { condition, body } => {
            out.push_str(&pad);
            out.push_str("while (");
            write_expr(out, condition);
            out.push_str(") {\n");
            for s in body {
                write_stmt(out, s, indent + 1);
                out.push('\n');
            }
            out.push_str(&pad);
            out.push('}');
        }
        Stmt::TryCatch {
            body,
            catch_var,
            handler,
        } => {
            out.push_str(&pad);
            out.push_str("try {\n");
            for s in body {
                write_stmt(out, s, indent + 1);
                out.push('\n');
            }
            out.push_str(&pad);
            out.push_str("} catch (");
            out.push_str(catch_var.as_str());
            out.push_str(") {\n");
            for s in handler {
                write_stmt(out, s, indent + 1);
                out.push('\n');
            }
            out.push_str(&pad);
            out.push('}');
        }
        Stmt::Block(stmts) => {
            out.push_str(&pad);
            out.push_str("{\n");
            for s in stmts {
                write_stmt(out, s, indent + 1);
                out.push('\n');
            }
            out.push_str(&pad);
            out.push('}');
        }
        Stmt::Comment(text) => {
            out.push_str(&pad);
            out.push_str("// ");
            out.push_str(text);
        }
        Stmt::Class(class) => {
            write_class(out, class, indent);
        }
        Stmt::Switch(switch) => {
            write_switch(out, switch, indent);
        }
        Stmt::OnMessage(body) => {
            out.push_str(&pad);
            out.push_str("self.onmessage = async function(e) {\n");
            for s in body {
                write_stmt(out, s, indent + 1);
                out.push('\n');
            }
            out.push_str(&pad);
            out.push_str("};");
        }
        Stmt::RegisterProcessor { name, class } => {
            out.push_str(&pad);
            let _ = write!(out, "registerProcessor(\"{}\", {});", name, class.as_str());
        }
    }
}

fn write_class(out: &mut String, class: &JsClass, indent: usize) {
    let pad = "    ".repeat(indent);
    let inner_pad = "    ".repeat(indent + 1);

    // Class declaration
    out.push_str(&pad);
    out.push_str("class ");
    out.push_str(class.name.as_str());
    if let Some(ref parent) = class.extends {
        out.push_str(" extends ");
        out.push_str(parent.as_str());
    }
    out.push_str(" {\n");

    // Constructor
    if let Some(ref body) = class.constructor {
        out.push_str(&inner_pad);
        out.push_str("constructor() {\n");
        // Auto-insert super() if extends
        if class.extends.is_some() {
            let body_pad = "    ".repeat(indent + 2);
            out.push_str(&body_pad);
            out.push_str("super();\n");
        }
        for s in body {
            write_stmt(out, s, indent + 2);
            out.push('\n');
        }
        out.push_str(&inner_pad);
        out.push_str("}\n");
    }

    // Methods
    for method in &class.methods {
        out.push_str(&inner_pad);
        out.push_str(method.name.as_str());
        out.push('(');
        for (i, param) in method.params.iter().enumerate() {
            if i > 0 {
                out.push_str(", ");
            }
            out.push_str(param.as_str());
        }
        out.push_str(") {\n");
        for s in &method.body {
            write_stmt(out, s, indent + 2);
            out.push('\n');
        }
        out.push_str(&inner_pad);
        out.push_str("}\n");
    }

    out.push_str(&pad);
    out.push('}');
}

fn write_switch(out: &mut String, switch: &JsSwitch, indent: usize) {
    let pad = "    ".repeat(indent);
    let case_pad = "    ".repeat(indent + 1);
    let body_pad = "    ".repeat(indent + 2);

    out.push_str(&pad);
    out.push_str("switch (");
    write_expr(out, &switch.expr);
    out.push_str(") {\n");

    for (value, body) in &switch.cases {
        out.push_str(&case_pad);
        out.push_str("case ");
        write_expr(out, value);
        out.push_str(":\n");
        for s in body {
            write_stmt(out, s, indent + 2);
            out.push('\n');
        }
        out.push_str(&body_pad);
        out.push_str("break;\n");
    }

    if let Some(ref body) = switch.default {
        out.push_str(&case_pad);
        out.push_str("default:\n");
        for s in body {
            write_stmt(out, s, indent + 2);
            out.push('\n');
        }
    }

    out.push_str(&pad);
    out.push('}');
}

// Note: write! to String never fails, so we use `let _ =` to suppress warnings
#[allow(clippy::unwrap_used)]
fn write_expr(out: &mut String, expr: &Expr) {
    match expr {
        Expr::Null => out.push_str("null"),
        Expr::Bool(b) => {
            let _ = write!(out, "{b}");
        }
        Expr::Num(n) => {
            // Format integers without decimal point
            #[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
            if n.fract() == 0.0 && *n >= i64::MIN as f64 && *n <= i64::MAX as f64 {
                let _ = write!(out, "{}", *n as i64);
            } else {
                let _ = write!(out, "{n}");
            }
        }
        Expr::Str(s) => {
            out.push('"');
            for c in s.chars() {
                match c {
                    '"' => out.push_str("\\\""),
                    '\\' => out.push_str("\\\\"),
                    '\n' => out.push_str("\\n"),
                    '\r' => out.push_str("\\r"),
                    '\t' => out.push_str("\\t"),
                    c => out.push(c),
                }
            }
            out.push('"');
        }
        Expr::Ident(id) => out.push_str(id.as_str()),
        Expr::This => out.push_str("this"),
        Expr::Member { object, property } => {
            write_expr(out, object);
            out.push('.');
            out.push_str(property.as_str());
        }
        Expr::Index { object, index } => {
            write_expr(out, object);
            out.push('[');
            write_expr(out, index);
            out.push(']');
        }
        Expr::Call { callee, args } => {
            write_expr(out, callee);
            out.push('(');
            for (i, arg) in args.iter().enumerate() {
                if i > 0 {
                    out.push_str(", ");
                }
                write_expr(out, arg);
            }
            out.push(')');
        }
        Expr::New { constructor, args } => {
            out.push_str("new ");
            write_expr(out, constructor);
            out.push('(');
            for (i, arg) in args.iter().enumerate() {
                if i > 0 {
                    out.push_str(", ");
                }
                write_expr(out, arg);
            }
            out.push(')');
        }
        Expr::Await(inner) => {
            out.push_str("await ");
            write_expr(out, inner);
        }
        Expr::Import(path) => {
            out.push_str("import(");
            write_expr(out, path);
            out.push(')');
        }
        Expr::Binary { left, op, right } => {
            out.push('(');
            write_expr(out, left);
            out.push(' ');
            out.push_str(op.as_str());
            out.push(' ');
            write_expr(out, right);
            out.push(')');
        }
        Expr::Unary { op, operand } => {
            out.push_str(op.as_str());
            write_expr(out, operand);
        }
        Expr::Ternary {
            condition,
            then_expr,
            else_expr,
        } => {
            out.push('(');
            write_expr(out, condition);
            out.push_str(" ? ");
            write_expr(out, then_expr);
            out.push_str(" : ");
            write_expr(out, else_expr);
            out.push(')');
        }
        Expr::Object(pairs) => {
            out.push_str("{ ");
            for (i, (key, value)) in pairs.iter().enumerate() {
                if i > 0 {
                    out.push_str(", ");
                }
                out.push_str(key);
                out.push_str(": ");
                write_expr(out, value);
            }
            out.push_str(" }");
        }
        Expr::Array(items) => {
            out.push('[');
            for (i, item) in items.iter().enumerate() {
                if i > 0 {
                    out.push_str(", ");
                }
                write_expr(out, item);
            }
            out.push(']');
        }
        Expr::Arrow { params, body } => {
            if params.len() == 1 {
                out.push_str(params[0].as_str());
            } else {
                out.push('(');
                for (i, p) in params.iter().enumerate() {
                    if i > 0 {
                        out.push_str(", ");
                    }
                    out.push_str(p.as_str());
                }
                out.push(')');
            }
            out.push_str(" => ");
            write_expr(out, body);
        }
        Expr::ArrowBlock { params, body } => {
            if params.len() == 1 {
                out.push_str(params[0].as_str());
            } else {
                out.push('(');
                for (i, p) in params.iter().enumerate() {
                    if i > 0 {
                        out.push_str(", ");
                    }
                    out.push_str(p.as_str());
                }
                out.push(')');
            }
            out.push_str(" => {\n");
            for s in body {
                write_stmt(out, s, 1);
                out.push('\n');
            }
            out.push('}');
        }
        Expr::Assign { target, value } => {
            write_expr(out, target);
            out.push_str(" = ");
            write_expr(out, value);
        }
    }
}

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
    use super::*;
    use crate::builder::*;

    #[test]
    fn generate_empty_module() {
        let module = JsModuleBuilder::new().build();
        let js = generate(&module);
        assert_eq!(js, "");
    }

    #[test]
    fn generate_let_declaration() {
        let module = JsModuleBuilder::new()
            .let_decl("x", Expr::num(42))
            .unwrap()
            .build();
        let js = generate(&module);
        assert!(js.contains("let x = 42;"));
    }

    #[test]
    fn generate_class() {
        let class = JsClassBuilder::new("Foo")
            .unwrap()
            .extends("Bar")
            .unwrap()
            .constructor(vec![])
            .method("test", &[], vec![Stmt::ret()])
            .unwrap()
            .build();

        let module = JsModuleBuilder::new().class(class).build();
        let js = generate(&module);

        assert!(js.contains("class Foo extends Bar"));
        assert!(js.contains("super();"));
        assert!(js.contains("test()"));
    }

    #[test]
    fn generate_if_else() {
        let stmt = Stmt::if_else(
            Expr::ident("x").unwrap().lt(Expr::num(10)),
            vec![Stmt::expr(
                Expr::ident("console")
                    .unwrap()
                    .dot("log")
                    .unwrap()
                    .call(vec![Expr::str("small")]),
            )],
            vec![Stmt::expr(
                Expr::ident("console")
                    .unwrap()
                    .dot("log")
                    .unwrap()
                    .call(vec![Expr::str("big")]),
            )],
        );

        let module = JsModuleBuilder::new().stmt(stmt).build();
        let js = generate(&module);

        assert!(js.contains("if ((x < 10))"));
        assert!(js.contains("} else {"));
    }

    #[test]
    fn generate_for_loop() {
        let stmt = Stmt::for_loop("i", Expr::num(0), Expr::num(10), vec![]).unwrap();
        let module = JsModuleBuilder::new().stmt(stmt).build();
        let js = generate(&module);

        assert!(js.contains("for (let i = 0; i < 10; i++)"));
    }

    #[test]
    fn generate_string_escaping() {
        let module = JsModuleBuilder::new()
            .const_decl("s", Expr::str("hello\n\"world\""))
            .unwrap()
            .build();
        let js = generate(&module);

        assert!(js.contains(r#""hello\n\"world\"""#));
    }

    #[test]
    fn generate_with_metadata() {
        let module = JsModuleBuilder::new()
            .metadata(GenerationMetadata {
                tool: "probar-js-gen".to_string(),
                version: "0.1.0".to_string(),
                input_hash: "abc123".to_string(),
                timestamp: "2024-01-01T00:00:00Z".to_string(),
                regenerate_cmd: "probar gen js".to_string(),
            })
            .let_decl("x", Expr::num(1))
            .unwrap()
            .build();

        let js = generate(&module);

        assert!(js.contains("@generated"));
        assert!(js.contains("Do not edit manually"));
        assert!(js.contains("probar-js-gen"));
        assert!(js.contains("abc123"));
    }

    #[test]
    fn deterministic_output() {
        let module = JsModuleBuilder::new()
            .let_decl("x", Expr::num(1))
            .unwrap()
            .const_decl("y", Expr::str("hello"))
            .unwrap()
            .build();

        let js1 = generate(&module);
        let js2 = generate(&module);

        assert_eq!(js1, js2, "Same HIR must produce same output");
    }
}
