///|
/// MoonBit to JavaScript conversion utilities
///
/// This module provides functions to convert MoonBit built-in types to JavaScript values.
/// These conversions have runtime overhead due to iteration and recursive processing.
///
/// For zero-cost conversions, use @core.identity() or @core.any() instead.
///
/// ## Conversion Overhead Summary
/// - Map[String, T] -> JS Object: O(n) iteration + property setting
/// - Json -> JS value: O(n) recursive matching
/// - JS value -> Json: O(n) recursive conversion

///|
/// Convert a MoonBit Map to a JavaScript object.
///
/// # Example
///
/// ```moonbit nocheck
/// let obj = @convert.from_map({ "name": @core.any("Alice"), "age": @core.any(30) })
/// // JavaScript: { name: "Alice", age: 30 }
/// ```
pub fn from_map(map : Map[String, @core.Any]) -> @core.Any {
  let obj = ffi_create_object()
  for k, v in map {
    ffi_set_property(obj, k, v)
  }
  obj
}

///|
extern "js" fn ffi_create_object() -> @core.Any =
  #| () => ({})

///|
extern "js" fn ffi_set_property(
  obj : @core.Any,
  key : String,
  val : @core.Any,
) -> Unit =
  #| (obj, key, val) => { obj[key] = val }

///|
/// Convert a MoonBit Map with optional values to a JavaScript object.
/// Only sets properties where the value is Some and not nullish.
///
/// # Example
///
/// ```moonbit nocheck
/// let _obj = @convert.from_option_map({
///   "name": Some(@core.any("Alice")),
///   "age": Some(@core.any(30)),
///   "email": None,
/// })
/// // JavaScript: { name: "Alice", age: 30 }
/// ```
pub fn from_option_map(map : Map[String, @core.Any?]) -> @core.Any {
  let obj = ffi_create_object()
  for k, v in map {
    match v {
      Some(val) => if !ffi_is_nullish(val) { ffi_set_property(obj, k, val) }
      None => ()
    }
  }
  obj
}

///|
extern "js" fn ffi_is_nullish(v : @core.Any) -> Bool =
  #| (v) => v == null

///|
/// Convert a MoonBit Map with optional values to a JavaScript object,
/// returning undefined if all properties are None or nullish.
///
/// This is useful for JS APIs where passing undefined means "use defaults"
/// while passing an empty object `{}` might have different behavior.
///
/// # Example
///
/// ```moonbit nocheck
/// // All None - returns undefined
/// let result = @convert.from_option_map_or_undefined({
///   "timeout": None,
///   "retries": None,
/// })
/// // JavaScript: undefined
///
/// // Some values - returns object
///
/// let result = @convert.from_option_map_or_undefined({
///   "timeout": Some(@core.any(5000)),
///   "retries": None,
/// })
/// // JavaScript: { timeout: 5000 }
/// ```
pub fn from_option_map_or_undefined(map : Map[String, @core.Any?]) -> @core.Any {
  let obj = ffi_create_object()
  let mut has_value = false
  for k, v in map {
    match v {
      Some(val) =>
        if !ffi_is_nullish(val) {
          ffi_set_property(obj, k, val)
          has_value = true
        }
      None => ()
    }
  }
  if has_value {
    obj
  } else {
    ffi_undefined()
  }
}

///|
extern "js" fn ffi_undefined() -> @core.Any =
  #| () => undefined

///|
/// Convert MoonBit's builtin `Json` type to JavaScript value.
///
/// **Important**: This converts MoonBit's `Json` type (from `@json` package),
/// NOT JavaScript's JSON string. For parsing JSON strings, use `JSON.parse()`.
///
/// # Example
///
/// ```moonbit nocheck
/// // Convert MoonBit Json to JS
/// let json : Json = { "name": "Alice", "age": 30 }
///
/// let js_obj = @convert.from_json(json)
/// ```
pub fn from_json(j : Json) -> @core.Any {
  match j {
    Null => ffi_null()
    String(s) => @core.any(s)
    Number(n, ..) => @core.any(n)
    True => @core.any(true)
    False => @core.any(false)
    Object(o) => {
      let obj = ffi_create_object()
      for k, v in o {
        ffi_set_property(obj, k, from_json(v))
      }
      obj
    }
    Array(a) => {
      let arr = ffi_create_array()
      for v in a {
        ffi_array_push(arr, from_json(v))
      }
      arr
    }
  }
}

///|
extern "js" fn ffi_null() -> @core.Any =
  #| () => null

///|
extern "js" fn ffi_create_array() -> @core.Any =
  #| () => []

///|
extern "js" fn ffi_array_push(arr : @core.Any, val : @core.Any) -> Unit =
  #| (arr, val) => { arr.push(val) }

///|
/// Convert a JavaScript value to MoonBit's `Json` type.
///
/// # Example
///
/// ```moonbit nocheck
/// let js_obj = @core.any({ "name": "Alice" })
///
/// let json = @convert.to_json(js_obj)
/// ```
pub fn to_json(v : @core.Any) -> Json {
  if ffi_is_nullish(v) {
    return Json::null()
  }
  match ffi_typeof(v) {
    "boolean" => {
      let b : Bool = @core.identity(v)
      return b.to_json()
    }
    "number" => {
      let n : Double = @core.identity(v)
      return n.to_json()
    }
    "string" => {
      let s : String = @core.identity(v)
      return s.to_json()
    }
    _ => ()
  }
  if ffi_is_array(v) {
    let len : Int = ffi_get_length(v)
    let arr : Array[Json] = []
    for i = 0; i < len; i = i + 1 {
      let item = ffi_get_index(v, i)
      arr.push(to_json(item))
    }
    return arr.to_json()
  }
  if ffi_typeof(v) == "object" {
    let out : Map[String, Json] = Map([])
    let keys = ffi_object_keys(v)
    let keys_len : Int = ffi_get_length(keys)
    for i = 0; i < keys_len; i = i + 1 {
      let k : String = @core.identity(ffi_get_index(keys, i))
      let val = ffi_get_property(v, k)
      out[k] = to_json(val)
    }
    return out.to_json()
  }
  return Json::object({})
}

///|
extern "js" fn ffi_typeof(v : @core.Any) -> String =
  #| (v) => typeof v

///|
extern "js" fn ffi_is_array(v : @core.Any) -> Bool =
  #| (v) => Array.isArray(v)

///|
extern "js" fn ffi_get_length(v : @core.Any) -> Int =
  #| (v) => v.length

///|
extern "js" fn ffi_get_index(v : @core.Any, i : Int) -> @core.Any =
  #| (v, i) => v[i]

///|
extern "js" fn ffi_object_keys(v : @core.Any) -> @core.Any =
  #| (v) => Object.keys(v)

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

///|
/// Convert an Option to @core.Any, mapping None to null and Some(v) to v.
///
/// # Usage
///
/// - `Some(@core.any(42)) |> from_option` returns 42
/// - `None |> from_option` returns null
pub fn from_option(opt : @core.Any?) -> @core.Any {
  match opt {
    Some(v) => v
    None => ffi_null()
  }
}

///|
/// Safely convert a JavaScript value to an Option type.
///
/// Converts JavaScript `null` or `undefined` to `None`, otherwise returns `Some(value)`.
pub fn[A] to_option(v : @core.Any) -> A? {
  if ffi_is_nullish(v) {
    None
  } else {
    Some(@core.identity(v))
  }
}

///|
/// ## Result Type Utilities
///
/// MoonBit's Result[T, E] is compiled to JavaScript as:
/// - `Ok(value)` → `{ _0: value }` with constructor name containing "Ok"
/// - `Err(error)` → `{ _0: error }` with constructor name containing "Err"
///
/// Use constructor.name.includes("Ok") or .includes("Err") to distinguish.

///|
extern "js" fn ffi_get_constructor_name(v : @core.Any) -> String =
  #|(v) => v?.constructor?.name ?? ""

///|
/// Check if a MoonBit Result value is Ok variant.
///
/// # Example
///
/// ```moonbit nocheck
/// let ok_result : Result[Int, String] = Ok(42)
///
/// let is_ok = @convert.is_result_ok(@core.any(ok_result)) // true
/// ```
pub fn is_result_ok(v : @core.Any) -> Bool {
  ffi_get_constructor_name(v).contains("Ok")
}

///|
/// Check if a MoonBit Result value is Err variant.
///
/// # Example
///
/// ```moonbit nocheck
/// let err_result : Result[Int, String] = Err("error")
///
/// let is_err = @convert.is_result_err(@core.any(err_result)) // true
/// ```
pub fn is_result_err(v : @core.Any) -> Bool {
  ffi_get_constructor_name(v).contains("Err")
}

///|
/// Unwrap the inner value from Ok variant.
/// Returns the inner value if Ok, otherwise returns undefined.
///
/// # Example
///
/// ```moonbit nocheck
/// let ok_result : Result[Int, String] = Ok(42)
///
/// let value : Int = @convert.unwrap_ok(@core.any(ok_result)) // 42
/// ```
pub fn[T] unwrap_ok(v : @core.Any) -> T {
  @core.identity(v["_0"])
}

///|
/// Unwrap the inner value from Err variant.
/// Returns the inner value if Err, otherwise returns undefined.
///
/// # Example
///
/// ```moonbit nocheck
/// let err_result : Result[Int, String] = Err("error")
///
/// let error : String = @convert.unwrap_err(@core.any(err_result)) // "error"
/// ```
pub fn[E] unwrap_err(v : @core.Any) -> E {
  @core.identity(v["_0"])
}

///|
/// Convert a JS value (MoonBit Result) back to MoonBit Result type.
///
/// # Example
///
/// ```moonbit nocheck
/// let ok_result : Result[Int, String] = Ok(42)
///
/// let js_val = @core.any(ok_result)
///
/// let back : Result[Int, String] = @convert.to_result(js_val)
/// ```
pub fn[T, E] to_result(v : @core.Any) -> Result[T, E] {
  if is_result_ok(v) {
    Ok(unwrap_ok(v))
  } else {
    Err(unwrap_err(v))
  }
}

///|
/// ## Enum Type Utilities
///
/// MoonBit enums are compiled to JavaScript as:
/// - Unit variants (no args): `{ $tag: n, $name: "VariantName" }`
/// - Tuple variants: `{ _0: arg0, _1: arg1, ... }` with constructor name "EnumName$VariantName"

///|
/// Get the enum variant name from a MoonBit enum value.
/// Works for both unit variants ($name property) and tuple variants (constructor name).
///
/// # Example
///
/// ```moonbit nocheck
/// // For unit variant: returns "Red"
/// // For tuple variant: returns "Rgb" (parsed from "Color$Rgb")
/// ```
pub fn get_enum_variant_name(v : @core.Any) -> String {
  // Try $name property first (unit variants)
  let name_prop = v["$name"]
  if !ffi_is_nullish(name_prop) {
    return @core.identity(name_prop)
  }
  // Fall back to constructor name (tuple variants)
  let ctor_name = ffi_get_constructor_name(v)
  // Parse "EnumName$VariantName" to get "VariantName"
  let parts : FixedArray[String] = ffi_split(ctor_name, "$")
  let len = parts.length()
  if len > 1 {
    parts[len - 1]
  } else {
    ctor_name
  }
}

///|
extern "js" fn ffi_split(s : String, sep : String) -> FixedArray[String] =
  #|(s, sep) => s.split(sep)

///|
/// Get the $tag index from an enum variant.
/// All enum variants (both unit and tuple) have a $tag property.
/// Returns -1 if not an enum.
pub fn get_enum_tag(v : @core.Any) -> Int {
  let tag = v["$tag"]
  if ffi_is_nullish(tag) {
    -1
  } else {
    @core.identity(tag)
  }
}

///|
/// ## Safe Serialization for JSON Roundtrip
///
/// These functions serialize MoonBit types with explicit type tags,
/// allowing safe JSON roundtrip without losing type information.

///|
/// Serialize a Result with type tag preserved.
/// Output format: `{ "_tag": "Ok" | "Err", "_0": value }`
///
/// # Example
///
/// ```moonbit nocheck
/// let result : Result[Int, String] = Ok(42)
///
/// let serialized = @convert.serialize_result(result)
/// // JavaScript: { "_tag": "Ok", "_0": 42 }
/// ```
pub fn[T, E] serialize_result(result : Result[T, E]) -> @core.Any {
  let obj = @core.new_object()
  match result {
    Ok(v) => {
      obj["_tag"] = @core.any("Ok")
      obj["_0"] = @core.any(v)
    }
    Err(e) => {
      obj["_tag"] = @core.any("Err")
      obj["_0"] = @core.any(e)
    }
  }
  obj
}

///|
/// Deserialize a Result from serialized form.
///
/// # Example
///
/// ```moonbit nocheck
/// let serialized = json_parse("{\"_tag\":\"Ok\",\"_0\":42}")
///
/// let result : Result[Int, String] = @convert.deserialize_result(serialized)
/// ```
pub fn[T, E] deserialize_result(v : @core.Any) -> Result[T, E] {
  let tag : String = @core.identity(v["_tag"])
  if tag == "Ok" {
    Ok(@core.identity(v["_0"]))
  } else {
    Err(@core.identity(v["_0"]))
  }
}

///|
/// Serialize an Option with explicit None representation.
/// Output format: `{ "_some": true, "_0": value }` or `{ "_some": false }`
///
/// # Example
///
/// ```moonbit nocheck
/// let some_val : Int? = Some(42)
///
/// let serialized = @convert.serialize_option(some_val)
/// // JavaScript: { "_some": true, "_0": 42 }
/// ```
pub fn[T] serialize_option(opt : T?) -> @core.Any {
  let obj = @core.new_object()
  match opt {
    Some(v) => {
      obj["_some"] = @core.any(true)
      obj["_0"] = @core.any(v)
    }
    None => obj["_some"] = @core.any(false)
  }
  obj
}

///|
/// Deserialize an Option from serialized form.
///
/// # Example
///
/// ```moonbit nocheck
/// let serialized = json_parse("{\"_some\":true,\"_0\":42}")
///
/// let opt : Int? = @convert.deserialize_option(serialized)
/// ```
pub fn[T] deserialize_option(v : @core.Any) -> T? {
  let is_some : Bool = @core.identity(v["_some"])
  if is_some {
    Some(@core.identity(v["_0"]))
  } else {
    None
  }
}