///|
extern "js" fn ffi_property_is_enumerable(
  obj : @core.Any,
  key : String,
) -> Bool =
  #| (obj, key) => obj.propertyIsEnumerable(key)

///|
extern "js" fn ffi_is_prototype_of(obj : @core.Any, target : @core.Any) -> Bool =
  #| (obj, target) => obj.isPrototypeOf(target)

///|
extern "js" fn ffi_object_entries(obj : @core.Any) -> Array[@core.Any] =
  #| (obj) => Object.entries(obj)

///|
extern "js" fn ffi_json_stringify(
  value : @core.Any,
  replacer : @core.Any,
  space : @core.Any,
) -> String =
  #| (value, replacer, space) => JSON.stringify(value, replacer, space)

///|
extern "js" fn ffi_has_own_property(obj : @core.Any, key : String) -> Bool =
  #| (obj, key) => obj.hasOwnProperty(key)

///|
/// JavaScript Object
#external
pub type Object

///|
pub fn Object::to_any(self : Object) -> @core.Any = "%identity"

///|
/// JS: new Object()
/// Returns Any directly - use from_map() for creating objects with initial properties
/// Example: `let obj = @js.from_map({ "key": @core.any(value) })` for typed object creation
pub extern "js" fn Object::new() -> @core.Any =
  #| () => ({})

///|
/// get the Object class
extern "js" fn object_class() -> @core.Any =
  #| () => Object

///|
/// JS: Object.keys(v)
///
/// Returns an array of a given object's own enumerable property names.
/// Note: The returned array is a snapshot and should be treated as immutable.
pub fn Object::keys(v : @core.Any) -> Array[String] {
  @core.any(object_class())._call("keys", [v]) |> @core.identity
}

///|
/// JS: Object.values(v)
///
/// Returns an array of a given object's own enumerable property values.
/// Note: The returned array is a snapshot and should be treated as immutable.
pub fn Object::values(v : @core.Any) -> Array[@core.Any] {
  @core.any(object_class())._call("values", [v]) |> @core.identity
}

///|
/// JS: Object.entries(v)
///
/// Returns an array of a given object's own enumerable property [key, value] pairs.
/// Note: The returned array is a snapshot and should be treated as immutable.
pub fn Object::entries(v : @core.Any) -> Array[(String, @core.Any)] {
  let entries = ffi_object_entries(v |> @core.identity)
  entries.map(entry => {
    (@core.any(entry)["0"] |> @core.identity, @core.any(entry)["1"])
  })
}

///|
/// JS: Object.assign(target, source)
pub fn Object::assign(target : @core.Any, source : @core.Any) -> @core.Any {
  @core.any(object_class())._call("assign", [target, source])
}

///|
/// JS: Object.create(v)
pub fn Object::create(v : @core.Any) -> @core.Any {
  @core.any(object_class())._call("create", [v])
}

///|
/// JS: Object.is(a, b)
/// Determines whether two values are the same value
pub fn Object::is_(a : @core.Any, b : @core.Any) -> Bool {
  @core.any(object_class())._call("is", [a, b]) |> @core.identity
}

///|
/// JS: Object.freeze(obj)
/// Freezes an object, preventing new properties from being added and existing properties from being removed or changed
pub fn Object::freeze(obj : @core.Any) -> @core.Any {
  @core.any(object_class())._call("freeze", [obj])
}

///|
/// JS: Object.seal(obj)
/// Seals an object, preventing new properties from being added and marking all existing properties as non-configurable
pub fn Object::seal(obj : @core.Any) -> @core.Any {
  @core.any(object_class())._call("seal", [obj])
}

///|
/// JS: Object.isFrozen(obj)
/// Determines if an object is frozen
pub fn Object::isFrozen(obj : @core.Any) -> Bool {
  @core.any(object_class())._call("isFrozen", [obj]) |> @core.identity
}

///|
/// JS: Object.isSealed(obj)
/// Determines if an object is sealed
pub fn Object::isSealed(obj : @core.Any) -> Bool {
  @core.any(object_class())._call("isSealed", [obj]) |> @core.identity
}

///|
/// JS: Object.preventExtensions(obj)
/// Prevents new properties from being added to an object
pub fn Object::preventExtensions(obj : @core.Any) -> @core.Any {
  @core.any(object_class())._call("preventExtensions", [obj])
}

///|
/// JS: Object.isExtensible(obj)
/// Determines if an object is extensible
pub fn Object::isExtensible_static(obj : @core.Any) -> Bool {
  @core.any(object_class())._call("isExtensible", [obj]) |> @core.identity
}

///|
/// JS: object.propertyIsEnumerable(k)
pub fn Object::propertyIsEnumerable(self : Self, key : String) -> Bool {
  ffi_property_is_enumerable(self |> @core.identity, key |> @core.identity)
}

///|
/// JS: object.isPrototypeOf(target)
pub fn Object::isPrototypeOf(self : Self, target : @core.Any) -> Bool {
  ffi_is_prototype_of(self.to_any() |> @core.identity, target |> @core.identity)
}

///|
pub fn Object::hasOwnProperty(self : Self, key : String) -> Bool {
  ffi_has_own_property(self |> @core.identity, key |> @core.identity)
}

///|
/// JS: Object.isExtensible(obj)
/// Instance method that calls the static Object.isExtensible
pub fn Object::isExtensible(self : Self) -> Bool {
  @core.any(object_class())._call("isExtensible", [self.to_any()])
  |> @core.identity
}

///|
pub fn Object::to_string(self : Self) -> String {
  ffi_json_stringify(
    self.to_any() |> @core.identity,
    @core.undefined(),
    @core.undefined(),
  )
}

// Object.defineProperty and related APIs
// https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

///|
/// Property descriptor returned by Object.getOwnPropertyDescriptor
/// https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
pub(all) struct PropertyDescriptor {
  /// The value associated with the property (data descriptors only)
  value : @core.Any?
  /// true if the value can be changed with an assignment operator (data descriptors only)
  writable : Bool?
  /// true if the property shows up during enumeration
  enumerable : Bool?
  /// true if the property descriptor can be changed and deleted from the object
  configurable : Bool?
  /// A function serving as a getter for the property (accessor descriptors only)
  get : (() -> @core.Any)?
  /// A function serving as a setter for the property (accessor descriptors only)
  set : ((@core.Any) -> Unit)?
}

///|
/// JS: Object.defineProperty(obj, prop, descriptor)
/// Defines a new property directly on an object, or modifies an existing property
/// https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
pub fn Object::defineProperty(
  obj : @core.Any,
  prop : String,
  value? : @core.Any,
  writable? : Bool,
  enumerable? : Bool,
  configurable? : Bool,
  get? : () -> @core.Any,
  set? : (@core.Any) -> Unit,
) -> @core.Any {
  let descriptor = Object::new()
  if value is Some(v) {
    descriptor["value"] = v |> @core.any
  }
  if writable is Some(v) {
    descriptor["writable"] = v |> @core.any
  }
  if enumerable is Some(v) {
    descriptor["enumerable"] = v |> @core.any
  }
  if configurable is Some(v) {
    descriptor["configurable"] = v |> @core.any
  }
  if get is Some(getter) {
    descriptor["get"] = getter |> @core.any
  }
  if set is Some(setter) {
    descriptor["set"] = setter |> @core.any
  }
  @core.any(object_class())._call("defineProperty", [
    obj,
    @core.any(prop),
    @core.any(descriptor),
  ])
  |> @core.identity
}

///|
/// JS: Object.defineProperties(obj, props)
/// Defines new or modifies existing properties directly on an object
/// https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperties
pub fn Object::defineProperties(
  obj : @core.Any,
  props : @core.Any,
) -> @core.Any {
  @core.any(object_class())._call("defineProperties", [obj, @core.any(props)])
  |> @core.identity
}

///|
extern "js" fn ffi_get(obj : @core.Any, key : String) -> @core.Any =
  #| (obj, key) => obj[key]

///|
/// JS: Object.getOwnPropertyDescriptor(obj, prop)
/// Returns a property descriptor for an own property
/// https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor
pub fn Object::getOwnPropertyDescriptor(
  obj : @core.Any,
  prop : String,
) -> PropertyDescriptor? {
  let desc : @core.Any = @core.any(object_class())._call(
    "getOwnPropertyDescriptor",
    [obj, @core.any(prop)],
  )
  if @core.is_nullish(desc) {
    None
  } else {
    Some({
      value: if ffi_has_own_property(desc, "value") {
        Some(ffi_get(desc, "value"))
      } else {
        None
      },
      writable: if ffi_has_own_property(desc, "writable") {
        Some(ffi_get(desc, "writable").cast())
      } else {
        None
      },
      enumerable: if ffi_has_own_property(desc, "enumerable") {
        Some(ffi_get(desc, "enumerable").cast())
      } else {
        None
      },
      configurable: if ffi_has_own_property(desc, "configurable") {
        Some(ffi_get(desc, "configurable").cast())
      } else {
        None
      },
      get: if ffi_has_own_property(desc, "get") {
        Some(ffi_get(desc, "get").cast())
      } else {
        None
      },
      set: if ffi_has_own_property(desc, "set") {
        Some(ffi_get(desc, "set").cast())
      } else {
        None
      },
    })
  }
}

///|
/// JS: Object.getOwnPropertyDescriptors(obj)
/// Returns all own property descriptors of an object
/// https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptors
pub fn Object::getOwnPropertyDescriptors(obj : @core.Any) -> @core.Any {
  @core.any(object_class())._call("getOwnPropertyDescriptors", [obj])
  |> @core.identity
}

///|
/// JS: Object.getOwnPropertyNames(obj)
/// Returns an array of all own property names (including non-enumerable)
/// https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyNames
pub fn Object::getOwnPropertyNames(obj : @core.Any) -> Array[String] {
  @core.any(object_class())._call("getOwnPropertyNames", [obj])
  |> @core.identity
}

///|
/// JS: Object.getOwnPropertySymbols(obj)
/// Returns an array of all own symbol properties
/// https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertySymbols
pub fn Object::getOwnPropertySymbols(obj : @core.Any) -> Array[@symbol.Symbol] {
  @core.any(object_class())._call("getOwnPropertySymbols", [obj])
  |> @core.identity
}

///|
/// JS: Object.groupBy(items, callbackFn)
/// Groups the elements of an iterable according to the string values returned by a callback function
/// https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/groupBy
pub fn[T] Object::groupBy(
  items : Array[T],
  callback : (T, Int) -> String,
) -> @core.Any {
  let js_callback : (@core.Any, Int) -> String = fn(item, index) {
    callback(item |> @core.identity, index)
  }
  @core.any(object_class())._call("groupBy", [
    @core.any(items),
    @core.any(@core.from_fn2(js_callback)),
  ])
  |> @core.identity
}