//! Tag-aware dynamic index get/set + helpers for ambiguous index access.

use super::*;

/// Tag-aware dynamic index dispatch for `obj[key]` where `obj` has unknown
/// static type. Issue #514. Strings → js_string_char_at; everything else
/// uses the same `raw_ptr + 8 + idx*8` direct-read offset hack the existing
/// IndexGet fallback uses (which happens to be load-bearing for
/// Object-with-numeric-keys + TypedArrays). LAZY_ARRAY / FORWARDED arrays
/// route through `js_array_get_f64` to chase the materialized chain.
#[no_mangle]
pub extern "C" fn js_dyn_index_get(value: f64, index: f64) -> f64 {
    let bits = value.to_bits();
    let jsval = JSValue::from_bits(bits);
    if jsval.is_string() || jsval.is_short_string() {
        let s_ptr = js_get_string_pointer_unified(value) as *const crate::StringHeader;
        if s_ptr.is_null() {
            return f64::from_bits(TAG_UNDEFINED);
        }
        let idx_i32 = if index.is_nan() || index.is_infinite() {
            0
        } else {
            index as i32
        };
        let result = crate::string::js_string_char_at(s_ptr, idx_i32);
        if result.is_null() {
            return f64::from_bits(TAG_UNDEFINED);
        }
        return f64::from_bits(JSValue::string_ptr(result).bits());
    }
    let raw_ptr = if jsval.is_pointer() {
        (bits & POINTER_MASK) as usize
    } else if !value.is_nan()
        && bits != 0
        && bits < 0x0001_0000_0000_0000
        && (bits & 0x3) == 0
        && bits >= 0x10000
    {
        bits as usize
    } else {
        return f64::from_bits(TAG_UNDEFINED);
    };
    if raw_ptr < 0x10000 {
        return f64::from_bits(TAG_UNDEFINED);
    }
    // Issue #957: if the index itself is a string, route through the
    // by-name object getter. Pre-fix, `obj["foo"]` lowered through
    // `IndexUpdate` re-entered this helper with a NaN-boxed string index
    // and the `index as i32` coercion produced garbage offsets, so
    // `++obj["foo"]` silently returned undefined.
    let idx_bits = index.to_bits();
    let idx_top16 = idx_bits >> 48;
    if idx_top16 == 0x7FFF || idx_top16 == 0x7FF9 {
        let key_ptr = js_get_string_pointer_unified(index) as *const crate::StringHeader;
        if !key_ptr.is_null() {
            return crate::object::js_object_get_field_by_name_f64(
                raw_ptr as *const crate::object::ObjectHeader,
                key_ptr,
            );
        }
        return f64::from_bits(TAG_UNDEFINED);
    }
    let idx_i32 = if index.is_nan() || index.is_infinite() {
        return f64::from_bits(TAG_UNDEFINED);
    } else {
        index as i32
    };
    if idx_i32 < 0 {
        return f64::from_bits(TAG_UNDEFINED);
    }
    // Registry-backed Buffer (`Buffer.from(...)`, `js_buffer_alloc`, the
    // `'data'`-event chunk an http/net listener receives). These carry NO
    // GcHeader (see `crates/perry-runtime/src/buffer.rs` — "Buffers carry
    // no GcHeader") and store one byte per element after an 8-byte
    // `BufferHeader { length, capacity }`. The generic fall-through below
    // does `raw_ptr - GC_HEADER_SIZE` to read an `obj_type` that doesn't
    // exist for a buffer (garbage that never matches GC_TYPE_ARRAY), then
    // reads an 8-byte f64 at `raw_ptr + 8 + idx*8` straight out of the
    // buffer's 1-byte-per-element data region — `chunk[0]` came back as a
    // denormal/garbage f64 that printed `0`, while `.toString()` /
    // `.length` / `Array.from(chunk)` (which all probe BUFFER_REGISTRY)
    // were correct. Probe the registry first and read the byte the same
    // way the working accessors do (`js_buffer_get` → `buffer_data()`).
    // Node semantics: in-range → the byte (0..255); out-of-range → undefined.
    if crate::buffer::is_registered_buffer(raw_ptr) {
        let buf = raw_ptr as *const crate::buffer::BufferHeader;
        let len = unsafe { (*buf).length };
        if (idx_i32 as u32) >= len {
            return f64::from_bits(TAG_UNDEFINED);
        }
        let byte_val = crate::buffer::js_buffer_get(buf, idx_i32);
        return byte_val as f64;
    }
    if raw_ptr >= crate::gc::GC_HEADER_SIZE {
        let gc_hdr = unsafe {
            (raw_ptr as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader
        };
        let obj_type = unsafe { (*gc_hdr).obj_type };
        let gc_flags = unsafe { (*gc_hdr).gc_flags };
        if obj_type == crate::gc::GC_TYPE_LAZY_ARRAY
            || (gc_flags & crate::gc::GC_FLAG_FORWARDED) != 0
        {
            let arr = raw_ptr as *const crate::array::ArrayHeader;
            return crate::array::js_array_get_f64(arr, idx_i32 as u32);
        }
        // Issue #1069: bounds-check regular arrays so out-of-range reads
        // return TAG_UNDEFINED instead of whatever's in the slot. Without
        // this, an empty (or short) array — most visibly the synthetic
        // `arguments` array bundled by the call-site for caller arity 0 —
        // returns the raw 0.0 slot value because `js_array_alloc` rounds
        // capacity up to MIN_ARRAY_CAPACITY and the unchecked load reads
        // past `length` into zeroed-but-allocated storage. `arguments[0]`
        // on `function f() { arguments[0] }; f()` printed `0` instead of
        // `undefined`. The narrow gate (GC_TYPE_ARRAY) keeps object
        // numeric-key fast path unchanged.
        if obj_type == crate::gc::GC_TYPE_ARRAY {
            let arr = raw_ptr as *const crate::array::ArrayHeader;
            let length = unsafe { (*arr).length };
            if (idx_i32 as u32) >= length {
                return f64::from_bits(TAG_UNDEFINED);
            }
        }
    }
    let elem_addr = raw_ptr.wrapping_add(8 + (idx_i32 as usize) * 8);
    let v = unsafe { *(elem_addr as *const f64) };
    if v.to_bits() == crate::value::TAG_HOLE {
        return f64::from_bits(TAG_UNDEFINED);
    }
    v
}

/// Issue #957 — tag-aware dynamic index write counterpart to
/// `js_dyn_index_get`. Used by `Expr::IndexUpdate` codegen to write back
/// the incremented value without duplicating the IndexSet dispatch tree.
///
/// Routes by the receiver's `gc_type` byte: arrays go through
/// `js_array_set_index_or_string` (numeric/string-key spec dispatch);
/// everything else stringifies the index and routes through
/// `js_object_set_field_by_name`. Strings are immutable — no-op (matches
/// strict-mode `s[i] = x` semantics, close enough for the `++result[key]`
/// pattern this is added for).
#[no_mangle]
pub extern "C" fn js_dyn_index_set(obj: f64, index: f64, value: f64) -> f64 {
    let bits = obj.to_bits();
    let jsval = JSValue::from_bits(bits);
    if jsval.is_string() || jsval.is_short_string() {
        return value;
    }
    let raw_ptr = if jsval.is_pointer() {
        (bits & POINTER_MASK) as usize
    } else if !obj.is_nan()
        && bits != 0
        && bits < 0x0001_0000_0000_0000
        && (bits & 0x3) == 0
        && bits >= 0x10000
    {
        bits as usize
    } else {
        return value;
    };
    if raw_ptr < crate::gc::GC_HEADER_SIZE + 0x1000 {
        return value;
    }
    let is_array = unsafe {
        let gc_header =
            (raw_ptr as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader;
        (*gc_header).obj_type == crate::gc::GC_TYPE_ARRAY
    };
    if is_array {
        crate::array::js_array_set_index_or_string(
            raw_ptr as *mut crate::array::ArrayHeader,
            index,
            value,
        );
        return value;
    }
    // Non-array object: stringify the index and write via the object setter.
    let bits = index.to_bits();
    let top16 = bits >> 48;
    let key_ptr: *const crate::StringHeader = if top16 == 0x7FFF {
        (bits & 0x0000_FFFF_FFFF_FFFF) as *const crate::StringHeader
    } else if top16 == 0x7FF9 {
        crate::value::js_get_string_pointer_unified(index) as *const crate::StringHeader
    } else {
        // Numeric (or other) index — stringify and intern as a UTF-8 key.
        let idx_i32 = if index.is_nan() || index.is_infinite() {
            0
        } else {
            index as i32
        };
        let s = idx_i32.to_string();
        crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32)
    };
    if key_ptr.is_null() {
        return value;
    }
    crate::object::js_object_set_field_by_name(
        raw_ptr as *mut crate::object::ObjectHeader,
        key_ptr,
        value,
    );
    value
}

/// Check if a value should trigger a destructuring default.
/// Returns 1 if the value is TAG_UNDEFINED, or a bare IEEE NaN (e.g., from
/// out-of-bounds array read), 0 otherwise. All other NaN-boxed values
/// (strings, pointers, booleans, etc.) return 0 because their NaN payload
/// does not match NaN or TAG_UNDEFINED exactly.
#[no_mangle]
pub extern "C" fn js_is_undefined_or_bare_nan(value: f64) -> i32 {
    let bits = value.to_bits();
    // TAG_UNDEFINED = 0x7FFC_0000_0000_0001
    if bits == 0x7FFC_0000_0000_0001 {
        return 1;
    }
    // Bare IEEE NaN (0.0/0.0) — produced by OOB array reads
    // Canonical NaN is 0x7FF8_0000_0000_0000 on most platforms
    if bits == 0x7FF8_0000_0000_0000 {
        return 1;
    }
    0
}
