// Memo - Cached computed value that updates when dependencies change
//

///|
/// Memo state
priv struct MemoState[T] {
  mut value : T?
  mut dirty : Bool
  subscribers : Array[EffectRunner]
}

///|
/// Create a memoized computation.
/// Returns a getter function that caches the result and recomputes only when dependencies change.
/// ```mbt check
/// test {
///   let a = @signal.signal(2)
///   let b = @signal.signal(3)
///   let compute_count : Ref[Int] = { val: 0 }
///   let sum = @signal.memo(fn() {
///     compute_count.val = compute_count.val + 1
///     a.get() + b.get()
///   })
///   inspect(sum(), content="5")
///   inspect(sum(), content="5") // Cached, no recompute
///   inspect(compute_count.val, content="1")
///   a.set(10)
///   inspect(sum(), content="13") // Recomputes
///   inspect(compute_count.val, content="2")
/// }
/// ```
pub fn[T] memo(compute : () -> T) -> () -> T {
  let id = new_effect_id()
  let state : MemoState[T] = { value: None, dirty: true, subscribers: [] }

  // Create EffectRunner reference for self-registration
  let runner_ref : Ref[EffectRunner?] = { val: None }

  // The subscriber function for this memo
  fn mark_dirty() -> Unit {
    if not(state.dirty) {
      state.dirty = true
      // Notify our own subscribers that we changed
      for subscriber in state.subscribers {
        schedule_effect(subscriber)
      }
    }
  }

  let runner : EffectRunner = { id, run: mark_dirty }
  runner_ref.val = Some(runner)

  // Return getter function
  fn() {
    // Track this memo as a dependency if inside an effect
    match get_current_subscriber() {
      Some(subscriber) => {
        // Check if already subscribed (by ID)
        let already_subscribed = state.subscribers
          .iter()
          .any(fn(s) { s.id == subscriber.id })
        if not(already_subscribed) {
          state.subscribers.push(subscriber)
        }
      }
      None => ()
    }

    // Recompute if dirty
    if state.dirty {
      // Run computation with mark_dirty as subscriber to track dependencies
      match runner_ref.val {
        Some(r) => {
          let new_value = run_with_subscriber(r, compute)
          state.value = Some(new_value)
          state.dirty = false
        }
        None => ()
      }
    }

    // Return cached value
    match state.value {
      Some(v) => v
      None => {
        // This should never happen after first computation
        let v = compute()
        state.value = Some(v)
        v
      }
    }
  }
}

///|
/// Create a computed signal (alias for memo with Signal-like API).
/// Same as `memo`, but named to be more familiar for those coming from other reactive frameworks.
/// ```mbt check
/// test {
///   let count = @signal.signal(5)
///   let doubled = @signal.computed(fn() { count.get() * 2 })
///   inspect(doubled(), content="10")
///   count.set(7)
///   inspect(doubled(), content="14")
/// }
/// ```
pub fn[T] computed(compute : () -> T) -> () -> T {
  memo(compute)
}