///|
let need_to_rerender : Ref[Bool] = Ref::new(false)

///|
/// check where need to trigger rerendering, also resets the status to false
fn drain_rerender_status() -> Bool {
  let ret = need_to_rerender.val
  // @dom_ffi.log("draining: " + ret.to_string())
  if ret {
    // @dom_ffi.warn_log("rerender")
    need_to_rerender.val = false
  }
  ret
}

///|
pub fn mark_need_rerender() -> Unit {
  let ret = need_to_rerender.val
  if not(ret) {
    need_to_rerender.val = true
    // @dom_ffi.warn_log("mark need rerender")
  }
}

///|
/// render elements
pub fn[T, U : Eq] render_node(
  mount_target : @dom_ffi.Node,
  // TODO it copies the whole store, need to optimize
  store : Ref[U],
  renderer : () -> @node.RespoNode[T] raise @node.RespoCommonError,
  dispatch_action : (T) -> Unit raise @node.RespoCommonError,
  _interval : Float?,
) -> Unit raise @node.RespoCommonError {
  let prev_store = @ref.new(store.val)
  let tree0 : @node.RespoNode[T] = renderer()
  let prev_tree = @ref.new(tree0)
  let handle_event = fn(
    mark : @node.RespoEventMark,
  ) -> Unit raise @node.RespoCommonError {
    // @dom_ffi.warn_log(
    //   "start rendering: " + mark.name.to_string() + " " + mark.coord.to_string(),
    // )
    let handler = request_for_target_handler(
      prev_tree.val,
      mark.name,
      mark.coord,
    )
    handler(mark.event_info, dispatch_action)
    // @dom_ffi.log(
    //   "finished event: " + mark.name.to_string() + " " + mark.coord.to_string(),
    // )
    mark_need_rerender()
  }
  let element = @node.build_dom_tree(tree0, @immut/array.new(), handle_event)

  // collection mounted effects
  let mount_changes : Ref[Array[@node.DomChange[T]]] = Ref::new([])
  @node.collect_effects_outside_in_as(
    tree0,
    @immut/array.new(),
    @immut/array.new(),
    Mounted,
    mount_changes,
  )
  mount_target.append_child(element)
  // println("mounted changed: \{mount_changes.val}")
  @node.patch_tree(
    tree0,
    prev_tree.val,
    mount_target,
    mount_changes.val,
    handle_event,
  )
  @node.raf_loop(fn(_t : Float) -> Unit raise @node.RespoCommonError {
    if drain_rerender_status() {
      // Skip re-render if store hasn't changed (immutable data optimization)
      if physical_equal(store.val, prev_store.val) {
        debug_render("skipped - store reference unchanged")
        return
      }
      // Skip re-render if store changed but logically equal
      if store.val == prev_store.val {
        debug_render("skipped - store logically equal")
        return
      }
      prev_store.val = store.val
      debug_render("starting render cycle")
      let new_tree = renderer()
      let changes : Ref[Array[@node.DomChange[T]]] = Ref::new([])
      @node.diff_tree(
        new_tree,
        prev_tree.val,
        @immut/array.new(),
        @immut/array.new(),
        changes,
      )
      debug_render("\{changes.val.length()} DOM changes")
      @node.patch_tree(
        new_tree,
        prev_tree.val,
        mount_target,
        changes.val,
        handle_event,
      )
      prev_tree.val = new_tree
    }
  })
}

///|
fn[T] request_for_target_handler(
  tree : @node.RespoNode[T],
  event_name : @node.RespoEventType,
  coord : @immut/array.T[@node.RespoCoord],
) -> ((@node.RespoEvent, @node.DispatchFn[T]) -> Unit raise @node.RespoCommonError) raise @node.RespoCommonError {
  let target_node = @node.load_coord_target_tree(tree, coord.to_array())
  match target_node {
    Component({ name, .. }) =>
      raise @node.RespoCommonError(
        "expected element, found target being a component: " + name,
      )
    Element({ name: tag_name, event, .. }) =>
      match event.get(event_name) {
        Some(v) => v
        None =>
          raise @node.RespoCommonError(
            "No event handler found for event '\{event_name}' on element '\{tag_name}', available events: TODO{event.keys()}",
          )
      }
  }
}