///|
using @css {respo_style, type RespoStyle}

///|
extern "js" fn react_create_element(
  tag : String,
  props : @dom.JsObjectObscure,
  children : FixedArray[@dom.JsObscure],
) -> @dom.JsObscure =
  #| (tag, props, children) => globalThis.React.createElement(tag, props, ...children)

///|
extern "js" fn react_fragment(
  props : @dom.JsObjectObscure,
  children : FixedArray[@dom.JsObscure],
) -> @dom.JsObscure =
  #| (props, children) => globalThis.React.createElement(globalThis.React.Fragment, props, ...children)

///|
extern "js" fn react_clone_element_with_key(
  node : @dom.JsObscure,
  key : String,
) -> @dom.JsObscure =
  #| (node, key) => globalThis.React.cloneElement(node, { key })

///|
extern "js" fn react_use_state(initial : JsObscure) -> JsObscure =
  #| (initial) => { return globalThis.React.useState(initial)}

///|
// React hook values are emitted by the MoonBit JavaScript runtime. These
// identity bridges intentionally preserve that runtime representation instead
// of serializing values across the JavaScript boundary.
fn state_updater_from_value(v : JsObscure) -> (JsObscure) -> Unit = "%identity"

///|
fn[T] any_to_js_value(v : T) -> JsObscure = "%identity"

///|
fn[T] any_from_js_value(v : JsObscure) -> T = "%identity"

///|
/// Creates React state initialized with `initial` and returns its current value
/// plus a setter that replaces the state directly.
///
/// Use `use_state_with_updater` when the next state depends on the previous
/// value.
pub fn[T] use_state(initial : T) -> (T, (T) -> Unit) {
  let pair = react_use_state(any_to_js_value(initial))
  let s0 = pair.get("0")
  let s1 = state_updater_from_value(pair.get("1"))
  (any_from_js_value(s0), fn(value : T) { s1(any_to_js_value(value)) })
}

///|
/// Represents a React state update. `Set` replaces state directly and `Update`
/// calculates the next state from React's current value.
pub(all) enum StateUpdate[T] {
  Set(T)
  Update((T) -> T)
}

///|
/// Like `use_state`, but its setter supports functional updates through
/// `StateUpdate::Update`. Prefer this form when the next state depends on the
/// previous value and React may batch or defer updates.
pub fn[T] use_state_with_updater(initial : T) -> (T, (StateUpdate[T]) -> Unit) {
  let pair = react_use_state(any_to_js_value(initial))
  let state : T = any_from_js_value(pair.get("0"))
  let set_state = state_updater_from_value(pair.get("1"))
  (
    state,
    fn(update) {
      match update {
        Set(value) => set_state(any_to_js_value(value))
        Update(update_fn) =>
          set_state(
            @dom.v_to_js_obscure(fn(current) {
              let current : T = any_from_js_value(current)
              any_to_js_value(update_fn(current))
            }),
          )
      }
    },
  )
}

///|
enum ElementPropValue {
  StringProp(String)
  BoolProp(Bool)
  IntProp(Int)
  FloatProp(Float)
  JsProp(JsObscure)
}

///|
/// A mutable collection of React element properties.
///
/// Use the typed setters for primitive values and `set_js_value` only for
/// values such as refs that are already represented as JavaScript objects.
pub struct ElementAttrs(Map[String, ElementPropValue]) derive(Default)

///|
/// Creates an empty React element-property collection.
pub fn ElementAttrs::new() -> ElementAttrs {
  ElementAttrs(Map([]))
}

///|
/// Adds a string-valued React property and returns the same attribute map.
pub fn ElementAttrs::add(
  self : ElementAttrs,
  key : String,
  value : String,
) -> ElementAttrs {
  self.0.set(key, StringProp(value))
  self
}

///|
/// Sets a string-valued React property.
pub fn ElementAttrs::set(
  self : ElementAttrs,
  key : String,
  value : String,
) -> Unit {
  self.0.set(key, StringProp(value))
}

///|
/// Sets a boolean React property without converting it to a string.
pub fn ElementAttrs::set_bool(
  self : ElementAttrs,
  key : String,
  value : Bool,
) -> Unit {
  self.0.set(key, BoolProp(value))
}

///|
/// Sets an integer React property without converting it to a string.
pub fn ElementAttrs::set_int(
  self : ElementAttrs,
  key : String,
  value : Int,
) -> Unit {
  self.0.set(key, IntProp(value))
}

///|
/// Sets a floating-point React property without converting it to a string.
pub fn ElementAttrs::set_float(
  self : ElementAttrs,
  key : String,
  value : Float,
) -> Unit {
  self.0.set(key, FloatProp(value))
}

///|
/// Sets an explicitly pre-converted JavaScript property. Use this escape hatch
/// for React values such as `ref` that cannot be represented as strings,
/// booleans, or integers.
pub fn ElementAttrs::set_js_value(
  self : ElementAttrs,
  key : String,
  value : JsObscure,
) -> Unit {
  self.0.set(key, JsProp(value))
}

///|
fn ElementAttrs::prop_is_array(self : ElementAttrs, key : String) -> Bool {
  match self.0.get(key) {
    Some(JsProp(value)) => value.is_array()
    _ => false
  }
}

///|
fn ElementAttrs::has_true_bool(self : ElementAttrs, key : String) -> Bool {
  match self.0.get(key) {
    Some(BoolProp(value)) => value
    Some(StringProp("true")) => true
    _ => false
  }
}

///|
fn ElementAttrs::string_prop(
  self : ElementAttrs,
  key : String,
  fallback : String,
) -> String {
  match self.0.get(key) {
    Some(StringProp(value)) => value
    _ => fallback
  }
}

///|
fn ElementAttrs::input_value_requires_change(self : ElementAttrs) -> Bool {
  match self.string_prop("type", "text") {
    "button" | "checkbox" | "hidden" | "image" | "radio" | "reset" | "submit" =>
      false
    _ => true
  }
}

///|
/// An opaque React synthetic event passed to MoonBit event handlers.
///
/// Use the typed accessors where available or `to_js_obscure` as an escape
/// hatch for event fields that do not yet have a binding.
pub type DOMEvent

///|
/// Returns the underlying React synthetic event for advanced JavaScript
/// interop.
pub fn DOMEvent::to_js_obscure(self : DOMEvent) -> JsObscure = "%identity"

///|
/// Returns an explicit view of the browser event wrapped by this React
/// SyntheticEvent. This view is intentionally distinct from `DOMEvent`: use
/// `native_pointer_event` or `native_wheel_event` to obtain a checked
/// dom-ffi event-family value.
///
/// [React SyntheticEvent](https://react.dev/reference/react-dom/components/common#react-event-object)
pub extern "js" fn DOMEvent::native_event(self : DOMEvent) -> NativeEvent =
  #| (event) => ({ value: event.nativeEvent ?? event })

///|
/// An opaque view of the browser event associated with a React SyntheticEvent.
///
/// It is not a React SyntheticEvent and only exposes checked conversions to
/// specific dom-ffi event families.
#external
pub type NativeEvent

///|
extern "js" fn NativeEvent::pointer_event_raw(self : NativeEvent) -> JsObscure =
  #| (nativeEvent) =>
  #|   typeof nativeEvent.value?.pointerId === "number" &&
  #|   typeof nativeEvent.value?.button === "number" &&
  #|   typeof nativeEvent.value?.buttons === "number" &&
  #|   typeof nativeEvent.value?.pressure === "number" &&
  #|   typeof nativeEvent.value?.clientX === "number" &&
  #|   typeof nativeEvent.value?.clientY === "number"
  #|     ? nativeEvent.value
  #|     : null

///|
/// Returns a dom-ffi `PointerEvent` when this native event has the required
/// pointer payload. The structural check keeps non-pointer React handlers
/// from being reinterpreted as pointer events.
///
/// [PointerEvent](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent)
pub fn NativeEvent::pointer_event(self : NativeEvent) -> @dom.PointerEvent? {
  let value = self.pointer_event_raw()
  if value.is_nullish() {
    None
  } else {
    Some(@dom.js_obscure_to_v(value))
  }
}

///|
extern "js" fn NativeEvent::wheel_event_raw(self : NativeEvent) -> JsObscure =
  #| (nativeEvent) =>
  #|   typeof nativeEvent.value?.deltaX === "number" &&
  #|   typeof nativeEvent.value?.deltaY === "number" &&
  #|   typeof nativeEvent.value?.deltaMode === "number"
  #|     ? nativeEvent.value
  #|     : null

///|
/// Returns a dom-ffi `WheelEvent` when this native event has the required
/// wheel payload. Non-wheel React handlers receive `None` instead of an
/// unchecked cast.
///
/// [WheelEvent](https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent)
pub fn NativeEvent::wheel_event(self : NativeEvent) -> @dom.WheelEvent? {
  let value = self.wheel_event_raw()
  if value.is_nullish() {
    None
  } else {
    Some(@dom.js_obscure_to_v(value))
  }
}

///|
/// Returns the checked dom-ffi `PointerEvent` carried by this React event, or
/// `None` when the handler received another event family.
pub fn DOMEvent::native_pointer_event(self : DOMEvent) -> @dom.PointerEvent? {
  self.native_event().pointer_event()
}

///|
/// Returns the checked dom-ffi `WheelEvent` carried by this React event, or
/// `None` when the handler received another event family.
pub fn DOMEvent::native_wheel_event(self : DOMEvent) -> @dom.WheelEvent? {
  self.native_event().wheel_event()
}

///|
/// 获取事件目标元素的值(通常用于 input、textarea 等表单元素)
pub extern "js" fn DOMEvent::target_value(self : DOMEvent) -> String =
  #| (event) => event.target ? event.target.value || "" : ""

///|
/// Returns whether the event target is checked. This is intended for controlled
/// checkbox and radio inputs; non-checkable targets return false.
pub extern "js" fn DOMEvent::target_checked(self : DOMEvent) -> Bool =
  #| (event) => event.target?.checked === true

///|
/// 获取键盘事件的键值
pub extern "js" fn DOMEvent::key(self : DOMEvent) -> String =
  #| (event) => event.key || ""

///|
/// 获取键盘事件的键码
pub extern "js" fn DOMEvent::key_code(self : DOMEvent) -> Int =
  #| (event) => event.keyCode || 0

///|
/// 阻止事件的默认行为
pub extern "js" fn DOMEvent::prevent_default(self : DOMEvent) -> Unit =
  #| (event) => event.preventDefault()

///|
/// 阻止事件冒泡
pub extern "js" fn DOMEvent::stop_propagation(self : DOMEvent) -> Unit =
  #| (event) => event.stopPropagation()

///|
/// 获取鼠标事件的 X 坐标
pub extern "js" fn DOMEvent::client_x(self : DOMEvent) -> Int =
  #| (event) => event.clientX || 0

///|
/// 获取鼠标事件的 Y 坐标
pub extern "js" fn DOMEvent::client_y(self : DOMEvent) -> Int =
  #| (event) => event.clientY || 0

///|
/// 检查是否按下了 Ctrl 键
pub extern "js" fn DOMEvent::ctrl_key(self : DOMEvent) -> Bool =
  #| (event) => event.ctrlKey || false

///|
/// 检查是否按下了 Shift 键
pub extern "js" fn DOMEvent::shift_key(self : DOMEvent) -> Bool =
  #| (event) => event.shiftKey || false

///|
/// 检查是否按下了 Alt 键
pub extern "js" fn DOMEvent::alt_key(self : DOMEvent) -> Bool =
  #| (event) => event.altKey || false

///|
/// 检查是否按下了 Meta 键(Mac 上的 Cmd 键)
pub extern "js" fn DOMEvent::meta_key(self : DOMEvent) -> Bool =
  #| (event) => event.metaKey || false

///|
/// Logs a message and an opaque JavaScript value to the browser console.
///
/// This is a low-level debugging helper; application rendering must not depend
/// on its side effect.
pub extern "js" fn console_log2(msg : String, v : JsObscure) -> Unit =
  #| (msg, v) => { console.log(msg, v) }

///|
priv struct DOMEventHandler((DOMEvent) -> Unit)

///|
/// DOM 事件类型枚举
pub(all) enum DOMEventType {
  // 剪贴板事件
  Copy
  Cut
  Paste

  // 输入法组合事件
  CompositionEnd
  CompositionStart
  CompositionUpdate

  // 鼠标事件
  Click
  DoubleClick
  MouseDown
  MouseUp
  MouseMove
  MouseEnter
  MouseLeave
  MouseOver
  MouseOut
  ContextMenu

  // 指针事件
  PointerDown
  PointerMove
  PointerUp
  PointerCancel
  PointerEnter
  PointerLeave
  PointerOver
  PointerOut
  GotPointerCapture
  LostPointerCapture

  // 键盘事件
  KeyDown
  KeyUp
  KeyPress
  BeforeInput

  // 表单事件
  Input
  Change
  Submit
  Reset
  Focus
  Blur
  Select

  // 窗口事件
  Load
  Unload
  Resize
  Scroll
  Wheel

  // 拖拽事件
  Drag
  DragStart
  DragEnd
  DragEnter
  DragLeave
  DragOver
  Drop

  // 触摸事件
  TouchStart
  TouchMove
  TouchEnd
  TouchCancel

  // 其他常用事件
  Error
  Abort
  CanPlay
  CanPlayThrough
  DurationChange
  Ended
  LoadedData
  LoadedMetadata
  LoadStart
  Pause
  Play
  Playing
  Progress
  RateChange
  Seeked
  Seeking
  Stalled
  Suspend
  TimeUpdate
  VolumeChange
  Waiting

  // CSS 动画和过渡事件
  AnimationStart
  AnimationEnd
  AnimationIteration
  TransitionEnd

  // 其他可冒泡的元素事件
  Invalid
  Toggle
  Cancel
  Close
} derive(Eq, Compare, Hash)

///|
/// 将 DOMEventType 转换为字符串
pub fn DOMEventType::to_string(self : DOMEventType) -> String {
  match self {
    Copy => "copy"
    Cut => "cut"
    Paste => "paste"
    CompositionEnd => "compositionend"
    CompositionStart => "compositionstart"
    CompositionUpdate => "compositionupdate"
    Click => "click"
    DoubleClick => "dblclick"
    MouseDown => "mousedown"
    MouseUp => "mouseup"
    MouseMove => "mousemove"
    MouseEnter => "mouseenter"
    MouseLeave => "mouseleave"
    MouseOver => "mouseover"
    MouseOut => "mouseout"
    ContextMenu => "contextmenu"
    PointerDown => "pointerdown"
    PointerMove => "pointermove"
    PointerUp => "pointerup"
    PointerCancel => "pointercancel"
    PointerEnter => "pointerenter"
    PointerLeave => "pointerleave"
    PointerOver => "pointerover"
    PointerOut => "pointerout"
    GotPointerCapture => "gotpointercapture"
    LostPointerCapture => "lostpointercapture"
    KeyDown => "keydown"
    KeyUp => "keyup"
    KeyPress => "keypress"
    BeforeInput => "beforeinput"
    Input => "input"
    Change => "change"
    Submit => "submit"
    Reset => "reset"
    Focus => "focus"
    Blur => "blur"
    Select => "select"
    Load => "load"
    Unload => "unload"
    Resize => "resize"
    Scroll => "scroll"
    Wheel => "wheel"
    Drag => "drag"
    DragStart => "dragstart"
    DragEnd => "dragend"
    DragEnter => "dragenter"
    DragLeave => "dragleave"
    DragOver => "dragover"
    Drop => "drop"
    TouchStart => "touchstart"
    TouchMove => "touchmove"
    TouchEnd => "touchend"
    TouchCancel => "touchcancel"
    Error => "error"
    Abort => "abort"
    CanPlay => "canplay"
    CanPlayThrough => "canplaythrough"
    DurationChange => "durationchange"
    Ended => "ended"
    LoadedData => "loadeddata"
    LoadedMetadata => "loadedmetadata"
    LoadStart => "loadstart"
    Pause => "pause"
    Play => "play"
    Playing => "playing"
    Progress => "progress"
    RateChange => "ratechange"
    Seeked => "seeked"
    Seeking => "seeking"
    Stalled => "stalled"
    Suspend => "suspend"
    TimeUpdate => "timeupdate"
    VolumeChange => "volumechange"
    Waiting => "waiting"
    AnimationStart => "animationstart"
    AnimationEnd => "animationend"
    AnimationIteration => "animationiteration"
    TransitionEnd => "transitionend"
    Invalid => "invalid"
    Toggle => "toggle"
    Cancel => "cancel"
    Close => "close"
  }
}

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

///|
struct ElementEvents(Map[DOMEventType, DOMEventHandler]) derive(Default)

///|
/// Creates an empty mapping from DOM event types to React event handlers.
pub fn ElementEvents::new() -> ElementEvents {
  ElementEvents(Map([]))
}

///|
fn DOMEventHandler::to_js_func(self : DOMEventHandler) -> JsObscure = "%identity"

///|
/// 使用 DOMEventType 添加事件处理器
pub fn ElementEvents::add(
  self : ElementEvents,
  event_type : DOMEventType,
  value : (DOMEvent) -> Unit,
) -> ElementEvents {
  self.set(event_type, value)
  self
}

///|

///|
/// 使用 DOMEventType 设置事件处理器
pub fn ElementEvents::set(
  self : ElementEvents,
  event_type : DOMEventType,
  value : (DOMEvent) -> Unit,
) -> Unit {
  self.0.set(event_type, value)
}

///|
/// Converts this event map to a JavaScript React-props object whose keys use
/// handler names such as `onClick`.
pub fn ElementEvents::to_js_value(self : ElementEvents) -> JsObscure {
  // React consumes handler properties such as onClick, not native names like click.
  let obj = @dom.JsObjectObscure::new()
  for event_type, value in self.0 {
    obj.set(dom_event_to_react_handler(event_type), value.to_js_func())
  }
  obj.to_js_obscure()
}

///|
/// Represents a virtual DOM node in the React rendering system.
///
/// This is the core type for building virtual DOM trees. Each variant represents
/// a different kind of node that can be rendered to the actual DOM.
pub(all) enum VirtualNode {
  /// A standard HTML element with attributes, events, and children
  Element(VirtualElement)
  /// A container for multiple child nodes without creating a wrapper element
  Fragment(Array[VirtualNode])
  /// A text node containing plain string content
  Text(String)
  /// A pre-converted JavaScript value for advanced usage (e.g., from `component` function)
  /// already converted to JsObscure, hard to convert back
  JsNode(@dom.JsObscure) // for advanced usage, e.g. connect
}

///|
/// Converts a virtual node into the JavaScript value consumed by React.
///
/// Most applications pass virtual nodes to `render`, `component`, or another
/// element helper instead of calling this interop method directly.
pub fn VirtualNode::to_js_obscure(self : VirtualNode) -> @dom.JsObscure {
  let ret = match self {
    Element(el) => el.to_js_value()
    Fragment(children) => {
      let v = []
      for child in children {
        v.push(child.to_js_obscure())
      }
      react_fragment(@dom.JsObjectObscure::new(), FixedArray::from_array(v))
    }
    Text(t) => JsObscure::from_string(t)
    JsNode(v) => v
  }
  ret
}

///|
/// Assigns a stable React reconciliation key to a virtual node.
///
/// This works uniformly for elements, components, fragments, and text without
/// adding a DOM wrapper. Use stable application identities rather than list
/// indexes when rendering dynamic collections.
pub fn VirtualNode::with_key(self : VirtualNode, key : String) -> VirtualNode {
  match self {
    Element(element) => {
      element.attrs.set("key", key)
      Element(element)
    }
    Fragment(children) => {
      let props = @dom.JsObjectObscure::new()
      props.set("key", JsObscure::from_string(key))
      JsNode(
        react_fragment(
          props,
          FixedArray::from_array(
            children.map(fn(child) { child.to_js_obscure() }),
          ),
        ),
      )
    }
    JsNode(node) => JsNode(react_clone_element_with_key(node, key))
    Text(text) => {
      let props = @dom.JsObjectObscure::new()
      props.set("key", JsObscure::from_string(key))
      JsNode(
        react_fragment(
          props,
          FixedArray::from_array([JsObscure::from_string(text)]),
        ),
      )
    }
  }
}

///|
/// Converts legacy DOM attribute names to React-compatible property names.
/// This function provides backward compatibility for traditional HTML attribute names.
/// Most attributes should use the correct React property names directly in element definitions.
///
/// # Parameters
/// - `attr_name`: HTML attribute name such as "class", "for", or "innerHTML".
///
/// # Returns
/// `String` - React-compatible property name.
///
/// ```
/// inspect(dom_attr_to_react_prop("class"), content="className")
/// inspect(dom_attr_to_react_prop("for"), content="htmlFor")
/// inspect(dom_attr_to_react_prop("innerHTML"), content="dangerouslySetInnerHTML")
/// inspect(dom_attr_to_react_prop("id"), content="id")
/// inspect(dom_attr_to_react_prop("data-test"), content="data-test")
/// ```
pub fn dom_attr_to_react_prop(attr_name : String) -> String {
  match attr_name {
    // 保留向后兼容性,支持传统的 HTML 属性名
    "class" => "className"
    "for" => "htmlFor"
    "innerHTML" => "dangerouslySetInnerHTML"
    // 其他属性名应该在元素定义时就使用正确的 React 属性名
    _ => attr_name
  }
}

///|
/// Represents a virtual DOM element with all its properties and children.
///
/// This structure encapsulates all the information needed to create and manage
/// a DOM element in the virtual DOM tree, including its tag name, attributes,
/// event handlers, styles, and child nodes.
///
pub struct VirtualElement {
  /// The HTML tag name of the element (e.g., "div", "span", "button").
  name : String
  /// HTML attributes for the element (e.g., id, class, data-* attributes).
  attrs : ElementAttrs
  /// Event handlers attached to the element (e.g., click, input, focus).
  event : ElementEvents
  /// CSS styles applied to the element.
  style : RespoStyle
  /// Child virtual nodes contained within this element.
  children : Array[VirtualNode]
}

///|
/// Wraps this virtual element as a general `VirtualNode`.
pub fn VirtualElement::to_node(self : VirtualElement) -> VirtualNode {
  Element(self)
}

///|
/// Converts CSS property names from hyphen-case (kebab-case) to React camelCase.
/// Supports vendor prefixes (leading hyphen) by capitalizing the first segment.
///
/// # Parameters
/// - `name`: CSS property name such as "background-color" or "-webkit-line-clamp".
///
/// # Returns
/// `String` - React-style camelCase property.
///
/// ```
/// inspect(css_prop_to_camel_case("background-color"), content="backgroundColor")
/// inspect(css_prop_to_camel_case("border-top-left-radius"), content="borderTopLeftRadius")
/// inspect(css_prop_to_camel_case("-webkit-line-clamp"), content="WebkitLineClamp")
/// ```
pub fn css_prop_to_camel_case(name : String) -> String {
  // Detect leading hyphen (e.g., -webkit-)
  let had_leading = name.has_prefix("-")
  // Trim leading/trailing hyphens (typically we only expect leading ones)
  let s0 = name.trim(chars="-")
  // Split into parts by hyphen
  let parts = s0.split("-")
  // Assemble camelCase
  let mut ret = "".to_string()
  let mut idx = 0
  for part in parts {
    if idx == 0 {
      ret = ret + part.to_owned()
    } else if part.length() > 0 {
      ret = ret + first_letter_to_uppercase(part.to_owned())
    }
    idx = idx + 1
  }
  // For vendor prefixes: capitalize first letter (e.g., WebkitLineClamp)
  if had_leading && ret.length() > 0 {
    ret = first_letter_to_uppercase(ret)
  }
  ret
}

///|
/// Converts a RespoStyle to a JavaScript style object.
/// This function takes CSS properties from RespoStyle and converts them to
/// React-compatible camelCase property names with string values.
///
/// # Parameters
/// - `style`: RespoStyle containing CSS properties
///
/// # Returns
/// `JsObject` - JavaScript object with camelCase CSS properties
///
/// # Example
/// ```moonbit_nocheck
/// let style = @css.respo_style(background_color=Red, font_size=16.0 |> Px)
/// let js_style = convert_style_to_js_object(style)
/// // Results in: { backgroundColor: "red", fontSize: "16px" }
/// ```
pub fn convert_style_to_js_object(
  style : @css.RespoStyle,
) -> @dom.JsObjectObscure {
  let style_obj = @dom.JsObjectObscure::new()
  for _idx, pair in style.0 {
    let (key, value) = pair
    style_obj.set(css_prop_to_camel_case(key), JsObscure::from_string(value))
  }
  style_obj
}

///|
fn VirtualElement::to_js_value(self : VirtualElement) -> @dom.JsObscure {
  let has_inner_html = self.attrs.0.contains("innerHTML") ||
    self.attrs.0.contains("dangerouslySetInnerHTML")
  if has_inner_html && !self.children.is_empty() {
    abort(
      "React elements cannot combine children with innerHTML; choose exactly one content source",
    )
  }
  if self.name == "input" || self.name == "textarea" || self.name == "select" {
    if self.attrs.0.contains("value") && self.attrs.0.contains("defaultValue") {
      abort(
        "React form elements cannot combine value with defaultValue; choose controlled or uncontrolled state",
      )
    }
    if self.name == "select" {
      let is_multiple = self.attrs.has_true_bool("multiple")
      for prop_name in ["value", "defaultValue"] {
        if self.attrs.0.contains(prop_name) &&
          self.attrs.prop_is_array(prop_name) != is_multiple {
          if is_multiple {
            abort(
              "A multiple React select requires value and defaultValue to be arrays",
            )
          } else {
            abort(
              "A single React select requires value and defaultValue to be scalar values",
            )
          }
        }
      }
    }
    let input_type = self.attrs.string_prop("type", "text")
    if self.name == "input" &&
      input_type == "file" &&
      (self.attrs.0.contains("value") || self.attrs.0.contains("defaultValue")) {
      abort("React file inputs cannot receive value or defaultValue")
    }
    let value_is_controlled = self.attrs.0.contains("value") &&
      (self.name != "input" || self.attrs.input_value_requires_change())
    let controlled = value_is_controlled ||
      (self.name == "input" && self.attrs.0.contains("checked"))
    let can_be_read_only = self.name == "input" || self.name == "textarea"
    let explicitly_read_only = can_be_read_only &&
      self.attrs.has_true_bool("readOnly")
    if controlled &&
      !self.event.0.contains(Change) &&
      !self.attrs.has_true_bool("disabled") &&
      !explicitly_read_only {
      abort(
        "Controlled React form elements require on_change, read_only=true, or disabled=true",
      )
    }
  }
  if self.name == "input" &&
    self.attrs.0.contains("checked") &&
    self.attrs.0.contains("defaultChecked") {
    abort(
      "React inputs cannot combine checked with defaultChecked; choose controlled or uncontrolled state",
    )
  }
  if self.name == "option" && self.attrs.0.contains("selected") {
    abort(
      "React options do not support selected; set value or defaultValue on the parent select",
    )
  }
  let props = @dom.JsObjectObscure::new()
  for key, value in self.attrs.0 {
    let react_prop_name = dom_attr_to_react_prop(key)
    let prop_value = match value {
      StringProp(value) => convert_prop_value(react_prop_name, value)
      BoolProp(value) => @dom.v_to_js_obscure(value)
      IntProp(value) => @dom.v_to_js_obscure(value)
      FloatProp(value) => @dom.v_to_js_obscure(value)
      JsProp(value) => value
    }
    props.set(react_prop_name, prop_value)
  }
  let style = convert_style_to_js_object(self.style)
  props.set("style", style.to_js_obscure())
  let children = []
  for child in self.children {
    children.push(child.to_js_obscure())
  }
  for event_type, value in self.event.0 {
    let event_name = dom_event_to_react_handler(event_type)
    props.set(event_name, @dom.v_to_js_obscure(value))
  }
  react_create_element(self.name, props, FixedArray::from_array(children))
}

///|
/// Creates a virtual DOM element with the specified properties.
///
/// This is the internal implementation used by built-in element functions like `div`, `span`, etc.
/// It can also be used to create custom HTML elements that are not provided by the library.
///
/// # Parameters
/// - `name`: HTML tag name (e.g., "div", "span", "custom-element")
/// - `attrs`: Element attributes
/// - `event`: Event handlers
/// - `style`: CSS styles
/// - `children`: Child virtual nodes
///
/// # Example
/// ```moonbit nocheck
/// // Create a custom element
/// let _custom_elem = create_element(
///   "my-custom-element",
///   ElementAttrs::new(),
///   ElementEvents::new(),
///   style=@css.respo_style(),
///   [Text("Custom content")],
/// )
/// ```
pub fn create_element(
  name : String,
  attrs : ElementAttrs,
  event : ElementEvents,
  style~ : RespoStyle,
  children : Array[VirtualNode],
) -> VirtualElement {
  VirtualElement::{ name, attrs, event, style, children }
}

///|
extern "js" fn create_factory(
  component_key : @dom.JsObscure,
  render_component : (@dom.JsObscure) -> @dom.JsObscure,
  props : @dom.JsObscure,
  children : FixedArray[@dom.JsObscure],
) -> @dom.JsObscure =
  #| (componentKey, renderComponent, props, children) => {
  #|   const factories = globalThis.__moonbitReactComponentFactories ??= new WeakMap();
  #|   let factory = factories.get(componentKey);
  #|   if (!factory) {
  #|     factory = (internalProps) =>
  #|       internalProps.__moonbitRenderComponent(internalProps.__moonbitProps);
  #|     factories.set(componentKey, factory);
  #|   }
  #|   const internalProps = {
  #|     __moonbitRenderComponent: renderComponent,
  #|     __moonbitProps: props,
  #|   };
  #|   let h0 = globalThis.React.createElement(factory, internalProps, ...children);
  #|   return h0;
  #| }

///|
/// Creates a component virtual node from a function, props, and children.
///
/// This bridges MoonBit component functions with React's rendering system,
/// enabling type-safe component creation with strongly-typed props.
///
/// # Example
/// ```moonbit_nocheck
/// struct ButtonProps {
///   text : String
///   disabled : Bool
/// } derive(Default)
///
/// fn my_button(props : ButtonProps) -> VirtualNode {
///   button(disabled=props.disabled, [Text(props.text)])
/// }
///
/// let node = component(my_button, ButtonProps { text: "Click me", disabled: false }, [])
/// ```
pub fn[T] component(
  /// Component function that transforms props into a virtual node
  f : (T) -> VirtualNode,
  /// Props to pass to the component function
  props : T,
  /// Child nodes to render inside the component
  children : Array[VirtualNode],
) -> VirtualNode {
  let children_js = children.map(fn(child) { child.to_js_obscure() })
  let r = create_factory(
    @dom.v_to_js_obscure(f),
    fn(p) { f(@dom.js_obscure_to_v(p)).to_js_obscure() },
    // Props retain their MoonBit JavaScript representation without JSON
    // conversion or cloning, nested inside the factory's internal React props.
    @dom.v_to_js_obscure(props),
    FixedArray::from_array(children_js),
  )
  JsNode(r)
}

///|
/// Creates a component virtual node whose function receives the child nodes.
///
/// Use this when a component needs to decide where to place its children. The
/// existing `component` function remains the concise choice for leaf components.
pub fn[T] component_with_children(
  /// Component function that receives props and the original MoonBit child nodes.
  f : (T, Array[VirtualNode]) -> VirtualNode,
  /// Props to pass to the component function.
  props : T,
  /// Child nodes supplied by the component's caller.
  children : Array[VirtualNode],
) -> VirtualNode {
  let children_js = children.map(fn(child) { child.to_js_obscure() })
  let r = create_factory(
    @dom.v_to_js_obscure(f),
    fn(p) { f(@dom.js_obscure_to_v(p), children).to_js_obscure() },
    @dom.v_to_js_obscure(props),
    FixedArray::from_array(children_js),
  )
  JsNode(r)
}

///|
fn first_letter_to_uppercase(s : String) -> String {
  match s.get_char(0) {
    Some(first) => first.to_string().to_upper() + s[1:].to_owned()
    None => ""
  }
}

///|
/// Converts DOM event types to React-compatible event handler names.
/// Ensures proper camelCase formatting for React event handlers.
///
/// # Parameters
/// - `event_type`: DOM event type enum value.
///
/// # Returns
/// `String` - React-compatible event handler name with "on" prefix.
/// ```
/// inspect(dom_event_to_react_handler(KeyDown), content="onKeyDown")
/// inspect(dom_event_to_react_handler(Click), content="onClick")
/// inspect(dom_event_to_react_handler(MouseEnter), content="onMouseEnter")
/// inspect(dom_event_to_react_handler(DoubleClick), content="onDoubleClick")
/// ```
pub fn dom_event_to_react_handler(event_type : DOMEventType) -> String {
  match event_type {
    Copy => "onCopy"
    Cut => "onCut"
    Paste => "onPaste"
    CompositionEnd => "onCompositionEnd"
    CompositionStart => "onCompositionStart"
    CompositionUpdate => "onCompositionUpdate"
    Click => "onClick"
    DoubleClick => "onDoubleClick"
    MouseDown => "onMouseDown"
    MouseUp => "onMouseUp"
    MouseMove => "onMouseMove"
    MouseEnter => "onMouseEnter"
    MouseLeave => "onMouseLeave"
    MouseOver => "onMouseOver"
    MouseOut => "onMouseOut"
    ContextMenu => "onContextMenu"
    PointerDown => "onPointerDown"
    PointerMove => "onPointerMove"
    PointerUp => "onPointerUp"
    PointerCancel => "onPointerCancel"
    PointerEnter => "onPointerEnter"
    PointerLeave => "onPointerLeave"
    PointerOver => "onPointerOver"
    PointerOut => "onPointerOut"
    GotPointerCapture => "onGotPointerCapture"
    LostPointerCapture => "onLostPointerCapture"
    KeyDown => "onKeyDown"
    KeyUp => "onKeyUp"
    KeyPress => "onKeyPress"
    BeforeInput => "onBeforeInput"
    Input => "onInput"
    Change => "onChange"
    Submit => "onSubmit"
    Reset => "onReset"
    Focus => "onFocus"
    Blur => "onBlur"
    Select => "onSelect"
    Load => "onLoad"
    Unload => "onUnload"
    Resize => "onResize"
    Scroll => "onScroll"
    Wheel => "onWheel"
    Drag => "onDrag"
    DragStart => "onDragStart"
    DragEnd => "onDragEnd"
    DragEnter => "onDragEnter"
    DragLeave => "onDragLeave"
    DragOver => "onDragOver"
    Drop => "onDrop"
    TouchStart => "onTouchStart"
    TouchMove => "onTouchMove"
    TouchEnd => "onTouchEnd"
    TouchCancel => "onTouchCancel"
    Error => "onError"
    Abort => "onAbort"
    CanPlay => "onCanPlay"
    CanPlayThrough => "onCanPlayThrough"
    DurationChange => "onDurationChange"
    Ended => "onEnded"
    LoadedData => "onLoadedData"
    LoadedMetadata => "onLoadedMetadata"
    LoadStart => "onLoadStart"
    Pause => "onPause"
    Play => "onPlay"
    Playing => "onPlaying"
    Progress => "onProgress"
    RateChange => "onRateChange"
    Seeked => "onSeeked"
    Seeking => "onSeeking"
    Stalled => "onStalled"
    Suspend => "onSuspend"
    TimeUpdate => "onTimeUpdate"
    VolumeChange => "onVolumeChange"
    Waiting => "onWaiting"
    AnimationStart => "onAnimationStart"
    AnimationEnd => "onAnimationEnd"
    AnimationIteration => "onAnimationIteration"
    TransitionEnd => "onTransitionEnd"
    Invalid => "onInvalid"
    Toggle => "onToggle"
    Cancel => "onCancel"
    Close => "onClose"
  }
}

///|
/// Checks if a React property should be treated as a boolean attribute.
/// Returns true for attributes that React expects as boolean values.
///
/// # Parameters
/// - `prop_name`: React property name (already converted from DOM attribute).
///
/// # Returns
/// `Bool` - true if the property should be treated as boolean.
///
/// ```
/// inspect(is_boolean_prop("checked"), content="true")
/// inspect(is_boolean_prop("disabled"), content="true")
/// inspect(is_boolean_prop("readOnly"), content="true")
/// inspect(is_boolean_prop("className"), content="false")
/// ```
pub fn is_boolean_prop(prop_name : String) -> Bool {
  match prop_name {
    "checked"
    | "disabled"
    | "readOnly"
    | "required"
    | "autoFocus"
    | "autoPlay"
    | "controls"
    | "defer"
    | "hidden"
    | "loop"
    | "multiple"
    | "muted"
    | "open"
    | "reversed"
    | "selected"
    | "autoComplete"
    | "autoCorrect"
    | "autoCapitalize"
    | "spellCheck"
    | "translate"
    | "contentEditable"
    | "draggable"
    | "suppressContentEditableWarning"
    | "suppressHydrationWarning" => true
    _ => false
  }
}

///|
/// Converts string values to appropriate JavaScript values for React props.
/// Handles boolean attributes by converting "true"/"false" strings to actual booleans.
/// The internal `innerHTML` attribute is converted to React's
/// `dangerouslySetInnerHTML` object; only provide trusted HTML to that API.
///
/// # Parameters
/// - `prop_name`: React property name.
/// - `value`: String value from the attribute.
///
/// # Returns
/// `JsObscure` - Properly typed JavaScript value for React.
///
/// # Inspect Examples
/// inspect(convert_prop_value("checked", "true"), content="JsObscure::from_bool(true)")
/// inspect(convert_prop_value("className", "my-class"), content="JsObscure::from_string(\"my-class\")")
pub fn convert_prop_value(prop_name : String, value : String) -> @dom.JsObscure {
  if prop_name == "dangerouslySetInnerHTML" {
    let html = @dom.JsObjectObscure::new()
    html.set("__html", JsObscure::from_string(value))
    html.to_js_obscure()
  } else if is_boolean_prop(prop_name) {
    match value {
      "true" => JsObscure::from_bool(true)
      "false" => JsObscure::from_bool(false)
      _ => @dom.v_to_js_obscure(value) // fallback for non-standard values
    }
  } else {
    @dom.v_to_js_obscure(value)
  }
}