//! Code generation: AST -> JavaScript source.

use tishlang_ast::{
    ArrayElement, ArrowBody, BinOp, CallArg, CompoundOp, DestructElement, DestructPattern, Expr,
    JsxAttrValue, JsxChild, JsxProp, Literal, LogicalAssignOp, MemberProp, ObjectProp, Program,
    Statement, UnaryOp,
};

use crate::error::CompileError;
use crate::js_intrinsics::{JsIntrinsic, JsIntrinsics};

/// How JSX lowers for `--target js`. Native targets never use this.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum JsxMode {
    /// `h(tag, props, [children])` + `Fragment` (Lattish.tish). Import `h` / `Fragment` from lattish.
    #[default]
    LattishH,
    /// Vnode `__vdom_h` + `window.__lattishVdomPatch` (Lattish createRoot must use patch).
    Vdom,
}

struct Codegen {
    output: String,
    indent: usize,
    in_async: bool,
    uses_jsx: bool,
    intrinsics: JsIntrinsics,
    jsx_mode: JsxMode,
}

fn stmt_terminates_switch(stmt: Option<&Statement>) -> bool {
    matches!(
        stmt,
        Some(Statement::Break { .. }) | Some(Statement::Return { .. }) | Some(Statement::Throw { .. })
    )
}

impl Codegen {
    fn new(jsx_mode: JsxMode) -> Self {
        Self {
            output: String::new(),
            indent: 0,
            in_async: false,
            uses_jsx: false,
            intrinsics: JsIntrinsics::new(),
            jsx_mode,
        }
    }

    fn indent_str(&self) -> String {
        "  ".repeat(self.indent)
    }

    fn write(&mut self, s: &str) {
        self.output.push_str(s);
    }

    fn writeln(&mut self, s: &str) {
        self.output.push_str(&self.indent_str());
        self.output.push_str(s);
        self.output.push('\n');
    }

    fn escape_ident(s: &str) -> String {
        let s = s.to_string();
        if s == "await" || s == "default" {
            format!("_{}", s)
        } else {
            s
        }
    }

    fn emit_program(&mut self, program: &Program) -> Result<(), CompileError> {
        self.write("// Generated by tishlang_compile_js\n");
        // First pass: check if JSX is used (we'll set uses_jsx during emit)
        for stmt in &program.statements {
            self.emit_statement(stmt)?;
        }
        self.output = self
            .intrinsics
            .prepend_runtime_preamble(std::mem::take(&mut self.output));
        if self.uses_jsx {
            match self.jsx_mode {
                JsxMode::Vdom => {
                    self.output = format!(
                        "{}\n{}",
                        tishlang_jsx_web::VDOM_PRELUDE,
                        self.output
                    );
                }
                JsxMode::LattishH => {}
            }
        }
        Ok(())
    }

    fn emit_statement(&mut self, stmt: &Statement) -> Result<(), CompileError> {
        match stmt {
            Statement::Block { statements, .. } => {
                self.writeln("{");
                self.indent += 1;
                for s in statements {
                    self.emit_statement(s)?;
                }
                self.indent -= 1;
                self.writeln("}");
            }
            Statement::VarDecl {
                name,
                mutable,
                type_ann: _,
                init,
                ..
            } => {
                let decl = if *mutable { "let" } else { "const" };
                let escaped = Self::escape_ident(name.as_ref());
                if let Some(expr) = init {
                    let e = self.emit_expr(expr)?;
                    self.writeln(&format!("{} {} = {};", decl, escaped, e));
                } else {
                    self.writeln(&format!("{} {};", decl, escaped));
                }
            }
            Statement::VarDeclDestructure {
                pattern,
                mutable,
                init,
                ..
            } => {
                let decl = if *mutable { "let" } else { "const" };
                let rhs = self.emit_expr(init)?;
                let pat = self.emit_destruct_pattern(pattern)?;
                self.writeln(&format!("{} {} = {};", decl, pat, rhs));
            }
            Statement::ExprStmt { expr, .. } => {
                let e = self.emit_expr(expr)?;
                self.writeln(&format!("{};", e));
            }
            Statement::If {
                cond,
                then_branch,
                else_branch,
                ..
            } => {
                let c = self.emit_expr(cond)?;
                self.writeln(&format!("if ({})", c));
                self.indent += 1;
                self.emit_statement(then_branch)?;
                self.indent -= 1;
                if let Some(eb) = else_branch {
                    self.writeln("else");
                    self.indent += 1;
                    self.emit_statement(eb)?;
                    self.indent -= 1;
                }
            }
            Statement::While { cond, body, .. } => {
                let c = self.emit_expr(cond)?;
                self.writeln(&format!("while ({})", c));
                self.indent += 1;
                self.emit_statement(body)?;
                self.indent -= 1;
            }
            Statement::For {
                init,
                cond,
                update,
                body,
                ..
            } => {
                self.write("for (");
                if let Some(i) = init {
                    match i.as_ref() {
                        Statement::VarDecl {
                            name,
                            mutable,
                            init: opt_init,
                            ..
                        } => {
                            let decl = if *mutable { "let" } else { "const" };
                            let escaped = Self::escape_ident(name.as_ref());
                            if let Some(e) = opt_init {
                                let ex = self.emit_expr(e)?;
                                self.write(&format!("{} {} = {}", decl, escaped, ex));
                            } else {
                                self.write(&format!("{} {}", decl, escaped));
                            }
                        }
                        Statement::ExprStmt { expr, .. } => {
                            let ex = self.emit_expr(expr)?;
                            self.write(&ex);
                        }
                        _ => return Err(CompileError::new("Unsupported for init")),
                    }
                }
                self.write("; ");
                if let Some(c) = cond {
                    let ce = self.emit_expr(c)?;
                    self.write(&ce);
                }
                self.write("; ");
                if let Some(u) = update {
                    let ue = self.emit_expr(u)?;
                    self.write(&ue);
                }
                self.writeln(")");
                self.indent += 1;
                self.emit_statement(body)?;
                self.indent -= 1;
            }
            Statement::ForOf { name, iterable, body, .. } => {
                let escaped = Self::escape_ident(name.as_ref());
                let it = self.emit_expr(iterable)?;
                self.writeln(&format!("for (const {} of {})", escaped, it));
                self.indent += 1;
                self.emit_statement(body)?;
                self.indent -= 1;
            }
            Statement::Return { value, .. } => {
                if let Some(v) = value {
                    let e = self.emit_expr(v)?;
                    self.writeln(&format!("return {};", e));
                } else {
                    self.writeln("return;");
                }
            }
            Statement::Break { .. } => self.writeln("break;"),
            Statement::Continue { .. } => self.writeln("continue;"),
            Statement::FunDecl {
                async_,
                name,
                params,
                rest_param,
                return_type: _,
                body,
                ..
            } => {
                let async_prefix = if *async_ { "async " } else { "" };
                let escaped = Self::escape_ident(name.as_ref());
                let params_str = self.emit_params(params, rest_param.as_ref())?;
                self.writeln(&format!("{}function {} ({}) {{", async_prefix, escaped, params_str));
                self.indent += 1;
                if *async_ {
                    self.in_async = true;
                }
                self.emit_statement(body)?;
                if *async_ {
                    self.in_async = false;
                }
                self.indent -= 1;
                self.writeln("}");
            }
            Statement::Switch {
                expr,
                cases,
                default_body,
                ..
            } => {
                let e = self.emit_expr(expr)?;
                self.writeln(&format!("switch ({}) {{", e));
                self.indent += 1;
                for (case_expr, stmts) in cases {
                    if let Some(ce) = case_expr {
                        let c = self.emit_expr(ce)?;
                        self.writeln(&format!("case {}:", c));
                    }
                    for s in stmts {
                        self.emit_statement(s)?;
                    }
                    // Tish has no fall-through; add break unless case ends with break/return/throw
                    if !stmt_terminates_switch(stmts.last()) {
                        self.writeln("break;");
                    }
                }
                if let Some(stmts) = default_body {
                    self.writeln("default:");
                    for s in stmts {
                        self.emit_statement(s)?;
                    }
                    if !stmt_terminates_switch(stmts.last()) {
                        self.writeln("break;");
                    }
                }
                self.indent -= 1;
                self.writeln("}");
            }
            Statement::DoWhile { body, cond, .. } => {
                self.writeln("do {");
                self.indent += 1;
                self.emit_statement(body)?;
                self.indent -= 1;
                let c = self.emit_expr(cond)?;
                self.writeln(&format!("}} while ({});", c));
            }
            Statement::Throw { value, .. } => {
                let v = self.emit_expr(value)?;
                self.writeln(&format!("throw {};", v));
            }
            Statement::Try {
                body,
                catch_param,
                catch_body,
                finally_body,
                ..
            } => {
                self.writeln("try {");
                self.indent += 1;
                self.emit_statement(body)?;
                self.indent -= 1;
                if let (Some(param), Some(cb)) = (catch_param, catch_body) {
                    let p = Self::escape_ident(param.as_ref());
                    self.writeln(&format!("}} catch ({}) {{", p));
                    self.indent += 1;
                    self.emit_statement(cb)?;
                    self.indent -= 1;
                }
                if let Some(fb) = finally_body {
                    self.writeln("} finally {");
                    self.indent += 1;
                    self.emit_statement(fb)?;
                    self.indent -= 1;
                }
                self.writeln("}");
            }
            Statement::Import { .. } | Statement::Export { .. } => {
                // Resolved away by merge_modules
            }
        }
        Ok(())
    }

    fn emit_params(
        &mut self,
        params: &[tishlang_ast::TypedParam],
        rest_param: Option<&tishlang_ast::TypedParam>,
    ) -> Result<String, CompileError> {
        let mut parts: Vec<String> = params
            .iter()
            .map(|p| {
                let n = Self::escape_ident(p.name.as_ref());
                if let Some(ref d) = p.default {
                    format!("{} = {}", n, self.emit_expr(d).unwrap())
                } else {
                    n
                }
            })
            .collect();
        if let Some(rest) = rest_param {
            parts.push(format!("...{}", Self::escape_ident(rest.name.as_ref())));
        }
        Ok(parts.join(", "))
    }

    fn emit_destruct_pattern(&mut self, pattern: &DestructPattern) -> Result<String, CompileError> {
        match pattern {
            DestructPattern::Array(elements) => {
                let parts: Vec<String> = elements
                    .iter()
                    .map(|el| match el {
                        Some(DestructElement::Ident(n)) => {
                            Ok(Self::escape_ident(n.as_ref()))
                        }
                        Some(DestructElement::Pattern(p)) => self.emit_destruct_pattern(p),
                        Some(DestructElement::Rest(n)) => {
                            Ok(format!("...{}", Self::escape_ident(n.as_ref())))
                        }
                        None => Ok("".to_string()),
                    })
                    .collect::<Result<_, _>>()?;
                Ok(format!("[{}]", parts.join(", ")))
            }
            DestructPattern::Object(props) => {
                let parts: Vec<String> = props
                    .iter()
                    .map(|p| {
                        let k = p.key.as_ref();
                        match &p.value {
                            DestructElement::Ident(n) => {
                                if k == n.as_ref() {
                                    Ok(k.to_string())
                                } else {
                                    Ok(format!("{}: {}", k, Self::escape_ident(n.as_ref())))
                                }
                            }
                            DestructElement::Pattern(pat) => {
                                Ok(format!("{}: {}", k, self.emit_destruct_pattern(pat)?))
                            }
                            DestructElement::Rest(n) => {
                                Ok(format!("...{}", Self::escape_ident(n.as_ref())))
                            }
                        }
                    })
                    .collect::<Result<_, _>>()?;
                Ok(format!("{{ {} }}", parts.join(", ")))
            }
        }
    }

    fn emit_expr(&mut self, expr: &Expr) -> Result<String, CompileError> {
        Ok(match expr {
            Expr::Literal { value, .. } => match value {
                Literal::Number(n) => format!("{}", n),
                Literal::String(s) => format!("{:?}", s.as_ref()),
                Literal::Bool(b) => format!("{}", b),
                Literal::Null => "null".to_string(),
            },
            Expr::Ident { name, .. } => Self::escape_ident(name.as_ref()),
            Expr::Binary { left, op, right, .. } => {
                let l = self.emit_expr(left)?;
                let r = self.emit_expr(right)?;
                let op_str = match op {
                    BinOp::Add => "+",
                    BinOp::Sub => "-",
                    BinOp::Mul => "*",
                    BinOp::Div => "/",
                    BinOp::Mod => "%",
                    BinOp::Pow => "**",
                    BinOp::Eq => "==",
                    BinOp::Ne => "!=",
                    BinOp::StrictEq => "===",
                    BinOp::StrictNe => "!==",
                    BinOp::Lt => "<",
                    BinOp::Le => "<=",
                    BinOp::Gt => ">",
                    BinOp::Ge => ">=",
                    BinOp::And => "&&",
                    BinOp::Or => "||",
                    BinOp::BitAnd => "&",
                    BinOp::BitOr => "|",
                    BinOp::BitXor => "^",
                    BinOp::Shl => "<<",
                    BinOp::Shr => ">>",
                    BinOp::In => {
                        // key in object (property/index existence check)
                        return Ok(format!("({} in {})", l, r));
                    }
                };
                format!("({} {} {})", l, op_str, r)
            }
            Expr::Unary { op, operand, .. } => {
                let o = self.emit_expr(operand)?;
                match op {
                    UnaryOp::Not => format!("!{}", o),
                    UnaryOp::Neg => format!("(-{})", o),
                    UnaryOp::Pos => format!("(+{})", o),
                    UnaryOp::BitNot => format!("(~{})", o),
                    UnaryOp::Void => format!("((void {}), null)", o), // Tish void returns null, not undefined
                }
            }
            Expr::Call { callee, args, .. } => {
                if let Some(kind) =
                    JsIntrinsics::classify_call(callee.as_ref(), args)?
                {
                    self.intrinsics.mark(kind);
                    if kind == JsIntrinsic::Uint8Array {
                        let n = self.emit_call_arg(&args[0])?;
                        return Ok(JsIntrinsics::emit_expr(kind, &n));
                    }
                    return Ok(JsIntrinsics::emit_expr(kind, ""));
                }
                let c = self.emit_expr(callee)?;
                let arg_strs: Result<Vec<_>, _> =
                    args.iter().map(|a| self.emit_call_arg(a)).collect();
                let arg_strs = arg_strs?.join(", ");
                // Tish uses null for undefined (e.g. empty array pop/shift)
                format!("({}({}) ?? null)", c, arg_strs)
            }
            Expr::Member {
                object,
                prop,
                optional,
                ..
            } => {
                let obj = self.emit_expr(object)?;
                let expr = match prop {
                    MemberProp::Name(p) => {
                        if p.parse::<u32>().is_ok() || !p.chars().all(|c| c.is_alphanumeric() || c == '_') {
                            format!("{}[{:?}]", obj, p.as_ref())
                        } else {
                            let sep = if *optional { "?." } else { "." };
                            format!("{}{}{}", obj, sep, p.as_ref())
                        }
                    }
                    MemberProp::Expr(e) => {
                        let idx = self.emit_expr(e)?;
                        format!("{}[{}]", obj, idx)
                    }
                };
                // Tish uses null where JS uses undefined for optional chaining short-circuit only
                if *optional {
                    format!("({} ?? null)", expr)
                } else {
                    expr
                }
            }
            Expr::Index {
                object,
                index,
                optional,
                ..
            } => {
                let obj = self.emit_expr(object)?;
                let idx = self.emit_expr(index)?;
                let sep = if *optional { "?." } else { "" };
                let expr = format!("{}{}[{}]", obj, sep, idx);
                // Tish uses null for array holes / missing indices (JS returns undefined)
                format!("({} ?? null)", expr)
            }
            Expr::Conditional {
                cond,
                then_branch,
                else_branch,
                ..
            } => {
                let c = self.emit_expr(cond)?;
                let t = self.emit_expr(then_branch)?;
                let e = self.emit_expr(else_branch)?;
                format!("({} ? {} : {})", c, t, e)
            }
            Expr::NullishCoalesce { left, right, .. } => {
                let l = self.emit_expr(left)?;
                let r = self.emit_expr(right)?;
                format!("({} ?? {})", l, r)
            }
            Expr::Array { elements, .. } => {
                let parts: Result<Vec<_>, _> = elements
                    .iter()
                    .map(|el| match el {
                        ArrayElement::Expr(e) => self.emit_expr(e),
                        ArrayElement::Spread(e) => Ok(format!("...{}", self.emit_expr(e)?)),
                    })
                    .collect();
                format!("[{}]", parts?.join(", "))
            }
            Expr::Object { props, .. } => {
                let parts: Result<Vec<_>, _> = props
                    .iter()
                    .map(|p| match p {
                        ObjectProp::KeyValue(k, v) => {
                            let key = k.as_ref();
                            let val = self.emit_expr(v)?;
                            Ok(if key.chars().all(|c| c.is_alphanumeric() || c == '_') {
                                format!("{}: {}", key, val)
                            } else {
                                format!("{:?}: {}", key, val)
                            })
                        }
                        ObjectProp::Spread(e) => Ok(format!("...{}", self.emit_expr(e)?)),
                    })
                    .collect();
                format!("{{ {} }}", parts?.join(", "))
            }
            Expr::Assign { name, value, .. } => {
                let n = Self::escape_ident(name.as_ref());
                let v = self.emit_expr(value)?;
                format!("({} = {})", n, v)
            }
            Expr::TypeOf { operand, .. } => {
                let o = self.emit_expr(operand)?;
                format!("(typeof {})", o)
            }
            Expr::PostfixInc { name, .. } => {
                format!("{}++", Self::escape_ident(name.as_ref()))
            }
            Expr::PostfixDec { name, .. } => {
                format!("{}--", Self::escape_ident(name.as_ref()))
            }
            Expr::PrefixInc { name, .. } => {
                format!("++{}", Self::escape_ident(name.as_ref()))
            }
            Expr::PrefixDec { name, .. } => {
                format!("--{}", Self::escape_ident(name.as_ref()))
            }
            Expr::CompoundAssign { name, op, value, .. } => {
                let n = Self::escape_ident(name.as_ref());
                let v = self.emit_expr(value)?;
                let op_str = match op {
                    CompoundOp::Add => "+=",
                    CompoundOp::Sub => "-=",
                    CompoundOp::Mul => "*=",
                    CompoundOp::Div => "/=",
                    CompoundOp::Mod => "%=",
                };
                format!("({} {} {})", n, op_str, v)
            }
            Expr::LogicalAssign { name, op, value, .. } => {
                let n = Self::escape_ident(name.as_ref());
                let v = self.emit_expr(value)?;
                let op_str = match op {
                    LogicalAssignOp::AndAnd => "&&=",
                    LogicalAssignOp::OrOr => "||=",
                    LogicalAssignOp::Nullish => "??=",
                };
                format!("({} {} {})", n, op_str, v)
            }
            Expr::MemberAssign { object, prop, value, .. } => {
                let obj = self.emit_expr(object)?;
                let val = self.emit_expr(value)?;
                format!("({}.{} = {})", obj, prop.as_ref(), val)
            }
            Expr::IndexAssign {
                object,
                index,
                value,
                ..
            } => {
                let obj = self.emit_expr(object)?;
                let idx = self.emit_expr(index)?;
                let val = self.emit_expr(value)?;
                format!("({}[{}] = {})", obj, idx, val)
            }
            Expr::ArrowFunction { params, body, .. } => {
                let ps = self.emit_params(params, None)?;
                let body_str = match body {
                    ArrowBody::Expr(e) => self.emit_expr(e)?,
                    ArrowBody::Block(s) => {
                        let saved = std::mem::take(&mut self.output);
                        self.writeln("{");
                        self.indent += 1;
                        self.emit_statement(s)?;
                        self.indent -= 1;
                        self.writeln("}");
                        let block = self.output.trim().to_string();
                        self.output = saved;
                        block
                    }
                };
                if matches!(body, ArrowBody::Expr(_)) {
                    format!("({}) => ({})", ps, body_str)
                } else {
                    format!("({}) => {}", ps, body_str)
                }
            }
            Expr::TemplateLiteral { quasis, exprs, .. } => {
                let mut s = String::from('`');
                for (i, q) in quasis.iter().enumerate() {
                    let escaped = q
                        .replace('\\', "\\\\")
                        .replace('`', "\\`")
                        .replace('$', "\\$");
                    s.push_str(&escaped);
                    if i < exprs.len() {
                        s.push_str("${");
                        s.push_str(&self.emit_expr(&exprs[i])?);
                        s.push('}');
                    }
                }
                s.push('`');
                s
            }
            Expr::Await { operand, .. } => {
                let o = self.emit_expr(operand)?;
                format!("(await {})", o)
            }
            Expr::JsxElement { tag, props, children, .. } => {
                self.uses_jsx = true;
                let tag_str = if tag.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) {
                    tag.as_ref().to_string()
                } else {
                    format!("{:?}", tag.as_ref())
                };
                let props_str = self.emit_jsx_props(props)?;
                let children_strs: Result<Vec<_>, _> =
                    children.iter().map(|c| self.emit_jsx_child(c)).collect();
                let children_str = children_strs?.join(", ");
                match self.jsx_mode {
                    JsxMode::LattishH => {
                        format!("h({}, {}, [{}])", tag_str, props_str, children_str)
                    }
                    JsxMode::Vdom => {
                        format!("__vdom_h({}, {}, [{}])", tag_str, props_str, children_str)
                    }
                }
            }
            Expr::JsxFragment { children, .. } => {
                self.uses_jsx = true;
                let children_strs: Result<Vec<_>, _> =
                    children.iter().map(|c| self.emit_jsx_child(c)).collect();
                let children_str = children_strs?.join(", ");
                match self.jsx_mode {
                    JsxMode::LattishH => {
                        format!("h(Fragment, null, [{}])", children_str)
                    }
                    JsxMode::Vdom => {
                        format!("__vdom_h(__Fragment, null, [{}])", children_str)
                    }
                }
            }
            Expr::NativeModuleLoad { spec, .. } => {
                return Err(CompileError {
                    message: format!(
                        "Native module imports ({}) are only supported when compiling to Rust. Omit --target js.",
                        spec.as_ref()
                    ),
                });
            }
        })
    }

    fn emit_call_arg(&mut self, arg: &CallArg) -> Result<String, CompileError> {
        match arg {
            CallArg::Expr(e) => self.emit_expr(e),
            CallArg::Spread(e) => Ok(format!("...{}", self.emit_expr(e)?)),
        }
    }

    fn emit_jsx_props(&mut self, props: &[JsxProp]) -> Result<String, CompileError> {
        if props.is_empty() {
            return Ok("null".to_string());
        }
        let parts: Result<Vec<_>, _> = props
            .iter()
            .map(|p| match p {
                JsxProp::Attr { name, value } => {
                    let val = match value {
                        JsxAttrValue::String(s) => format!("{:?}", s.as_ref()),
                        JsxAttrValue::Expr(e) => self.emit_expr(e)?,
                        JsxAttrValue::ImplicitTrue => "true".to_string(),
                    };
                    let key = name.as_ref();
                    Ok(if key.chars().all(|c| c.is_alphanumeric() || c == '_') {
                        format!("{}: {}", key, val)
                    } else {
                        format!("{:?}: {}", key, val)
                    })
                }
                JsxProp::Spread(e) => Ok(format!("...{}", self.emit_expr(e)?)),
            })
            .collect();
        Ok(format!("{{ {} }}", parts?.join(", ")))
    }

    fn emit_jsx_child(&mut self, child: &JsxChild) -> Result<String, CompileError> {
        match child {
            JsxChild::Text(s) => Ok(format!("{:?}", s.as_ref())),
            JsxChild::Expr(e) => {
                let inner = self.emit_expr(e)?;
                // Only wrap literals we know are primitives (number, bool, null). Never wrap:
                // string/template (already strings), JSX (elements), Call (components), Array/Ident (may hold elements).
                let needs_string = matches!(
                    e,
                    Expr::Literal {
                        value: Literal::Number(_) | Literal::Bool(_) | Literal::Null,
                        ..
                    }
                );
                Ok(if needs_string {
                    format!("String({})", inner)
                } else {
                    inner
                })
            }
        }
    }
}

/// Compile a single program (no imports) to JavaScript.
pub fn compile_with_jsx(
    program: &Program,
    optimize: bool,
    jsx_mode: JsxMode,
) -> Result<String, CompileError> {
    let program = if optimize {
        tishlang_opt::optimize(program)
    } else {
        program.clone()
    };
    let mut g = Codegen::new(jsx_mode);
    g.emit_program(&program)?;
    Ok(g.output)
}

/// Compile a project from entry path, resolving and merging modules.
/// Uses shared resolve from tishlang_compile (same pipeline as native/WASM).
pub fn compile_project_with_jsx(
    entry_path: &std::path::Path,
    project_root: Option<&std::path::Path>,
    optimize: bool,
    jsx_mode: JsxMode,
) -> Result<String, CompileError> {
    use tishlang_ast::Statement;
    let modules = tishlang_compile::resolve_project(entry_path, project_root)
        .map_err(|e| CompileError { message: e })?;
    tishlang_compile::detect_cycles(&modules).map_err(|e| CompileError { message: e })?;
    let program = {
        let prog = tishlang_compile::merge_modules(modules).map_err(|e| CompileError { message: e })?;
        if optimize {
            tishlang_opt::optimize(&prog)
        } else {
            prog
        }
    };
    let default_export = program.statements.iter().find_map(|s| {
        if let Statement::VarDecl { name, .. } = s {
            let n = name.as_ref();
            if n.starts_with("__default_") {
                Some(n.to_string())
            } else {
                None
            }
        } else {
            None
        }
    });
    let mut js = compile_with_jsx(&program, optimize, jsx_mode)?;
    if let Some(name) = default_export {
        js.push_str(&format!("\nexport default {};\n", name));
    }
    Ok(js)
}
