///|
/// A mounted Luna tree. The scene, renderer, camera and animation loop belong to the host.
pub struct Root {
  priv container : @three.Object3D
  priv mut children : Array[Mounted]
  priv mut active : Bool
  priv mut updating : Int
  priv on_change : () -> Unit
  priv claims : Map[Int, Bool]
}

///|
priv struct Mounted {
  root : Root
  mut source : Node
  tag : String
  key : String?
  owner : @signals.Owner
  mut stop_binding : () -> Unit
  mut object : @three.Object3D?
  mut children : Array[Mounted]
  mut geometry : (Geometry, @three.BufferGeometry)?
  mut material : (Material, @three.Material)?
  mut previous : Map[String, Property]
  mut defaults : Map[String, Property]
  mut error : String?
  mut children_error : String?
}

///|
fn Mounted::dispose(self : Mounted) -> Unit {
  (self.stop_binding)()
  self.stop_binding = fn() { () }
  for child in self.children {
    child.dispose()
  }
  self.children = []
  self.owner.dispose()
  if self.object is Some(object) {
    object.remove_from_parent() |> ignore
    self.root.claims.remove(object.id())
  }
  if self.geometry is Some((spec, geometry)) {
    dispose_geometry(spec, geometry)
  }
  if self.material is Some((spec, material)) {
    dispose_material(spec, material)
  }
  if self.tag != "primitive" && self.object is Some(object) {
    if object.as_light() is Some(light) {
      light.dispose()
    }
  }
  self.geometry = None
  self.material = None
  self.object = None
}

///|
pub fn Root::unmount(self : Root) -> Unit {
  if !self.active {
    return
  }
  self.active = false
  for child in self.children {
    child.dispose()
  }
  self.children = []
  @reactivity.untracked(self.on_change)
}

///|
/// Invalid low-level Luna nodes/properties are reported here, including reactive updates.
/// An invalid update retains the last valid element or list until corrected.
pub fn Root::error(self : Root) -> String? {
  first_error(self.children)
}

///|
fn first_error(nodes : Array[Mounted]) -> String? {
  for node in nodes {
    if node.error is Some(_) {
      return node.error
    }
    if node.children_error is Some(_) {
      return node.children_error
    }
    if first_error(node.children) is Some(error) {
      return Some(error)
    }
  }
  None
}

///|
fn Root::changed(self : Root) -> Unit {
  if self.active && self.updating == 0 {
    sync_children(self.container, self.children)
    self.notify()
  }
}

///|
/// Property updates need no tree traversal. Host reads must not become dependencies.
fn Root::notify(self : Root) -> Unit {
  if self.active && self.updating == 0 {
    @reactivity.untracked(self.on_change)
  }
}

///|
fn collect_objects(
  nodes : Array[Mounted],
  objects : Array[@three.Object3D],
) -> Unit {
  for node in nodes {
    match node.object {
      Some(object) => {
        objects.push(object)
        sync_children(object, node.children)
      }
      None => collect_objects(node.children, objects)
    }
  }
}

///|
fn sync_children(parent : @three.Object3D, nodes : Array[Mounted]) -> Unit {
  let objects : Array[@three.Object3D] = []
  collect_objects(nodes, objects)
  for object in objects {
    if !(object.parent() is Some(p) && p.same_reference(parent)) {
      parent.add(object) |> ignore
    }
  }
  order_children(parent, FixedArray::from_array(objects))
}

///|
/// Preserve host children and only reorder the slots occupied by this root's objects.
/// This is the sole runtime FFI: Object3D has no public insert-before operation.
extern "js" fn order_children(
  parent : @three.Object3D,
  objects : FixedArray[@three.Object3D],
) -> Unit =
  #| (parent, objects) => {
  #|   const owned = new Set(objects);
  #|   let index = 0;
  #|   for (let i = 0; i < parent.children.length; i++) {
  #|     if (owned.has(parent.children[i])) parent.children[i] = objects[index++];
  #|   }
  #| }