//! Core `JSON.stringify` traversal: scalar/object/array/buffer emitters,
//! shape-template fast path, and number/escape-string formatters.
//!
//! Public FFI entry points (`js_json_stringify`, etc.) live in
//! `stringify_api.rs`; this file is the shared traversal those entry points
//! and the replacer path call into.

use super::*;
use crate::{js_string_from_bytes, JSValue, StringHeader};
use std::fmt::Write as FmtWrite;

// ─── JSON.stringify ───────────────────────────────────────────────────────────

/// Issue #639: emit a Buffer / Uint8Array as JSON in the Node-compatible shape.
///
/// `Buffer.from(...)` returns `{"type":"Buffer","data":[b0,b1,...]}` (Node's
/// `Buffer.prototype.toJSON()` output). `new Uint8Array(...)` returns
/// `{"0":b0,"1":b1,...}` (the typed-array shape Node falls through to with
/// no custom `toJSON`). Distinguished via `is_uint8array_buffer`, which the
/// Uint8Array constructor path explicitly marks (see `buffer.rs::js_uint8array_*`).
///
/// Must be called BEFORE `gc_obj_type(ptr)` — `BufferHeader` has no `GcHeader`,
/// so reading 8 bytes before the header reads unrelated memory and would
/// dispatch to the wrong arm (or panic when `is_object_pointer` deref's a
/// bogus `keys_array` pointer).
pub(crate) unsafe fn stringify_buffer(ptr: *const u8, buf: &mut String) {
    let buf_ptr = ptr as *const crate::buffer::BufferHeader;
    if buf_ptr.is_null() {
        buf.push_str("null");
        return;
    }
    let len = (*buf_ptr).length as usize;
    let data = (buf_ptr as *const u8).add(std::mem::size_of::<crate::buffer::BufferHeader>());
    let bytes = std::slice::from_raw_parts(data, len);

    if crate::buffer::is_uint8array_buffer(ptr as usize) {
        buf.push('{');
        for (i, b) in bytes.iter().enumerate() {
            if i > 0 {
                buf.push(',');
            }
            buf.push('"');
            let mut idx_buf = itoa::Buffer::new();
            buf.push_str(idx_buf.format(i));
            buf.push_str("\":");
            let mut byte_buf = itoa::Buffer::new();
            buf.push_str(byte_buf.format(*b));
        }
        buf.push('}');
    } else {
        buf.push_str(r#"{"type":"Buffer","data":["#);
        for (i, b) in bytes.iter().enumerate() {
            if i > 0 {
                buf.push(',');
            }
            let mut byte_buf = itoa::Buffer::new();
            buf.push_str(byte_buf.format(*b));
        }
        buf.push_str("]}");
    }
}

#[inline]
pub(crate) unsafe fn is_object_pointer(ptr: *const u8) -> bool {
    let obj = ptr as *const crate::ObjectHeader;
    let potential_keys_ptr = (*obj).keys_array as u64;
    let top_16_bits = potential_keys_ptr >> 48;
    let is_likely_heap_pointer = top_16_bits == 0 || top_16_bits == 1;
    let looks_like_valid_pointer =
        is_likely_heap_pointer && potential_keys_ptr > 0x10000 && (potential_keys_ptr & 0x7) == 0;

    if looks_like_valid_pointer {
        let keys_arr = (*obj).keys_array;
        let keys_len = (*keys_arr).length;
        let keys_cap = (*keys_arr).capacity;
        let field_count = (*obj).field_count;
        // keys_len is authoritative — the logical property count. field_count
        // can be EITHER less than keys_len (parser-built objects with ≥9
        // fields cap field_count at the inline alloc_limit; closes #307;
        // overflow values live in OVERFLOW_FIELDS — see object.rs:32) OR
        // greater than keys_len (pre-allocated objects like
        // `js_object_alloc(0, 8)` for 2 actual keys). Both shapes are real
        // objects worth stringifying; just sanity-check both fields are
        // within reasonable bounds.
        // Previously caps were `< 1000` — any object with 1000+ keys
        // failed the check and `JSON.stringify` emitted "null". Raised
        // to 10M which still catches a corrupted ObjectHeader (first-
        // fields bytes reading as 0x4059... — orders of magnitude
        // above 10M) but allows realistic object sizes through.
        keys_len <= keys_cap && keys_len > 0 && keys_cap < 10_000_000 && field_count < 10_000_000
    } else {
        false
    }
}

#[inline]
pub(crate) unsafe fn write_number(buf: &mut String, value: f64) {
    // Date — JSON.stringify must call toJSON/toISOString per ECMA-262
    // 24.5.4.1. Perry stores Date as a raw f64 timestamp; the
    // `DATE_REGISTRY` HashSet records which finite numbers came from a
    // `new Date(...)`. Every numeric write that's actually a Date routes
    // here as the last-resort fallback, so centralizing the check at
    // this single funnel covers all template / fast-path / replacer
    // sites without per-call-site changes. Guarded on "finite integer"
    // first since the lookup involves a thread-local HashSet — only
    // millisecond-shaped values can be Dates.
    if value.is_finite() && value.fract() == 0.0 && value >= 0.0 && value < 1e16 {
        if crate::date::is_registered_date_bits(value.to_bits()) {
            let s_ptr = crate::date::js_date_to_iso_string(value);
            if let Some(s) = str_from_header(s_ptr) {
                write_escaped_string(buf, s);
            } else {
                buf.push_str("null");
            }
            return;
        }
    }
    if value.is_nan() || value.is_infinite() {
        buf.push_str("null");
    } else if value.fract() == 0.0 && value.abs() < (i64::MAX as f64) {
        let mut itoa_buf = itoa::Buffer::new();
        buf.push_str(itoa_buf.format(value as i64));
    } else {
        let mut ryu_buf = ryu::Buffer::new();
        buf.push_str(ryu_buf.format(value));
    }
}

#[inline]
pub(crate) unsafe fn write_escaped_string(buf: &mut String, s: &str) {
    let bytes = s.as_bytes();
    // Fast path: scan for any escape-triggering byte. JSON output is
    // overwhelmingly escape-free (ASCII identifiers, simple values), so
    // a straight-line SIMD-friendly scan + one `push_str` beats the
    // scalar per-byte escape loop. Needs_escape fires for `"`, `\`, or
    // any control byte (< 0x20).
    // Also trip the slow path for WTF-8 lone surrogate sequences
    // (issue #1182): a lead byte of 0xED followed by 0xA0..=0xBF means
    // we have a 3-byte encoding of U+D800..=U+DFFF and need to emit a
    // `\uXXXX` escape rather than the raw (invalid-UTF-8) bytes.
    let needs_escape = bytes
        .iter()
        .any(|&b| b < 0x20 || b == b'"' || b == b'\\' || b == 0xED);
    if !needs_escape {
        buf.reserve(bytes.len() + 2);
        buf.push('"');
        buf.push_str(s);
        buf.push('"');
        return;
    }

    buf.push('"');
    let mut start = 0;
    // Issue #548: `s` reaches us via `str_from_header`, which uses
    // `from_utf8_unchecked` — a misclassified pointer (e.g. an
    // ArrayHeader interpreted as a StringHeader through the GC-type
    // fallback heuristic) can produce a `&str` whose bytes are not
    // valid UTF-8. The original `&s[start..i]` slice operation
    // panics in `core::str::is_char_boundary` whenever `i` lands
    // mid-multibyte (or on a stray continuation byte). Switching to
    // byte-level `extend_from_slice` writes the raw bytes through and
    // never inspects char boundaries; the JSON output stays
    // byte-identical for valid UTF-8 inputs and degrades gracefully
    // (non-UTF-8 bytes pass through verbatim) instead of aborting the
    // whole process. The String we hand back is technically
    // ill-formed in the worst case, but every consumer in this
    // codebase treats stringify output as a byte stream — and an
    // ill-formed result is strictly preferable to a SIGABRT.
    let buf_vec = buf.as_mut_vec();
    let mut i = 0;
    while i < bytes.len() {
        let b = bytes[i];
        // WTF-8 surrogate handling (issue #1182). A 0xED 0xA0..=0xBF
        // 0x80..=0xBF triple encodes a code unit in U+D800..=U+DFFF.
        // Two cases mirror Node's JSON.stringify:
        //
        //   * A high surrogate (0xA0..=0xAF mid byte) immediately
        //     followed by a low surrogate (0xB0..=0xBF mid byte) is
        //     a *valid* UTF-16 pair — re-encode it as the 4-byte
        //     UTF-8 of the astral codepoint, no escape. This is the
        //     only way `'\uD83D' + '\uDC4D'` round-trips through
        //     `dec.end()` + concat as 👍 instead of two escapes.
        //
        //   * Any remaining (lone) surrogate triple emits the
        //     `\uXXXX` escape.
        if b == 0xED
            && i + 2 < bytes.len()
            && (0xA0..=0xBF).contains(&bytes[i + 1])
            && (0x80..=0xBF).contains(&bytes[i + 2])
        {
            let high_cu: u32 = (((b & 0x0F) as u32) << 12)
                | (((bytes[i + 1] & 0x3F) as u32) << 6)
                | ((bytes[i + 2] & 0x3F) as u32);
            // Valid pair? Need the next 3 bytes to be a low surrogate
            // (0xED 0xB0..=0xBF 0x80..=0xBF) directly adjacent.
            let pair_low = if (0xD800..=0xDBFF).contains(&high_cu)
                && i + 5 < bytes.len()
                && bytes[i + 3] == 0xED
                && (0xB0..=0xBF).contains(&bytes[i + 4])
                && (0x80..=0xBF).contains(&bytes[i + 5])
            {
                let low_cu: u32 = (((bytes[i + 3] & 0x0F) as u32) << 12)
                    | (((bytes[i + 4] & 0x3F) as u32) << 6)
                    | ((bytes[i + 5] & 0x3F) as u32);
                Some(low_cu)
            } else {
                None
            };
            if start < i {
                buf_vec.extend_from_slice(&bytes[start..i]);
            }
            if let Some(low_cu) = pair_low {
                let cp = 0x10000 + ((high_cu - 0xD800) << 10) + (low_cu - 0xDC00);
                if let Some(c) = char::from_u32(cp) {
                    let mut tmp = [0u8; 4];
                    let s = c.encode_utf8(&mut tmp);
                    buf_vec.extend_from_slice(s.as_bytes());
                    i += 6;
                    start = i;
                    continue;
                }
            }
            buf_vec.extend_from_slice(format!("\\u{:04x}", high_cu).as_bytes());
            i += 3;
            start = i;
            continue;
        }
        let escape = match b {
            b'"' => Some("\\\""),
            b'\\' => Some("\\\\"),
            b'\n' => Some("\\n"),
            b'\r' => Some("\\r"),
            b'\t' => Some("\\t"),
            0..=0x1f => {
                if start < i {
                    buf_vec.extend_from_slice(&bytes[start..i]);
                }
                buf_vec.extend_from_slice(format!("\\u{:04x}", b).as_bytes());
                start = i + 1;
                i += 1;
                continue;
            }
            _ => None,
        };
        if let Some(esc) = escape {
            if start < i {
                buf_vec.extend_from_slice(&bytes[start..i]);
            }
            buf_vec.extend_from_slice(esc.as_bytes());
            start = i + 1;
        }
        i += 1;
    }
    if start < bytes.len() {
        buf_vec.extend_from_slice(&bytes[start..]);
    }
    buf_vec.push(b'"');
}

/// Check if a NaN-boxed value is a closure (function).
#[inline]
pub(crate) unsafe fn is_closure_value(bits: u64) -> bool {
    if let Some(ptr) = extract_pointer(bits) {
        // Check for ClosureHeader magic at offset 8 (type_tag field)
        let type_tag = *((ptr as *const u8).add(12) as *const u32);
        type_tag == crate::closure::CLOSURE_MAGIC
    } else {
        false
    }
}

/// Check if an object has a toJSON method. If so, call it and return the result as f64.
/// Returns None if no toJSON method exists.
#[inline]
pub(crate) unsafe fn object_get_to_json(ptr: *const u8) -> Option<f64> {
    let obj = ptr as *const crate::ObjectHeader;
    let keys_arr = (*obj).keys_array;
    if keys_arr.is_null() {
        return None;
    }
    let keys_len = (*keys_arr).length;
    let keys_elements =
        (keys_arr as *const u8).add(std::mem::size_of::<crate::ArrayHeader>()) as *const f64;
    let fields_ptr =
        (ptr as *const u8).add(std::mem::size_of::<crate::ObjectHeader>()) as *const f64;

    for f in 0..keys_len {
        let key_f64 = *keys_elements.add(f as usize);
        let key_bits = key_f64.to_bits();
        let key_tag = key_bits & 0xFFFF_0000_0000_0000;
        let key_ptr = if key_tag == STRING_TAG || key_tag == POINTER_TAG {
            (key_bits & POINTER_MASK) as *const StringHeader
        } else {
            key_bits as *const StringHeader
        };
        if let Some(key_str) = str_from_header(key_ptr) {
            if key_str == "toJSON" {
                let field_val = *fields_ptr.add(f as usize);
                let field_bits = field_val.to_bits();
                // Check if this field is a closure
                if is_closure_value(field_bits) {
                    let closure_ptr = if (field_bits & 0xFFFF_0000_0000_0000) == POINTER_TAG {
                        (field_bits & POINTER_MASK) as *const crate::closure::ClosureHeader
                    } else {
                        field_bits as *const crate::closure::ClosureHeader
                    };
                    // Call toJSON() with no arguments (pass empty string key per spec)
                    let empty_str = js_string_from_bytes(b"".as_ptr(), 0);
                    let key_f64_arg =
                        f64::from_bits(STRING_TAG | (empty_str as u64 & POINTER_MASK));
                    let result = crate::js_closure_call1(closure_ptr, key_f64_arg);
                    return Some(result);
                }
            }
        }
    }
    None
}

#[inline]
pub(crate) unsafe fn stringify_value(value: f64, type_hint: u32, buf: &mut String) {
    let bits: u64 = value.to_bits();

    if bits == TAG_NULL {
        buf.push_str("null");
        return;
    }
    if bits == TAG_TRUE {
        buf.push_str("true");
        return;
    }
    if bits == TAG_FALSE {
        buf.push_str("false");
        return;
    }

    let tag = bits & 0xFFFF_0000_0000_0000;
    if tag == STRING_TAG {
        let str_ptr = (bits & POINTER_MASK) as *const StringHeader;
        if let Some(s) = str_from_header(str_ptr) {
            write_escaped_string(buf, s);
        } else {
            buf.push_str("null");
        }
        return;
    }
    // SSO (v0.5.213): decode inline 5-byte string, emit escaped.
    if tag == crate::value::SHORT_STRING_TAG {
        let jsval = JSValue::from_bits(bits);
        let mut scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN];
        let n = jsval.short_string_to_buf(&mut scratch);
        if let Ok(s) = std::str::from_utf8(&scratch[..n]) {
            write_escaped_string(buf, s);
        } else {
            buf.push_str("null");
        }
        return;
    }

    // BigInt: serialize as quoted string (matching JSON.stringify with BigInt replacer behavior)
    if tag == BIGINT_TAG {
        let bigint_ptr = (bits & POINTER_MASK) as *const crate::BigIntHeader;
        let str_ptr = crate::bigint::js_bigint_to_string(bigint_ptr);
        if let Some(s) = str_from_header(str_ptr) {
            write_escaped_string(buf, s);
        } else {
            buf.push_str("null");
        }
        return;
    }

    if let Some(ptr) = extract_pointer(bits) {
        if type_hint == TYPE_OBJECT {
            stringify_object(ptr, buf);
            return;
        }
        if type_hint == TYPE_ARRAY {
            stringify_array(ptr, buf);
            return;
        }

        // Issue #639: Buffer/Uint8Array detection BEFORE gc_obj_type —
        // BufferHeader has no GcHeader, so the gc-tag read would read
        // unrelated memory and the resulting dispatch could segfault on
        // is_object_pointer's `keys_array` deref.
        if crate::buffer::is_registered_buffer(ptr as usize) {
            stringify_buffer(ptr, buf);
            return;
        }

        // Prefer the GC header's obj_type tag for dispatch — the old
        // capacity heuristic (`cap < 10000`) misidentified legitimate
        // arrays that had grown past 10k as strings, panicking on
        // `JSON.stringify(arr)` where `arr.length >= 10000` (issue #43).
        match gc_obj_type(ptr) {
            crate::gc::GC_TYPE_ARRAY => stringify_array(ptr, buf),
            crate::gc::GC_TYPE_OBJECT => {
                if is_object_pointer(ptr) {
                    stringify_object(ptr, buf);
                } else {
                    buf.push_str("null");
                }
            }
            crate::gc::GC_TYPE_STRING => {
                let str_ptr = ptr as *const StringHeader;
                if let Some(s) = str_from_header(str_ptr) {
                    write_escaped_string(buf, s);
                } else {
                    buf.push_str("null");
                }
            }
            crate::gc::GC_TYPE_ERROR => {
                // Issue #928: Built-in Error objects (and subclasses
                // like TypeError) have a dedicated `ErrorHeader` layout —
                // not the JSObject keys/values layout. Routing them
                // through `stringify_object` derefs garbage as a
                // `keys_array` pointer and segfaults the process.
                // Node's `JSON.stringify(new Error("x"))` returns "{}"
                // because Error's intrinsic props (`message`, `name`,
                // `stack`) are non-enumerable; mirror that.
                buf.push_str("{}");
            }
            _ => {
                // Unknown/untagged pointer: fall back to the structural
                // heuristics for safety (e.g. pointers to non-GC-tracked
                // memory). Arrays up to 10k cap are dispatched here;
                // above that we defensively emit "null" rather than
                // trying to treat them as strings.
                if is_object_pointer(ptr) {
                    stringify_object(ptr, buf);
                } else {
                    let arr = ptr as *const crate::ArrayHeader;
                    if !arr.is_null() {
                        let len = (*arr).length;
                        let cap = (*arr).capacity;
                        if len <= cap && cap > 0 && cap < 10000 {
                            stringify_array(ptr, buf);
                            return;
                        }
                    }
                    let str_ptr = ptr as *const StringHeader;
                    if let Some(s) = str_from_header(str_ptr) {
                        write_escaped_string(buf, s);
                    } else {
                        buf.push_str("null");
                    }
                }
            }
        }
        return;
    }

    write_number(buf, value);
}

/// Depth-aware stringify for recursive calls from stringify_object_inner.
/// For non-pointer values this is identical to stringify_value; for
/// objects/arrays it threads the depth counter through.
#[inline]
pub(crate) unsafe fn stringify_value_depth(
    value: f64,
    type_hint: u32,
    buf: &mut String,
    depth: u32,
) {
    let bits: u64 = value.to_bits();

    // Fast path: non-pointer values don't recurse
    if bits == TAG_NULL {
        buf.push_str("null");
        return;
    }
    if bits == TAG_TRUE {
        buf.push_str("true");
        return;
    }
    if bits == TAG_FALSE {
        buf.push_str("false");
        return;
    }

    let tag = bits & 0xFFFF_0000_0000_0000;
    if tag == STRING_TAG {
        let str_ptr = (bits & POINTER_MASK) as *const StringHeader;
        if let Some(s) = str_from_header(str_ptr) {
            write_escaped_string(buf, s);
        } else {
            buf.push_str("null");
        }
        return;
    }
    // SSO (v0.5.213): decode inline 5-byte string, emit escaped.
    if tag == crate::value::SHORT_STRING_TAG {
        let jsval = JSValue::from_bits(bits);
        let mut scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN];
        let n = jsval.short_string_to_buf(&mut scratch);
        if let Ok(s) = std::str::from_utf8(&scratch[..n]) {
            write_escaped_string(buf, s);
        } else {
            buf.push_str("null");
        }
        return;
    }

    if tag == BIGINT_TAG {
        let bigint_ptr = (bits & POINTER_MASK) as *const crate::BigIntHeader;
        let str_ptr = crate::bigint::js_bigint_to_string(bigint_ptr);
        if let Some(s) = str_from_header(str_ptr) {
            write_escaped_string(buf, s);
        } else {
            buf.push_str("null");
        }
        return;
    }

    if let Some(ptr) = extract_pointer(bits) {
        if type_hint == TYPE_OBJECT {
            stringify_object_inner(ptr, buf, depth);
            return;
        }
        if type_hint == TYPE_ARRAY {
            stringify_array_depth(ptr, buf, depth);
            return;
        }
        // Issue #639: Buffer/Uint8Array detection BEFORE gc_obj_type — see
        // the matching branch in `stringify_value`.
        if crate::buffer::is_registered_buffer(ptr as usize) {
            stringify_buffer(ptr, buf);
            return;
        }
        match gc_obj_type(ptr) {
            crate::gc::GC_TYPE_OBJECT => stringify_object_inner(ptr, buf, depth),
            crate::gc::GC_TYPE_ARRAY => stringify_array_depth(ptr, buf, depth),
            crate::gc::GC_TYPE_STRING => {
                let str_ptr = ptr as *const StringHeader;
                if let Some(s) = str_from_header(str_ptr) {
                    write_escaped_string(buf, s);
                } else {
                    buf.push_str("null");
                }
            }
            crate::gc::GC_TYPE_ERROR => {
                // Issue #928: see the matching branch in `stringify_value`.
                buf.push_str("{}");
            }
            _ => {
                if is_object_pointer(ptr) {
                    stringify_object_inner(ptr, buf, depth);
                } else {
                    let arr = ptr as *const crate::ArrayHeader;
                    if !arr.is_null() {
                        let len = (*arr).length;
                        let cap = (*arr).capacity;
                        if len <= cap && cap > 0 && cap < 10000 {
                            stringify_array_depth(ptr, buf, depth);
                            return;
                        }
                    }
                    let str_ptr = ptr as *const StringHeader;
                    if let Some(s) = str_from_header(str_ptr) {
                        write_escaped_string(buf, s);
                    } else {
                        buf.push_str("null");
                    }
                }
            }
        }
        return;
    }

    write_number(buf, value);
}

#[inline]
pub(crate) unsafe fn stringify_object(ptr: *const u8, buf: &mut String) {
    stringify_object_inner(ptr, buf, 0)
}

pub(crate) unsafe fn stringify_object_inner(ptr: *const u8, buf: &mut String, depth: u32) {
    if depth > MAX_FAST_DEPTH {
        // Deep nesting — switch to full circular detection
        if STRINGIFY_STACK.with(|s| s.borrow().contains(&(ptr as usize))) {
            let msg = "Converting circular structure to JSON";
            let msg_ptr = js_string_from_bytes(msg.as_ptr(), msg.len() as u32);
            let err_ptr = crate::error::js_typeerror_new(msg_ptr);
            crate::exception::js_throw(f64::from_bits(
                POINTER_TAG | (err_ptr as u64 & POINTER_MASK),
            ));
        }
        STRINGIFY_STACK.with(|s| s.borrow_mut().push(ptr as usize));
    }

    let obj = ptr as *const crate::ObjectHeader;
    let num_fields = (*obj).field_count;

    // Templated fast path (#64 follow-up): if this object's shape has been
    // seen before in this stringify call, emit via the cached prefix table
    // and skip per-object `has_pointer_fields` / `object_get_to_json` /
    // key-lookup work. `try_emit_shape_element` rolls back the buffer and
    // returns false on any element-specific mismatch (different shape,
    // stray UNDEFINED, closure), at which point we fall through to the
    // slow path below.
    //
    // Guard (issue #67): skip the template machinery for small objects.
    // `shape_template_for` allocates a Box<ShapeTemplate> + Vec<String>
    // + one String per field on miss (~4-5 heap allocs), and the cache
    // is wiped at every top-level call exit — so for a one-shot small
    // top-level stringify the build is pure overhead vs. the inline slow
    // path below. The arrayof-objects fast path (stringify_array_depth)
    // uses a separate build_shape_prefix_template that's unaffected.
    // Skip the shape-template fast path when the object has overflow fields
    // (keys_len > num_fields — see object.rs:32 OVERFLOW_FIELDS, ≥9 stored
    // fields per #307). The template's per-field key prefix array is built
    // from `min(keys_len, field_count)`, so an overflow object would only
    // emit its first 8 fields. Falling through to the slow path below uses
    // `read_field_bits` which routes overflow reads through
    // `js_object_get_field`'s overflow_get fallback.
    let has_overflow_fields = unsafe {
        let keys_arr = (*obj).keys_array;
        !keys_arr.is_null() && (*keys_arr).length > num_fields
    };
    if num_fields >= 5 && !has_overflow_fields {
        if let Some(tmpl_ptr) = shape_template_for(ptr) {
            if try_emit_shape_element(make_pointer_bits(ptr), &*tmpl_ptr, buf, depth) {
                if depth > MAX_FAST_DEPTH {
                    STRINGIFY_STACK.with(|s| s.borrow_mut().pop());
                }
                return;
            }
        }
    }
    let keys_arr = (*obj).keys_array;
    let keys_len = (*keys_arr).length;
    let keys_elements =
        (keys_arr as *const u8).add(std::mem::size_of::<crate::ArrayHeader>()) as *const f64;
    let fields_ptr =
        (ptr as *const u8).add(std::mem::size_of::<crate::ObjectHeader>()) as *const f64;
    // Closes #307: iterate up to keys_len, not min(num_fields, keys_len).
    // Parser-built objects with ≥9 fields cap field_count at the inline
    // alloc_limit (max(field_count, 8) physical slots) and store the overflow
    // values in OVERFLOW_FIELDS (object.rs:32) — so num_fields can be smaller
    // than keys_len. For inline slots (f < alloc_limit) we still read directly
    // off fields_ptr; for overflow slots we route through `js_object_get_field`
    // which checks field_count and falls through to `overflow_get`. Pre-fix
    // (`std::cmp::min(num_fields, keys_len)`) silently dropped the overflow
    // fields and `is_object_pointer`'s `keys_len <= field_count` guard
    // returned false, so `JSON.stringify` emitted the literal string "null"
    // for any parsed object with ≥9 fields.
    let alloc_limit = std::cmp::max(num_fields, 8);
    let read_field_bits = |f: u32| -> u64 {
        if f < alloc_limit {
            (*fields_ptr.add(f as usize)).to_bits()
        } else {
            crate::object::js_object_get_field(obj, f).bits()
        }
    };
    let actual_fields = keys_len;

    // Deferred toJSON + closure checks (issue #67 tightening): scan fields
    // once to detect if any field is actually a closure. For data-only
    // objects with nested arrays/objects (e.g. `{a:1, b:"", c:[...]}`) the
    // earlier has_pointer_fields heuristic false-positived because any
    // POINTER_TAG field triggered the `object_get_to_json` key walk — even
    // though a toJSON method requires the *value* at the "toJSON" key to
    // be a closure. Reading offset 12 (CLOSURE_MAGIC) per pointer field is
    // cheaper (~3ns/field) than walking the keys array looking for a
    // "toJSON" string that almost never exists (~15ns).
    let has_closure_field = {
        let mut found = false;
        for f in 0..actual_fields {
            let bits = read_field_bits(f);
            let tag = bits & 0xFFFF_0000_0000_0000;
            let ptr_candidate = if tag == POINTER_TAG {
                (bits & POINTER_MASK) as *const u8
            } else if is_raw_pointer(bits) {
                bits as *const u8
            } else {
                std::ptr::null()
            };
            if !ptr_candidate.is_null() {
                let type_tag = *(ptr_candidate.add(12) as *const u32);
                if type_tag == crate::closure::CLOSURE_MAGIC {
                    found = true;
                    break;
                }
            }
        }
        found
    };

    if has_closure_field {
        // Only objects with closure-typed fields can have a toJSON method.
        // Check toJSON first, then filter closures in the loop below.
        if let Some(to_json_val) = object_get_to_json(ptr) {
            if depth > MAX_FAST_DEPTH {
                STRINGIFY_STACK.with(|s| s.borrow_mut().pop());
            }
            stringify_value(to_json_val, TYPE_UNKNOWN, buf);
            return;
        }
    }

    buf.push('{');
    let mut first = true;
    for f in 0..actual_fields {
        let field_bits = read_field_bits(f);
        let field_val = f64::from_bits(field_bits);
        // Skip undefined per JSON spec
        if field_bits == TAG_UNDEFINED {
            continue;
        }
        // Skip closures per JSON spec (only possible for pointer-tagged values).
        // Guarded by has_closure_field: if no field is a closure, the in-loop
        // check is skipped entirely for every field.
        if has_closure_field && is_closure_value(field_bits) {
            continue;
        }

        if !first {
            buf.push(',');
        }
        first = false;

        let key_f64 = *keys_elements.add(f as usize);
        let key_bits = key_f64.to_bits();
        let key_tag = key_bits & 0xFFFF_0000_0000_0000;
        let key_ptr = if key_tag == STRING_TAG || key_tag == POINTER_TAG {
            (key_bits & POINTER_MASK) as *const StringHeader
        } else {
            key_bits as *const StringHeader
        };
        if let Some(key_str) = str_from_header(key_ptr) {
            buf.push('"');
            buf.push_str(key_str);
            buf.push_str("\":");
        } else {
            let _ = write!(buf, "\"field{}\":", f);
        }

        // Inline value dispatch for common types to avoid function call overhead
        let val_tag = field_bits & 0xFFFF_0000_0000_0000;
        if field_bits == TAG_NULL {
            buf.push_str("null");
        } else if field_bits == TAG_TRUE {
            buf.push_str("true");
        } else if field_bits == TAG_FALSE {
            buf.push_str("false");
        } else if val_tag == STRING_TAG {
            let str_ptr = (field_bits & POINTER_MASK) as *const StringHeader;
            if let Some(s) = str_from_header(str_ptr) {
                write_escaped_string(buf, s);
            } else {
                buf.push_str("null");
            }
        } else if val_tag == crate::value::SHORT_STRING_TAG {
            // v0.5.213 SSO — decode inline 5-byte string and emit.
            let jsval = JSValue::from_bits(field_bits);
            let mut scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN];
            let n = jsval.short_string_to_buf(&mut scratch);
            if let Ok(s) = std::str::from_utf8(&scratch[..n]) {
                write_escaped_string(buf, s);
            } else {
                buf.push_str("null");
            }
        } else if val_tag == POINTER_TAG || is_raw_pointer(field_bits) {
            // Nested object/array — recurse with depth
            stringify_value_depth(field_val, TYPE_UNKNOWN, buf, depth + 1);
        } else {
            // Number (most common for data objects) — or Date, handled
            // centrally by `write_number` via DATE_REGISTRY lookup.
            write_number(buf, field_val);
        }
    }
    buf.push('}');
    if depth > MAX_FAST_DEPTH {
        STRINGIFY_STACK.with(|s| s.borrow_mut().pop());
    }
}

pub(crate) unsafe fn stringify_array(ptr: *const u8, buf: &mut String) {
    stringify_array_depth(ptr, buf, 0)
}

/// Cached shape template for a homogeneous array of objects.
pub(crate) struct ShapeTemplate {
    pub(crate) keys_arr: *mut crate::ArrayHeader,
    pub(crate) prefixes: Vec<String>,
    pub(crate) shape_fields: u32,
    /// True when element 0's fields are all primitives (no POINTER_TAG /
    /// UNDEFINED). Lets the emit path skip its per-element pre-scan.
    pub(crate) primitive_only: bool,
}

/// Look up (or build & insert) the shape template for an object. Returns
/// `None` if the object isn't templatable (no keys array, too many fields,
/// malformed key strings) or if the cache is full and missed.
///
/// Returns a raw pointer because lifetimes can't survive the TLS borrow.
/// The pointer stays valid until the next `take_shape_cache` (top-level
/// entry/exit) — within one stringify traversal we only `push`, and
/// `Box`'s heap address is stable across `Vec` growth.
#[inline]
pub(crate) unsafe fn shape_template_for(obj_ptr: *const u8) -> Option<*const ShapeTemplate> {
    let obj = obj_ptr as *const crate::ObjectHeader;
    let keys_arr = (*obj).keys_array;
    if keys_arr.is_null() {
        return None;
    }

    SHAPE_CACHE.with(|c| {
        // Fast path: linear scan from the back — recently-used entries
        // cluster there for typical traversal orders (shape A's elements
        // recurse into shape B repeatedly).
        {
            let cache = c.borrow();
            for entry in cache.iter().rev() {
                if entry.0 == keys_arr {
                    return Some(&*entry.1 as *const ShapeTemplate);
                }
            }
            if cache.len() >= SHAPE_CACHE_CAP {
                return None;
            }
        }

        // Miss — build, insert, return raw pointer to the boxed template.
        let elem_bits = make_pointer_bits(obj_ptr);
        let template = build_shape_prefix_template(elem_bits)?;
        let mut cache = c.borrow_mut();
        // Re-check cap after the borrow round-trip (a recursive call
        // during template build could have filled the cache).
        if cache.len() >= SHAPE_CACHE_CAP {
            return None;
        }
        cache.push((keys_arr, Box::new(template)));
        Some(&*cache.last().unwrap().1 as *const ShapeTemplate)
    })
}

/// Build a per-shape key-prefix template for a homogeneous array of objects.
///
/// When every element of an array shares the same `keys_array` pointer (same
/// shape), we can pre-format the key portion of each field once and reuse it
/// across every element — turning the per-field key lookup (load key f64,
/// extract pointer, `str_from_header`, 3 `push`/`push_str` calls) into a
/// single `push_str` of a cached prefix.
///
/// Prefix layout for N fields with keys k0..kN-1:
///   `prefixes[0]   = "{\"k0\":"`        (opening brace fused with first key)
///   `prefixes[f>0] = ",\"kf\":"`        (comma fused with key)
/// Close with `}`. This compresses ~7 per-field write ops down to ~2.
///
/// Returns `None` when the first element isn't a regular object, the keys
/// array is invalid, or any key string is malformed — callers fall back to
/// the generic slow path in that case.
pub(crate) unsafe fn build_shape_prefix_template(first_elem_bits: u64) -> Option<ShapeTemplate> {
    let tag = first_elem_bits & 0xFFFF_0000_0000_0000;
    let first_ptr = if tag == POINTER_TAG {
        (first_elem_bits & POINTER_MASK) as *const u8
    } else if is_raw_pointer(first_elem_bits) {
        first_elem_bits as *const u8
    } else {
        return None;
    };
    // Issue #639: Buffer / Uint8Array have no GcHeader, so `gc_obj_type`
    // would read 8 bytes before the BufferHeader (unrelated memory) and
    // could randomly return GC_TYPE_OBJECT. Bail to the per-element
    // slow path which dispatches via `is_registered_buffer`.
    if crate::buffer::is_registered_buffer(first_ptr as usize) {
        return None;
    }
    if gc_obj_type(first_ptr) != crate::gc::GC_TYPE_OBJECT {
        return None;
    }
    let obj = first_ptr as *const crate::ObjectHeader;
    let keys_arr = (*obj).keys_array;
    if keys_arr.is_null() {
        return None;
    }
    let keys_len = (*keys_arr).length;
    let field_count = (*obj).field_count;
    let shape_fields = std::cmp::min(keys_len, field_count);
    if shape_fields == 0 || shape_fields > 32 {
        return None;
    }

    let keys_elements =
        (keys_arr as *const u8).add(std::mem::size_of::<crate::ArrayHeader>()) as *const f64;
    let mut prefixes: Vec<String> = Vec::with_capacity(shape_fields as usize);
    for f in 0..shape_fields {
        let key_bits = (*keys_elements.add(f as usize)).to_bits();
        let key_tag = key_bits & 0xFFFF_0000_0000_0000;
        let key_ptr = if key_tag == STRING_TAG || key_tag == POINTER_TAG {
            (key_bits & POINTER_MASK) as *const StringHeader
        } else {
            key_bits as *const StringHeader
        };
        let key_str = str_from_header(key_ptr)?;
        let needs_escape = key_str.bytes().any(|b| b == b'"' || b == b'\\' || b < 0x20);
        let mut prefix = String::with_capacity(key_str.len() + 4);
        prefix.push(if f == 0 { '{' } else { ',' });
        if needs_escape {
            write_escaped_string(&mut prefix, key_str);
        } else {
            prefix.push('"');
            prefix.push_str(key_str);
            prefix.push('"');
        }
        prefix.push(':');
        prefixes.push(prefix);
    }

    // Sample first element to decide whether every field slot is already
    // a primitive (number/bool/null/string). When true, per-element emit
    // can skip the undefined/closure pre-scan.
    let fields_ptr =
        (first_ptr as *const u8).add(std::mem::size_of::<crate::ObjectHeader>()) as *const f64;
    let mut primitive_only = true;
    for f in 0..shape_fields {
        let fb = (*fields_ptr.add(f as usize)).to_bits();
        if fb == TAG_UNDEFINED || (fb & 0xFFFF_0000_0000_0000) == POINTER_TAG {
            primitive_only = false;
            break;
        }
    }

    Some(ShapeTemplate {
        keys_arr,
        prefixes,
        shape_fields,
        primitive_only,
    })
}

/// Fast emission path for an object element that matches the cached shape
/// template. Returns `true` when the element was emitted via the template;
/// `false` when the element diverges (different shape, skippable field, or
/// has a `toJSON` that must produce the replacement value). On `false` the
/// buffer is unchanged — the caller is responsible for falling back.
pub(crate) unsafe fn try_emit_shape_element(
    elem_bits: u64,
    template: &ShapeTemplate,
    buf: &mut String,
    depth: u32,
) -> bool {
    let tag = elem_bits & 0xFFFF_0000_0000_0000;
    let elem_ptr = if tag == POINTER_TAG {
        (elem_bits & POINTER_MASK) as *const u8
    } else if is_raw_pointer(elem_bits) {
        elem_bits as *const u8
    } else {
        return false;
    };
    if gc_obj_type(elem_ptr) != crate::gc::GC_TYPE_OBJECT {
        return false;
    }
    let obj = elem_ptr as *const crate::ObjectHeader;
    if (*obj).keys_array != template.keys_arr {
        return false;
    }

    let fields_ptr =
        (elem_ptr as *const u8).add(std::mem::size_of::<crate::ObjectHeader>()) as *const f64;
    let shape_fields = template.shape_fields;
    let prefixes = template.prefixes.as_slice();

    // Primitive-only fast path (common case for JSON.parse output): skip
    // the undefined/closure pre-scan and trust that the sampled element 0
    // was representative. The emit loop handles stray POINTER_TAG values
    // via `stringify_value_depth`; a stray UNDEFINED is rare enough that
    // we save `buf.len()` pre-emit and roll back on detection.
    if template.primitive_only {
        let save_pos = buf.len();
        for f in 0..shape_fields as usize {
            let field_val = *fields_ptr.add(f);
            let fb = field_val.to_bits();
            // UNDEFINED desyncs comma placement → roll back and let the
            // slow object path emit this element correctly.
            if fb == TAG_UNDEFINED {
                buf.truncate(save_pos);
                return false;
            }
            buf.push_str(&prefixes[f]);
            let vtag = fb & 0xFFFF_0000_0000_0000;
            if fb == TAG_NULL {
                buf.push_str("null");
            } else if fb == TAG_TRUE {
                buf.push_str("true");
            } else if fb == TAG_FALSE {
                buf.push_str("false");
            } else if vtag == STRING_TAG {
                let str_ptr = (fb & POINTER_MASK) as *const StringHeader;
                if let Some(s) = str_from_header(str_ptr) {
                    write_escaped_string(buf, s);
                } else {
                    buf.push_str("null");
                }
            } else if vtag == crate::value::SHORT_STRING_TAG {
                let jsval = JSValue::from_bits(fb);
                let mut scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN];
                let n = jsval.short_string_to_buf(&mut scratch);
                if let Ok(s) = std::str::from_utf8(&scratch[..n]) {
                    write_escaped_string(buf, s);
                } else {
                    buf.push_str("null");
                }
            } else if vtag == POINTER_TAG || is_raw_pointer(fb) {
                stringify_value_depth(field_val, TYPE_UNKNOWN, buf, depth + 1);
            } else {
                write_number(buf, field_val);
            }
        }
        buf.push('}');
        return true;
    }

    // General path: template contains (or may contain) pointer/undefined
    // fields. Pre-scan to honor JSON spec (skip undefined, skip closures,
    // respect toJSON).
    let mut has_pointer_fields = false;
    for f in 0..shape_fields as usize {
        let fb = (*fields_ptr.add(f)).to_bits();
        if fb == TAG_UNDEFINED {
            return false;
        }
        if (fb & 0xFFFF_0000_0000_0000) == POINTER_TAG {
            has_pointer_fields = true;
            if is_closure_value(fb) {
                return false;
            }
        }
    }
    if has_pointer_fields {
        if let Some(to_json_val) = object_get_to_json(elem_ptr) {
            stringify_value_depth(to_json_val, TYPE_UNKNOWN, buf, depth + 1);
            return true;
        }
    }
    for f in 0..shape_fields as usize {
        buf.push_str(&prefixes[f]);
        let field_val = *fields_ptr.add(f);
        let fb = field_val.to_bits();
        let vtag = fb & 0xFFFF_0000_0000_0000;
        if fb == TAG_NULL {
            buf.push_str("null");
        } else if fb == TAG_TRUE {
            buf.push_str("true");
        } else if fb == TAG_FALSE {
            buf.push_str("false");
        } else if vtag == STRING_TAG {
            let str_ptr = (fb & POINTER_MASK) as *const StringHeader;
            if let Some(s) = str_from_header(str_ptr) {
                write_escaped_string(buf, s);
            } else {
                buf.push_str("null");
            }
        } else if vtag == crate::value::SHORT_STRING_TAG {
            let jsval = JSValue::from_bits(fb);
            let mut scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN];
            let n = jsval.short_string_to_buf(&mut scratch);
            if let Ok(s) = std::str::from_utf8(&scratch[..n]) {
                write_escaped_string(buf, s);
            } else {
                buf.push_str("null");
            }
        } else if vtag == POINTER_TAG || is_raw_pointer(fb) {
            stringify_value_depth(field_val, TYPE_UNKNOWN, buf, depth + 1);
        } else {
            write_number(buf, field_val);
        }
    }
    buf.push('}');
    true
}

/// Depth-aware variant of stringify_array for recursive calls.
pub(crate) unsafe fn stringify_array_depth(ptr: *const u8, buf: &mut String, depth: u32) {
    let arr = ptr as *const crate::ArrayHeader;
    let len = (*arr).length;
    let elements = (arr as *const u8).add(std::mem::size_of::<crate::ArrayHeader>()) as *const f64;

    // Homogeneous-shape fast path for arrays of objects sharing one
    // `keys_array` (issue #59). The template is built from element 0 and
    // reused for every subsequent element whose shape matches; mismatches
    // fall back per-element via `stringify_value_depth`, so mixed arrays
    // still produce correct output. Pre-check the tag inline to skip the
    // function call entirely for arrays of primitives (issue #64) — common
    // for nested fields like `tags: ["x","y"]` that fired per-element.
    let template = if len >= 2 {
        let first_bits = (*elements).to_bits();
        let tag = first_bits & 0xFFFF_0000_0000_0000;
        if tag == POINTER_TAG || is_raw_pointer(first_bits) {
            build_shape_prefix_template(first_bits)
        } else {
            None
        }
    } else {
        None
    };

    if let Some(ref tmpl) = template {
        buf.push('[');
        for i in 0..len {
            if i > 0 {
                buf.push(',');
            }
            let elem = *elements.add(i as usize);
            let elem_bits = elem.to_bits();
            if !try_emit_shape_element(elem_bits, tmpl, buf, depth) {
                // Match the slow path: array descent does not bump depth.
                stringify_value_depth(elem, TYPE_UNKNOWN, buf, depth);
            }
        }
        buf.push(']');
        return;
    }

    buf.push('[');
    for i in 0..len {
        if i > 0 {
            buf.push(',');
        }
        let elem = *elements.add(i as usize);
        let elem_bits = elem.to_bits();
        let elem_tag = elem_bits & 0xFFFF_0000_0000_0000;

        if elem_bits == TAG_UNDEFINED {
            buf.push_str("null");
        } else if elem_tag == STRING_TAG {
            let str_ptr = (elem_bits & POINTER_MASK) as *const StringHeader;
            if let Some(s) = str_from_header(str_ptr) {
                write_escaped_string(buf, s);
            } else {
                buf.push_str("null");
            }
        } else if elem_tag == crate::value::SHORT_STRING_TAG {
            let jsval = JSValue::from_bits(elem_bits);
            let mut scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN];
            let n = jsval.short_string_to_buf(&mut scratch);
            if let Ok(s) = std::str::from_utf8(&scratch[..n]) {
                write_escaped_string(buf, s);
            } else {
                buf.push_str("null");
            }
        } else if elem_bits == TAG_NULL {
            buf.push_str("null");
        } else if elem_bits == TAG_TRUE {
            buf.push_str("true");
        } else if elem_bits == TAG_FALSE {
            buf.push_str("false");
        } else if elem_tag == BIGINT_TAG {
            let bigint_ptr = (elem_bits & POINTER_MASK) as *const crate::BigIntHeader;
            let str_ptr = crate::bigint::js_bigint_to_string(bigint_ptr);
            if let Some(s) = str_from_header(str_ptr) {
                write_escaped_string(buf, s);
            } else {
                buf.push_str("null");
            }
        } else if elem_tag == POINTER_TAG || is_raw_pointer(elem_bits) {
            let elem_ptr = if elem_tag == POINTER_TAG {
                (elem_bits & POINTER_MASK) as *const u8
            } else {
                elem_bits as *const u8
            };
            // Issue #639: Buffer/Uint8Array detection BEFORE gc_obj_type — see
            // the matching branch in `stringify_value`.
            if crate::buffer::is_registered_buffer(elem_ptr as usize) {
                stringify_buffer(elem_ptr, buf);
                continue;
            }
            match gc_obj_type(elem_ptr) {
                crate::gc::GC_TYPE_OBJECT => stringify_object_inner(elem_ptr, buf, depth),
                crate::gc::GC_TYPE_ARRAY => stringify_array_depth(elem_ptr, buf, depth),
                crate::gc::GC_TYPE_STRING => {
                    let str_ptr = elem_ptr as *const StringHeader;
                    if let Some(s) = str_from_header(str_ptr) {
                        write_escaped_string(buf, s);
                    } else {
                        buf.push_str("null");
                    }
                }
                _ => {
                    if is_object_pointer(elem_ptr) {
                        stringify_object_inner(elem_ptr, buf, depth);
                    } else {
                        let arr_elem = elem_ptr as *const crate::ArrayHeader;
                        let arr_len = (*arr_elem).length;
                        let arr_cap = (*arr_elem).capacity;
                        if arr_len <= arr_cap && arr_cap > 0 && arr_cap < 10000 {
                            stringify_array_depth(elem_ptr, buf, depth);
                        } else {
                            let str_ptr = elem_ptr as *const StringHeader;
                            if let Some(s) = str_from_header(str_ptr) {
                                write_escaped_string(buf, s);
                            } else {
                                buf.push_str("null");
                            }
                        }
                    }
                }
            }
        } else {
            // Number — or Date, handled centrally by `write_number`
            // via DATE_REGISTRY lookup.
            write_number(buf, elem);
        }
    }
    buf.push(']');
}

#[inline]
pub(crate) unsafe fn estimate_json_size(value: f64, type_hint: u32) -> usize {
    let bits = value.to_bits();
    if let Some(ptr) = extract_pointer(bits) {
        if type_hint == TYPE_ARRAY || (!is_object_pointer(ptr) && type_hint != TYPE_OBJECT) {
            let arr = ptr as *const crate::ArrayHeader;
            let len = (*arr).length as usize;
            return (len * 300).max(256);
        }
        if type_hint == TYPE_OBJECT || is_object_pointer(ptr) {
            let obj = ptr as *const crate::ObjectHeader;
            let fields = (*obj).field_count as usize;
            return (fields * 200).max(256);
        }
    }
    4096
}
