///|
/// A native JavaScript `Array` whose length and elements may be mutated in
/// place.
///
/// Unlike MoonBit's `Array`, this external type has the JavaScript value ABI
/// by definition and can therefore cross a JavaScript FFI boundary directly.
#cfg(target="js")
#external
pub type JsArray[T]

///|
/// Creates an empty native JavaScript array.
#cfg(target="js")
pub fn[T] JsArray::new() -> JsArray[T] {
  Value::cast_from(js_array_new()).cast()
}

///|
#cfg(target="js")
pub fn[T] JsArray::length(self : JsArray[T]) -> Int {
  js_array_length(Value::cast_from(self))
}

///|
#cfg(target="js")
pub fn[T] JsArray::is_empty(self : JsArray[T]) -> Bool {
  self.length() == 0
}

///|
/// Returns `None` for an out-of-bounds index, a hole, or `undefined`.
#cfg(target="js")
pub fn[T] JsArray::get(self : JsArray[T], index : Int) -> T? {
  guard js_array_has_value(Value::cast_from(self), index) else { None }
  Some(Value::cast_from(js_array_get(Value::cast_from(self), index)).cast())
}

///|
#cfg(target="js")
pub fn[T] JsArray::unsafe_get(self : JsArray[T], index : Int) -> T {
  Value::cast_from(js_array_get(Value::cast_from(self), index)).cast()
}

///|
/// Sets an element in place, expanding the array with holes when needed.
/// Aborts when `index` is negative.
#cfg(target="js")
pub fn[T] JsArray::set(self : JsArray[T], index : Int, value : T) -> Unit {
  guard index >= 0 else { abort("JsArray::set: negative index") }
  js_array_set(Value::cast_from(self), index, Value::cast_from(value))
}

///|
#cfg(target="js")
pub fn[T] JsArray::push(self : JsArray[T], value : T) -> Unit {
  js_array_push(Value::cast_from(self), Value::cast_from(value))
}

///|
#cfg(target="js")
pub fn[T] JsArray::pop(self : JsArray[T]) -> T? {
  guard !self.is_empty() else { None }
  let value = js_array_pop(Value::cast_from(self))
  guard !value.is_undefined() else { None }
  Some(value.cast())
}

///|
/// Iterates the current JavaScript array by index without copying it.
#cfg(target="js")
pub fn[T] JsArray::iter(self : JsArray[T]) -> Iter[T?] {
  let mut index = 0
  Iter::new(fn() {
    guard index < self.length() else { None }
    let value = self.get(index)
    index += 1
    Some(value)
  })
}

///|
#cfg(target="js")
extern "js" fn js_array_new() -> Value = "() => []"

///|
#cfg(target="js")
extern "js" fn js_array_length(array : Value) -> Int = "(array) => array.length"

///|
#cfg(target="js")
extern "js" fn js_array_has_value(array : Value, index : Int) -> Bool =
  #| (array, index) => index >= 0
  #|   && Object.hasOwn(array, index)
  #|   && array[index] !== undefined

///|
#cfg(target="js")
extern "js" fn js_array_get(array : Value, index : Int) -> Value = "(array, index) => array[index]"

///|
#cfg(target="js")
extern "js" fn js_array_set(array : Value, index : Int, value : Value) -> Unit = "(array, index, value) => { array[index] = value }"

///|
#cfg(target="js")
extern "js" fn js_array_push(array : Value, value : Value) -> Unit = "(array, value) => { array.push(value) }"

///|
#cfg(target="js")
extern "js" fn js_array_pop(array : Value) -> Value = "(array) => array.pop()"