//! Generator for raw transfer deserializer.

use std::{borrow::Cow, fmt::Debug, str};

use cow_utils::CowUtils;
use lazy_regex::{Captures, Lazy, Regex, lazy_regex, regex::Replacer};
use proc_macro2::TokenStream;
use quote::quote;
use rustc_hash::FxHashSet;

use oxc_allocator::Allocator;
use oxc_ast::{
    ast::{
        Expression, LogicalOperator, ObjectExpression, ObjectPropertyKind, Program, PropertyKey,
        PropertyKind,
    },
    builder::AstBuilder,
};
use oxc_ast_visit::{VisitMut, walk_mut};
use oxc_span::SPAN;

use crate::{
    ALLOCATOR_CRATE_PATH, Generator, NAPI_PARSER_PACKAGE_PATH, OXLINT_APP_PATH,
    codegen::{Codegen, DeriveId},
    derives::estree::{
        get_fieldless_variant_value, get_struct_field_name, should_flatten_field,
        should_skip_enum_variant, should_skip_field,
    },
    output::{Output, javascript::VariantGenerator},
    schema::{
        BoxDef, CellDef, Def, EnumDef, FieldDef, MetaType, OptionDef, PointerDef, PrimitiveDef,
        Schema, StructDef, TypeDef, TypeId, VecDef,
        extensions::layout::{GetLayout, GetOffset},
    },
    utils::{FxIndexMap, format_cow, number_lit, upper_case_first, write_it},
};

use super::define_generator;

/// Offset of length field in `&str`
const STR_LEN_OFFSET: u32 = 8;

/// Bytes reserved for `malloc`'s metadata
const MALLOC_RESERVED_SIZE: u32 = 16;

/// Minimum alignment of arena (`Arena::MIN_ALIGN`).
/// Code in `oxc_allocator` crate checks that this is correct.
const ARENA_CURSOR_MIN_ALIGN: u32 = 1;

/// Size of `ChunkFooter` struct.
/// Code in `oxc_allocator` crate checks that this is correct.
const CHUNK_FOOTER_SIZE: u32 = 48;

/// Minimum alignment requirement for end of `Allocator`'s chunk
const ALLOCATOR_CHUNK_END_ALIGN: u32 = 16;

/// Size of block of memory used for raw transfer.
/// This size includes metadata stored after the `Allocator` chunk which contains AST data.
///
/// Must be a multiple of [`ALLOCATOR_CHUNK_END_ALIGN`].
/// 16 bytes less than 2 GiB, to allow 16 bytes for `malloc` metadata.
const BLOCK_SIZE: u32 = (1 << 31) - MALLOC_RESERVED_SIZE; // 2 GiB - 16 bytes
const _: () = assert!(BLOCK_SIZE.is_multiple_of(ALLOCATOR_CHUNK_END_ALIGN));

/// Alignment of block of memory used for raw transfer.
const BLOCK_ALIGN: u64 = 1 << 32; // 4 GiB

// Offsets of `Vec`'s fields.
// `Vec` is `#[repr(transparent)]` and `RawVec` is `#[repr(C)]`, so these offsets are fixed.
pub(super) const VEC_PTR_FIELD_OFFSET: u32 = 0;
pub(super) const VEC_LEN_FIELD_OFFSET: u32 = 8;

/// Generator for raw transfer deserializer.
pub struct RawTransferGenerator;

define_generator!(RawTransferGenerator);

impl Generator for RawTransferGenerator {
    fn generate_many(&self, schema: &Schema, codegen: &Codegen) -> Vec<Output> {
        let consts = get_constants(schema);

        let deserializers = generate_deserializers(consts, schema, codegen);

        let (constants_js, constants_rust) = generate_constants(consts);

        let mut outputs = deserializers
            .into_iter()
            .map(|(path, code)| Output::Javascript { path, code })
            .collect::<Vec<_>>();

        outputs.extend([
            Output::Javascript {
                path: format!("{NAPI_PARSER_PACKAGE_PATH}/src-js/generated/constants.js"),
                code: constants_js.clone(),
            },
            Output::Javascript {
                // This file is also valid as TS
                path: format!("{OXLINT_APP_PATH}/src-js/generated/constants.ts"),
                code: constants_js,
            },
            Output::Rust {
                path: format!("{NAPI_PARSER_PACKAGE_PATH}/src/generated/raw_transfer_constants.rs"),
                tokens: constants_rust.clone(),
            },
            Output::Rust {
                path: format!("{OXLINT_APP_PATH}/src/generated/raw_transfer_constants.rs"),
                tokens: constants_rust.clone(),
            },
            Output::Rust {
                path: format!("{ALLOCATOR_CRATE_PATH}/src/generated/fixed_size_constants.rs"),
                tokens: constants_rust,
            },
        ]);

        outputs
    }
}

/// Generate deserializer functions for all types.
///
/// Generates a single file which is the base of both the JS and TS deserializers.
/// Code which is specific to JS or TS deserializer is gated by the `IS_TS` const,
/// code which adds `range` fields is gated by `RANGE` const.
/// e.g.:
/// * `if (IS_TS) node.typeAnnotation = null;`
/// * `return { type: 'Function', id, params, ...(IS_TS && { typeAnnotation: null }) };`
/// * `return { type: 'ThisExpression', start, end, ...(RANGE && { range: [start, end] }) };`
///
/// When printing the JS and TS deserializers, the value of `IS_TS` is set to `true` or `false`,
/// and minifier then shakes out the dead code for each.
#[expect(clippy::items_after_statements)]
fn generate_deserializers(
    consts: Constants,
    schema: &Schema,
    codegen: &Codegen,
) -> Vec<(/* path */ String, /* code */ String)> {
    let estree_derive_id = codegen.get_derive_id_by_name("ESTree");
    let span_type_id = schema.type_names["Span"];

    // Prelude to generated deserializer.
    // Defines the main `deserialize` function.
    let data_pointer_pos_32 = consts.data_pointer_pos / 4;

    #[rustfmt::skip]
    let mut code = format!("
        /* IF LINTER */
        import {{ tokens, initTokens }} from '../plugins/tokens.js';
        import {{ comments, initComments }} from '../plugins/comments.js';
        import {{ getNodeLoc }} from '../plugins/location.js';
        /* END_IF */

        let uint8, int32, float64, sourceText, sourceTextLatin,
            sourceStartPos = 0, firstNonAsciiPos = 0;

        let parent = null;

        const {{ fromCharCode }} = String,
            {{ utf8Slice, latin1Slice }} = Buffer.prototype;

        const STRING_DECODE_CROSSOVER = 64;

        // Arrays used by `deserializeStr` for passing to `String.fromCharCode`.
        // These arrays are reused over and over, avoiding allocating a new temporary array for each string.
        const stringDecodeArrays = new Array(STRING_DECODE_CROSSOVER + 1).fill(null);
        for (let i = 0; i <= STRING_DECODE_CROSSOVER; i++) {{
            stringDecodeArrays[i] = new Array(i).fill(0);
        }}

        /* IF LINTER */
        const NodeProto = Object.create(Object.prototype, {{
            loc: {{
                // Note: Not configurable
                get() {{
                    return getNodeLoc(this);
                }},
                enumerable: true,
            }}
        }});
        /* END_IF */

        /* IF !LINTER */
        export function deserialize(buffer, sourceText, sourceStartPos, sourceByteLen) {{
            return deserializeWith(buffer, sourceText, sourceStartPos, sourceByteLen, deserializeRawTransferData);
        }}
        /* END_IF */

        /* IF LINTER */
        export function deserializeProgramOnly(buffer, sourceText, sourceStartPos, sourceByteLen) {{
            return deserializeWith(buffer, sourceText, sourceStartPos, sourceByteLen, deserializeProgram);
        }}
        /* END_IF */

        function deserializeWith(buffer, sourceTextInput, sourceStartPosInput, sourceByteLen, deserialize) {{
            uint8 = buffer;
            int32 = buffer.int32;
            float64 = buffer.float64;

            sourceText = sourceTextInput;
            sourceStartPos = sourceStartPosInput;

            const sourceIsAscii = sourceText.length === sourceByteLen;

            // Find first non-ASCII byte in source region.
            // `sourceText.substr()` can be used for strings which are within source text and ending before
            // this position, since byte offsets equal char offsets in the all-ASCII prefix.
            // Also decode source text as Latin-1 (or reuse `sourceText` if it's all ASCII).
            if (sourceIsAscii === true) {{
                firstNonAsciiPos = sourceStartPos + sourceByteLen;
                sourceTextLatin = sourceText;
            }} else {{
                let i = sourceStartPos;
                const sourceEndPos = sourceStartPos + sourceByteLen;
                for (; i < sourceEndPos && uint8[i] < 128; i++);
                firstNonAsciiPos = i;

                sourceTextLatin = latin1Slice.call(uint8, sourceStartPos, sourceEndPos);
            }}

            const data = deserialize(int32[{data_pointer_pos_32}]);
            resetBuffer();
            return data;
        }}

        export function resetBuffer() {{
            // Clear buffer and source text strings to allow them to be garbage collected
            uint8 = int32 = float64 = sourceText = sourceTextLatin = undefined;
        }}
    ");

    // Type definition for deserialize.js (linter variant)
    #[rustfmt::skip]
    let code_type_definition_linter = "
        import type { Program } from './types.d.ts';

        type BufferWithArrays = Uint8Array & {
            int32: Int32Array;
            float64: Float64Array;
        };

        export declare function deserializeProgramOnly(
            buffer: BufferWithArrays,
            sourceText: string,
            sourceStartPosInput: number,
            sourceByteLen: number,
        ): Program;

        export declare function resetBuffer(): void;
    ".to_string();

    // Type definition for deserialize variants (parser)
    #[rustfmt::skip]
    let code_type_definition_parser = "
        import type * as ESTree from '@oxc-project/types';

        type BufferWithArrays = Uint8Array & {
            int32: Int32Array;
            float64: Float64Array;
        };

        export declare function deserialize(
            buffer: BufferWithArrays,
            sourceText: string,
            sourceByteLen: number
        ): ESTree.Program;

        export declare function resetBuffer(): void;
    ".to_string();

    for type_def in &schema.types {
        match type_def {
            TypeDef::Struct(struct_def) => {
                generate_struct(struct_def, &mut code, span_type_id, estree_derive_id, schema);
            }
            TypeDef::Enum(enum_def) => {
                generate_enum(enum_def, &mut code, estree_derive_id, schema);
            }
            TypeDef::Primitive(primitive_def) => {
                generate_primitive(primitive_def, &mut code, schema);
            }
            TypeDef::Option(option_def) => {
                generate_option(option_def, &mut code, estree_derive_id, schema);
            }
            TypeDef::Box(box_def) => {
                generate_box(box_def, &mut code, estree_derive_id, schema);
            }
            TypeDef::Vec(vec_def) => {
                generate_vec(vec_def, &mut code, estree_derive_id, schema);
            }
            TypeDef::Cell(_cell_def) => {
                // No deserializers for `Cell`s - use inner type's deserializer
            }
            TypeDef::Pointer(_pointer_def) => {
                // No deserializers for pointers - use `Box`'s deserializer.
                // TODO: Need to make sure deserializer for `Box<T>` is generated.
            }
        }
    }

    // Create deserializers with various settings, by setting `IS_TS`, `RANGE`, `LOC`, `PARENT`,
    // and `PRESERVE_PARENS` consts, and running through minifier to shake out
    // irrelevant code
    struct VariantGen {
        variant_paths: Vec<String>,
    }

    impl VariantGenerator<6> for VariantGen {
        const FLAG_NAMES: [&str; 6] =
            ["IS_TS", "RANGE", "LOC", "PARENT", "PRESERVE_PARENS", "LINTER"];

        fn variants(&mut self) -> Vec<[bool; 6]> {
            let mut variants = Vec::with_capacity(9);

            // Parser deserializers
            for is_ts in [false, true] {
                for range in [false, true] {
                    for parent in [false, true] {
                        self.variant_paths.push(format!(
                            "{NAPI_PARSER_PACKAGE_PATH}/src-js/generated/deserialize/{}{}{}.js",
                            if is_ts { "ts" } else { "js" },
                            if range { "_range" } else { "" },
                            if parent { "_parent" } else { "" },
                        ));

                        variants.push([
                            is_ts, range, /* loc */ false, parent,
                            /* preserve_parens */ true, /* linter */ false,
                        ]);
                    }
                }
            }

            // Linter deserializer
            self.variant_paths.push(format!("{OXLINT_APP_PATH}/src-js/generated/deserialize.js"));
            variants.push([
                /* is_ts */ true, /* range */ true, /* loc */ true,
                /* parent */ true, /* preserve_parens */ false, /* linter */ true,
            ]);

            variants
        }

        fn pre_process_variant<'a>(
            &self,
            program: &mut Program<'a>,
            flags: [bool; 6],
            allocator: &'a Allocator,
        ) {
            if flags[2] {
                // `loc` enabled
                LocFieldAdder::new(allocator).visit_program(program);
            }
        }
    }

    let mut generator = VariantGen { variant_paths: vec![] };
    let codes = generator.generate(&code);

    let mut outputs: Vec<(String, String)> =
        generator.variant_paths.into_iter().zip(codes).collect();

    // Add type definition files for parser variants
    for is_ts in [false, true] {
        for range in [false, true] {
            for parent in [false, true] {
                outputs.push((
                    format!(
                        "{NAPI_PARSER_PACKAGE_PATH}/src-js/generated/deserialize/{}{}{}.d.ts",
                        if is_ts { "ts" } else { "js" },
                        if range { "_range" } else { "" },
                        if parent { "_parent" } else { "" },
                    ),
                    code_type_definition_parser.clone(),
                ));
            }
        }
    }

    // Add type definition file for linter
    outputs.push((
        format!("{OXLINT_APP_PATH}/src-js/generated/deserialize.d.ts"),
        code_type_definition_linter,
    ));

    outputs
}

/// Type of deserializer in which some code appears.
#[derive(Clone, Copy, PartialEq, Eq)]
enum DeserializerType {
    Both,
    JsOnly,
    TsOnly,
}

/// Generate deserialize function for a struct.
fn generate_struct(
    struct_def: &StructDef,
    code: &mut String,
    span_type_id: TypeId,
    estree_derive_id: DeriveId,
    schema: &Schema,
) {
    if !struct_def.generates_derive(estree_derive_id) || struct_def.estree.skip {
        return;
    }

    let fn_name = struct_def.deser_name(schema);
    let mut generator = StructDeserializerGenerator::new(span_type_id, schema);

    let body = struct_def.estree.via.as_deref().and_then(|converter_name| {
        let converter = schema.meta_by_name(converter_name);
        generator.apply_converter(converter, struct_def, 0).map(|(value, _)| {
            let preamble = if generator.inline_preamble.is_empty() {
                generator.assignments_preamble.as_slice()
            } else {
                generator.inline_preamble.as_slice()
            };

            if preamble.is_empty() {
                format!("return {value};")
            } else {
                let preamble = preamble.join("");
                format!(
                    "
                        {preamble}
                        return {value};
                    "
                )
            }
        })
    });

    let body = body.unwrap_or_else(|| {
        let mut inline_preamble_str = String::new();
        let mut fields_str = String::new();
        let mut assignments_preamble_str = String::new();
        let mut assignments_str = String::new();

        generator.generate_struct_fields(struct_def, 0, DeserializerType::Both);

        let needs_parent_field =
            generator.fields.contains_key("type") && !struct_def.estree.no_parent;

        let mut all_fields_inline = true;
        for (field_name, StructFieldValue { value, deser_type, inline }) in generator.fields {
            if let Some(value) = value.strip_prefix("...") {
                assert!(inline, "Spread fields must be inlined");
                match deser_type {
                    DeserializerType::Both => write_it!(fields_str, "...{value},"),
                    DeserializerType::JsOnly => write_it!(fields_str, "...(!IS_TS && {value}),"),
                    DeserializerType::TsOnly => write_it!(fields_str, "...(IS_TS && {value}),"),
                }
            } else if inline || !needs_parent_field {
                let value: Cow<str> =
                    if generator.inline_assignment_field_names.contains(&field_name) {
                        // Use inline assignment pattern: `let x; return { x: x = value };`
                        // This allows minifiers to eliminate the variable when RANGE is false.
                        write_it!(inline_preamble_str, "let {field_name};\n");
                        Cow::Owned(format!("{field_name} = {value}"))
                    } else if generator.dependent_field_names.contains(&field_name) {
                        write_it!(inline_preamble_str, "const {field_name} = {value};\n");
                        Cow::Borrowed(field_name.as_str())
                    } else {
                        Cow::Borrowed(value.as_str())
                    };

                if deser_type == DeserializerType::Both {
                    write_it!(fields_str, "{field_name}: {value},");
                } else {
                    let condition = match deser_type {
                        DeserializerType::JsOnly => "!IS_TS",
                        DeserializerType::TsOnly => "IS_TS",
                        DeserializerType::Both => unreachable!(),
                    };
                    write_it!(fields_str, "...({condition} && {{ {field_name}: {value} }}),");
                }
            } else {
                all_fields_inline = false;

                let value = if generator.dependent_field_names.contains(&field_name) {
                    write_it!(assignments_preamble_str, "const {field_name} = {value};\n");
                    &field_name
                } else {
                    &value
                };

                if deser_type == DeserializerType::Both {
                    write_it!(fields_str, "{field_name}: null,");
                    if value != "null" {
                        write_it!(assignments_str, "node.{field_name} = {value};");
                    }
                } else {
                    let condition = match deser_type {
                        DeserializerType::JsOnly => "!IS_TS",
                        DeserializerType::TsOnly => "IS_TS",
                        DeserializerType::Both => unreachable!(),
                    };
                    write_it!(fields_str, "...({condition} && {{ {field_name}: null }}),");
                    if value != "null" {
                        write_it!(assignments_str, "if ({condition}) node.{field_name} = {value};");
                    }
                }
            }
        }

        for preamble_part in generator.inline_preamble {
            inline_preamble_str.push_str(preamble_part.trim());
        }

        for preamble_part in generator.assignments_preamble {
            assignments_preamble_str.push_str(preamble_part.trim());
        }

        let mut parent_assignment_str = "";
        if needs_parent_field {
            fields_str.push_str("...(PARENT && { parent }),\n");

            if !all_fields_inline {
                inline_preamble_str.push_str("const previousParent = parent;\n");
                parent_assignment_str = "parent = ";
                assignments_str.push_str("if (PARENT) parent = previousParent;\n");
            }
        }

        format!(
            "
            {inline_preamble_str}
            const node = {parent_assignment_str} {{
                {fields_str}
            }};
            {assignments_preamble_str}
            {assignments_str}
            return node;
        "
        )
    });

    #[rustfmt::skip]
    write_it!(code, "
        function {fn_name}(pos) {{
            {body}
        }}
    ");
}

struct StructDeserializerGenerator<'s> {
    /// Dependencies - fields that must be computed before they're used
    dependent_field_names: FxHashSet<String>,
    /// Fields that should use inline assignment pattern: `let x; return { x: x = value };`
    /// This allows minifiers to eliminate unused variables when RANGE is false.
    inline_assignment_field_names: FxHashSet<String>,
    /// Preamble before object definition
    inline_preamble: Vec<String>,
    /// Preamble before assignments
    assignments_preamble: Vec<String>,
    /// Fields, keyed by fields name (field name in ESTree AST)
    fields: FxIndexMap<String, StructFieldValue>,
    /// `TypeId` for `Span`
    span_type_id: TypeId,
    /// Schema
    schema: &'s Schema,
}

struct StructFieldValue {
    /// Value of the field
    value: String,
    /// Which deserializer(s) should include this field
    deser_type: DeserializerType,
    /// `true` if value can be inlined in object definition
    inline: bool,
}

impl<'s> StructDeserializerGenerator<'s> {
    fn new(span_type_id: TypeId, schema: &'s Schema) -> Self {
        Self {
            dependent_field_names: FxHashSet::default(),
            inline_assignment_field_names: FxHashSet::default(),
            inline_preamble: vec![],
            assignments_preamble: vec![],
            fields: FxIndexMap::default(),
            span_type_id,
            schema,
        }
    }

    fn generate_struct_fields(
        &mut self,
        struct_def: &StructDef,
        struct_offset: u32,
        deser_type: DeserializerType,
    ) {
        for &field_index in &struct_def.estree.field_indices {
            let field_index = field_index as usize;
            if let Some(field) = struct_def.fields.get(field_index) {
                self.generate_struct_field_owned(field, struct_def, struct_offset, deser_type);
            } else {
                let (field_name, converter_name) =
                    &struct_def.estree.add_fields[field_index - struct_def.fields.len()];
                self.generate_struct_field_added(
                    struct_def,
                    field_name,
                    converter_name,
                    struct_offset,
                    deser_type,
                );
            }
        }

        // Add `type` field if there isn't one already, and struct isn't marked `#[estree(no_type)]`
        if !struct_def.estree.no_type && !self.fields.contains_key("type") {
            let struct_name =
                struct_def.estree.rename.as_deref().unwrap_or_else(|| struct_def.name());
            self.fields.insert_before(
                0,
                "type".to_string(),
                StructFieldValue {
                    value: format!("'{struct_name}'"),
                    deser_type: DeserializerType::Both,
                    inline: true,
                },
            );
        }
    }

    fn generate_struct_field_owned(
        &mut self,
        field: &FieldDef,
        struct_def: &StructDef,
        struct_offset: u32,
        mut deser_type: DeserializerType,
    ) {
        if should_skip_field(field, self.schema) {
            return;
        }

        if field.estree.is_js {
            deser_type = DeserializerType::JsOnly;
        } else if field.estree.is_ts {
            deser_type = DeserializerType::TsOnly;
        }

        let field_name = get_struct_field_name(field).to_string();
        let field_type_id = field.type_id;
        let field_type = &self.schema.types[field_type_id];
        let field_offset = struct_offset + field.offset_64();

        if should_flatten_field(field, self.schema) {
            let TypeDef::Struct(field_struct_def) = field_type else {
                panic!(
                    "Cannot flatten a field which is not a struct: `{}::{}`",
                    struct_def.name(),
                    field.name(),
                );
            };

            self.generate_struct_fields(field_struct_def, field_offset, deser_type);

            if field_type_id == self.span_type_id {
                self.fields.insert(
                    "range".to_string(),
                    StructFieldValue {
                        value: "...(RANGE && { range: [start, end] })".to_string(),
                        deser_type,
                        inline: true,
                    },
                );

                // Use inline assignment for `start` and `end` so minifiers can eliminate
                // them when RANGE is false. Pattern: `let start, end; { start: start = v1, end: end = v2 }`
                // But only if no other field depends on them (e.g., `Comment.value` uses `THIS.start`).
                for name in ["start", "end"] {
                    if !self.dependent_field_names.contains(name) {
                        self.inline_assignment_field_names.insert(name.to_string());
                    }
                }
            }

            return;
        }

        // Get fields to concatenate
        // (if fields marked `#[estree(prepend_to)]` or `#[estree(append_to)]` targeting this field)
        let mut concat_fields = [field; 3];
        let mut concat_field_count = 1;
        if let Some(prepend_field_index) = field.estree.prepend_field_index {
            concat_fields[0] = &struct_def.fields[prepend_field_index];
            concat_field_count = 2;
        }
        if let Some(append_field_index) = field.estree.append_field_index {
            concat_fields[concat_field_count] = &struct_def.fields[append_field_index];
            concat_field_count += 1;
        }

        let mut inline = false;
        let value = if concat_field_count > 1 {
            // Concatenate fields
            for (index, &field) in concat_fields[..concat_field_count].iter().enumerate() {
                let field_pos = pos_offset(struct_offset + field.offset_64());
                match field.type_def(self.schema) {
                    TypeDef::Vec(vec_def) => {
                        let field_fn = vec_def.deser_name(self.schema);
                        if index == 0 {
                            self.assignments_preamble
                                .push(format!("const {field_name} = {field_fn}({field_pos});"));
                        } else {
                            self.assignments_preamble
                                .push(format!("{field_name}.push(...{field_fn}({field_pos}));"));
                        }
                    }
                    TypeDef::Option(option_def) => {
                        let option_field_name = get_struct_field_name(field).to_string();
                        let field_fn = option_def.deser_name(self.schema);
                        self.assignments_preamble
                            .push(format!("const {option_field_name} = {field_fn}({field_pos});"));
                        if index == 0 {
                            self.assignments_preamble.push(format!(
                                "const {field_name} = {option_field_name} === null ? [] : [{option_field_name}];"
                            ));
                        } else {
                            self.assignments_preamble.push(format!(
                                "if ({option_field_name} !== null) {field_name}.push({option_field_name});"
                            ));
                        }
                    }
                    _ => panic!("Cannot append: `{}::{}`", struct_def.name(), field.name()),
                }
            }

            field_name.clone()
        } else if field.estree.from_span {
            // Derive value from `sourceText.slice(start, end)` instead of deserializing.
            // Field must be `Str`, `Ident`, `&str`, or `Option` containing one of those types.
            let (inner_type, prefix) = if let TypeDef::Option(option_def) = field_type {
                let inner_type = option_def.inner_type(self.schema);
                let (none_condition, _) =
                    get_option_none_condition_and_offset(option_def, inner_type, field_offset);
                (inner_type, format!("({none_condition}) ? null : "))
            } else {
                (field_type, String::new())
            };

            if inner_type.as_primitive().is_none_or(|primitive_def| {
                !matches!(primitive_def.name(), "Str" | "Ident" | "&str")
            }) {
                panic!(
                    "`#[estree(from_span)]` can only be on a field of type `Str`, `Ident`, `&str`, or `Option` containing one of those types: `{}::{}`",
                    struct_def.name(),
                    field.name(),
                );
            }

            // Check that struct has a `span: Span` field, and that it is flattened, so `start` and `end` are available
            assert!(
                struct_def.fields.iter().any(|field| {
                    field.type_id == self.span_type_id
                        && field.name() == "span"
                        && should_flatten_field(field, self.schema)
                }),
                "`#[estree(from_span)]` can only be used on a field of a struct with a flattened `span: Span` field: `{}::{}`",
                struct_def.name(),
                field.name(),
            );

            inline = true;
            self.dependent_field_names.insert("start".to_string());
            self.dependent_field_names.insert("end".to_string());

            format!("{prefix}sourceText.slice(start, end)")
        } else if let Some(converter_name) = &field.estree.via {
            let converter = self.schema.meta_by_name(converter_name);
            let (value, can_inline) =
                self.apply_converter(converter, struct_def, struct_offset).unwrap();
            inline = can_inline;
            value
        } else {
            // Primitives and fieldless enums can be inlined into object literal,
            // because they don't have a `parent` field
            inline = match field_type.innermost_type(self.schema) {
                TypeDef::Primitive(_) => true,
                TypeDef::Enum(enum_def) => enum_def.is_fieldless(),
                _ => false,
            };

            let value_fn = field_type.deser_name(self.schema);
            let pos = pos_offset(field_offset);
            format!("{value_fn}({pos})")
        };

        self.fields.insert(field_name, StructFieldValue { value, deser_type, inline });
    }

    fn generate_struct_field_added(
        &mut self,
        struct_def: &StructDef,
        field_name: &str,
        converter_name: &str,
        struct_offset: u32,
        mut deser_type: DeserializerType,
    ) {
        let converter = self.schema.meta_by_name(converter_name);

        if converter.estree.is_js {
            deser_type = DeserializerType::JsOnly;
        } else if converter.estree.is_ts {
            deser_type = DeserializerType::TsOnly;
        }

        let (value, inline) = self.apply_converter(converter, struct_def, struct_offset).unwrap();
        self.fields.insert(field_name.to_string(), StructFieldValue { value, deser_type, inline });
    }

    fn apply_converter(
        &mut self,
        converter: &MetaType,
        struct_def: &StructDef,
        struct_offset: u32,
    ) -> Option<(String, bool)> {
        let raw_deser = converter.estree.raw_deser.as_deref()?;
        let inline = converter.estree.raw_deser_inline;

        let value = THIS_REGEX.replace_all(raw_deser, ThisReplacer::new(self));
        let value = DESER_REGEX.replace_all(&value, DeserReplacer::new(self.schema));
        let value = POS_OFFSET_REGEX
            .replace_all(&value, PosOffsetReplacer::new(self, struct_def, struct_offset));
        let value = POS_REGEX.replace_all(&value, PosReplacer::new(struct_offset));
        let value = value.cow_replace("SOURCE_TEXT", "sourceText");

        let value = if let Some((preamble, value)) = value.trim().rsplit_once('\n') {
            if inline {
                self.inline_preamble.push(preamble.to_string());
            } else {
                self.assignments_preamble.push(preamble.to_string());
            }
            value.trim().to_string()
        } else {
            value.trim().to_string()
        };
        Some((value, inline))
    }
}

/// Generate deserialize function for an enum.
fn generate_enum(
    enum_def: &EnumDef,
    code: &mut String,
    estree_derive_id: DeriveId,
    schema: &Schema,
) {
    if !enum_def.generates_derive(estree_derive_id) || enum_def.estree.skip {
        return;
    }

    let type_name = enum_def.name();
    let fn_name = enum_def.deser_name(schema);
    let payload_offset = enum_def.layout_64().align;

    let body = if let Some(converter_name) = &enum_def.estree.via {
        apply_converter_for_enum(converter_name, 0, schema)
    } else {
        None
    };

    let body = body.unwrap_or_else(|| {
        let mut variants = enum_def
            .all_variants(schema)
            .filter(|variant| !should_skip_enum_variant(variant))
            .collect::<Vec<_>>();
        variants.sort_by_key(|variant| variant.discriminant);

        let mut switch_cases = String::new();
        for variant in variants {
            write_it!(switch_cases, "case {}: ", variant.discriminant);

            if let Some(converter_name) = &variant.estree.via {
                let ret = apply_converter_for_enum(converter_name, payload_offset, schema).unwrap();
                switch_cases.push_str(&ret);
            } else if let Some(variant_type) = variant.field_type(schema) {
                let variant_fn_name = variant_type.deser_name(schema);
                let payload_pos = pos_offset(payload_offset);
                write_it!(switch_cases, "return {variant_fn_name}({payload_pos});");
            } else {
                write_it!(
                    switch_cases,
                    "return '{}';",
                    get_fieldless_variant_value(enum_def, variant)
                );
            }
        }

        format!(
            "
            switch(uint8[pos]) {{
                {switch_cases}
                default: throw new Error(`Unexpected discriminant ${{uint8[pos]}} for {type_name}`);
            }}
            "
        )
    });

    #[rustfmt::skip]
    write_it!(code, "
        function {fn_name}(pos) {{
            {body}
        }}
    ");
}

/// Generate deserialize function for a primitive.
fn generate_primitive(primitive_def: &PrimitiveDef, code: &mut String, schema: &Schema) {
    #[expect(clippy::match_same_arms)]
    let ret = match primitive_def.name() {
        // Reuse deserializer for `&str`
        "Str" | "Ident" => return,
        // Dummy type
        "PointerAlign" => return,
        "bool" => "return uint8[pos] === 1;",
        "u8" => "return uint8[pos];",
        // "u16" => "return uint16[pos >> 1];",
        "u32" => "return int32[pos >> 2] >>> 0;",
        "i32" => "return int32[pos >> 2];",
        // Will be approximate if larger than `Number.MAX_SAFE_INTEGER`
        #[rustfmt::skip]
        "u64" => "
            const pos32 = pos >> 2;
            return (int32[pos32] >>> 0)
                + (int32[pos32 + 1] >>> 0) * /* 2^32 */ 4294967296;
        ",
        // Will be approximate if larger than `Number.MAX_SAFE_INTEGER`
        #[rustfmt::skip]
        "u128" => "
            const pos32 = pos >> 2;
            return (int32[pos32] >>> 0)
                + (int32[pos32 + 1] >>> 0) * /* 2^32 */ 4294967296
                + (int32[pos32 + 2] >>> 0) * /* 2^64 */ 18446744073709551616
                + (int32[pos32 + 3] >>> 0) * /* 2^96 */ 79228162514264337593543950336;
        ",
        "f64" => "return float64[pos >> 3];",
        "&str" => STR_DESERIALIZER_BODY,
        // Reuse deserializers for zeroed and atomic types
        type_name if type_name.starts_with("NonZero") => return,
        type_name if type_name.starts_with("Atomic") => return,
        type_name => panic!("Cannot generate deserializer for primitive `{type_name}`"),
    };

    let fn_name = primitive_def.deser_name(schema);

    #[rustfmt::skip]
    write_it!(code, "
        function {fn_name}(pos) {{
            {ret}
        }}
    ");
}

// Source text is towards the end of the buffer, and all other strings are before it
static STR_DESERIALIZER_BODY: &str = "
    const pos32 = pos >> 2,
        len = int32[pos32 + 2];

    if (len === 0) return '';

    pos = int32[pos32];

    const end = pos + len;

    // Note: Tried reducing this check to a single branch by making the comparison the equivalent of this Rust:
    // `end.wrapping_sub(sourceStartPos) <= firstNonAsciiOffset`.
    //
    // The JS versions tried were:
    // - `((end - sourceStartPos) >>> 0) <= firstNonAsciiOffset`
    // - `((end - sourceStartPos) & 0x7FFF_FFFF) <= firstNonAsciiOffset`
    // But it turned out that these are both slower by 5-10% on files which are all ASCII.
    //
    // `>>>` is slower as V8 can't assume result fits in an SMI (which is a 32-bit *signed* integer),
    // as result could be greater or equal to `2 ** 31`. So it converts both the comparison's operands to `float64`s
    // and does float compare (which is slower than integer compare).
    //
    // `& 0x7FFF_FFFF` is slower as it has a longer chain of data dependencies than the 2 independent
    // branch comparisons.
    //
    // Both branches are very predictable, so 2 branches wins.
    const isInSourceRegion = pos >= sourceStartPos;
    if (isInSourceRegion && end <= firstNonAsciiPos) {
        return sourceTextLatin.substr(pos - sourceStartPos, len);
    }

    // Use `utf8Slice` for strings longer than 64 bytes
    if (len > STRING_DECODE_CROSSOVER) return utf8Slice.call(uint8, pos, end);

    // If string is in source region, use slice of `sourceTextLatin` if all ASCII
    if (isInSourceRegion) {
        // Check if all bytes are ASCII, use `utf8Slice` if not
        for (let i = pos; i < end; i++) {
            if (uint8[i] >= 128) return utf8Slice.call(uint8, pos, end);
        }

        // String is all ASCII, so slice from `sourceTextLatin`
        return sourceTextLatin.substr(pos - sourceStartPos, len);
    }

    // String is not in source region - use `fromCharCode.apply` with a temp array of correct length.
    // Copy bytes into temp array.
    // If any byte is non-ASCII, use `utf8Slice`.
    const arr = stringDecodeArrays[len];
    for (let i = 0; i < len; i++) {
        const b = uint8[pos + i];
        if (b >= 128) return utf8Slice.call(uint8, pos, end);
        arr[i] = b;
    }

    // Call `fromCharCode` with temp array
    return fromCharCode.apply(null, arr);
";

/// Generate deserialize function for an `Option`.
fn generate_option(
    option_def: &OptionDef,
    code: &mut String,
    estree_derive_id: DeriveId,
    schema: &Schema,
) {
    let inner_type = option_def.inner_type(schema);
    if should_skip_innermost_type(inner_type, estree_derive_id, schema) {
        return;
    }

    let fn_name = option_def.deser_name(schema);
    let inner_fn_name = inner_type.deser_name(schema);

    let (none_condition, payload_offset) =
        get_option_none_condition_and_offset(option_def, inner_type, 0);

    #[rustfmt::skip]
    write_it!(code, "
        function {fn_name}(pos) {{
            return ({none_condition}) ? null : {inner_fn_name}({payload_offset});
        }}
    ");
}

/// Get condition for `Option`'s `None` value and offset of `Option`'s payload.
fn get_option_none_condition_and_offset(
    option_def: &OptionDef,
    inner_type: &TypeDef,
    offset: u32,
) -> (String, Cow<'static, str>) {
    let inner_layout = inner_type.layout_64();

    if option_def.layout_64().size == inner_layout.size {
        let niche = inner_layout.niche.clone().unwrap();
        let niche_offset = offset + niche.offset;
        let none_condition = match niche.size {
            1 => format!("uint8[{}] === {}", pos_offset(niche_offset), niche.value()),
            // 2 => format!("uint16[{}] === {}", pos_offset_shift(offset, 1), niche.value()),
            4 => format!("int32[{}] === {}", pos_offset_shift(niche_offset, 2), niche.value()),
            8 => {
                // TODO: Use `float64[pos >> 3] === 0` instead of
                // `int32[pos >> 2] === 0 && int32[(pos >> 2) + 1] === 0`?
                let value = niche.value();
                format!(
                    "int32[{}] === {} && int32[{}] === {}",
                    pos_offset_shift(niche_offset, 2),
                    value & u128::from(u32::MAX),
                    pos_offset_shift(niche_offset + 4, 2),
                    value >> 32,
                )
            }
            size => panic!("Invalid niche size: {size}"),
        };
        (none_condition, pos_offset(offset))
    } else {
        (format!("uint8[{}] === 0", pos_offset(offset)), pos_offset(offset + inner_layout.align))
    }
}

/// Generate deserialize function for a `Box`.
fn generate_box(box_def: &BoxDef, code: &mut String, estree_derive_id: DeriveId, schema: &Schema) {
    let inner_type = box_def.inner_type(schema);
    if should_skip_innermost_type(inner_type, estree_derive_id, schema) {
        return;
    }

    let fn_name = box_def.deser_name(schema);
    let inner_fn_name = inner_type.deser_name(schema);

    #[rustfmt::skip]
    write_it!(code, "
        function {fn_name}(pos) {{
            return {inner_fn_name}(int32[pos >> 2]);
        }}
    ");
}

/// Generate deserialize function for a `Vec`.
fn generate_vec(vec_def: &VecDef, code: &mut String, estree_derive_id: DeriveId, schema: &Schema) {
    let inner_type = vec_def.inner_type(schema);
    if should_skip_innermost_type(inner_type, estree_derive_id, schema) {
        return;
    }

    let fn_name = vec_def.deser_name(schema);
    let inner_fn_name = inner_type.deser_name(schema);
    let inner_type_size = inner_type.layout_64().size;

    let ptr_pos32 = pos32_offset(VEC_PTR_FIELD_OFFSET);
    let len_pos32 = pos32_offset(VEC_LEN_FIELD_OFFSET);

    // Use shift if possible (stride is a power of 2)
    let end_pos_offset = if inner_type_size.is_power_of_two() {
        format!("(int32[{len_pos32}] << {})", inner_type_size.trailing_zeros())
    } else {
        format!("int32[{len_pos32}] * {inner_type_size}")
    };

    #[rustfmt::skip]
    write_it!(code, "
        function {fn_name}(pos) {{
            const arr = [],
                pos32 = pos >> 2;
            pos = int32[{ptr_pos32}];
            const endPos = pos + {end_pos_offset};
            while (pos !== endPos) {{
                arr.push({inner_fn_name}(pos));
                pos += {inner_type_size};
            }}
            return arr;
        }}
    ");
}

/// Check if innermost type does not require a deserializer.
pub(super) fn should_skip_innermost_type(
    type_def: &TypeDef,
    estree_derive_id: DeriveId,
    schema: &Schema,
) -> bool {
    match type_def.innermost_type(schema) {
        TypeDef::Struct(struct_def) => {
            !struct_def.generates_derive(estree_derive_id) || struct_def.estree.skip
        }
        TypeDef::Enum(enum_def) => {
            !enum_def.generates_derive(estree_derive_id) || enum_def.estree.skip
        }
        _ => false,
    }
}

/// Generate pos offset string.
///
/// * If `offset == 0` -> `pos`.
/// * Otherwise -> `pos + <offset>` (e.g. `pos + 8`).
pub(super) fn pos_offset<O>(offset: O) -> Cow<'static, str>
where
    O: TryInto<u64>,
    <O as TryInto<u64>>::Error: Debug,
{
    let offset = offset.try_into().unwrap();
    if offset == 0 { Cow::Borrowed("pos") } else { format_cow!("pos + {offset}") }
}

/// Generate pos offset and shift string.
///
/// * If `offset == 0` and `shift == 0` -> `pos`.
/// * If `offset == 0` -> `pos >> <shift>` (e.g. `pos >> 2`).
/// * If `shift == 0` -> `pos + <offset>` (e.g. `pos + 8`).
/// * Otherwise -> `(pos >> <shift>) + <offset shifted>` (e.g. `(pos >> 2) + 2`).
pub(super) fn pos_offset_shift<O, S>(offset: O, shift: S) -> Cow<'static, str>
where
    O: TryInto<u64>,
    <O as TryInto<u64>>::Error: Debug,
    S: TryInto<u64>,
    <S as TryInto<u64>>::Error: Debug,
{
    let offset = offset.try_into().unwrap();
    let shift = shift.try_into().unwrap();
    match (offset, shift) {
        (0, 0) => Cow::Borrowed("pos"),
        (0, _) => format_cow!("pos >> {shift}"),
        (_, 0) => format_cow!("pos + {offset}"),
        (_, _) => {
            let shifted_offset = offset >> shift;
            format_cow!("(pos >> {shift}) + {shifted_offset}")
        }
    }
}

/// Generate pos32 offset string.
///
/// * If `offset == 0` -> `pos32`.
/// * Otherwise -> `pos32 + <offset>` (e.g. `pos32 + 4`).
pub(super) fn pos32_offset<O>(offset: O) -> Cow<'static, str>
where
    O: TryInto<u64>,
    <O as TryInto<u64>>::Error: Debug,
{
    let offset = offset.try_into().unwrap();
    let offset32 = offset >> 2;
    if offset32 == 0 { Cow::Borrowed("pos32") } else { format_cow!("pos32 + {offset32}") }
}

// `raw_deser` replacements

/// Get `raw_deser` for converter and replace `POS`, `DESER` and `SOURCE_TEXT` within it.
///
/// Returns `None` if converter is not annotated `#[estree(raw_deser = "...")]`.
fn apply_converter_for_enum(converter_name: &str, offset: u32, schema: &Schema) -> Option<String> {
    let converter = schema.meta_by_name(converter_name);
    let raw_deser = converter.estree.raw_deser.as_deref()?;

    let value = POS_REGEX.replace_all(raw_deser, PosReplacer::new(offset));
    let value = DESER_REGEX.replace_all(&value, DeserReplacer::new(schema));
    let value = value.cow_replace("SOURCE_TEXT", "sourceText");
    let value = if let Some((preamble, value)) = value.trim().rsplit_once('\n') {
        format!("{preamble} return {value};")
    } else {
        format!("return {value};")
    };

    Some(value)
}

static THIS_REGEX: Lazy<Regex> = lazy_regex!(r"THIS\.([a-zA-Z_]+)");

struct ThisReplacer<'d> {
    dependent_field_names: &'d mut FxHashSet<String>,
}

impl<'d> ThisReplacer<'d> {
    fn new(generator: &'d mut StructDeserializerGenerator<'_>) -> Self {
        Self { dependent_field_names: &mut generator.dependent_field_names }
    }
}

impl Replacer for ThisReplacer<'_> {
    fn replace_append(&mut self, caps: &Captures, dst: &mut String) {
        assert_eq!(caps.len(), 2);
        let field_name = caps.get(1).unwrap().as_str();
        dst.push_str(field_name);
        self.dependent_field_names.insert(field_name.to_string());
    }
}

static DESER_REGEX: Lazy<Regex> = lazy_regex!(r"DESER\[([A-Za-z0-9<>_]+)\]");

struct DeserReplacer<'s> {
    schema: &'s Schema,
}

impl<'s> DeserReplacer<'s> {
    fn new(schema: &'s Schema) -> Self {
        Self { schema }
    }
}

impl Replacer for DeserReplacer<'_> {
    fn replace_append(&mut self, caps: &Captures, dst: &mut String) {
        assert_eq!(caps.len(), 2);
        let type_name = caps.get(1).unwrap().as_str();
        let type_def = self.schema.type_by_name(type_name);
        let fn_name = type_def.deser_name(self.schema);
        dst.push_str(&fn_name);
    }
}

#[expect(clippy::trivial_regex)]
static POS_REGEX: Lazy<Regex> = lazy_regex!("POS");

struct PosReplacer {
    offset: u32,
}

impl PosReplacer {
    fn new(offset: u32) -> Self {
        Self { offset }
    }
}

impl Replacer for PosReplacer {
    fn replace_append(&mut self, _caps: &Captures, dst: &mut String) {
        dst.push_str(&pos_offset(self.offset));
    }
}

static POS_OFFSET_REGEX: Lazy<Regex> = lazy_regex!(
    r"POS_OFFSET(?:<([A-Za-z]+)>)?\.([a-zA-Z_]+(?:\.[a-zA-Z_]+)*)(?:\s*(\+|>>)\s*(\d+))?"
);

struct PosOffsetReplacer<'s, 'd> {
    schema: &'s Schema,
    struct_def: &'d StructDef,
    struct_offset: u32,
}

impl<'s, 'd> PosOffsetReplacer<'s, 'd> {
    fn new(
        generator: &StructDeserializerGenerator<'s>,
        struct_def: &'d StructDef,
        struct_offset: u32,
    ) -> Self {
        Self { schema: generator.schema, struct_def, struct_offset }
    }
}

impl Replacer for PosOffsetReplacer<'_, '_> {
    fn replace_append(&mut self, caps: &Captures, dst: &mut String) {
        assert_eq!(caps.len(), 5);

        let struct_def = if let Some(struct_name) = caps.get(1) {
            self.schema.type_by_name(struct_name.as_str()).as_struct().unwrap()
        } else {
            self.struct_def
        };

        let mut field_names = caps.get(2).unwrap().as_str().split('.');
        let field_name = field_names.next().unwrap();
        let field = struct_def.field_by_name(field_name);
        let mut offset = self.struct_offset + field.offset_64();
        let mut type_def = field.type_def(self.schema);
        for field_name in field_names {
            let struct_def = type_def.as_struct().unwrap();
            let field = struct_def.field_by_name(field_name);
            offset += field.offset_64();
            type_def = field.type_def(self.schema);
        }

        if let Some(operator) = caps.get(3) {
            let right = str::parse::<u32>(caps.get(4).unwrap().as_str()).unwrap();

            if operator.as_str() == ">>" {
                if offset == 0 {
                    write_it!(dst, "pos >> {right}");
                } else {
                    let offset_shifted = offset >> right;
                    write_it!(dst, "(pos >> {right}) + {offset_shifted}");
                }
                return;
            }

            offset += right;
        }

        if offset == 0 {
            write_it!(dst, "pos");
        } else {
            write_it!(dst, "pos + {offset}");
        }
    }
}

/// Trait to get deserializer function name for a type.
pub(super) trait DeserializeFunctionName {
    fn deser_name(&self, schema: &Schema) -> String {
        format!("deserialize{}", self.plain_name(schema))
    }

    fn plain_name<'s>(&'s self, schema: &'s Schema) -> Cow<'s, str>;
}

impl DeserializeFunctionName for TypeDef {
    fn plain_name<'s>(&'s self, schema: &'s Schema) -> Cow<'s, str> {
        match self {
            TypeDef::Struct(def) => def.plain_name(schema),
            TypeDef::Enum(def) => def.plain_name(schema),
            TypeDef::Primitive(def) => def.plain_name(schema),
            TypeDef::Option(def) => def.plain_name(schema),
            TypeDef::Box(def) => def.plain_name(schema),
            TypeDef::Vec(def) => def.plain_name(schema),
            TypeDef::Cell(def) => def.plain_name(schema),
            TypeDef::Pointer(def) => def.plain_name(schema),
        }
    }
}

macro_rules! impl_deser_name_simple {
    ($ty:ident) => {
        impl DeserializeFunctionName for $ty {
            fn plain_name<'s>(&'s self, _schema: &'s Schema) -> Cow<'s, str> {
                Cow::Borrowed(self.name())
            }
        }
    };
}

impl_deser_name_simple!(StructDef);
impl_deser_name_simple!(EnumDef);

macro_rules! impl_deser_name_concat {
    ($ty:ident, $prefix:expr) => {
        impl DeserializeFunctionName for $ty {
            fn plain_name<'s>(&'s self, schema: &'s Schema) -> Cow<'s, str> {
                format_cow!("{}{}", $prefix, self.inner_type(schema).plain_name(schema))
            }
        }
    };
}

impl_deser_name_concat!(OptionDef, "Option");
impl_deser_name_concat!(BoxDef, "Box");
impl_deser_name_concat!(VecDef, "Vec");

impl DeserializeFunctionName for PrimitiveDef {
    fn plain_name<'s>(&'s self, _schema: &'s Schema) -> Cow<'s, str> {
        let type_name = self.name();
        if matches!(type_name, "&str" | "Str" | "Ident") {
            // Use 1 deserializer for `&str`, `Str`, and `Ident`
            Cow::Borrowed("Str")
        } else if let Some(type_name) = type_name.strip_prefix("NonZero") {
            // Use zeroed type's deserializer for `NonZero*` types
            Cow::Borrowed(type_name)
        } else if let Some(type_name) = type_name.strip_prefix("Atomic") {
            // Use standard type's deserializer for `Atomic*` types
            Cow::Borrowed(type_name)
        } else {
            upper_case_first(type_name)
        }
    }
}

impl DeserializeFunctionName for CellDef {
    fn plain_name<'s>(&'s self, schema: &'s Schema) -> Cow<'s, str> {
        // `Cell`s use same deserializer as inner type, as layout is identical
        self.inner_type(schema).plain_name(schema)
    }
}

impl DeserializeFunctionName for PointerDef {
    fn plain_name<'s>(&'s self, schema: &'s Schema) -> Cow<'s, str> {
        // Pointers use same deserializer as `Box`, as layout is identical
        format_cow!("Box{}", self.inner_type(schema).plain_name(schema))
    }
}

/// Constants for position of fields in buffer which deserialization starts from.
#[derive(Clone, Copy)]
struct Constants {
    /// Size of buffer in bytes
    buffer_size: u32,
    /// Size of active data section of buffer in bytes (excluding `ChunkFooter`)
    active_size: u32,
    /// Offset within buffer of `u32` containing position of `RawTransferData`
    data_pointer_pos: u32,
    /// Offset within buffer of `bool` indicating if AST is TS or JS
    is_ts_pos: u32,
    /// Offset within buffer of `bool` indicating if AST is JSX
    is_jsx_pos: u32,
    /// Offset within buffer of `bool` indicating if source text has BOM
    has_bom_pos: u32,
    /// Offset within buffer of `u32` containing position of lexer `Token`s
    tokens_offset_pos: u32,
    /// Offset within buffer of `u32` containing number of lexer `Token`s
    tokens_len_pos: u32,
    /// Offset of `Program` in buffer, relative to position of `RawTransferData`
    program_offset: u32,
    /// Offset of `u32` source text start pos, relative to position of `Program`
    source_start_offset: u32,
    /// Offset of `u32` source text length, relative to position of `Program`
    source_len_offset: u32,
    /// Offset of comments `Vec` pointer, relative to position of `Program`
    comments_offset: u32,
    /// Offset of comments `Vec` length, relative to position of `Program`
    comments_len_offset: u32,
    /// Size of `Comment` struct in bytes
    comment_size: u32,
    /// Offset of `kind` field within `Comment` struct
    comment_kind_offset: u32,
    /// Offset of `content` field within `Comment` struct.
    /// JS side uses this byte as a "deserialized" flag for lazy deserialization of tokens/comments.
    deserialized_flag_offset: u32,
    /// Discriminant value for `CommentKind::Line`
    comment_line_kind: u8,
    /// Size of `RawTransferMetadata` in bytes
    raw_metadata_size: u32,
    /// Alignment of `RawTransferMetadata`
    raw_metadata_align: u32,
}

/// Generate constants file.
fn generate_constants(consts: Constants) -> (String, TokenStream) {
    let Constants {
        buffer_size,
        active_size,
        data_pointer_pos,
        is_ts_pos,
        is_jsx_pos,
        has_bom_pos,
        tokens_offset_pos,
        tokens_len_pos,
        program_offset,
        source_start_offset,
        source_len_offset,
        comments_offset,
        comments_len_offset,
        comment_size,
        comment_kind_offset,
        deserialized_flag_offset,
        comment_line_kind,
        raw_metadata_size,
        raw_metadata_align,
    } = consts;

    let data_pointer_pos_32 = data_pointer_pos / 4;
    let tokens_offset_pos_32 = tokens_offset_pos / 4;
    let tokens_len_pos_32 = tokens_len_pos / 4;

    #[rustfmt::skip]
    let js_output = format!("
        // See `crates/oxc_allocator/src/pool/fixed_size.rs` for a diagram showing
        // how the constituent parts of the arena fit together.

        /**
         * Total size of the allocator block (including metadata and allocator `ChunkFooter`).
         */
        export const BLOCK_SIZE = {BLOCK_SIZE};

        /**
         * Required alignment of the allocator block (4 GiB).
         */
        export const BLOCK_ALIGN = {BLOCK_ALIGN};

        /**
         * Total size of the transfer buffer used on JS side, in bytes
         * (`BLOCK_SIZE` minus `FixedSizeAllocatorMetadata` and `ChunkFooter`).
         */
        export const BUFFER_SIZE = {buffer_size};

        /**
         * Size of the active data region in bytes - the region where source text and AST live
         * (`BUFFER_SIZE` minus `RawTransferMetadata`).
         */
        export const ACTIVE_SIZE = {active_size};

        /**
         * Byte offset of the data pointer within the buffer, divided by 4 (for `Int32Array` indexing).
         */
        export const DATA_POINTER_POS_32 = {data_pointer_pos_32};

        /**
         * Byte offset of the `is_ts` flag within the buffer.
         */
        export const IS_TS_FLAG_POS = {is_ts_pos};

        /**
         * Byte offset of the `is_jsx` flag within the buffer.
         */
        export const IS_JSX_FLAG_POS = {is_jsx_pos};

        /**
         * Byte offset of the `has_bom` flag within the buffer.
         */
        export const HAS_BOM_FLAG_POS = {has_bom_pos};

        /**
         * Byte offset of the tokens offset within the buffer, divided by 4 (for `Int32Array` indexing).
         */
        export const TOKENS_OFFSET_POS_32 = {tokens_offset_pos_32};

        /**
         * Byte offset of the tokens length within the buffer, divided by 4 (for `Int32Array` indexing).
         */
        export const TOKENS_LEN_POS_32 = {tokens_len_pos_32};

        /**
         * Byte offset of the `program` field, relative to start of `RawTransferData`.
         */
        export const PROGRAM_OFFSET = {program_offset};

        /**
         * Byte offset of pointer to start of source text, relative to start of `Program`.
         */
        export const SOURCE_START_OFFSET = {source_start_offset};

        /**
         * Byte offset of length of source text, relative to start of `Program`.
         */
        export const SOURCE_LEN_OFFSET = {source_len_offset};

        /**
         * Byte offset of comments `Vec` pointer, relative to start of `Program`.
         */
        export const COMMENTS_OFFSET = {comments_offset};

        /**
         * Byte offset of comments `Vec` length, relative to start of `Program`.
         */
        export const COMMENTS_LEN_OFFSET = {comments_len_offset};

        /**
         * Size of `Comment` struct in bytes.
         */
        export const COMMENT_SIZE = {comment_size};

        /**
         * Byte offset of `kind` field, relative to start of `Comment` struct.
         */
        export const COMMENT_KIND_OFFSET = {comment_kind_offset};

        /**
         * Byte offset of the deserialized flag within each token/comment entry.
         *
         * Corresponds to `content` field of `Comment` struct, and unused bytes in `Token`.
         * Initialized to 0 by Rust. JS side sets to 1 after deserialization.
         */
        export const DESERIALIZED_FLAG_OFFSET = {deserialized_flag_offset};

        /**
         * Discriminant value for `CommentKind::Line`.
         */
        export const COMMENT_LINE_KIND = {comment_line_kind};
    ");

    let block_size = number_lit(BLOCK_SIZE);
    let block_align = number_lit(BLOCK_ALIGN);
    let buffer_size = number_lit(buffer_size);
    let active_size = number_lit(active_size);
    let raw_metadata_size = number_lit(raw_metadata_size);
    let raw_metadata_align = number_lit(raw_metadata_align);
    let chunk_footer_size = number_lit(CHUNK_FOOTER_SIZE);
    let arena_cursor_min_align = number_lit(ARENA_CURSOR_MIN_ALIGN);

    let rust_output = quote! {
        //! Constants for raw transfer / fixed-sized allocators.
        //!
        //! See `crates/oxc_allocator/src/pool/fixed_size.rs` for a diagram showing
        //! how the constituent parts of the arena fit together.
        //!
        //! This file must be feature-gated so it is not loaded on 32-bit platforms.
        //! It will cause a compilation error on 32-bit platforms, as some values are too large for 32-bit `usize`.

        //!@@line_break
        #![expect(clippy::unreadable_literal)]
        #![allow(dead_code)]

        ///@@line_break
        /// Total size of the allocator block (including metadata and allocator `ChunkFooter`).
        pub const BLOCK_SIZE: usize = #block_size;

        ///@@line_break
        /// Required alignment of the allocator block (4 GiB).
        pub const BLOCK_ALIGN: usize = #block_align;

        ///@@line_break
        /// Total size of the transfer buffer used on JS side, in bytes
        /// (`BLOCK_SIZE` minus `FixedSizeAllocatorMetadata` and `ChunkFooter`).
        pub const BUFFER_SIZE: usize = #buffer_size;

        ///@@line_break
        /// Size of the active data region in bytes - the region where source text and AST live
        /// (`BUFFER_SIZE` minus `RawTransferMetadata`).
        pub const ACTIVE_SIZE: usize = #active_size;

        ///@@line_break
        /// Size of `RawTransferMetadata` in bytes.
        pub const RAW_METADATA_SIZE: usize = #raw_metadata_size;

        ///@@line_break
        /// Alignment of `RawTransferMetadata`.
        pub const RAW_METADATA_ALIGN: usize = #raw_metadata_align;

        ///@@line_break
        /// Size of `ChunkFooter` struct in bytes.
        pub const CHUNK_FOOTER_SIZE: usize = #chunk_footer_size;

        ///@@line_break
        /// Minimum alignment requirement for `Arena`'s cursor pointer (`ARENA::MIN_ALIGN`).
        pub const CURSOR_MIN_ALIGN: usize = #arena_cursor_min_align;
    };

    (js_output, rust_output)
}

/// Calculate constants.
fn get_constants(schema: &Schema) -> Constants {
    let raw_metadata_struct = schema.type_by_name("RawTransferMetadata").as_struct().unwrap();
    let raw_metadata2_struct = schema.type_by_name("RawTransferMetadata2").as_struct().unwrap();

    // Check layout and fields of `RawTransferMetadata` and `RawTransferMetadata2` are identical
    assert_eq!(raw_metadata_struct.layout, raw_metadata2_struct.layout);
    assert_eq!(raw_metadata_struct.fields.len(), raw_metadata2_struct.fields.len());

    let mut data_offset_field = None;
    let mut is_ts_field = None;
    let mut is_jsx_field = None;
    let mut has_bom_field = None;
    let mut tokens_offset_field = None;
    let mut tokens_len_field = None;
    for (field1, field2) in raw_metadata_struct.fields.iter().zip(&raw_metadata2_struct.fields) {
        assert_eq!(field1.name(), field2.name());
        assert_eq!(field1.type_id, field2.type_id);
        assert_eq!(field1.offset_64(), field2.offset_64());
        match field1.name() {
            "data_offset" => data_offset_field = Some(field1),
            "is_ts" => is_ts_field = Some(field1),
            "is_jsx" => is_jsx_field = Some(field1),
            "has_bom" => has_bom_field = Some(field1),
            "tokens_offset" => tokens_offset_field = Some(field1),
            "tokens_len" => tokens_len_field = Some(field1),
            _ => {}
        }
    }
    let data_offset_field = data_offset_field.unwrap();
    let is_ts_field = is_ts_field.unwrap();
    let is_jsx_field = is_jsx_field.unwrap();
    let has_bom_field = has_bom_field.unwrap();
    let tokens_offset_field = tokens_offset_field.unwrap();
    let tokens_len_field = tokens_len_field.unwrap();

    let raw_metadata_size = raw_metadata_struct.layout_64().size;
    let raw_metadata_align = raw_metadata_struct.layout_64().align;

    let fixed_metadata_struct =
        schema.type_by_name("FixedSizeAllocatorMetadata").as_struct().unwrap();
    let fixed_metadata_size = fixed_metadata_struct.layout_64().size;

    // Buffer on JS side excludes `FixedSizeAllocatorMetadata` and `ChunkFooter`, but includes `RawTransferMetadata`.
    // In the linter, `FixedSizeAllocatorMetadata` sits between `RawTransferMetadata` and `ChunkFooter`
    // (`fixed_metadata_size` bytes). In `napi/parser`, there is no `FixedSizeAllocatorMetadata`, but the same slot
    // is reserved (left unused) so layout is the same in both cases, with `RawTransferMetadata` in the same position.
    let buffer_size = BLOCK_SIZE - fixed_metadata_size - CHUNK_FOOTER_SIZE;
    let active_size = buffer_size - raw_metadata_size;

    // Get offsets of data within buffer.
    // `RawTransferMetadata` sits immediately before `FixedSizeAllocatorMetadata`, which sits immediately before
    // `ChunkFooter` (which lives in the last `CHUNK_FOOTER_SIZE` bytes of the buffer).
    let raw_metadata_pos = active_size;
    let data_pointer_pos = raw_metadata_pos + data_offset_field.offset_64();
    let is_ts_pos = raw_metadata_pos + is_ts_field.offset_64();
    let is_jsx_pos = raw_metadata_pos + is_jsx_field.offset_64();
    let has_bom_pos = raw_metadata_pos + has_bom_field.offset_64();
    let tokens_offset_pos = raw_metadata_pos + tokens_offset_field.offset_64();
    let tokens_len_pos = raw_metadata_pos + tokens_len_field.offset_64();

    let program_offset = schema
        .type_by_name("RawTransferData")
        .as_struct()
        .unwrap()
        .field_by_name("program")
        .offset_64();

    let program_struct = schema.type_by_name("Program").as_struct().unwrap();

    let source_start_offset = program_struct.field_by_name("source_text").offset_64();
    let source_len_offset = source_start_offset + STR_LEN_OFFSET;

    let comments_field_offset = program_struct.field_by_name("comments").offset_64();
    let comments_offset = comments_field_offset + VEC_PTR_FIELD_OFFSET;
    let comments_len_offset = comments_field_offset + VEC_LEN_FIELD_OFFSET;

    let comment_struct = schema.type_by_name("Comment").as_struct().unwrap();
    let comment_size = comment_struct.layout_64().size;
    let comment_kind_offset = comment_struct.field_by_name("kind").offset_64();
    let deserialized_flag_offset = comment_struct.field_by_name("content").offset_64();

    let comment_kind_enum = schema.type_by_name("CommentKind").as_enum().unwrap();
    let comment_line_kind =
        comment_kind_enum.variants.iter().find(|v| v.name() == "Line").unwrap().discriminant;

    Constants {
        buffer_size,
        active_size,
        data_pointer_pos,
        is_ts_pos,
        is_jsx_pos,
        has_bom_pos,
        tokens_offset_pos,
        tokens_len_pos,
        program_offset,
        source_start_offset,
        source_len_offset,
        comments_offset,
        comments_len_offset,
        comment_size,
        comment_kind_offset,
        deserialized_flag_offset,
        comment_line_kind,
        raw_metadata_size,
        raw_metadata_align,
    }
}

/// Visitor to add `__proto__` field to all objects with `range` field in all deserialize functions,
/// to give access to `loc`.
///
/// Works on AST pre-minification.
struct LocFieldAdder<'a> {
    ast: AstBuilder<'a>,
}

impl<'a> LocFieldAdder<'a> {
    fn new(allocator: &'a Allocator) -> Self {
        Self { ast: AstBuilder::new(allocator) }
    }
}

impl<'a> VisitMut<'a> for LocFieldAdder<'a> {
    fn visit_object_expression(&mut self, obj_expr: &mut ObjectExpression<'a>) {
        // Check if object has `range` field (`...(RANGE && { range: [start, end] })`)
        let has_range_field = obj_expr.properties.iter().any(|prop| {
            if let ObjectPropertyKind::SpreadProperty(spread) = prop
                && let Expression::ParenthesizedExpression(paren_expr) = &spread.argument
                && let Expression::LogicalExpression(logical_expr) = &paren_expr.expression
                && logical_expr.operator == LogicalOperator::And
                && let Expression::Identifier(ident) = &logical_expr.left
                && ident.name == "RANGE"
            {
                true
            } else {
                false
            }
        });

        if has_range_field {
            // Insert `__proto__: NodeProto` as first field
            let prop = ObjectPropertyKind::new_object_property(
                SPAN,
                PropertyKind::Init,
                PropertyKey::new_static_identifier(SPAN, "__proto__", &self.ast),
                Expression::new_identifier(SPAN, "NodeProto", &self.ast),
                false,
                false,
                false,
                &self.ast,
            );
            obj_expr.properties.insert(0, prop);
        }

        walk_mut::walk_object_expression(self, obj_expr);
    }
}
