///|
/// An input cell with an externally-set value.
///
/// Signals are the leaves of the dependency graph — values you control directly.
/// The `T: Eq` constraint enables same-value optimization: setting a signal to
/// its current value is a no-op (no revision bump, no downstream recomputation).
///
/// # Example
///
/// ```moonbit nocheck
/// let rt = Runtime()
/// let count = Signal(rt, 0)
/// count.set(5)
/// inspect(count.get(), content="5")
/// ```
pub(all) struct Signal[T] {
  priv label : String?
  priv rt : Runtime
  priv cell_id : CellId
  priv mut value : T
  priv mut pending_value : T?
  priv durability : Durability
} derive(Debug(ignore=[Runtime, CellId]))

///|
/// Constructor-syntax alias of `Signal::new`. Enables `Signal(rt, value)` /
/// `Signal(rt, value, label="...", durability=High)` call sites.
pub fn[T] Signal::Signal(
  rt : Runtime,
  initial : T,
  durability? : Durability = Low,
  label? : String,
) -> Signal[T] {
  Signal::new(rt, initial, durability~, label?)
}

///|
/// Creates a new signal with the given initial value.
///
/// # Parameters
///
/// - `rt`: The runtime that will manage this signal
/// - `initial`: The initial value of the signal
/// - `durability`: How often this signal is expected to change (default: `Low`)
/// - `label`: An optional human-readable name for debugging and cycle error output
///
/// # Returns
///
/// A new signal containing the initial value
///
/// # Example
///
/// ```moonbit nocheck
/// let count = Signal(rt, 0)
///
/// let config = Signal(rt, "prod", durability=High)
///
/// let named = Signal(rt, 0, label="count")
/// ```
pub fn[T] Signal::new(
  rt : Runtime,
  initial : T,
  durability? : Durability = Low,
  label? : String,
) -> Signal[T] {
  let (cell_id, _) = rt.install_cell(
    rt.pull.free_signals,
    rt.pull.signals,
    idx => PullSignal(idx),
    cell_id => {
      meta: {
        cell_id,
        label,
        changed_at: Revision::initial(),
        durability,
        subscribers: @hashset.HashSet([]),
        push_reachable_count: 0,
      },
      on_change: None,
      commit_pending: None,
    },
  )
  { label, rt, cell_id, value: initial, pending_value: None, durability }
}

///|
/// Returns the current value of the signal.
///
/// If called inside a memo's compute function, this automatically records
/// a dependency from the memo to this signal. When the signal changes,
/// the memo will know to reverify.
///
/// # Returns
///
/// The current value of the signal
pub fn[T] Signal::get(self : Signal[T]) -> T {
  guard !self.rt.is_cell_disposed(self.cell_id) else {
    abort("Signal::get called on a disposed signal")
  }
  self.rt.check_cross_runtime(self.rt.core.runtime_id, "Signal")
  self.rt.record_dependency(self.cell_id)
  let value = self.value
  if self.rt.has_pending_events() {
    self.rt.drain_pending_events_if_idle()
  }
  value
}

///|
/// Returns the current value of the signal without recording a dependency.
///
/// Use `peek()` to read a signal's value from outside the dependency graph
/// (e.g., in event handlers, logging, or tests). Unlike `get()`, this never
/// records a dependency even when called inside a compute function.
///
/// # Returns
///
/// The current value of the signal
pub fn[T] Signal::peek(self : Signal[T]) -> T {
  guard !self.rt.is_cell_disposed(self.cell_id) else {
    abort("Signal::peek called on a disposed signal")
  }
  self.value
}

///|
/// Returns the current value of the signal as a Result.
///
/// This method exists for API consistency with `Memo::get_result()`.
/// Since signals cannot have cycles (they are input cells with no dependencies),
/// this method always returns `Ok(value)`.
///
/// # Returns
///
/// `Ok(value)` with the current value of the signal
pub fn[T] Signal::get_result(self : Signal[T]) -> Result[T, CycleError] {
  Ok(self.get())
}

///|
/// Returns the unique identifier for this signal.
///
/// The CellId can be used with `Runtime::cell_info()` to retrieve
/// metadata, or to compare cell identities.
///
/// # Returns
///
/// The cell identifier for this signal
///
/// # Example
///
/// ```moonbit nocheck
/// let sig = Signal(rt, 42)
/// let id = sig.id()
/// match rt.cell_info(id) {
///   Some(info) => println("Signal changed at: " + info.changed_at.to_string())
///   None => ()
/// }
/// ```
pub fn[T] Signal::id(self : Signal[T]) -> CellId {
  self.cell_id
}

///|
/// Returns the durability level of this signal.
///
/// Durability indicates how often this signal is expected to change:
/// - `High`: Rarely changes (e.g., configuration)
/// - `Medium`: Moderately stable
/// - `Low`: Frequently changes (e.g., user input)
///
/// # Returns
///
/// The durability level set at construction time
pub fn[T] Signal::durability(self : Signal[T]) -> Durability {
  self.durability
}

///|
/// Sets the signal to a new value.
///
/// If the new value equals the current value (via `Eq`), this is a no-op:
/// no revision bump occurs, and downstream memos won't reverify.
///
/// During a batch (`Runtime::batch`), the write is deferred. At batch end,
/// only signals whose final value differs from the pre-batch value trigger
/// a revision bump. This enables revert detection.
///
/// # Parameters
///
/// - `new_value`: The new value to set
///
/// # Same-Value Optimization
///
/// ```moonbit nocheck
/// let s = Signal(rt, 5)
/// s.set(5)  // No-op: value unchanged, no revision bump
/// s.set(6)  // Bumps revision, memos depending on s will reverify
/// ```
pub fn[T : Eq] Signal::set(self : Signal[T], new_value : T) -> Unit {
  guard !self.rt.is_cell_disposed(self.cell_id) else {
    abort("Signal::set called on a disposed signal")
  }
  if self.rt.core.batch.depth > 0 {
    self.set_batch(new_value)
  } else {
    if self.value == new_value {
      if self.rt.has_pending_events() {
        self.rt.drain_pending_events_if_idle()
      }
      return
    }
    self.set_unconditional(new_value)
  }
}

///|
/// Sets the signal to a new value, always bumping the revision.
///
/// Unlike `set`, this does not check for equality. Use this when you want
/// to force downstream memos to reverify even if the value is the same,
/// or when your type doesn't implement `Eq`.
///
/// During a batch, the value is stored as pending and committed at batch end.
///
/// # Parameters
///
/// - `new_value`: The new value to set
pub fn[T] Signal::set_unconditional(self : Signal[T], new_value : T) -> Unit {
  guard !self.rt.is_cell_disposed(self.cell_id) else {
    abort("Signal::set_unconditional called on a disposed signal")
  }
  if self.rt.core.batch.depth > 0 {
    self.set_batch_unconditional(new_value)
  } else {
    self.value = new_value
    // Snapshot callback before propagation — same invariant as commit_batch:
    // push propagation must not affect which handler fires.
    let cb = self.rt.get_pull_signal(self.cell_id).on_change
    self.rt.propagate_changes([self.cell_id], self.durability)
    match cb {
      Some(f) => @kernel.run_callback(self.rt.core, f)
      None => ()
    }
    self.rt.fire_on_change()
    if self.rt.has_pending_events() {
      self.rt.drain_pending_events_if_idle()
    }
  }
}

///|
/// Store a pending value during a batch (with Eq check).
fn[T : Eq] Signal::set_batch(self : Signal[T], new_value : T) -> Unit {
  // Compare against current pending value (if any) or actual value
  let current = match self.pending_value {
    Some(pv) => pv
    None => self.value
  }
  if current == new_value {
    return
  }
  let previous_pending = self.pending_value
  let sig_data = self.rt.get_pull_signal(self.cell_id)
  let was_registered = match sig_data.commit_pending {
    Some(_) => true
    None => false
  }
  self.rt.record_batch_rollback(self.cell_id, () => {
    self.pending_value = previous_pending
    if !was_registered {
      sig_data.commit_pending = None
      self.rt.remove_batch_signal(self.cell_id)
    }
  })
  self.pending_value = Some(new_value)
  // Register commit closure if not already registered
  if sig_data.commit_pending is None {
    sig_data.commit_pending = Some(() => self.commit())
    let sig_committable : &Committable = sig_data
    self.rt.record_batch_signal(sig_committable)
  }
  self.rt.bump_revision(self.durability)
}

///|
/// Store a pending value during a batch (unconditional).
fn[T] Signal::set_batch_unconditional(self : Signal[T], new_value : T) -> Unit {
  let previous_pending = self.pending_value
  let sig_data = self.rt.get_pull_signal(self.cell_id)
  let was_registered = match sig_data.commit_pending {
    Some(_) => true
    None => false
  }
  self.rt.record_batch_rollback(self.cell_id, () => {
    self.pending_value = previous_pending
    if !was_registered {
      sig_data.commit_pending = None
      self.rt.remove_batch_signal(self.cell_id)
    }
  })
  self.pending_value = Some(new_value)
  if sig_data.commit_pending is None {
    sig_data.commit_pending = Some(() => self.commit_unconditional())
    let sig_committable : &Committable = sig_data
    self.rt.record_batch_signal(sig_committable)
  }
  self.rt.bump_revision(self.durability)
}

///|
/// Commit the pending value. Returns true if the value actually changed.
fn[T : Eq] Signal::commit(self : Signal[T]) -> Bool {
  match self.pending_value {
    None => false
    Some(pv) => {
      let changed = self.value != pv
      if changed {
        self.value = pv
      }
      self.pending_value = None
      changed
    }
  }
}

///|
/// Commit the pending value unconditionally. Always returns true if there
/// is a pending value (no equality check).
fn[T] Signal::commit_unconditional(self : Signal[T]) -> Bool {
  match self.pending_value {
    None => false
    Some(pv) => {
      self.value = pv
      self.pending_value = None
      true
    }
  }
}

///|
/// Registers a callback that fires whenever this signal's value changes.
///
/// The callback receives the new value. It fires after the value is updated
/// but before `Runtime::fire_on_change()`. Only one callback can be
/// registered at a time; calling this again replaces the previous callback.
///
/// Note: When using `set_unconditional`, the callback fires even if the
/// new value equals the current value, since `set_unconditional` bypasses
/// the equality check.
///
/// # Parameters
///
/// - `f`: Called with the new value whenever this signal changes
pub fn[T] Signal::on_change(self : Signal[T], f : (T) -> Unit) -> Unit {
  self.rt.get_pull_signal(self.cell_id).on_change = Some(() => f(self.value))
}

///|
/// Removes the `on_change` callback for this signal.
pub fn[T] Signal::clear_on_change(self : Signal[T]) -> Unit {
  self.rt.get_pull_signal(self.cell_id).on_change = None
}

///|
/// Disposes this signal, releasing its resources and marking it as Disposed.
///
/// After disposal, calling `get()` or `set()` will abort. Disposal is
/// idempotent — calling it multiple times is a no-op.
pub fn[T] Signal::dispose(self : Signal[T]) -> Unit {
  self.rt.dispose_cell(self.cell_id)
}

///|
/// Returns true if this signal has been disposed.
pub fn[T] Signal::is_disposed(self : Signal[T]) -> Bool {
  self.rt.is_cell_disposed(self.cell_id)
}

///|
/// Returns true. Signals are always up-to-date since they are input cells
/// with directly-set values.
pub fn[T] Signal::is_up_to_date(self : Signal[T]) -> Bool {
  ignore(self)
  true
}