///|
// React Hooks wrappers, aligned with existing use_state style

using @dom {type JsObscure}

///|
extern "js" fn react_use_effect(
  effect : @dom.JsObscure,
  deps : FixedArray[@dom.JsObscure],
) -> Unit =
  #| (effect, deps) => globalThis.React.useEffect(effect, deps)

///|
extern "js" fn react_use_layout_effect(
  effect : @dom.JsObscure,
  deps : FixedArray[@dom.JsObscure],
) -> Unit =
  #| (effect, deps) => globalThis.React.useLayoutEffect(effect, deps)

///|
extern "js" fn react_use_memo(
  factory : @dom.JsObscure,
  deps : FixedArray[@dom.JsObscure],
) -> @dom.JsObscure =
  #| (factory, deps) => globalThis.React.useMemo(factory, deps)

///|
extern "js" fn react_use_callback(
  callback : @dom.JsObscure,
  deps : FixedArray[@dom.JsObscure],
) -> JsObscure =
  #| (callback, deps) => globalThis.React.useCallback(callback, deps)

///|
extern "js" fn react_use_effect_event(callback : JsObscure) -> JsObscure =
  #| (callback) => globalThis.React.useEffectEvent(callback)

///|
extern "js" fn react_use_action_state(
  action : JsObscure,
  initial : JsObscure,
) -> JsObscure =
  #| (action, initial) => globalThis.React.useActionState(action, initial)

///|
extern "js" fn react_use_ref(initial : @dom.JsObscure) -> @dom.JsObscure =
  #| (initial) => globalThis.React.useRef(initial)

///|
extern "js" fn react_ref_get_value(r : @dom.JsObscure) -> @dom.JsObscure =
  #| (ref) => ref.current

///|
extern "js" fn react_ref_set_value(
  r : @dom.JsObscure,
  value : JsObscure,
) -> Unit =
  #| (ref, value) => ref.current = value

///|
extern "js" fn react_use_reducer(
  reducer : JsObscure,
  initial : JsObscure,
) -> JsObscure =
  #| (reducer, initial) => globalThis.React.useReducer(reducer, initial)

///|
extern "js" fn react_use_id() -> String =
  #| () => globalThis.React.useId()

///|
extern "js" fn react_use_deferred_value(value : JsObscure) -> JsObscure =
  #| (value) => globalThis.React.useDeferredValue(value)

///|
extern "js" fn react_use_transition() -> JsObscure =
  #| () => globalThis.React.useTransition()

///|
extern "js" fn react_start_transition(action : JsObscure) -> Unit =
  #| (action) => globalThis.React.startTransition(action)

///|
extern "js" fn react_use_sync_external_store(
  subscribe : JsObscure,
  get_snapshot : JsObscure,
  get_server_snapshot : JsObscure,
) -> JsObscure =
  #| (subscribe, getSnapshot, getServerSnapshot) =>
  #|   globalThis.React.useSyncExternalStore(
  #|     subscribe,
  #|     getSnapshot,
  #|     getServerSnapshot ?? undefined,
  #|   )

///|
extern "js" fn react_use_imperative_handle(
  handle_ref : JsObscure,
  create_handle : JsObscure,
  deps : FixedArray[JsObscure],
) -> Unit =
  #| (ref, createHandle, deps) =>
  #|   globalThis.React.useImperativeHandle(ref, createHandle, deps)

///|
fn fn0_to_js_obscure(f : () -> Unit) -> @dom.JsObscure = "%identity"

///|
fn fn1_from_js_obscure(v : JsObscure) -> (JsObscure) -> Unit = "%identity"

///|
/// Runs a layout effect after React mutates the DOM and before the browser
/// paints. Use `obscure` to construct dependencies that are not already
/// `JsObscure` values.
pub fn use_layout_effect_deps(
  effect : () -> Unit,
  deps : Array[@dom.JsObscure],
) -> Unit {
  react_use_layout_effect(
    fn0_to_js_obscure(effect),
    FixedArray::from_array(deps),
  )
}

///|
/// Memoizes the value returned by `factory` until one of `deps` changes.
///
/// Treat this as a performance optimization rather than a semantic guarantee.
pub fn[A] use_memo_deps(factory : () -> A, deps : Array[@dom.JsObscure]) -> A {
  let v = react_use_memo(
    @dom.v_to_js_obscure(factory),
    FixedArray::from_array(deps),
  )
  @dom.js_obscure_to_v(v)
}

///|
/// Preserves `callback` identity until one of `deps` changes.
pub fn[F] use_callback_deps(callback : F, deps : Array[@dom.JsObscure]) -> F {
  let v = react_use_callback(
    @dom.v_to_js_obscure(callback),
    FixedArray::from_array(deps),
  )
  @dom.js_obscure_to_v(v)
}

///|
/// Creates a non-reactive callback for use from an effect. The callback always
/// observes the latest props and state without becoming an effect dependency.
/// React requires effect events to be invoked only from effects.
pub fn[F] use_effect_event(callback : F) -> F {
  let v = react_use_effect_event(@dom.v_to_js_obscure(callback))
  @dom.js_obscure_to_v(v)
}

///|
/// Stores state managed by a React 19 action. The action receives the current
/// MoonBit state and its dispatched value, then returns the next state.
///
/// React reports whether an action transition is pending in the final tuple
/// value. Dispatch from a form action or `start_transition` when appropriate.
pub fn[S, A] use_action_state(
  initial : S,
  action : (S, A) -> S,
) -> (S, (A) -> Unit, Bool) {
  let pair = react_use_action_state(
    @dom.v_to_js_obscure(action),
    any_to_js_value(initial),
  )
  let state : S = any_from_js_value(pair.get("0"))
  let dispatch = fn1_from_js_obscure(pair.get("1"))
  let is_pending : Bool = any_from_js_value(pair.get("2"))
  (state, fn(value : A) { dispatch(any_to_js_value(value)) }, is_pending)
}

///|
/// Runs an effect after the component mounts and does not rerun it for later
/// renders. Use `use_effect_once_with_cleanup` when cleanup is required.
pub fn use_effect_once(effect : () -> Unit) -> Unit {
  react_use_effect(fn0_to_js_obscure(effect), FixedArray::from_array([]))
}

///|
/// Runs an effect with dependencies and registers its returned cleanup function.
pub fn use_effect_cleanup_deps(
  effect : () -> () -> Unit,
  deps : Array[@dom.JsObscure],
) -> Unit {
  react_use_effect(@dom.v_to_js_obscure(effect), FixedArray::from_array(deps))
}

///|
/// Runs an effect once and registers its returned cleanup function.
pub fn use_effect_once_with_cleanup(effect : () -> () -> Unit) -> Unit {
  use_effect_cleanup_deps(effect, [])
}

///|
/// a short hand to turn value into JsObscure in hook deps
pub fn[T] obscure(v : T) -> @dom.JsObscure = "%identity"

///|
/// Runs an effect after rendering whenever one of `deps` changes.
///
/// Use `use_effect_cleanup_deps` when the effect owns subscriptions or other
/// resources that must be released.
pub fn use_effect_deps(
  effect : () -> Unit,
  deps : Array[@dom.JsObscure],
) -> Unit {
  react_use_effect(fn0_to_js_obscure(effect), FixedArray::from_array(deps))
}

///|
/// Runs a layout effect with dependencies and registers its returned cleanup
/// function. Prefer this only when the effect must run before the browser paints.
pub fn use_layout_effect_cleanup_deps(
  effect : () -> () -> Unit,
  deps : Array[@dom.JsObscure],
) -> Unit {
  react_use_layout_effect(
    @dom.v_to_js_obscure(effect),
    FixedArray::from_array(deps),
  )
}

///|
/// Preserves the identity of a zero-argument callback until one of `deps`
/// changes.
pub fn use_callback0_deps(
  f : () -> Unit,
  deps : Array[@dom.JsObscure],
) -> () -> Unit {
  let raw = react_use_callback(
    @dom.v_to_js_obscure(f),
    FixedArray::from_array(deps),
  )
  @dom.js_obscure_to_v(raw)
}

///|
struct ReactRef[T] {
  js_value : @dom.JsObscure
  mut _v0 : T
}

///|
/// Returns the current value stored in this React ref.
pub fn[T] ReactRef::get(self : ReactRef[T]) -> T {
  @dom.js_obscure_to_v(react_ref_get_value(self.js_value))
}

///|
/// Returns the underlying React ref object for an explicit JavaScript property,
/// for example `attrs.set_js_value("ref", input_ref.to_js_obscure())`.
pub fn[T] ReactRef::to_js_obscure(self : ReactRef[T]) -> JsObscure {
  self.js_value
}

///|
/// Replaces the current value stored in this React ref.
pub fn[T] ReactRef::set(self : ReactRef[T], value : T) -> Unit {
  react_ref_set_value(self.js_value, any_to_js_value(value))
  self._v0 = value
}

///|
/// Creates a React ref Hook with an explicit initial value.
///
/// Call this only at the top level of a React component or another Hook. For a
/// DOM element ref with a safe empty state, prefer `use_dom_ref`.
pub fn[T] use_ref(initial : T) -> ReactRef[T] {
  ReactRef::{
    js_value: react_use_ref(@dom.v_to_js_obscure(initial)),
    _v0: initial,
  }
}

///|
/// A DOM element ref whose current value is absent before mount and after
/// unmount. Attach it with `ElementAttrs::set_js_value("ref", ref.to_js_obscure())`.
struct ReactDomRef {
  js_value : @dom.JsObscure
}

///|
/// Creates a nullable DOM ref Hook.
///
/// Call this only at the top level of a React component or another Hook.
pub fn use_dom_ref() -> ReactDomRef {
  ReactDomRef::{ js_value: react_use_ref(JsObscure::null()) }
}

///|
/// Returns the mounted DOM element, or `None` before mount and after unmount.
pub fn ReactDomRef::current(self : ReactDomRef) -> @dom.Element? {
  let current = react_ref_get_value(self.js_value)
  if current.is_nil() {
    None
  } else {
    Some(@dom.js_obscure_to_v(current))
  }
}

///|
/// Returns the underlying React ref object for an explicit `ref` prop.
pub fn ReactDomRef::to_js_obscure(self : ReactDomRef) -> JsObscure {
  self.js_value
}

///|
/// A nullable typed ref for a custom imperative component handle.
struct ImperativeRef[T] {
  js_value : @dom.JsObscure
  _handle_marker : T?
}

///|
/// Creates an empty typed imperative ref. Pass it through typed component props
/// and bind it inside the child with `use_imperative_handle_deps`.
pub fn[T] use_imperative_ref() -> ImperativeRef[T] {
  ImperativeRef::{
    js_value: react_use_ref(JsObscure::null()),
    _handle_marker: None,
  }
}

///|
/// Returns the committed handle, or `None` before mount and after cleanup.
pub fn[T] ImperativeRef::current(self : ImperativeRef[T]) -> T? {
  let current = react_ref_get_value(self.js_value)
  if current.is_nil() {
    None
  } else {
    Some(@dom.js_obscure_to_v(current))
  }
}

///|
/// Returns the underlying React ref object for an explicit JavaScript bridge.
pub fn[T] ImperativeRef::to_js_obscure(self : ImperativeRef[T]) -> JsObscure {
  self.js_value
}

///|
/// Creates a reducer with an explicit initial state. Unlike the compatibility
/// `use_reducer` overload, this accepts state types that do not implement
/// `Default`.
pub fn[S, A] use_reducer_with_initial(
  initial : S,
  reducer : (S, A) -> S,
) -> (S, (A) -> Unit) {
  let pair = react_use_reducer(
    any_to_js_value(reducer),
    any_to_js_value(initial),
  )
  let s0 = any_from_js_value(pair.get("0"))
  let dispatch_raw = fn1_from_js_obscure(pair.get("1"))
  (s0, fn(a : A) { dispatch_raw(any_to_js_value(a)) })
}

///|
/// Compatibility reducer helper with an optional initial state. Prefer
/// `use_reducer_with_initial` for new code so the state type need not derive
/// `Default`.
pub fn[S : Default, A] use_reducer(
  initial? : S,
  reducer : (S, A) -> S,
) -> (S, (A) -> Unit) {
  use_reducer_with_initial(initial.unwrap_or_default(), reducer)
}

///|
/// Returns a stable identifier suitable for associating component-local DOM
/// elements, such as a label and its input.
pub fn use_id() -> String {
  react_use_id()
}

///|
/// Defers a value so that urgent updates can render before expensive consumers
/// of that value. The returned value keeps the original MoonBit type.
pub fn[T] use_deferred_value(value : T) -> T {
  react_use_deferred_value(any_to_js_value(value)) |> any_from_js_value
}

///|
/// Marks state updates made by `action` as non-urgent. The returned boolean is
/// true while React is rendering the transition.
pub fn use_transition() -> (Bool, (() -> Unit) -> Unit) {
  let pair = react_use_transition()
  let is_pending : Bool = any_from_js_value(pair.get("0"))
  let start_raw = fn1_from_js_obscure(pair.get("1"))
  (is_pending, fn(action) { start_raw(fn0_to_js_obscure(action)) })
}

///|
/// Marks updates made by `action` as non-urgent without reading transition
/// state. Use `use_transition` when the component also needs `is_pending`.
pub fn start_transition(action : () -> Unit) -> Unit {
  react_start_transition(fn0_to_js_obscure(action))
}

///|
/// Reads and subscribes to a typed immutable snapshot from an external store.
///
/// `subscribe` must return an unsubscribe callback and should keep stable
/// identity across renders. Repeated `get_snapshot` calls must return the same
/// value while the store has not changed. Supply `get_server_snapshot` when the
/// component can render on the server; its initial value must also match during
/// hydration.
pub fn[T] use_sync_external_store(
  subscribe : (() -> Unit) -> () -> Unit,
  get_snapshot : () -> T,
  get_server_snapshot? : () -> T,
) -> T {
  let server_snapshot = match get_server_snapshot {
    Some(callback) => @dom.v_to_js_obscure(callback)
    None => JsObscure::null()
  }
  react_use_sync_external_store(
    @dom.v_to_js_obscure(subscribe),
    @dom.v_to_js_obscure(get_snapshot),
    server_snapshot,
  )
  |> any_from_js_value
}

///|
/// Exposes a typed imperative handle through a nullable imperative ref.
///
/// React assigns `Some(handle)` after commit and restores `None` on cleanup.
/// In React 19 the ref can be carried in ordinary typed component props, so a
/// `forwardRef` wrapper is not required.
pub fn[T] use_imperative_handle_deps(
  handle_ref : ImperativeRef[T],
  create_handle : () -> T,
  deps : Array[JsObscure],
) -> Unit {
  react_use_imperative_handle(
    handle_ref.to_js_obscure(),
    @dom.v_to_js_obscure(create_handle),
    FixedArray::from_array(deps),
  )
}