// Lightweight JS interop without trait vtable overhead

///|
/// Opaque type for JavaScript values
#external
pub type Any

///|
/// Represents a JavaScript constructor/class type.
/// Similar to TypeScript's `typeof ClassName`.
///
/// Usage:
/// ```moonbit
/// // Define a constructor type
/// extern "js" fn get_date_constructor() -> TypeOf[Date] = ...
///
/// // Use with instanceof
/// fn is_date(v : Any) -> Bool {
///   instanceof_(v, Date.constructor_())
/// }
/// ```
#external
pub type TypeOf[T]

///|
pub impl Eq for Any with equal(self, other) -> Bool {
  equal(self, other)
}

///|
/// Check if value is null or undefined
pub extern "js" fn equal(a : Any, b : Any) -> Bool =
  #|(a, b) => a === b

///|
pub extern "js" fn new_object() -> Any =
  #| () => ({})

///|
pub extern "js" fn object_keys(obj : Any) -> Array[String] =
  #| (obj) => Object.keys(obj)

///|
pub extern "js" fn object_values(obj : Any) -> Array[Any] =
  #| (obj) => Object.values(obj)

///|
pub extern "js" fn new_array() -> Any =
  #| () => []

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

// ///|
// pub extern "js" fn new_object() -> Any =
//   #| () => ({})

// ///|
// /// JS: Object.keys(obj)
// pub extern "js" fn Object::keys(obj : Any) -> Array[String] =
//   #| (obj) => Object.keys(obj)

// ///|
// /// JS: Object.values(obj)
// pub extern "js" fn Object::values(obj : Any) -> Array[Any] =
//   #| (obj) => Object.values(obj)

///|
/// JS: Object.entries(obj) - returns array of [key, value] pairs
// pub extern "js" fn Object::entries(obj : Any) -> Array[Any] =
//   #| (obj) => Object.entries(obj)

///|
/// JS: Object.assign(target, ...sources)
pub extern "js" fn object_assign(target : Any, source : Any) -> Any =
  #| (target, source) => Object.assign(target, source)

///|
/// JS: Object.hasOwn(obj, key) or obj.hasOwnProperty(key)
pub extern "js" fn object_has_own(obj : Any, key : String) -> Bool =
  #| (obj, key) => Object.hasOwn(obj, key)

///| JSON functions

///|
/// JS: JSON.stringify(value)
pub extern "js" fn json_stringify(value : Any) -> String =
  #| (value) => JSON.stringify(value)

///|
/// JS: JSON.stringify(value, null, space)
pub extern "js" fn json_stringify_pretty(value : Any, space : Int) -> String =
  #| (value, space) => JSON.stringify(value, null, space)

///|
/// JS: JSON.parse(text)
pub extern "js" fn json_parse(text : String) -> Any =
  #| (text) => JSON.parse(text)

///| Console functions

///| Zero-cost type conversions using %identity
/// These are compile-time only and produce no runtime code

///|
pub fn[A, B] identity(value : A) -> B = "%identity"

///|
pub fn[T] any(value : T) -> Any = "%identity"

///|
pub fn[T] Any::cast(self : Any) -> T = "%identity"

///|
/// Convert Option[Any] to Any (None becomes undefined)
/// Note: Any? is boxed, so we need FFI to extract the value
pub extern "js" fn as_any(opt : Any?) -> Any =
  #| (opt) => opt?._0

///|
/// Convert Option[T] to Any (None becomes undefined)
/// Zero-cost: concrete Option[T] compiles to T | undefined in JS
/// Usage: from_entries([("key", nullable(maybe_value))])
pub fn[T] nullable(opt : T?) -> Any = "%identity"

///|
/// Set property: obj["key"] = value
/// Enables syntax: obj["key"] = val
#alias("_[_]=_")
pub extern "js" fn Any::_set(self : Any, key : String, value : Any) -> Unit =
  #| (obj, key, value) => { obj[key] = value }

///|
/// Call method: obj._call("method", [arg1, arg2])
pub extern "js" fn Any::_call(
  self : Any,
  key : String,
  args : Array[Any],
) -> Any =
  #| (obj, key, args) => obj[key](...args)

///|
/// Call function: func._invoke([arg1, arg2])
pub extern "js" fn Any::_invoke(self : Any, args : Array[Any]) -> Any =
  #| (func, args) => func(...args)

///|
/// JS: value[key]
/// Enables syntax: obj["key"]
#alias("_[_]")
pub extern "js" fn Any::_get(self : Any, key : String) -> Any =
  #| (obj, key) => obj[key]

///|
/// JS: value[key]
/// Enables syntax: obj[index]
pub extern "js" fn Any::_get_by_index(self : Any, key : Int) -> Any =
  #| (obj, key) => obj[key]

///| Global functions

///|
pub extern "js" fn global_this() -> Any =
  #| () => globalThis

///|
pub extern "js" fn undefined() -> Any =
  #| () => undefined

///|
pub extern "js" fn null() -> Any =
  #| () => null

///|
/// JS: new cls(...args)
pub extern "js" fn new(cls : Any, args : Array[Any]) -> Any =
  #|(cls, args) => new cls(...args)

///| Function wrappers

///|
/// Wrap MoonBit function as JS function (0 args)
pub fn[A] from_fn0(f : () -> A) -> Any = "%identity"

///|
/// Wrap MoonBit function as JS function (1 arg)
pub fn[A, B] from_fn1(f : (A) -> B) -> Any = "%identity"

///|
/// Wrap MoonBit function as JS function (2 args)
pub fn[A, B, C] from_fn2(f : (A, B) -> C) -> Any = "%identity"

///|
/// Wrap MoonBit function as JS function (3 args)
pub fn[A, B, C, D] from_fn3(f : (A, B, C) -> D) -> Any = "%identity"

///| Timer functions

///|
/// Convert Array of tuples to JS Object using Object.fromEntries
/// Tuples compile to {_0, _1} objects, which are converted to [key, value] in FFI
pub extern "js" fn from_entries(entries : Array[(String, Any)]) -> Any =
  #| (entries) => Object.fromEntries(entries.map(e => [e._0, e._1]))

///|
/// Convert Any to string representation
pub extern "js" fn Any::to_string(self : Any) -> String =
  #| (self) => self == null ? String(self) : self.toString()

///|
pub impl Show for Any with output(self, logger) {
  logger.write_string(self.to_string())
}

///|
/// Safely convert a JavaScript value to an Option type.
///
/// Converts JavaScript `null` or `undefined` to `None`, otherwise returns `Some(value)`.
/// This is essential for FFI because accessing properties on null/undefined will crash.
///
/// # Common Patterns
///
/// - DOM API nullable returns: `node._get("parentNode") |> identity_option()`
/// - Optional object properties: `obj._get("name") |> identity_option()`
/// - Converting back to Js: use `from_option(opt)`
///
/// # Related Functions
/// - `from_option(A?) -> Js` - Inverse conversion (Option to Js)
/// - `@core.Nullable[T]` - For FFI struct fields that need explicit null handling
pub fn[T] identity_option(v : Any) -> T? {
  if is_nullish(v) {
    None
  } else {
    Some(identity(v))
  }
}

///|
/// Array.from(v)
pub extern "js" fn array_from(v : Any) -> Array[Any] =
  #| (v) => Array.from(v)

///|
pub extern "js" fn instanceof_(v : Any, cls : Any) -> Bool =
  #|(v, cls) => v instanceof cls

///|
/// Type-safe instanceof check: TypeOf[T].is_instanceof(value)
pub fn[T] TypeOf::is_instanceof(self : TypeOf[T], v : Any) -> Bool {
  instanceof_(v, identity(self))
}

///|
/// Convert TypeOf[T] to Any for use with generic functions
pub fn[T] TypeOf::as_any(self : TypeOf[T]) -> Any = "%identity"

///|
/// Type-safe constructor call: new cls(...args) -> T
pub fn[T] new_instance(cls : TypeOf[T], args : Array[Any]) -> T {
  identity(new(identity(cls), args))
}

///|
/// JS: typeof v
pub extern "js" fn typeof_(v : Any) -> String =
  #|(v) => typeof v

///|
/// JS: new cls(...args)
// pub extern "js" fn new_(cls : Any, args : Array[Any]) -> Any =
//   #|(cls, args) => new cls(...args)

///|
/// Convert an Option to Any, mapping None to null and Some(v) to v.
///
/// This is the inverse of `identity_option`. Use this when you need to pass
/// MoonBit Option values back to JavaScript APIs.
///
/// # Usage
///
/// - `Some(42) |> from_option` returns Js(42)
/// - `None |> from_option` returns Js(null)
/// - Use with match to set optional properties on objects
///
/// # See Also
/// - `identity_option(Js) -> A?` - Inverse conversion (Js to Option)
pub fn[A] from_option(opt : A?) -> Any {
  match opt {
    Some(v) => identity(v)
    None => null()
  }
}

///|
pub extern "js" fn log(message : Any) -> Unit =
  #| (message) => { console.log(message); }

///|
pub extern "js" fn throw_(value : Any) -> Unit =
  #| (value) => { throw value }