// Watch - Advanced signal utilities
// Watch a computed expression and run callback when it changes
//

///|
/// Watch a computed expression and run callback when it changes.
/// Returns a dispose function.
/// The callback receives (newValue, oldValue).
/// ```mbt check
/// test {
///   let count = @signal.signal(0)
///   let changes : Array[(Int, Int)] = []
///   let dispose = @signal.watch(fn() { count.get() }, fn(new_val, old_val) {
///     changes.push((new_val, old_val))
///   })
///   count.set(1)
///   count.set(2)
///   count.set(2) // Same value, no callback
///   count.set(3)
///   dispose()
///   inspect(changes, content="[(1, 0), (2, 1), (3, 2)]")
/// }
/// ```
pub fn[T : Eq] watch(source : () -> T, callback : (T, T) -> Unit) -> () -> Unit {
  let prev_value : Ref[T?] = { val: None }
  render_effect(fn() {
    let new_value = source()
    match prev_value.val {
      Some(old) =>
        if old != new_value {
          callback(new_value, old)
          prev_value.val = Some(new_value)
        }
      None => prev_value.val = Some(new_value)
    }
  })
}

///|
/// Watch with immediate execution (runs callback on first value too).
/// The callback receives (newValue, oldValue?) where oldValue is None on first run.
/// ```mbt check
/// test {
///   let count = @signal.signal(5)
///   let log : Array[String] = []
///   let dispose = @signal.watch_immediate(fn() { count.get() }, fn(
///     new_val,
///     old_val,
///   ) {
///     match old_val {
///       Some(old) => log.push(old.to_string() + "->" + new_val.to_string())
///       None => log.push("init:" + new_val.to_string())
///     }
///   })
///   count.set(10)
///   dispose()
///   inspect(
///     log,
///     content=(
///       #|["init:5", "5->10"]
///     ),
///   )
/// }
/// ```
pub fn[T : Eq] watch_immediate(
  source : () -> T,
  callback : (T, T?) -> Unit,
) -> () -> Unit {
  let prev_value : Ref[T?] = { val: None }
  render_effect(fn() {
    let new_value = source()
    match prev_value.val {
      Some(old) =>
        if old != new_value {
          callback(new_value, Some(old))
          prev_value.val = Some(new_value)
        }
      None => {
        callback(new_value, None)
        prev_value.val = Some(new_value)
      }
    }
  })
}

///|
/// Create a signal that holds the previous value of another signal.
/// Returns a getter function for the previous value (None on first read).
/// ```mbt check
/// test {
///   let count = @signal.signal(1)
///   let prev = @signal.previous(count)
///   inspect(prev(), content="None") // No previous value yet
///   count.set(2)
///   inspect(prev(), content="Some(1)")
///   count.set(3)
///   inspect(prev(), content="Some(2)")
/// }
/// ```
pub fn[T] previous(sig : Signal[T]) -> () -> T? {
  let prev : Ref[T?] = { val: None }
  let current : Ref[T?] = { val: None }
  let _ = render_effect(fn() {
    let new_val = sig.get()
    prev.val = current.val
    current.val = Some(new_val)
  })
  fn() { prev.val }
}

///|
/// Create a signal that holds the previous value with initial value.
/// Unlike `previous`, this returns a plain T instead of T?.
/// ```mbt check
/// test {
///   let count = @signal.signal(10)
///   let prev = @signal.previous_with_initial(count, 0)
///   inspect(prev(), content="0") // Initial value
///   count.set(20)
///   inspect(prev(), content="10")
///   count.set(30)
///   inspect(prev(), content="20")
/// }
/// ```
pub fn[T] previous_with_initial(sig : Signal[T], initial : T) -> () -> T {
  let prev : Ref[T] = { val: initial }
  let current : Ref[T?] = { val: None }
  let _ = render_effect(fn() {
    let new_val = sig.get()
    match current.val {
      Some(curr) => prev.val = curr
      None => ()
    }
    current.val = Some(new_val)
  })
  fn() { prev.val }
}