///|
/// Hierarchical cell ownership with bulk disposal.
///
/// Scope owns cells and child scopes. Disposing a scope disposes all owned
/// cells and children recursively. This enables UI component lifecycle patterns:
/// create cells during mount, dispose the scope on unmount.
///
/// # Disposal Order
///
/// 1. Children (bottom-up, recursively)
/// 2. Dispose hooks (watches and `Scope::on_dispose` cleanups)
/// 3. Owned cells
///
/// # Example
///
/// ```moonbit nocheck
/// let scope = Scope::new(rt)
/// let local = scope.input(42)
/// let derived = scope.derived(fn() { local.get() * 2 })
///
/// // Component unmounts — one cleanup call
/// scope.dispose()
/// ```
pub struct Scope {
  priv runtime : Runtime
  priv cells : Array[CellId]
  priv children : Array[Scope]
  priv dispose_hooks : Array[() -> Unit]
  priv maintenance_hooks : Array[() -> Unit]
  priv mut disposed : Bool
}

///|
fn scope_dispose_request(disposed : Bool) -> (Bool, Bool) {
  if disposed {
    (true, false)
  } else {
    (true, true)
  }
}

///|
/// Contract for types that expose their runtime-tracked cell ids —
/// typically structs that own `InputField` fields, but any cell handle
/// qualifies.
///
/// The ordering of CellIds returned by cell_ids() must be stable across
/// calls (i.e., always return fields in the same order), and the ids must
/// belong to the runtime of any scope they are registered with.
pub(open) trait InputFieldOwner {
  fn cell_ids(Self) -> Array[CellId]
}

///|
/// Manual Debug impl because `Array[() -> Unit]` cannot derive Debug
/// (the `ignore` parameter only filters top-level field types, not nested types).
pub impl Debug for Scope with fn to_repr(self) {
  Repr::record({
    "cells": to_repr(self.cells),
    "children": to_repr(self.children),
    "dispose_hooks": Repr::literal(
      "<" + self.dispose_hooks.length().to_string() + " hooks>",
    ),
    "maintenance_hooks": Repr::literal(
      "<" + self.maintenance_hooks.length().to_string() + " hooks>",
    ),
    "disposed": to_repr(self.disposed),
  })
}

///|
/// Creates a new root scope.
pub fn Scope::new(rt : Runtime) -> Scope {
  {
    runtime: rt,
    cells: [],
    children: [],
    dispose_hooks: [],
    maintenance_hooks: [],
    disposed: false,
  }
}

///|
/// Creates a child scope owned by this scope.
///
/// Disposing the parent will dispose this child first (bottom-up order).
pub fn Scope::child(self : Scope) -> Scope {
  guard !self.disposed else { abort("Scope::child called on a disposed scope") }
  let child = Scope::new(self.runtime)
  self.children.push(child)
  child
}

///|
/// Returns true if this scope has been disposed.
pub fn Scope::is_disposed(self : Scope) -> Bool {
  self.disposed
}

///|
/// Disposes this scope: child scopes first, then hooks, then owned cells.
/// Scope is closed before any disposal effects run, so re-entrant
/// `dispose` calls are no-ops once teardown starts.
/// Idempotent — disposing an already-closed scope is a no-op.
pub fn Scope::dispose(self : Scope) -> Unit {
  let (next_disposed, should_dispose) = scope_dispose_request(self.disposed)
  self.disposed = next_disposed
  guard should_dispose else { return }
  // 1. Dispose children (bottom-up)
  for child in self.children {
    child.dispose()
  }
  // 2. Execute dispose hooks (read-root and callback cleanup)
  for hook in self.dispose_hooks {
    hook()
  }
  // 3. Dispose owned cells
  for cell_id in self.cells {
    self.runtime.dispose_cell(cell_id)
  }
  // Release references so a long-lived scope handle doesn't retain the subtree.
  self.children.clear()
  self.dispose_hooks.clear()
  self.maintenance_hooks.clear()
  self.cells.clear()
}

///|
fn Scope::retire_disposed_map_entries(self : Scope) -> Unit {
  // A long-lived parent may outlive many short-lived child scopes. Release
  // those closed ownership records before traversing the live subtree so
  // future collections do not pay for historical children.
  self.children.retain(child => !child.disposed)
  for child in self.children {
    child.retire_disposed_map_entries()
  }
  for hook in self.maintenance_hooks {
    hook()
  }
}

///|
/// Runs runtime-wide graph garbage collection, then retires GC-disposed
/// `DerivedMap` entries owned by this scope and its live descendants.
///
/// Call after reading the attachment's terminal `Watch` at a caller-selected
/// idle point. Aborts if this scope is disposed or collection is not legal for
/// the runtime's current phase. Disposed child ownership records are released
/// before traversing the live subtree. Repeated calls are idempotent.
pub fn Scope::collect(self : Scope) -> Unit {
  guard !self.disposed else {
    abort("Scope::collect called on a disposed scope")
  }
  self.runtime.gc()
  self.retire_disposed_map_entries()
}

///|
/// Creates an input facade owned by this scope.
pub fn[T] Scope::input(
  self : Scope,
  initial : T,
  durability? : Durability = Low,
  label? : String,
) -> Input[T] {
  guard !self.disposed else { abort("Scope::input called on a disposed scope") }
  let input = Input(self.runtime, initial, durability~, label?)
  self.cells.push(input.id())
  input
}

///|
/// Creates an input-field facade owned by this scope.
pub fn[T] Scope::input_field(
  self : Scope,
  initial : T,
  durability? : Durability = Low,
  label? : String,
) -> InputField[T] {
  guard !self.disposed else {
    abort("Scope::input_field called on a disposed scope")
  }
  let field = InputField(self.runtime, initial, durability~, label?)
  self.cells.push(field.id())
  field
}

///|
/// Creates a lazy derived facade owned by this scope.
pub fn[T : Eq] Scope::derived(
  self : Scope,
  f : () -> T raise Failure,
  label? : String,
) -> Derived[T] {
  guard !self.disposed else {
    abort("Scope::derived called on a disposed scope")
  }
  let derived = Derived(self.runtime, f, label?)
  self.cells.push(derived.id())
  derived
}

///|
/// Creates a lazy derived facade owned by this scope, without equality-based
/// backdating. Each recomputation advances the changed-at revision
/// unconditionally, even when the output equals the previous value. Accepts
/// output types that do not implement `Eq`.
pub fn[T] Scope::derived_no_backdate(
  self : Scope,
  f : () -> T raise Failure,
  label? : String,
) -> Derived[T] {
  guard !self.disposed else {
    abort("Scope::derived_no_backdate called on a disposed scope")
  }
  let derived : Derived[T] = Derived::derived_no_backdate(
    self.runtime,
    f,
    label?,
  )
  self.cells.push(derived.id())
  derived
}

///|
/// Creates a reachable lazy derived facade owned by this scope.
pub fn[T : Eq] Scope::reachable_derived(
  self : Scope,
  f : () -> T raise Failure,
  label? : String,
) -> ReachableDerived[T] {
  guard !self.disposed else {
    abort("Scope::reachable_derived called on a disposed scope")
  }
  let derived = ReachableDerived(self.runtime, f, label?)
  self.cells.push(derived.id())
  derived
}

///|
/// Materializes an expression as a lazy derived cell owned by this scope.
/// Aborts if `expr` belongs to a different runtime.
pub fn[T : Eq] Scope::derived_expr(
  self : Scope,
  expr : Expr[T],
  label? : String,
) -> Derived[T] {
  guard !self.disposed else {
    abort("Scope::derived_expr called on a disposed scope")
  }
  guard expr.rt.id() == self.runtime.id() else {
    abort(
      "Scope::derived_expr: expression Runtime " +
      expr.rt.id().to_string() +
      " does not match scope Runtime " +
      self.runtime.id().to_string(),
    )
  }
  let derived = expr.derived(label?)
  self.cells.push(derived.id())
  derived
}

///|
/// Creates a keyed derived facade whose cached entries are retired by
/// `Scope::collect()` and cleared on scope disposal.
pub fn[K : Hash + Eq, V] Scope::derived_map(
  self : Scope,
  compute : (K) -> V raise Failure,
  label? : String,
) -> DerivedMap[K, V] {
  guard !self.disposed else {
    abort("Scope::derived_map called on a disposed scope")
  }
  let map = DerivedMap(self.runtime, compute, label?)
  self.dispose_hooks.push(fn() { map.dispose_from_scope() })
  self.maintenance_hooks.push(fn() { ignore(map.sweep_cache()) })
  map
}

///|
/// Creates an accumulator owned by this scope. The accumulator is disposed
/// automatically when the scope is disposed (via `dispose_hooks`).
pub fn[T : Eq] Scope::accumulator(
  self : Scope,
  label? : String,
) -> Accumulator[T] {
  guard !self.disposed else {
    abort("Scope::accumulator called on a disposed scope")
  }
  let a : Accumulator[T] = Accumulator(self.runtime, label?)
  self.dispose_hooks.push(() => a.dispose())
  a
}

///|
/// Creates an effect owned by this scope.
pub fn Scope::effect(self : Scope, f : () -> Unit) -> Effect {
  guard !self.disposed else {
    abort("Scope::effect called on a disposed scope")
  }
  let eff = Effect(self.runtime, f)
  self.cells.push(eff.id())
  eff
}

///|
/// Creates an eager derived facade owned by this scope.
pub fn[T : Eq] Scope::eager_derived(
  self : Scope,
  compute_fn : () -> T,
) -> EagerDerived[T] {
  guard !self.disposed else {
    abort("Scope::eager_derived called on a disposed scope")
  }
  let derived = EagerDerived(self.runtime, compute_fn)
  self.cells.push(derived.id())
  derived
}

///|
/// Registers a watch with this scope for automatic disposal.
///
/// When the scope is disposed, the watch is disposed in the dispose_hooks
/// phase (step 2 of disposal order — after children, before owned cells).
///
/// Returns the watch for immediate use.
pub fn[T] Scope::add_watch(self : Scope, watch : Watch[T]) -> Watch[T] {
  guard !self.disposed else {
    abort("Scope::add_watch called on a disposed scope")
  }
  self.dispose_hooks.push(fn() { watch.dispose() })
  watch
}

///|
/// Creates a GC-safe watch on a derived value, owned by this scope.
///
/// Folds watch creation and scope registration into one call. `Derived::watch`
/// performs the priming read that records upstream `gc_dependencies`, so the
/// graph survives `Runtime::gc()` before the first consumer read. A priming
/// read error remains observable through `Watch::read()`.
pub fn[T] Scope::watch(self : Scope, derived : Derived[T]) -> Watch[T] {
  guard !self.disposed else { abort("Scope::watch called on a disposed scope") }
  self.add_watch(derived.watch())
}

///|
/// Creates a GC-safe watch on a reachable derived value, owned by this scope.
///
/// Folds watch creation and scope registration into one call. The priming read
/// is performed by `ReachableDerived::watch()`. MoonBit does not allow a second
/// `Scope::watch` overload; use this for `ReachableDerived` and `Scope::watch`
/// for `Derived`.
pub fn[T : Eq] Scope::watch_reachable(
  self : Scope,
  derived : ReachableDerived[T],
) -> Watch[T] {
  guard !self.disposed else {
    abort("Scope::watch_reachable called on a disposed scope")
  }
  self.add_watch(derived.watch())
}

///|
/// Registers a cleanup callback to run when this scope is disposed.
/// The callback runs at most once, during the dispose_hooks phase
/// (step 2 of disposal order — after children, before owned cells).
/// It does not run if the scope is already closed, and registration on a
/// closing/closed scope (including teardown-in-progress) aborts.
///
/// This is the intended hook for releasing resources that were acquired
/// at mount time, such as removing runtime-level listeners:
///
/// ```moonbit nocheck
/// let scope = Scope::new(rt)
/// let listener_id = rt.add_on_change_listener(() => sync())
/// scope.on_dispose(() => {
///   rt.remove_on_change_listener(listener_id)
/// })
/// ```
pub fn Scope::on_dispose(self : Scope, cleanup : () -> Unit) -> Unit {
  guard !self.disposed else {
    abort("Scope::on_dispose called on a disposed scope")
  }
  self.dispose_hooks.push(cleanup)
}

///|
/// Registers an array of CellIds with this scope for bulk disposal.
///
/// When the scope is disposed, all registered cells are disposed.
/// This is the low-level building block for `add_input_fields(scope, owner)`.
pub fn Scope::add_cell_ids(self : Scope, ids : Array[CellId]) -> Unit {
  guard !self.disposed else {
    abort("Scope::add_cell_ids called on a disposed scope")
  }
  let runtime_id = self.runtime.core.runtime_id
  for id in ids {
    guard id.runtime_id == runtime_id else {
      abort("Scope::add_cell_ids: CellId belongs to a different Runtime")
    }
    self.cells.push(id)
  }
}

///|
/// Adopts all cells from an `InputFieldOwner` into this scope for bulk disposal.
///
/// This is the method-style version of `add_input_fields(scope, owner)`. Use it
/// to register cells that were created outside a scope (e.g. via `map` or
/// raw constructors) with a scope's lifecycle.
///
/// Returns the trackable value for convenient chaining.
///
/// # Example
///
/// ```moonbit nocheck
/// let scope = Scope::new(rt)
/// let base = scope.derived(fn() { 42 })
/// let d = base.map(fn(v) { v + 1 })
/// scope.adopt(d)
/// scope.dispose()   // d is disposed
/// ```
pub fn[T : InputFieldOwner] Scope::adopt(self : Scope, tracked : T) -> T {
  guard !self.disposed else { abort("Scope::adopt called on a disposed scope") }
  self.add_cell_ids(tracked.cell_ids())
  tracked
}