///|
pub struct HotRegionRef {
  holder : NodeId
} derive(Eq, Debug)

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

///|
pub fn HotRegionRef::root() -> HotRegionRef {
  { holder: HOT_NONE_NODE, }
}

///|
fn hot_region_is_root(region_ref : HotRegionRef) -> Bool {
  region_ref.holder == HOT_NONE_NODE
}

///|
fn hot_region_require_index(
  length : Int32,
  idx : Int32,
  allow_end~ : Bool = false,
) -> Unit {
  let upper = if allow_end { length } else { length - 1 }
  if idx < 0 || idx > upper {
    abort("hot region index out of bounds")
  }
}

///|
fn hot_region_require_remove_count(
  length : Int32,
  idx : Int32,
  remove_count : Int32,
) -> Unit {
  if remove_count < 0 || idx + remove_count > length {
    abort("hot region remove_count out of bounds")
  }
}

///|
fn hot_region_is_region_holder(func : HotFunc, holder : NodeId) -> Bool {
  if !hot_node_is_live(func, holder) {
    return false
  }
  // The live admission above also covers incomplete deletion-index fixtures.
  match func.nodes[holder].op {
    HotOp::Loop => true
    HotOp::Block => {
      let label_id = func.nodes[holder].imm0
      if label_id < 0 || label_id >= func.labels.length() {
        return false
      }
      match hot_label_kind(func, label_id) {
        HotLabelKind::BlockLabel | HotLabelKind::RegionLabel => true
        _ => false
      }
    }
    _ => false
  }
}

///|
fn hot_region_require_holder(
  func : HotFunc,
  region_ref : HotRegionRef,
) -> NodeId {
  if hot_region_is_root(region_ref) {
    abort("hot root region does not have a node holder")
  }
  if !hot_region_is_region_holder(func, region_ref.holder) {
    abort("hot region ref must point at a valid region holder")
  }
  region_ref.holder
}

///|
fn hot_region_ref_from_holder(func : HotFunc, holder : NodeId) -> HotRegionRef {
  if !hot_region_is_region_holder(func, holder) {
    abort("hot region ref holder must point at a valid region body node")
  }
  { holder, }
}

///|
fn hot_region_body_base(func : HotFunc, holder : NodeId) -> Int32 {
  hot_node_require_live_read(func, holder)
  hot_region_body_base_live(func, holder)
}

///|
// Call only after checked live admission in this query. Region reads do not
// mutate the arena between admission and selecting its body boundary.
#inline
fn hot_region_body_base_live(func : HotFunc, holder : NodeId) -> Int32 {
  let op = func.nodes[holder].op
  if op == HotOp::Block && func.nodes[holder].imm1 > 0 {
    return func.nodes[holder].imm1
  }
  if op != HotOp::Loop {
    return 0
  }
  let label_id = func.nodes[holder].imm0
  if label_id < 0 {
    return 0
  }
  hot_label_branch_arity(func, label_id)
}

///|
fn hot_region_optional_slot_ref(
  func : HotFunc,
  control_id : NodeId,
  slot : Int32,
) -> HotRegionRef? {
  hot_node_require_live_read(func, control_id)
  if slot >= func.nodes[control_id].child_count {
    return None
  }
  let holder = func.children[func.nodes[control_id].child_base + slot]
  if holder == HOT_NONE_NODE {
    return None
  }
  Some(hot_region_ref_from_holder(func, holder))
}

///|
pub fn hot_region_body_ref(func : HotFunc, control_id : NodeId) -> HotRegionRef {
  // Blocks and loops own their body roots directly. Try-like nodes keep their
  // body in an explicit region-holder child so catch regions can be represented
  // uniformly beside it.
  match hot_node_op(func, control_id) {
    HotOp::Block | HotOp::Loop => hot_region_ref_from_holder(func, control_id)
    HotOp::Try | HotOp::TryTable =>
      hot_region_optional_slot_ref(func, control_id, 0).unwrap()
    HotOp::If =>
      abort("if bodies must be addressed through then/else region refs")
    _ => abort("hot_region_body_ref requires a structured control node")
  }
}

///|
pub fn hot_region_then_ref(func : HotFunc, if_id : NodeId) -> HotRegionRef {
  if hot_node_op(func, if_id) != HotOp::If {
    abort("hot_region_then_ref requires an if node")
  }
  hot_region_optional_slot_ref(func, if_id, 1).unwrap()
}

///|
pub fn hot_region_else_ref(func : HotFunc, if_id : NodeId) -> HotRegionRef? {
  if hot_node_op(func, if_id) != HotOp::If {
    abort("hot_region_else_ref requires an if node")
  }
  hot_region_optional_slot_ref(func, if_id, 2)
}

///|
pub fn hot_region_catch_ref(func : HotFunc, try_id : NodeId) -> HotRegionRef? {
  if hot_node_op(func, try_id) != HotOp::Try {
    abort("hot_region_catch_ref requires a try node")
  }
  hot_region_optional_slot_ref(func, try_id, 1)
}

///|
pub fn hot_region_catch_list_ref(
  func : HotFunc,
  try_table_id : NodeId,
) -> HotRegionRef {
  if hot_node_op(func, try_table_id) != HotOp::TryTable {
    abort("hot_region_catch_list_ref requires a try_table node")
  }
  hot_region_optional_slot_ref(func, try_table_id, 1).unwrap()
}

///|
fn hot_region_roots(func : HotFunc, region_ref : HotRegionRef) -> Array[NodeId] {
  if hot_region_is_root(region_ref) {
    return hot_root_span(func)
  }
  let holder = hot_region_require_holder(func, region_ref)
  let node = hot_node_get(func, holder)
  let roots : Array[NodeId] = []
  let base = hot_region_body_base(func, holder)
  let total = node.child_count
  for idx = base; idx < total; idx = idx + 1 {
    roots.push(func.children[node.child_base + idx])
  }
  roots
}

///|
fn hot_region_write_roots(
  func : HotFunc,
  region_ref : HotRegionRef,
  roots : Array[NodeId],
) -> Array[NodeId] {
  if hot_region_is_root(region_ref) {
    let removed : Array[NodeId] = []
    for old_root in func.roots {
      removed.push(old_root)
    }
    for root in roots {
      hot_mutate_require_live_node(func, root, "hot root slots")
    }
    func.roots.clear()
    for root in roots {
      func.roots.push(root)
    }
    hot_revision_bump(func)
    return removed
  }
  let holder = hot_region_require_holder(func, region_ref)
  let node = hot_node_get(func, holder)
  let next : Array[NodeId] = []
  let base = hot_region_body_base(func, holder)
  for idx = 0; idx < base; idx = idx + 1 {
    next.push(func.children[node.child_base + idx])
  }
  for root in roots {
    next.push(root)
  }
  hot_replace_child_span(func, holder, next)
}

///|
pub fn hot_region_root_count(
  func : HotFunc,
  region_ref : HotRegionRef,
) -> Int32 {
  if hot_region_is_root(region_ref) {
    return hot_root_count(func)
  }
  let holder = hot_region_require_holder(func, region_ref)
  func.nodes[holder].child_count - hot_region_body_base_live(func, holder)
}

///|
pub fn hot_region_result_arity(
  func : HotFunc,
  region_ref : HotRegionRef,
) -> Int32 {
  if hot_region_is_root(region_ref) {
    return hot_type_result_arity(func, hot_body_result_type(func))
  }
  let holder = hot_region_require_holder(func, region_ref)
  let label_id = hot_control_node_label(func, holder)
  hot_label_branch_arity(func, label_id)
}

///|
pub fn hot_region_get(
  func : HotFunc,
  region_ref : HotRegionRef,
  idx : Int32,
) -> NodeId {
  if hot_region_is_root(region_ref) {
    hot_region_require_index(hot_root_count(func), idx)
    return hot_root_get(func, idx)
  }
  let holder = hot_region_require_holder(func, region_ref)
  let base = hot_region_body_base_live(func, holder)
  hot_region_require_index(func.nodes[holder].child_count - base, idx)
  func.children[func.nodes[holder].child_base + base + idx]
}

///|
pub fn hot_region_set(
  func : HotFunc,
  region_ref : HotRegionRef,
  idx : Int32,
  node_id : NodeId,
) -> NodeId {
  let previous = hot_region_get(func, region_ref, idx)
  if hot_region_is_root(region_ref) {
    hot_root_set(func, idx, node_id)
  } else {
    let holder = hot_region_require_holder(func, region_ref)
    hot_child_set(
      func,
      holder,
      hot_region_body_base(func, holder) + idx,
      node_id,
    )
  }
  previous
}

///|
pub fn hot_region_insert(
  func : HotFunc,
  region_ref : HotRegionRef,
  idx : Int32,
  node_ids : Array[NodeId],
) -> Unit {
  ignore(hot_region_splice(func, region_ref, idx, 0, node_ids))
}

///|
pub fn hot_region_remove(
  func : HotFunc,
  region_ref : HotRegionRef,
  idx : Int32,
  count : Int32,
) -> Array[NodeId] {
  hot_region_splice(func, region_ref, idx, count, [])
}

///|
pub fn hot_region_truncate_suffix(
  func : HotFunc,
  region_ref : HotRegionRef,
  keep_count : Int32,
) -> Array[NodeId] {
  let root_count = hot_region_root_count(func, region_ref)
  hot_region_require_index(root_count, keep_count, allow_end=true)
  let removed : Array[NodeId] = []
  for idx = keep_count; idx < root_count; idx = idx + 1 {
    removed.push(hot_region_get(func, region_ref, idx))
  }
  if removed.is_empty() {
    return removed
  }
  if hot_region_is_root(region_ref) {
    while func.roots.length() > keep_count {
      ignore(func.roots.pop())
    }
  } else {
    let holder = hot_region_require_holder(func, region_ref)
    let node = hot_node_get(func, holder)
    let base = hot_region_body_base(func, holder)
    func.nodes[holder] = { ..node, child_count: base + keep_count, }
  }
  hot_revision_bump(func)
  removed
}

///|
pub fn hot_region_splice(
  func : HotFunc,
  region_ref : HotRegionRef,
  idx : Int32,
  remove_count : Int32,
  node_ids : Array[NodeId],
) -> Array[NodeId] {
  if hot_region_is_root(region_ref) {
    hot_region_require_index(hot_root_count(func), idx, allow_end=true)
    hot_region_require_remove_count(hot_root_count(func), idx, remove_count)
    if remove_count == 0 && node_ids.is_empty() {
      return []
    }
    return hot_root_splice(func, idx, remove_count, node_ids)
  }
  let holder = hot_region_require_holder(func, region_ref)
  let node = hot_node_get(func, holder)
  let base = hot_region_body_base(func, holder)
  let root_count = node.child_count - base
  hot_region_require_index(root_count, idx, allow_end=true)
  hot_region_require_remove_count(root_count, idx, remove_count)
  if remove_count == 0 && node_ids.is_empty() {
    return []
  }
  let next : Array[NodeId] = []
  for i = 0; i < base; i = i + 1 {
    next.push(func.children[node.child_base + i])
  }
  for i = 0; i < idx; i = i + 1 {
    next.push(func.children[node.child_base + base + i])
  }
  for node_id in node_ids {
    next.push(node_id)
  }
  for i = idx + remove_count; i < root_count; i = i + 1 {
    next.push(func.children[node.child_base + base + i])
  }
  let removed : Array[NodeId] = []
  for i = idx; i < idx + remove_count; i = i + 1 {
    removed.push(func.children[node.child_base + base + i])
  }
  if next.length() <= node.child_count {
    hot_mutate_write_child_span(func, node.child_base, next)
    func.nodes[holder] = { ..node, child_count: next.length(), }
    hot_revision_bump(func)
  } else if node.child_base + node.child_count == func.children.length() {
    ignore(hot_alloc_child_span(func, next.length() - node.child_count))
    hot_mutate_write_child_span(func, node.child_base, next)
    func.nodes[holder] = { ..node, child_count: next.length(), }
    hot_revision_bump(func)
  } else {
    let child_base = hot_alloc_child_span(func, next.length())
    hot_mutate_write_child_span(func, child_base, next)
    func.nodes[holder] = { ..node, child_base, child_count: next.length(), }
    hot_revision_bump(func)
  }
  removed
}

///|
pub fn hot_region_replace_body(
  func : HotFunc,
  region_ref : HotRegionRef,
  node_ids : Array[NodeId],
) -> Unit {
  ignore(hot_region_write_roots(func, region_ref, node_ids))
}