///|
/// A typed, identity-stable Promise resource for React 19 `use`.
///
/// Create resources outside render paths and reuse the same value across
/// retries. Recreating a Promise during render causes repeated suspension and a
/// React uncached-Promise warning.
struct ReactResource[T] {
  js_value : @dom.JsObscure
  _value_marker : T?
}

///|
/// Wraps an existing JavaScript Promise without changing its identity.
pub fn[T] resource_from_promise(
  promise : @js_async.Promise[T],
) -> ReactResource[T] {
  ReactResource::{
    js_value: @dom.v_to_js_obscure(promise),
    _value_marker: None,
  }
}

///|
/// Starts one MoonBit async operation and wraps its exported JavaScript Promise.
/// Call this outside component render paths so the resource identity is cached.
pub fn[T] resource_from_async(loader : async () -> T) -> ReactResource[T] {
  resource_from_promise(@js_async.Promise::from_async(loader))
}

///|
/// Returns the cached JavaScript Promise for explicit interoperation.
pub fn[T] ReactResource::to_js_obscure(
  self : ReactResource[T],
) -> @dom.JsObscure {
  self.js_value
}

///|
extern "js" fn react_use_resource(resource : @dom.JsObscure) -> @dom.JsObscure =
  #| (resource) => globalThis.React.use(resource)

///|
/// Reads a cached resource with React 19 `use`.
///
/// Unlike ordinary Hooks, React permits this call inside conditions and loops,
/// but it must still run while rendering a component. A pending resource
/// suspends to the nearest `suspense`; a rejected resource throws to the nearest
/// `error_boundary`.
pub fn[T] use_resource(resource : ReactResource[T]) -> T {
  react_use_resource(resource.js_value) |> @dom.js_obscure_to_v
}

///|
extern "js" fn react_error_boundary(
  fallback : @dom.JsObscure,
  on_error : @dom.JsObscure,
  reset_key : @dom.JsObscure,
  children : FixedArray[@dom.JsObscure],
) -> @dom.JsObscure =
  #| (fallback, onError, resetKey, children) => {
  #|   const Boundary = globalThis.__moonbitReactErrorBoundary ??= class
  #|     MoonBitReactErrorBoundary extends globalThis.React.Component {
  #|       constructor(props) {
  #|         super(props);
  #|         this.state = { error: null };
  #|       }
  #|       static getDerivedStateFromError(error) {
  #|         return { error };
  #|       }
  #|       componentDidCatch(error, info) {
  #|         this.props.onError?.(error, info);
  #|       }
  #|       render() {
  #|         return this.state.error == null
  #|           ? this.props.children
  #|           : this.props.fallback(this.state.error);
  #|       }
  #|     };
  #|   return globalThis.React.createElement(
  #|     Boundary,
  #|     {
  #|       key: resetKey ?? undefined,
  #|       fallback,
  #|       onError: onError ?? undefined,
  #|       resetKey: resetKey ?? undefined,
  #|     },
  #|     ...children,
  #|   );
  #| }

///|
/// Creates a local React Error Boundary around `children`.
///
/// Render failures and rejected resources produce `fallback(error)`. Changing
/// `reset_key` remounts the boundary and retries its children. `on_error`
/// receives React's component stack after the fallback commits. Event-handler
/// and arbitrary asynchronous errors outside render are not caught.
pub fn error_boundary(
  fallback : (ReactError) -> VirtualNode,
  children : Array[VirtualNode],
  reset_key? : String,
  on_error? : (ReactError, ReactErrorInfo) -> Unit,
) -> VirtualNode {
  let js_reset_key = match reset_key {
    Some(value) => @dom.JsObscure::from_string(value)
    None => @dom.JsObscure::null()
  }
  let js_on_error = match on_error {
    Some(callback) => @dom.v_to_js_obscure(callback)
    None => @dom.JsObscure::null()
  }
  JsNode(
    react_error_boundary(
      @dom.v_to_js_obscure(fn(error : ReactError) {
        fallback(error).to_js_obscure()
      }),
      js_on_error,
      js_reset_key,
      FixedArray::from_array(children.map(fn(child) { child.to_js_obscure() })),
    ),
  )
}