///|
priv enum SignalEvents {
  Trace
  Change
} derive(Eq, Hash)

///|
let signal_events : Events[SignalEvents, Int] = Events::new()

///|
let signal_counter : Counter = Counter::new()

///|
pub struct State[T] {
  priv id : Int
  priv mut val : T
}

///|
/// Create a new state with the given initial value.
pub fn[T] State::new(val : T) -> State[T] {
  { val, id: signal_counter.get() }
}

///|
/// Create a new state with the given initial value.
pub fn[T] state(val : T) -> State[T] {
  State::new(val)
}

///|
/// Get the current value of the state.
pub fn[T] State::get(self : State[T]) -> T {
  signal_events.emit(Trace, self.id)
  self.val
}

///|
/// Set the current value of the state.
pub fn[T : Eq] State::set(self : State[T], val : T) -> Unit {
  guard val != self.val
  self.val = val
  signal_events.emit(Change, self.id)
}

///|
/// Get the current value of the state and subscribe to changes.
pub fn[T] State::on(self : State[T], listener : (T) -> Unit) -> () -> Unit {
  effect(() => listener(self.get()))
}

///|
/// Update the current value of the state using a function.
pub fn[T : Eq] State::update(self : State[T], f : (T) -> T) -> Unit {
  self.set(f(self.get()))
}

///|
pub struct Computed[T] {
  priv callback : () -> T
}

///|
/// Create a new computed value with the given callback.
pub fn[T] Computed::new(callback : () -> T) -> Computed[T] {
  { callback, }
}

///|
/// Create a new computed value with the given callback.
pub fn[T] computed(callback : () -> T) -> Computed[T] {
  Computed::new(callback)
}

///|
/// Get the current value of the computed value.
pub fn[T] Computed::get(self : Computed[T]) -> T {
  (self.callback)()
}

///|
/// Subscribe to changes of the computed value.
pub fn[T] Computed::on(
  self : Computed[T],
  listener : (T) -> Unit,
) -> () -> Unit {
  effect(() => listener(self.get()))
}

///|
/// Update the current value of the computed value using a function.
pub fn effect(callback : () -> Unit) -> () -> Unit {
  let dependencies = []
  let off1 = signal_events.on(Trace, id => if !dependencies.contains(id) {
    dependencies.push(id)
  })
  let off2 = signal_events.on(Change, id => if dependencies.contains(id) {
    dependencies.clear()
    callback()
  })
  callback()
  () => {
    off1()
    off2()
  }
}

///|
test "signal" {
  let condition = state(true)
  let a = state(1)
  let b = state(2)
  let result = computed(() => if condition.get() {
    println("used a branch")
    a.get() * 2
  } else {
    println("used b branch")
    b.get() * 3
  })
  effect(() => println("result changed: \{result.get()}")) |> ignore
  condition.set(false)
  a.set(10)
  b.set(20)
  condition.set(true)
}