///|
/// A stable React component type carrying one typed MoonBit props value.
///
/// Render it with `ReactComponent::render`. JavaScript component bridges receive
/// the typed value in a single `moonbitProps` property rather than relying on
/// the generated representation being spread into JavaScript props.
struct ReactComponent[T] {
  js_value : @dom.JsObscure
  _props_marker : T?
}

///|
extern "js" fn react_define_component(
  component_key : @dom.JsObscure,
  render_component : @dom.JsObscure,
) -> @dom.JsObscure =
  #| (componentKey, renderComponent) => {
  #|   const components = globalThis.__moonbitReactComponentTypes ??= new WeakMap();
  #|   let component = components.get(componentKey);
  #|   if (!component) {
  #|     component = (props) => renderComponent(props.moonbitProps);
  #|     components.set(componentKey, component);
  #|   }
  #|   return component;
  #| }

///|
extern "js" fn react_component_element(
  component : @dom.JsObscure,
  props : @dom.JsObscure,
) -> @dom.JsObscure =
  #| (component, moonbitProps) =>
  #|   globalThis.React.createElement(component, { moonbitProps })

///|
extern "js" fn react_memo_component(
  component : @dom.JsObscure,
  are_props_equal : @dom.JsObscure,
) -> @dom.JsObscure =
  #| (component, arePropsEqual) => globalThis.React.memo(
  #|   component,
  #|   arePropsEqual == null
  #|     ? undefined
  #|     : (previous, next) =>
  #|         arePropsEqual(previous.moonbitProps, next.moonbitProps),
  #| )

///|
extern "js" fn react_lazy_component(loader : @dom.JsObscure) -> @dom.JsObscure =
  #| (loader) => globalThis.React.lazy(loader)

///|
extern "js" fn react_component_module(
  component : @dom.JsObscure,
) -> @dom.JsObscure =
  #| (component) => ({ default: component })

///|
extern "js" fn react_suspense(
  fallback : @dom.JsObscure,
  children : FixedArray[@dom.JsObscure],
) -> @dom.JsObscure =
  #| (fallback, children) => globalThis.React.createElement(
  #|   globalThis.React.Suspense,
  #|   { fallback },
  #|   ...children,
  #| )

///|
/// React 19.2 Activity visibility modes.
pub(all) enum ActivityMode {
  Visible
  Hidden
} derive(Eq)

///|
/// Returns the React `mode` property for this Activity mode.
pub fn ActivityMode::to_string(self : ActivityMode) -> String {
  match self {
    Visible => "visible"
    Hidden => "hidden"
  }
}

///|
extern "js" fn react_activity(
  mode : String,
  children : FixedArray[@dom.JsObscure],
) -> @dom.JsObscure =
  #| (mode, children) => globalThis.React.createElement(
  #|   globalThis.React.Activity,
  #|   { mode },
  #|   ...children,
  #| )

///|
/// Defines a stable typed React component from a MoonBit render function.
/// Prefer module-level declarations; repeated calls with the same function also
/// reuse the same JavaScript component type.
pub fn[T] define_component(
  render_component : (T) -> VirtualNode,
) -> ReactComponent[T] {
  ReactComponent::{
    js_value: react_define_component(
      @dom.v_to_js_obscure(render_component),
      @dom.v_to_js_obscure(fn(props : T) {
        render_component(props).to_js_obscure()
      }),
    ),
    _props_marker: None,
  }
}

///|
/// Declares the expected MoonBit props type for a trusted JavaScript React
/// component. The JavaScript component receives `{ moonbitProps }` and the
/// rendered value remains a `JsNode` escape hatch.
pub fn[T] component_from_js(component : @dom.JsObscure) -> ReactComponent[T] {
  ReactComponent::{ js_value: component, _props_marker: None }
}

///|
/// Renders this component with one typed MoonBit props value.
pub fn[T] ReactComponent::render(
  self : ReactComponent[T],
  props : T,
) -> VirtualNode {
  JsNode(react_component_element(self.js_value, @dom.v_to_js_obscure(props)))
}

///|
/// Returns the underlying React component type for explicit JavaScript
/// interoperation.
pub fn[T] ReactComponent::to_js_obscure(
  self : ReactComponent[T],
) -> @dom.JsObscure {
  self.js_value
}

///|
/// Returns a memoized component. Without `are_props_equal`, React compares the
/// single typed `moonbitProps` value with `Object.is`; a custom comparator
/// receives the previous and next MoonBit values directly.
pub fn[T] ReactComponent::memo(
  self : ReactComponent[T],
  are_props_equal? : (T, T) -> Bool,
) -> ReactComponent[T] {
  let comparator = match are_props_equal {
    Some(callback) => @dom.v_to_js_obscure(callback)
    None => @dom.JsObscure::null()
  }
  ReactComponent::{
    js_value: react_memo_component(self.js_value, comparator),
    _props_marker: None,
  }
}

///|
/// Defines a lazy typed component. Declare it outside render paths. React calls
/// and caches `loader` on first render; the MoonBit async bridge resolves to the
/// `{ default: component }` module shape required by `React.lazy`.
pub fn[T] lazy_component(
  loader : async () -> ReactComponent[T],
) -> ReactComponent[T] {
  let js_loader = fn() {
    @js_async.Promise::from_async(async fn() {
      let component = loader()
      react_component_module(component.js_value)
    })
  }
  ReactComponent::{
    js_value: react_lazy_component(@dom.v_to_js_obscure(js_loader)),
    _props_marker: None,
  }
}

///|
/// Creates a Suspense boundary that displays `fallback` until every child is
/// ready, then reveals the children together.
pub fn suspense(
  fallback : VirtualNode,
  children : Array[VirtualNode],
) -> VirtualNode {
  JsNode(
    react_suspense(
      fallback.to_js_obscure(),
      FixedArray::from_array(children.map(fn(child) { child.to_js_obscure() })),
    ),
  )
}

///|
/// Creates a React 19.2 Activity boundary.
///
/// A hidden Activity keeps its children and their state while hiding their DOM,
/// cleans up their Effects, and deprioritizes their updates. Restoring it to
/// `Visible` remounts those Effects without resetting child state.
pub fn activity(
  mode : ActivityMode,
  children : Array[VirtualNode],
) -> VirtualNode {
  JsNode(
    react_activity(
      mode.to_string(),
      FixedArray::from_array(children.map(fn(child) { child.to_js_obscure() })),
    ),
  )
}