Skip to main content

native/vize_ex_nif/src/lib.rs

use lightningcss::dependencies::{Dependency, DependencyOptions};
use lightningcss::printer::PrinterOptions;
use lightningcss::stylesheet::{
    ParserFlags as CssParserFlags, ParserOptions as CssParserOptions, StyleSheet,
};
use rustler::{Atom, Encoder, Env, NifResult, Term};
use rustler_match_spec::{MatchEvent, Selector, ValueRef};
use vize_atelier_core::options::{
    CodegenMode, CodegenOptions, ParserOptions, TemplateSyntaxMode, TransformOptions,
};
use vize_atelier_core::parser::{parse, parse_with_options};
use vize_atelier_core::transform::transform;
use vize_atelier_sfc::compile_script::typescript::transform_typescript_to_js;
use vize_atelier_sfc::croquis::{analyze_sfc_descriptor, SfcCroquisOptions};
use vize_atelier_sfc::script::analyze_script_setup_to_summary;
use vize_atelier_sfc::{
    bundle_css, compile_css, compile_sfc, parse_css_ast, parse_sfc, print_css_ast,
    CssCompileOptions, CssTargets, SfcCompileOptions, SfcParseOptions,
};
use vize_atelier_ssr::compile_ssr;
use vize_atelier_vapor::{
    compile_vapor, compile_vapor_with_template_syntax_and_diagnostics, ir::*, transform_to_ir,
    VaporCompilerOptions,
};
use vize_carton::Bump;

#[macro_use]
mod macros;
mod html_inject;
mod ir_encoding;
mod term_encoding;
mod vapor_split;

use crate::ir_encoding::{encode_ir_prop, encode_simple_expr};
use crate::term_encoding::{
    decode_json_value, error_term, nil_term, ok_term, EncodedBundleCssResult,
    EncodedCompileSfcResult, EncodedCssAstResult, EncodedCssCompileResult, EncodedLintDiagnostic,
    EncodedParseSfcResult, EncodedSsrCompileResult, EncodedTemplateCompileResult,
};
use crate::vapor_split::process_block;

mod atoms {
    rustler::atoms! {
        ok,
        error,

        // SFC descriptor fields
        template,
        script,
        script_setup,
        styles,
        custom_blocks,
        content,
        lang,
        scoped,
        module,
        setup,
        attrs,
        block_type,
        loc,
        start,
        end_ = "end",
        start_line,
        start_column,
        end_line,
        end_column,
        url,
        css_url,

        // Compile result fields
        code,
        stats,
        bindings,
        emits,
        models,
        used_components,
        used_directives,
        undefined_refs,
        component_usages,
        template_expressions,
        required,
        context,
        handler,
        events,
        has_spread_attrs,
        is_dynamic,
        range,
        css,
        errors,
        warnings,
        diagnostics,
        recoverable,
        location,
        template_hash,
        style_hash,
        script_hash,
        macro_artifacts,
        message,
        preamble,
        helpers,
        templates,

        // Vapor IR fields
        root,
        block,
        operations,
        effects,
        returns,
        element,
        key,
        values,
        value,
        condition,
        positive,
        negative,
        source,
        index,
        render,
        once,
        parent,
        anchor,
        key_prop,
        tag,
        props,
        slots,
        name,
        fallback,
        delegate,
        effect,
        modifiers,
        camel,
        prop_modifier,
        is_component,
        components,
        directives,
        kind,
        asset,
        dynamic_slots,
        child_id,
        parent_id,
        offset,
        line,
        column,

        // CSS result fields
        ast,
        map,
        css_vars,
        exports,
        minify,
        targets,
        scope_id,
        filename_opt,
        custom_media,

        // Expression tags
        static_,
        element_template_map,

        // IR node type atoms
        set_prop,
        set_dynamic_props,
        set_text,
        set_event,
        set_html,
        set_template_ref,
        insert_node,
        prepend_node,
        directive,
        if_node,
        for_node,
        create_component,
        slot_outlet,
        get_text_child,
        child_ref,
        next_ref,

        // Split result fields
        statics,

        // Declaration .d.ts fields
        dts,

        // Directive kinds
        v_show,
        v_model,

        // Component kinds
        regular,
        teleport,
        keep_alive,
        suspense,
        dynamic,
    }
}

// ── SFC Parsing ──

#[rustler::nif(schedule = "DirtyCpu")]
fn parse_sfc_nif<'a>(env: Env<'a>, source: &str) -> NifResult<Term<'a>> {
    let opts = SfcParseOptions::default();
    match parse_sfc(source, opts) {
        Ok(descriptor) => Ok(ok_term(
            env,
            EncodedParseSfcResult {
                descriptor: &descriptor,
            },
        )),
        Err(e) => Ok(error_term(env, format!("{e:?}"))),
    }
}

// ── SFC Analysis ──

#[rustler::nif(schedule = "DirtyCpu")]
fn analyze_sfc_nif<'a>(env: Env<'a>, source: &str, mode: &str) -> NifResult<Term<'a>> {
    let parse_opts = SfcParseOptions {
        filename: "component.vue".into(),
        ..Default::default()
    };

    let descriptor = match parse_sfc(source, parse_opts) {
        Ok(descriptor) => descriptor,
        Err(error) => return Ok(error_term(env, error.message.as_str())),
    };

    let allocator = Bump::new();
    let template_ast = descriptor.template.as_ref().map(|template| {
        let (root, _errors) = parse_with_options(
            &allocator,
            template.content.as_ref(),
            ParserOptions::default(),
        );
        root
    });

    let options = match mode {
        "lint" => SfcCroquisOptions::for_lint(),
        "compile" => SfcCroquisOptions::for_compile(),
        "declaration" => SfcCroquisOptions::for_declaration(),
        _ => SfcCroquisOptions::full(),
    };

    let croquis = analyze_sfc_descriptor(&descriptor, template_ast.as_ref(), options);
    let stats = croquis.stats();

    let bindings: Vec<Term<'a>> = croquis
        .bindings
        .iter()
        .map(|(name, binding_type)| {
            term_map!(env, {
                atoms::name() => name,
                atoms::kind() => format!("{:?}", binding_type),
            })
        })
        .collect();

    let props: Vec<Term<'a>> = croquis
        .get_props()
        .map(|(name, required)| {
            term_map!(env, {
                atoms::name() => name,
                atoms::required() => required,
            })
        })
        .collect();

    let emits: Vec<&str> = croquis.get_emits().collect();
    let models: Vec<&str> = croquis.get_models().collect();
    let used_components: Vec<&str> = croquis.used_components.iter().map(|s| s.as_str()).collect();
    let used_directives: Vec<&str> = croquis.used_directives.iter().map(|s| s.as_str()).collect();

    let undefined_refs: Vec<Term<'a>> = croquis
        .undefined_refs
        .iter()
        .map(|reference| {
            term_map!(env, {
                atoms::name() => reference.name.as_str(),
                atoms::offset() => reference.offset,
                atoms::context() => reference.context.as_str(),
            })
        })
        .collect();

    let component_usages: Vec<Term<'a>> = croquis
        .component_usages
        .iter()
        .map(|usage| {
            let props: Vec<Term<'a>> = usage
                .props
                .iter()
                .map(|prop| {
                    term_map!(env, {
                        atoms::name() => prop.name.as_str(),
                        atoms::value() => prop.value.as_ref().map(|v| v.as_str()),
                        atoms::is_dynamic() => prop.is_dynamic,
                    })
                })
                .collect();
            let events: Vec<Term<'a>> = usage
                .events
                .iter()
                .map(|event| {
                    term_map!(env, {
                        atoms::name() => event.name.as_str(),
                        atoms::handler() => event.handler.as_ref().map(|h| h.as_str()),
                    })
                })
                .collect();

            term_map!(env, {
                atoms::name() => usage.name.as_str(),
                atoms::props() => props,
                atoms::events() => events,
                atoms::has_spread_attrs() => usage.has_spread_attrs,
            })
        })
        .collect();

    let template_expressions: Vec<Term<'a>> = croquis
        .template_expressions
        .iter()
        .map(|expression| {
            term_map!(env, {
                atoms::source() => expression.content.as_str(),
                atoms::kind() => expression.kind.as_str(),
                atoms::range() => term_map!(env, {
                    atoms::start() => expression.start,
                    atoms::end_() => expression.end,
                }),
            })
        })
        .collect();

    let result = term_map!(env, {
        atoms::stats() => term_map!(env, {
            atoms::bindings() => stats.binding_count,
            atoms::props() => stats.prop_count,
            atoms::emits() => stats.emit_count,
            atoms::models() => stats.model_count,
            atoms::used_components() => stats.used_components,
            atoms::used_directives() => stats.used_directives,
            atoms::undefined_refs() => stats.undefined_ref_count,
        }),
        atoms::bindings() => bindings,
        atoms::props() => props,
        atoms::emits() => emits,
        atoms::models() => models,
        atoms::used_components() => used_components,
        atoms::used_directives() => used_directives,
        atoms::undefined_refs() => undefined_refs,
        atoms::component_usages() => component_usages,
        atoms::template_expressions() => template_expressions,
    });

    Ok(ok_term(env, result))
}

// ── SFC Compilation ──

#[rustler::nif(schedule = "DirtyCpu")]
#[allow(clippy::too_many_arguments)]
fn compile_sfc_nif<'a>(
    env: Env<'a>,
    source: &str,
    filename: &str,
    scope_id: &str,
    vapor: bool,
    ssr: bool,
    custom_renderer: bool,
    strip_types: bool,
) -> NifResult<Term<'a>> {
    let mut parse_opts = SfcParseOptions::default();
    if !filename.is_empty() {
        parse_opts.filename = filename.into();
    }

    let descriptor = match parse_sfc(source, parse_opts) {
        Ok(d) => d,
        Err(e) => return Ok(error_term(env, format!("{e:?}"))),
    };

    let mut compile_opts = SfcCompileOptions {
        vapor,
        template: vize_atelier_sfc::TemplateCompileOptions {
            ssr,
            custom_renderer,
            ..Default::default()
        },
        ..Default::default()
    };
    if !scope_id.is_empty() {
        compile_opts.scope_id = Some(scope_id.into());
    }
    if !filename.is_empty() {
        compile_opts.script.id = Some(filename.into());
    }

    match compile_sfc(&descriptor, compile_opts) {
        Ok(result) => {
            let stripped;
            let code_override = if strip_types {
                stripped = transform_typescript_to_js(result.code.as_str());
                Some(stripped.as_str())
            } else {
                None
            };

            Ok(ok_term(
                env,
                EncodedCompileSfcResult {
                    result: &result,
                    code_override,
                    template_hash: descriptor.template_hash(),
                    style_hash: descriptor.style_hash(),
                    script_hash: descriptor.script_hash(),
                },
            ))
        }
        Err(e) => Ok(error_term(env, e.message.as_str())),
    }
}

// ── Template Compilation ──

#[rustler::nif(schedule = "DirtyCpu")]
fn compile_template_nif<'a>(
    env: Env<'a>,
    source: &str,
    mode: &str,
    ssr: bool,
) -> NifResult<Term<'a>> {
    let allocator = Bump::new();
    let (mut root, errors) = parse(&allocator, source);

    if !errors.is_empty() {
        let msgs: Vec<std::string::String> = errors.iter().map(|e| e.message.to_string()).collect();
        return Ok(error_term(env, msgs));
    }

    let is_module = mode == "module";
    let transform_opts = TransformOptions {
        prefix_identifiers: is_module,
        ssr,
        ..Default::default()
    };
    transform(&allocator, &mut root, transform_opts, None);

    let codegen_opts = CodegenOptions {
        mode: if is_module {
            CodegenMode::Module
        } else {
            CodegenMode::Function
        },
        ssr,
        ..Default::default()
    };
    let result = vize_atelier_core::codegen::generate(&root, codegen_opts);

    let helpers: Vec<&str> = root.helpers.iter().map(|h| h.name()).collect();

    Ok(ok_term(
        env,
        EncodedTemplateCompileResult {
            code: result.code.as_str(),
            preamble: result.preamble.as_str(),
            helpers,
        },
    ))
}

// ── SSR Compilation ──

#[rustler::nif(schedule = "DirtyCpu")]
fn compile_ssr_nif<'a>(env: Env<'a>, source: &str) -> NifResult<Term<'a>> {
    let allocator = Bump::new();
    let (_root, errors, result) = compile_ssr(&allocator, source);

    if !errors.is_empty() {
        let msgs: Vec<std::string::String> = errors.iter().map(|e| e.message.to_string()).collect();
        return Ok(error_term(env, msgs));
    }

    Ok(ok_term(
        env,
        EncodedSsrCompileResult {
            code: result.code.as_str(),
            preamble: result.preamble.as_str(),
        },
    ))
}

// ── Vapor Compilation ──

fn encode_position<'a>(env: Env<'a>, position: &vize_atelier_core::Position) -> Term<'a> {
    term_map!(env, {
        atoms::offset() => position.offset,
        atoms::line() => position.line,
        atoms::column() => position.column,
    })
}

fn encode_source_location<'a>(
    env: Env<'a>,
    location: &vize_atelier_core::SourceLocation,
) -> Term<'a> {
    term_map!(env, {
        atoms::start() => encode_position(env, &location.start),
        atoms::end_() => encode_position(env, &location.end),
        atoms::source() => location.source.as_str(),
    })
}

fn encode_compiler_error<'a>(env: Env<'a>, error: &vize_atelier_core::CompilerError) -> Term<'a> {
    term_map!(env, {
        atoms::code() => format!("{:?}", error.code),
        atoms::message() => error.message.as_str(),
        atoms::recoverable() => error.is_recoverable(),
        atoms::location() => error
            .loc
            .as_ref()
            .map(|location| encode_source_location(env, location))
            .unwrap_or_else(|| nil_term(env)),
    })
}

fn template_syntax_mode(value: &str) -> TemplateSyntaxMode {
    match value {
        "quirks" => TemplateSyntaxMode::Quirks,
        _ => TemplateSyntaxMode::Standard,
    }
}

#[rustler::nif(schedule = "DirtyCpu")]
fn compile_vapor_nif<'a>(
    env: Env<'a>,
    source: &str,
    ssr: bool,
    diagnostics: bool,
    template_syntax: &str,
) -> NifResult<Term<'a>> {
    let allocator = Bump::new();
    let opts = VaporCompilerOptions {
        ssr,
        ..Default::default()
    };
    let syntax = template_syntax_mode(template_syntax);

    if diagnostics || syntax.is_quirks() {
        let (result, parser_diagnostics) =
            compile_vapor_with_template_syntax_and_diagnostics(&allocator, source, opts, syntax);

        let mut diagnostic_terms: Vec<Term<'a>> = parser_diagnostics
            .iter()
            .map(|diagnostic| encode_compiler_error(env, diagnostic))
            .collect();

        if !result.error_messages.is_empty() {
            let mut errors: Vec<Term<'a>> = result
                .error_messages
                .iter()
                .map(|message| {
                    term_map!(env, {
                        atoms::message() => message.as_str(),
                        atoms::recoverable() => false,
                    })
                })
                .collect();
            diagnostic_terms.append(&mut errors);
            return Ok(error_term(env, diagnostic_terms));
        }

        let templates: Vec<&str> = result.templates.iter().map(|s| s.as_str()).collect();
        let map = term_map!(env, {
            atoms::code() => result.code.as_str(),
            atoms::templates() => templates,
            atoms::diagnostics() => diagnostic_terms,
        });

        return Ok(ok_term(env, map));
    }

    let result = compile_vapor(&allocator, source, opts);

    if !result.error_messages.is_empty() {
        let msgs: Vec<&str> = result.error_messages.iter().map(|s| s.as_str()).collect();
        return Ok(error_term(env, msgs));
    }

    let templates: Vec<&str> = result.templates.iter().map(|s| s.as_str()).collect();

    let map = Term::map_from_arrays(
        env,
        &[atoms::code().encode(env), atoms::templates().encode(env)],
        &[result.code.as_str().encode(env), templates.encode(env)],
    )
    .unwrap();

    Ok(ok_term(env, map))
}

// ── Vapor IR ──

fn encode_operation<'a>(env: Env<'a>, op: &OperationNode) -> Term<'a> {
    match op {
        OperationNode::SetProp(node) => {
            let prop = encode_ir_prop(env, &node.prop);
            term_map!(env, {
                atoms::kind() => atoms::set_prop(),
                atoms::element() => node.element,
                atoms::tag() => node.tag.as_str(),
                atoms::camel() => node.camel,
                atoms::prop_modifier() => node.prop_modifier,
                atoms::value() => prop,
            })
        }
        OperationNode::SetDynamicProps(node) => {
            let props: Vec<Term<'a>> = node
                .props
                .iter()
                .map(|prop| encode_simple_expr(env, prop))
                .collect();
            term_map!(env, {
                atoms::kind() => atoms::set_dynamic_props(),
                atoms::element() => node.element,
                atoms::props() => props,
            })
        }
        OperationNode::SetText(node) => {
            let values: Vec<Term<'a>> = node
                .values
                .iter()
                .map(|value| encode_simple_expr(env, value))
                .collect();
            term_map!(env, {
                atoms::kind() => atoms::set_text(),
                atoms::element() => node.element,
                atoms::values() => values,
            })
        }
        OperationNode::SetEvent(node) => term_map!(env, {
            atoms::kind() => atoms::set_event(),
            atoms::element() => node.element,
            atoms::key() => encode_simple_expr(env, &node.key),
            atoms::value() => node
                .value
                .as_ref()
                .map(|value| encode_simple_expr(env, value))
                .unwrap_or_else(|| nil_term(env)),
            atoms::delegate() => node.delegate,
            atoms::effect() => node.effect,
        }),
        OperationNode::SetHtml(node) => term_map!(env, {
            atoms::kind() => atoms::set_html(),
            atoms::element() => node.element,
            atoms::value() => encode_simple_expr(env, &node.value),
        }),
        OperationNode::SetTemplateRef(node) => term_map!(env, {
            atoms::kind() => atoms::set_template_ref(),
            atoms::element() => node.element,
            atoms::value() => encode_simple_expr(env, &node.value),
        }),
        OperationNode::InsertNode(node) => {
            let elements: Vec<usize> = node.elements.clone();
            term_map!(env, {
                atoms::kind() => atoms::insert_node(),
                atoms::element() => elements,
                atoms::parent() => node.parent,
                atoms::anchor() => node.anchor,
            })
        }
        OperationNode::PrependNode(node) => {
            let elements: Vec<usize> = node.elements.clone();
            term_map!(env, {
                atoms::kind() => atoms::prepend_node(),
                atoms::element() => elements,
                atoms::parent() => node.parent,
            })
        }
        OperationNode::If(if_node) => encode_if_node(env, if_node),
        OperationNode::For(for_node) => encode_for_node(env, for_node),
        OperationNode::CreateComponent(node) => {
            let props: Vec<Term<'a>> = node
                .props
                .iter()
                .map(|prop| encode_ir_prop(env, prop))
                .collect();
            let kind_atom = match node.kind {
                ComponentKind::Regular => atoms::regular(),
                ComponentKind::Teleport => atoms::teleport(),
                ComponentKind::KeepAlive => atoms::keep_alive(),
                ComponentKind::Suspense => atoms::suspense(),
                ComponentKind::Dynamic => atoms::dynamic(),
            };
            term_map!(env, {
                atoms::kind() => atoms::create_component(),
                atoms::tag() => node.tag.as_str(),
                atoms::props() => props,
                atoms::asset() => node.asset,
                atoms::once() => node.once,
                atoms::dynamic_slots() => node.dynamic_slots,
                atoms::parent() => node.parent,
                atoms::anchor() => node.anchor,
                atoms::value() => kind_atom,
            })
        }
        OperationNode::SlotOutlet(node) => term_map!(env, {
            atoms::kind() => atoms::slot_outlet(),
            atoms::name() => encode_simple_expr(env, &node.name),
            atoms::props() => node
                .props
                .iter()
                .map(|prop| encode_ir_prop(env, prop))
                .collect::<Vec<_>>(),
        }),
        OperationNode::Directive(node) => {
            let exp = node
                .dir
                .exp
                .as_ref()
                .map(|expr| match expr {
                    vize_atelier_core::ExpressionNode::Simple(simple) => {
                        encode_simple_expr(env, simple)
                    }
                    vize_atelier_core::ExpressionNode::Compound(compound) => {
                        let content: std::string::String = compound
                            .children
                            .iter()
                            .map(|child| match child {
                                vize_atelier_core::CompoundExpressionChild::Simple(simple) => {
                                    simple.content.to_string()
                                }
                                vize_atelier_core::CompoundExpressionChild::String(string) => {
                                    string.to_string()
                                }
                                _ => std::string::String::new(),
                            })
                            .collect();
                        content.as_str().encode(env)
                    }
                })
                .unwrap_or_else(|| nil_term(env));

            term_map!(env, {
                atoms::kind() => atoms::directive(),
                atoms::element() => node.element,
                atoms::name() => node.name.as_str(),
                atoms::tag() => node.tag.as_str(),
                atoms::value() => exp,
            })
        }
        OperationNode::GetTextChild(node) => term_map!(env, {
            atoms::kind() => atoms::get_text_child(),
            atoms::parent() => node.parent,
        }),
        OperationNode::ChildRef(node) => term_map!(env, {
            atoms::kind() => atoms::child_ref(),
            atoms::child_id() => node.child_id,
            atoms::parent_id() => node.parent_id,
            atoms::offset() => node.offset,
        }),
        OperationNode::NextRef(node) => term_map!(env, {
            atoms::kind() => atoms::next_ref(),
            atoms::child_id() => node.child_id,
            atoms::parent_id() => node.prev_id,
            atoms::offset() => node.offset,
        }),
    }
}

fn encode_block<'a>(env: Env<'a>, block: &BlockIRNode) -> Term<'a> {
    let operations: Vec<Term<'a>> = block
        .operation
        .iter()
        .map(|operation| encode_operation(env, operation))
        .collect();

    let effects: Vec<Term<'a>> = block
        .effect
        .iter()
        .map(|effect| {
            effect
                .operations
                .iter()
                .map(|operation| encode_operation(env, operation))
                .collect::<Vec<_>>()
                .encode(env)
        })
        .collect();

    let returns: Vec<usize> = block.returns.iter().copied().collect();

    term_map!(env, {
        atoms::operations() => operations,
        atoms::effects() => effects,
        atoms::returns() => returns,
    })
}

fn encode_if_node<'a>(env: Env<'a>, if_node: &IfIRNode) -> Term<'a> {
    let negative = match &if_node.negative {
        Some(NegativeBranch::Block(block)) => encode_block(env, block),
        Some(NegativeBranch::If(nested)) => encode_if_node(env, nested),
        None => nil_term(env),
    };

    term_map!(env, {
        atoms::kind() => atoms::if_node(),
        atoms::condition() => encode_simple_expr(env, &if_node.condition),
        atoms::positive() => encode_block(env, &if_node.positive),
        atoms::negative() => negative,
        atoms::once() => if_node.once,
        atoms::parent() => if_node.parent,
        atoms::anchor() => if_node.anchor,
    })
}

fn encode_for_node<'a>(env: Env<'a>, for_node: &ForIRNode) -> Term<'a> {
    term_map!(env, {
        atoms::kind() => atoms::for_node(),
        atoms::source() => encode_simple_expr(env, &for_node.source),
        atoms::value() => for_node
            .value
            .as_ref()
            .map(|value| encode_simple_expr(env, value))
            .unwrap_or_else(|| nil_term(env)),
        atoms::key() => for_node
            .key
            .as_ref()
            .map(|key| encode_simple_expr(env, key))
            .unwrap_or_else(|| nil_term(env)),
        atoms::index() => for_node
            .index
            .as_ref()
            .map(|index| encode_simple_expr(env, index))
            .unwrap_or_else(|| nil_term(env)),
        atoms::key_prop() => for_node
            .key_prop
            .as_ref()
            .map(|key_prop| encode_simple_expr(env, key_prop))
            .unwrap_or_else(|| nil_term(env)),
        atoms::render() => encode_block(env, &for_node.render),
        atoms::once() => for_node.once,
        atoms::parent() => for_node.parent,
        atoms::anchor() => for_node.anchor,
    })
}

#[rustler::nif(schedule = "DirtyCpu")]
fn vapor_ir_nif<'a>(env: Env<'a>, source: &str) -> NifResult<Term<'a>> {
    let allocator = Bump::new();
    let parser_opts = ParserOptions::default();
    let (mut root, errors) = parse_with_options(&allocator, source, parser_opts);

    if !errors.is_empty() {
        let msgs: Vec<std::string::String> = errors.iter().map(|e| e.message.to_string()).collect();
        return Ok(error_term(env, msgs));
    }

    let transform_opts = TransformOptions {
        vapor: true,
        ..Default::default()
    };
    transform(&allocator, &mut root, transform_opts, None);

    let ir = transform_to_ir(&allocator, &root);

    let templates: Vec<&str> = ir.templates.iter().map(|s| s.as_str()).collect();
    let components: Vec<&str> = ir.component.iter().map(|s| s.as_str()).collect();
    let directives: Vec<&str> = ir.directive.iter().map(|s| s.as_str()).collect();

    let etm_keys: Vec<usize> = ir.element_template_map.keys().copied().collect();
    let etm_vals: Vec<usize> = etm_keys
        .iter()
        .map(|k| ir.element_template_map[k])
        .collect();
    let element_template_map: Vec<(usize, usize)> =
        etm_keys.into_iter().zip(etm_vals.into_iter()).collect();

    let map = Term::map_from_arrays(
        env,
        &[
            atoms::templates().encode(env),
            atoms::components().encode(env),
            atoms::directives().encode(env),
            atoms::block().encode(env),
            atoms::element_template_map().encode(env),
        ],
        &[
            templates.encode(env),
            components.encode(env),
            directives.encode(env),
            encode_block(env, &ir.block),
            element_template_map.encode(env),
        ],
    )
    .unwrap();

    Ok(ok_term(env, map))
}

// ── Linting ──

#[rustler::nif(schedule = "DirtyCpu")]
fn lint_nif<'a>(env: Env<'a>, source: &str, filename: &str) -> NifResult<Term<'a>> {
    use vize_patina::Linter;

    let linter = Linter::default();
    let result = linter.lint_sfc(source, filename);
    let diagnostics: Vec<Term<'a>> = result
        .diagnostics
        .iter()
        .map(|d| {
            EncodedLintDiagnostic {
                message: d.message.as_str(),
                name: d.rule_name,
            }
            .encode(env)
        })
        .collect();

    Ok(ok_term(env, diagnostics))
}

// ── CSS Compilation ──

fn css_targets(chrome: i64, firefox: i64, safari: i64) -> Option<CssTargets> {
    if chrome >= 0 || firefox >= 0 || safari >= 0 {
        Some(CssTargets {
            chrome: if chrome >= 0 {
                Some(chrome as u32)
            } else {
                None
            },
            firefox: if firefox >= 0 {
                Some(firefox as u32)
            } else {
                None
            },
            safari: if safari >= 0 {
                Some(safari as u32)
            } else {
                None
            },
            ..Default::default()
        })
    } else {
        None
    }
}

fn css_parser_options<'a>(
    filename: &'a str,
    custom_media: bool,
    css_modules: bool,
) -> CssParserOptions<'a, 'a> {
    let mut flags = CssParserFlags::NESTING | CssParserFlags::DEEP_SELECTOR_COMBINATOR;
    if custom_media {
        flags |= CssParserFlags::CUSTOM_MEDIA;
    }

    let css_modules_config = if css_modules {
        Some(lightningcss::css_modules::Config {
            pattern: lightningcss::css_modules::Pattern::default(),
            ..Default::default()
        })
    } else {
        None
    };

    CssParserOptions {
        filename: if filename.is_empty() {
            "style.css".into()
        } else {
            filename.into()
        },
        flags,
        css_modules: css_modules_config,
        ..Default::default()
    }
}

fn find_line_bounds(source: &str, line: u32) -> Option<(usize, usize)> {
    let mut current_line = 1_u32;
    let mut line_start = 0_usize;

    for (index, byte) in source.bytes().enumerate() {
        if byte == b'\n' {
            if current_line == line {
                return Some((line_start, index));
            }
            current_line += 1;
            line_start = index + 1;
        }
    }

    if current_line == line {
        Some((line_start, source.len()))
    } else {
        None
    }
}

fn find_url_range(source: &str, line: u32, column: u32, url: &str) -> Option<(usize, usize)> {
    let (line_start, line_end) = find_line_bounds(source, line)?;
    let line_source = &source[line_start..line_end];
    let target_column = column as usize;

    let match_start = line_source
        .match_indices(url)
        .map(|(index, _)| index)
        .min_by_key(|index| index.abs_diff(target_column))?;

    let start = line_start + match_start;
    Some((start, start + url.len()))
}

struct CssUrlEvent {
    url: String,
    start: usize,
    end: usize,
    start_line: u32,
    start_column: u32,
    end_line: u32,
    end_column: u32,
}

impl<'a> MatchEvent<'a> for &'a CssUrlEvent {
    fn tag(&self) -> Atom {
        atoms::css_url()
    }

    fn arity(&self) -> usize {
        8
    }

    fn positional_field(&self, index: usize) -> Option<ValueRef<'a>> {
        match index {
            1 => Some(ValueRef::Str(self.url.as_str())),
            2 => Some(ValueRef::U64(self.start as u64)),
            3 => Some(ValueRef::U64(self.end as u64)),
            4 => Some(ValueRef::U64(self.start_line.into())),
            5 => Some(ValueRef::U64(self.start_column.into())),
            6 => Some(ValueRef::U64(self.end_line.into())),
            7 => Some(ValueRef::U64(self.end_column.into())),
            _ => None,
        }
    }

    fn field(&self, name: Atom) -> Option<ValueRef<'a>> {
        if name == atoms::url() {
            Some(ValueRef::Str(self.url.as_str()))
        } else if name == atoms::start() {
            Some(ValueRef::U64(self.start as u64))
        } else if name == atoms::end_() {
            Some(ValueRef::U64(self.end as u64))
        } else if name == atoms::start_line() {
            Some(ValueRef::U64(self.start_line.into()))
        } else if name == atoms::start_column() {
            Some(ValueRef::U64(self.start_column.into()))
        } else if name == atoms::end_line() {
            Some(ValueRef::U64(self.end_line.into()))
        } else if name == atoms::end_column() {
            Some(ValueRef::U64(self.end_column.into()))
        } else {
            None
        }
    }
}

#[rustler::nif(schedule = "DirtyCpu")]
fn select_css_nif<'a>(
    env: Env<'a>,
    source: &str,
    filename: &str,
    custom_media: bool,
    css_modules: bool,
    selector_term: Term<'a>,
) -> NifResult<Term<'a>> {
    let selector = Selector::from_term(selector_term)?;

    let stylesheet = match StyleSheet::parse(
        source,
        css_parser_options(filename, custom_media, css_modules),
    ) {
        Ok(stylesheet) => stylesheet,
        Err(error) => return Ok(error_term(env, vec![format!("CSS parse error: {error}")])),
    };

    let result = match stylesheet.to_css(PrinterOptions {
        analyze_dependencies: Some(DependencyOptions {
            remove_imports: false,
        }),
        ..Default::default()
    }) {
        Ok(result) => result,
        Err(error) => return Ok(error_term(env, vec![format!("CSS print error: {error:?}")])),
    };

    let mut events = Vec::new();

    for dependency in result.dependencies.unwrap_or_default() {
        let Dependency::Url(dependency) = dependency else {
            continue;
        };

        let Some((start, end)) = find_url_range(
            source,
            dependency.loc.start.line,
            dependency.loc.start.column,
            &dependency.url,
        ) else {
            return Ok(error_term(
                env,
                vec![format!(
                    "Could not locate CSS URL range for {}",
                    dependency.url
                )],
            ));
        };

        events.push(CssUrlEvent {
            url: dependency.url.to_string(),
            start,
            end,
            start_line: dependency.loc.start.line,
            start_column: dependency.loc.start.column,
            end_line: dependency.loc.end.line,
            end_column: dependency.loc.end.column,
        });
    }

    let mut urls = Vec::new();

    for event in &events {
        selector.run_event(env, &event, &mut urls)?;
    }

    Ok(ok_term(env, urls))
}

#[rustler::nif(schedule = "DirtyCpu")]
fn parse_css_ast_nif<'a>(
    env: Env<'a>,
    source: &str,
    filename: &str,
    custom_media: bool,
    css_modules: bool,
) -> NifResult<Term<'a>> {
    let options = CssCompileOptions {
        filename: if filename.is_empty() {
            None
        } else {
            Some(filename.into())
        },
        custom_media,
        css_modules,
        ..Default::default()
    };

    let result = parse_css_ast(source, &options);

    Ok(ok_term(env, EncodedCssAstResult { result: &result }))
}

#[rustler::nif(schedule = "DirtyCpu")]
fn print_css_ast_nif<'a>(
    env: Env<'a>,
    ast: Term<'a>,
    minify: bool,
    chrome: i64,
    firefox: i64,
    safari: i64,
) -> NifResult<Term<'a>> {
    let ast = match decode_json_value(ast) {
        Ok(ast) => ast,
        Err(_) => {
            let result = vize_atelier_sfc::CssCompileResult {
                code: Default::default(),
                map: None,
                css_vars: vec![],
                errors: vec!["Invalid CSS AST term".into()],
                warnings: vec![],
                exports: None,
            };

            return Ok(ok_term(env, EncodedCssCompileResult { result: &result }));
        }
    };

    let options = CssCompileOptions {
        minify,
        targets: css_targets(chrome, firefox, safari),
        ..Default::default()
    };

    let result = print_css_ast(ast, &options);

    Ok(ok_term(env, EncodedCssCompileResult { result: &result }))
}

#[rustler::nif(schedule = "DirtyCpu")]
#[allow(clippy::too_many_arguments)]
fn compile_css_nif<'a>(
    env: Env<'a>,
    source: &str,
    minify: bool,
    scoped: bool,
    scope_id_str: &str,
    filename: &str,
    chrome: i64,
    firefox: i64,
    safari: i64,
    css_modules: bool,
) -> NifResult<Term<'a>> {
    let targets = if chrome >= 0 || firefox >= 0 || safari >= 0 {
        Some(CssTargets {
            chrome: if chrome >= 0 {
                Some(chrome as u32)
            } else {
                None
            },
            firefox: if firefox >= 0 {
                Some(firefox as u32)
            } else {
                None
            },
            safari: if safari >= 0 {
                Some(safari as u32)
            } else {
                None
            },
            ..Default::default()
        })
    } else {
        None
    };

    let options = CssCompileOptions {
        scope_id: if scope_id_str.is_empty() {
            None
        } else {
            Some(scope_id_str.into())
        },
        scoped,
        minify,
        source_map: false,
        targets,
        filename: if filename.is_empty() {
            None
        } else {
            Some(filename.into())
        },
        custom_media: false,
        css_modules,
    };

    let result = compile_css(source, &options);

    Ok(ok_term(env, EncodedCssCompileResult { result: &result }))
}

// ── CSS Bundling ──

#[rustler::nif(schedule = "DirtyCpu")]
fn bundle_css_nif<'a>(
    env: Env<'a>,
    entry_path: &str,
    minify: bool,
    chrome: i64,
    firefox: i64,
    safari: i64,
    css_modules: bool,
) -> NifResult<Term<'a>> {
    let targets = if chrome >= 0 || firefox >= 0 || safari >= 0 {
        Some(CssTargets {
            chrome: if chrome >= 0 {
                Some(chrome as u32)
            } else {
                None
            },
            firefox: if firefox >= 0 {
                Some(firefox as u32)
            } else {
                None
            },
            safari: if safari >= 0 {
                Some(safari as u32)
            } else {
                None
            },
            ..Default::default()
        })
    } else {
        None
    };

    let options = CssCompileOptions {
        minify,
        targets,
        css_modules,
        ..Default::default()
    };

    let result = bundle_css(entry_path, &options);

    Ok(ok_term(env, EncodedBundleCssResult { result: &result }))
}

#[rustler::nif(schedule = "DirtyCpu")]
fn vapor_split_nif<'a>(env: Env<'a>, source: &str) -> NifResult<Term<'a>> {
    let allocator = Bump::new();
    let parser_opts = ParserOptions::default();
    let (mut root, errors) = parse_with_options(&allocator, source, parser_opts);

    if !errors.is_empty() {
        let msgs: std::vec::Vec<std::string::String> =
            errors.iter().map(|e| e.message.to_string()).collect();
        return Ok(error_term(env, msgs));
    }

    let transform_opts = TransformOptions {
        vapor: true,
        ..Default::default()
    };
    transform(&allocator, &mut root, transform_opts, None);

    let ir = transform_to_ir(&allocator, &root);

    let (statics, slots) = process_block(env, &ir.block, &ir);

    let statics_term: std::vec::Vec<Term<'a>> =
        statics.iter().map(|s| s.as_str().encode(env)).collect();
    let templates: std::vec::Vec<&str> = ir.templates.iter().map(|s| s.as_str()).collect();
    let element_template_map: std::vec::Vec<(usize, usize)> = ir
        .element_template_map
        .iter()
        .map(|(&k, &v)| (k, v))
        .collect();

    let result = Term::map_from_arrays(
        env,
        &[
            atoms::statics().encode(env),
            atoms::slots().encode(env),
            atoms::templates().encode(env),
            atoms::element_template_map().encode(env),
        ],
        &[
            statics_term.encode(env),
            slots.encode(env),
            templates.encode(env),
            element_template_map.encode(env),
        ],
    )
    .unwrap();

    Ok(ok_term(env, result))
}

// ── Declaration .d.ts Generation ──

#[rustler::nif(schedule = "DirtyCpu")]
fn generate_dts_nif<'a>(env: Env<'a>, source: &str, filename: &str) -> NifResult<Term<'a>> {
    let parse_opts = SfcParseOptions {
        filename: if filename.is_empty() {
            "component.vue".into()
        } else {
            filename.into()
        },
        ..Default::default()
    };

    let descriptor = match parse_sfc(source, parse_opts) {
        Ok(d) => d,
        Err(e) => return Ok(error_term(env, format!("{e:?}"))),
    };

    let plain_script = descriptor.script.as_ref().map(|s| s.content.as_ref());
    let setup_script = descriptor.script_setup.as_ref().map(|s| s.content.as_ref());

    let summary = match setup_script {
        Some(content) => analyze_script_setup_to_summary(content),
        None => vize_croquis::Croquis::new(),
    };

    let output = match (plain_script, setup_script) {
        (Some(plain), Some(setup)) => {
            vize_croquis::declaration_ts::generate_declaration_ts_with_split_scripts(
                &summary, plain, setup,
            )
        }
        (_, Some(setup)) => {
            vize_croquis::declaration_ts::generate_declaration_ts(&summary, Some(setup))
        }
        (Some(plain), None) => {
            vize_croquis::declaration_ts::generate_declaration_ts(&summary, Some(plain))
        }
        (None, None) => vize_croquis::declaration_ts::generate_declaration_ts(&summary, None),
    };

    let result = term_map!(env, {
        atoms::dts() => output.content.as_str(),
    });

    Ok(ok_term(env, result))
}

rustler::init!("Elixir.Vize.Native");