// Reactive Context - Tracks current running effect for auto-subscription
//

///|
/// Effect wrapper with unique ID for deduplication
pub struct EffectRunner {
  id : Int
  run : () -> Unit
}

///|
/// Global effect ID counter
let effect_id_counter : Ref[Int] = Ref::new(0)

///|
/// Generate a new unique effect ID
pub fn new_effect_id() -> Int {
  let id = effect_id_counter.val
  effect_id_counter.val = id + 1
  id
}

///|
/// Owner - manages lifecycle of reactive computations (Solid.js style)
pub struct Owner {
  id : Int
  parent : Owner?
  children : Array[Owner]
  cleanups : Array[() -> Unit]
  disposers : Array[() -> Unit]
  mut disposed : Bool
}

///|
/// Create a new Owner
pub fn Owner::new(parent : Owner?) -> Owner {
  let owner : Owner = {
    id: new_effect_id(),
    parent,
    children: [],
    cleanups: [],
    disposers: [],
    disposed: false,
  }
  // Register with parent
  match parent {
    Some(p) => p.children.push(owner)
    None => ()
  }
  owner
}

///|
/// Dispose this owner and all its children
pub fn Owner::dispose(self : Owner) -> Unit {
  if self.disposed {
    return
  }
  self.disposed = true
  // Dispose children first (depth-first)
  for i = self.children.length() - 1; i >= 0; i = i - 1 {
    self.children[i].dispose()
  }
  // Run disposers (for effects)
  for i = self.disposers.length() - 1; i >= 0; i = i - 1 {
    self.disposers[i]()
  }
  // Run cleanups
  for i = self.cleanups.length() - 1; i >= 0; i = i - 1 {
    self.cleanups[i]()
  }
  // Clear arrays
  self.children.clear()
  self.disposers.clear()
  self.cleanups.clear()
  // Remove from parent's children
  match self.parent {
    Some(p) => p.children.retain(fn(child) { child.id != self.id })
    None => ()
  }
}

///|
/// Reactive context - tracks currently running computation
priv struct ReactiveContext {
  mut current_subscriber : EffectRunner?
  mut current_owner : Owner?
  mut current_cleanups : Array[() -> Unit]?
  mut batch_depth : Int
  pending_effects : Array[EffectRunner]
  pending_ids : Array[Int]
}

///|
/// Global reactive context instance
let reactive_context : ReactiveContext = {
  current_subscriber: None,
  current_owner: None,
  current_cleanups: None,
  batch_depth: 0,
  pending_effects: [],
  pending_ids: [],
}

///|
/// Get current subscriber (if any) during signal read
pub fn get_current_subscriber() -> EffectRunner? {
  reactive_context.current_subscriber
}

///|
/// Run a function with the given subscriber as current
/// This enables auto-tracking of signal dependencies
pub fn[T] run_with_subscriber(subscriber : EffectRunner, f : () -> T) -> T {
  let prev = reactive_context.current_subscriber
  reactive_context.current_subscriber = Some(subscriber)
  let result = f()
  reactive_context.current_subscriber = prev
  result
}

///|
/// Run a function without tracking (useful for avoiding circular deps).
/// Signal reads inside this function won't create subscriptions.
/// ```mbt check
/// test {
///   let count = @signal.signal(0)
///   let runs : Ref[Int] = Ref::new(0)
///   let _ = @signal.render_effect(fn() {
///     @signal.untracked(fn() {
///       let _ = count.get() // Won't create dependency
///       runs.val = runs.val + 1
///     })
///   })
///   count.set(1)
///   count.set(2)
///   // Effect only ran once (initial), not on updates
///   inspect(runs.val, content="1")
/// }
/// ```
pub fn[T] untracked(f : () -> T) -> T {
  // Disable both legacy and core tracking
  let prev_legacy = reactive_context.current_subscriber
  reactive_context.current_subscriber = None
  // Also disable core tracking
  let prev_core = set_active_sub(None)
  let result = f()
  let _ = set_active_sub(prev_core)
  reactive_context.current_subscriber = prev_legacy
  result
}

///|
/// Start a batch update - effects won't run until batch ends
pub fn batch_start() -> Unit {
  reactive_context.batch_depth = reactive_context.batch_depth + 1
  start_batch()
}

///|
/// End a batch update - run all pending effects
pub fn batch_end() -> Unit {
  reactive_context.batch_depth = reactive_context.batch_depth - 1
  end_batch()
  if reactive_context.batch_depth == 0 {
    flush_effects()
  }
}

///|
/// Run a function in a batch - all signal updates are batched.
/// Effects only run once after all updates complete.
/// ```mbt check
/// test {
///   let a = @signal.signal(0)
///   let b = @signal.signal(0)
///   let runs : Ref[Int] = Ref::new(0)
///   let _ = @signal.render_effect(fn() {
///     let _ = a.get() + b.get()
///     runs.val = runs.val + 1
///   })
///   inspect(runs.val, content="1") // Initial run
///   @signal.batch(fn() {
///     a.set(1)
///     b.set(2)
///   })
///   // Only one additional run, not two
///   inspect(runs.val, content="2")
/// }
/// ```
pub fn[T] batch(f : () -> T) -> T {
  batch_start()
  let result = f()
  batch_end()
  result
}

///|
/// Check if an effect ID is already pending
fn is_pending(id : Int) -> Bool {
  for pending_id in reactive_context.pending_ids {
    if pending_id == id {
      return true
    }
  }
  false
}

///|
/// Schedule an effect to run (respects batching, deduplicates)
pub fn schedule_effect(effect : EffectRunner) -> Unit {
  if reactive_context.batch_depth > 0 {
    // In batch mode, queue the effect if not already pending
    if not(is_pending(effect.id)) {
      reactive_context.pending_ids.push(effect.id)
      reactive_context.pending_effects.push(effect)
    }
  } else {
    // Run immediately
    (effect.run)()
  }
}

///|
/// Flush all pending effects
fn flush_effects() -> Unit {
  while reactive_context.pending_effects.length() > 0 {
    let effect = reactive_context.pending_effects.remove(0)
    let _ = reactive_context.pending_ids.remove(0)
    (effect.run)()
  }
}

///|
/// Check if we're currently inside a batch
pub fn is_batching() -> Bool {
  reactive_context.batch_depth > 0
}

///|
/// Register a cleanup function to run when the current scope disposes.
/// Works in:
/// - Effects: cleanup runs before effect re-runs or when effect disposes
/// - Components: cleanup runs when component unmounts
/// (Solid.js style - can be called directly in component body)
/// ```mbt check
/// test {
///   let count = @signal.signal(0)
///   let log : Array[String] = []
///   let dispose = @signal.render_effect(fn() {
///     let v = count.get()
///     log.push("run:" + v.to_string())
///     @signal.on_cleanup(fn() { log.push("cleanup:" + v.to_string()) })
///   })
///   count.set(1)
///   dispose()
///   inspect(
///     log,
///     content=(
///       #|["run:0", "cleanup:0", "run:1", "cleanup:1"]
///     ),
///   )
/// }
/// ```
pub fn on_cleanup(cleanup : () -> Unit) -> Unit {
  // First priority: effect-level cleanup tracking
  match reactive_context.current_cleanups {
    Some(cleanups) => cleanups.push(cleanup)
    None =>
      // Second priority: owner-level cleanup (for component body)
      match reactive_context.current_owner {
        Some(owner) => owner.cleanups.push(cleanup)
        None => () // No tracking scope, ignore
      }
  }
}

///|
/// Set the current cleanup array (used internally by effect)
pub fn set_current_cleanups(
  cleanups : Array[() -> Unit]?,
) -> Array[() -> Unit]? {
  let prev = reactive_context.current_cleanups
  reactive_context.current_cleanups = cleanups
  prev
}

///|
/// Run a function with cleanup tracking enabled
pub fn[T] run_with_cleanup_tracking(
  cleanups : Array[() -> Unit],
  f : () -> T,
) -> T {
  let prev = set_current_cleanups(Some(cleanups))
  let result = f()
  reactive_context.current_cleanups = prev
  result
}

// ============================================================================
// Owner-based scope management (Solid.js style)
// ============================================================================

///|
/// Get the current owner (if any)
pub fn get_owner() -> Owner? {
  reactive_context.current_owner
}

///|
/// Run a function with a specific owner as current
pub fn[T] run_with_owner(owner : Owner, f : () -> T) -> T {
  let prev = reactive_context.current_owner
  reactive_context.current_owner = Some(owner)
  let result = f()
  reactive_context.current_owner = prev
  result
}

///|
/// Create a new reactive root scope.
/// The function receives a dispose callback that cleans up all effects.
/// Returns the result of the function.
/// ```mbt check
/// test {
///   let count = @signal.signal(0)
///   let log : Array[Int] = []
///   let result = @signal.create_root(fn(dispose) {
///     let _ = @signal.render_effect(fn() { log.push(count.get()) })
///     dispose // Return dispose function
///   })
///   count.set(1)
///   result() // Dispose the root
///   count.set(2) // Won't trigger effect
///   inspect(log, content="[0, 1]")
/// }
/// ```
pub fn[T] create_root(f : (() -> Unit) -> T) -> T {
  let owner = Owner::new(reactive_context.current_owner)
  let dispose = fn() { owner.dispose() }
  run_with_owner(owner, fn() { f(dispose) })
}

///|
/// Create a reactive root and return both result and dispose function
pub fn[T] create_root_with_dispose(f : () -> T) -> (T, () -> Unit) {
  let owner = Owner::new(reactive_context.current_owner)
  let dispose = fn() { owner.dispose() }
  let result = run_with_owner(owner, f)
  (result, dispose)
}

///|
/// Register a disposer with the current owner
/// Called internally by effect() to register its dispose function
pub fn register_disposer(disposer : () -> Unit) -> Unit {
  match reactive_context.current_owner {
    Some(owner) => owner.disposers.push(disposer)
    None => () // No owner, disposer won't be tracked
  }
}

///|
/// Register a cleanup with the current owner (alternative to onCleanup in effect)
pub fn register_owner_cleanup(cleanup : () -> Unit) -> Unit {
  match reactive_context.current_owner {
    Some(owner) => owner.cleanups.push(cleanup)
    None => () // No owner, cleanup won't be tracked
  }
}

///|
/// Check if currently inside an owner scope
pub fn has_owner() -> Bool {
  reactive_context.current_owner is Some(_)
}

///|
/// Run a function with a captured parent owner context (if any)
/// This is useful for rendering child components that need to inherit the parent's owner.
/// If captured_owner is Some, runs with that owner as context.
/// If captured_owner is None, runs directly without owner context.
pub fn[T] with_parent_owner(captured_owner : Owner?, f : () -> T) -> T {
  match captured_owner {
    Some(parent) => run_with_owner(parent, f)
    None => f()
  }
}