///|
/// Subscriber ID
priv struct SubId(Int) derive(Eq, Hash)

///|
/// A reactive signal that can be subscribed to and updated.
struct Rsignal[T] {
  id : String // unique ID for this signal
  sub_id : () -> Int // for generating unique subscriber IDs
  mut value : T // any changes to this value will trigger subscribers
  subscribers : @hashmap.HashMap[SubId, Subscriber[T]] // subscribers to this signal
}

///|
struct Subscriber[T] {
  id : SubId // unique ID for this subscriber, used to subscribe/unsubscribe
  effect : (T) -> Unit // function to call when the signal changes
}

///|
fn[T] Subscriber::effect(self : Subscriber[T], value : T) -> Unit {
  (self.effect)(value)
}

///|
fn id_generator() -> () -> Int {
  let mut id : Int = -1
  fn() -> Int {
    id += 1
    id
  }
}

///|
/// Creates a new unique ID for signals
let signal_id : () -> Int = id_generator()

///|
/// Creates a new signal with the given initial value.
pub fn[T] new(value : T, label? : String = "signal") -> Rsignal[T] {
  let sub_id = id_generator()
  let id = "\{label}#\{signal_id()}"
  Rsignal::{ id, value, sub_id, subscribers: @hashmap.new() }
}

///|
/// Returns the unique ID of the signal
///
/// Parameters:
/// - `self`: The signal to get the ID from.
///
/// Returns:
/// - The unique ID of the signal.
///
/// Example:
/// ```moonbit
/// let s = new(0)
/// assert_eq(s.id(), "signal#0")  
/// let s2 = new(1, label="my_signal")
/// assert_eq(s2.id(), "my_signal#1")
/// ```
pub fn[T] Rsignal::id(self : Rsignal[T]) -> String {
  self.id
}

///|
/// Returns the current value of the signal `s`
/// 
/// Parameters:
/// - `s`: The signal to get the value from.
/// 
/// Returns:
/// - The current value of the signal.
/// 
/// Example:
/// ```moonbit
/// let s = new(0)
/// assert_eq(s.val(), 0)
/// s.update(5)
/// assert_eq(s.val(), 5)
/// ```
pub fn[T] Rsignal::val(s : Rsignal[T]) -> T {
  s.value
}

// ///|
// /// Sets the value of the signal `s` to `value` without notifying subscribers
// /// This is useful for cases where you want to set the value without triggering any side effects.
// ///
// /// Parameters:
// /// - `s`: The signal to set.
// /// - `value`: The new value to set.
// ///
// /// Example:
// /// ```moonbit
// /// let values = []
// /// let s = new(0)
// /// s.subscribe_permanent(fn(v) { values.push(v) })
// /// assert_eq(s.val(), 0)
// /// s.set(5) // This will set the signal to 5 without notifying subscribers.
// /// assert_eq(s.val(), 5)
// /// s.update(10) // This will notify subscribers with the new value.
// /// assert_eq(s.val(), 10)
// /// assert_eq(values, [0, 10])
// /// ```
// pub fn[T] Signal::set(s : Signal[T], value : T) -> Unit {
//   s.value = value
// }

///|
/// Returns the unique subscriber ID for this signal
fn[T] Rsignal::sub_id(self : Rsignal[T]) -> SubId {
  (self.sub_id)()
}

///|
/// Subscribes to a signal and immediately calls the effect function with the current value
pub fn[T] Rsignal::subscribe(
  self : Rsignal[T],
  effect : (T) -> Unit,
) -> Subscriber[T] {
  let subscriber = Subscriber::{ id: self.sub_id(), effect }
  self.subscribers[subscriber.id] = subscriber
  subscriber.effect(self.value)
  subscriber
}

///|
/// Subscribes to a signal without immediately calling the effect function
/// This is useful for cases where you want to set up a subscription but don't want to
/// trigger the effect function until the signal changes.
pub fn[T] Rsignal::subscribe_only(
  self : Rsignal[T],
  effect : (T) -> Unit,
) -> Subscriber[T] {
  let subscriber = Subscriber::{ id: self.sub_id(), effect }
  self.subscribers[subscriber.id] = subscriber
  subscriber
}

///|
/// Subscribes to a signal permanently and immediately calls the effect function with the current value.
/// This is useful for cases where you want to set up a subscription that will not be removed
pub fn[T] Rsignal::subscribe_permanent(
  self : Rsignal[T],
  effect : (T) -> Unit,
) -> Unit {
  self.subscribe(effect) |> ignore
}

///|
/// Subscribes to a signal permanently without immediately calling the effect function.
pub fn[T] Rsignal::subscribe_permanent_only(
  self : Rsignal[T],
  effect : (T) -> Unit,
) -> Unit {
  self.subscribe_only(effect) |> ignore
}

///|
/// Updates the value of the signal. If `notify` is `true`, notifies all of its observers
/// of the change.
/// 
/// Parameters:
/// - `self`: The signal to update.
/// - `new_value`: The new value to set.
/// - `notify` : If `true` notifies all observers of this signal - default is `true`.
///
/// Example:
/// ```moonbit
/// let values = []
/// let s = new(0)
/// s.subscribe_permanent(fn(v) { values.push(v) })
/// assert_eq(s.val(), 0)
/// s.update(5) // This will notify subscribers with the new value.
/// assert_eq(s.val(), 5)
/// assert_eq(values, [0, 5])
/// s.update(10) // This will notify subscribers with the new value.
/// assert_eq(s.val(), 10)
/// assert_eq(values, [0, 5, 10])
/// ```
pub fn[T] Rsignal::update(
  self : Rsignal[T],
  new_value : T,
  notify? : Bool = true,
) -> Unit {
  self.value = new_value
  if notify {
    for _, subscriber in self.subscribers {
      subscriber.effect(self.value)
    }
  }
}

///|
/// Unsubscribes a subscriber from a signal.
/// 
/// Parameters:
/// - `self`: The signal to unsubscribe from.
/// - `subscription`: The subscriber to unsubscribe.
///
/// Example:
/// ```moonbit
/// let values = []
/// let s = new(0)
/// let sub = s.subscribe(fn(v) { values.push(v) })
/// assert_eq(s.val(), 0)
/// s.update(5) // This will notify subscribers with the new value.
/// assert_eq(s.val(), 5)
/// assert_eq(values, [0, 5])
/// s.unsubscribe(sub) // This will unsubscribe the subscriber.
/// assert_eq(s.val(), 5)
/// s.update(10) // This will not notify the unsubscribed subscriber.
/// assert_eq(s.val(), 10)
/// assert_eq(values, [0, 5])
/// ```
pub fn[T] Rsignal::unsubscribe(
  self : Rsignal[T],
  subscription : Subscriber[T],
) -> Unit {
  self.subscribers.remove(subscription.id)
}

///|
/// Creates a new signal that maps the value of `self` using the function `f`.
/// 
/// Parameters:
/// - `self`: The original signal.
/// - `f`: The mapping function that takes the value of `self` and returns a new value.
/// 
/// Returns:
/// - A new signal that emits the result of applying `f` to the value of `self`.
///
/// Example:
/// ```moonbit
/// let values = []
/// let s = new(0)
/// let s2 = s.map(fn(v) { v + 1 })
/// s2.subscribe_permanent(fn(v) { values.push(v) })
/// s.update(5) // This will trigger the subscriber with value 6.
/// s.update(10) // This will trigger the subscriber with value 11.
/// s.update(20) // This will trigger the subscriber with value 21.
/// inspect(values, content="[1, 6, 11, 21]")
/// ```
pub fn[T, U] Rsignal::map(self : Rsignal[T], f : (T) -> U) -> Rsignal[U] {
  let s = new(f(self.value))
  self.subscribe_permanent(v => s.update(f(v)))
  s
}

///|
/// Creates a new signal that maps the values of two signals using the function `f`.
/// 
/// Parameters:
/// - `s1`: The first signal.
/// - `s2`: The second signal.
/// - `f`: The mapping function that takes the values of both signals and returns a new value.
/// 
/// Returns:
/// - A new signal that emits the result of applying `f` to the values of `s1` and `s2`.
/// 
/// Example:
/// ```moonbit
/// let values = []
/// let s1 = new(1)
/// let s2 = new(2)
/// let s3 = map2(s1, s2, fn(a, b) { a + b })
/// s3.subscribe_permanent(fn(v) { values.push(v) })
/// s1.update(3) // This will trigger the subscriber with value 5.
/// s2.update(4) // This will trigger the subscriber with value 7.
/// s1.update(5) // This will trigger the subscriber with value 9.
/// inspect(values, content="[3, 5, 7, 9]")
/// ```
pub fn[T, U, V] map2(
  s1 : Rsignal[T],
  s2 : Rsignal[U],
  f : (T, U) -> V,
) -> Rsignal[V] {
  let s = new(f(s1.value, s2.value))
  s1.subscribe_permanent(v => s.update(f(v, s2.value)))
  s2.subscribe_permanent(v => s.update(f(s1.value, v)))
  s
}

///|
/// Creates a new signal that receives values from `self` where `f(new_value)` is `true`.
/// If the initial value of `f(self.get()) == false`, the new signal will be 
/// initialized with the given `seed` value.
/// 
/// Parameters:
/// - `self`: The original signal.
/// - `f`: The filter function.
/// - `seed`: The initial value for the new signal if the filter function returns false during creation.
///
/// Returns:
/// - A new signal that emits only the values from `self` that pass the filter function `f`.
/// 
/// Example:
/// ```moonbit
/// let filtered_values = []
/// let s = new(1)
/// let s2 = s.filter(fn(v) { v > 5 }, 0)
/// s2.subscribe_permanent_only(fn(v) { filtered_values.push(v) })
/// s.update(10) // This will trigger the subscriber with value 10.
/// s.update(7) // This will trigger the subscriber with value 7.
/// s.update(3) // This will not trigger the subscriber.
/// inspect(filtered_values, content="[10, 7]")
/// ```
pub fn[T] Rsignal::filter(
  self : Rsignal[T],
  f : (T) -> Bool,
  seed : T,
) -> Rsignal[T] {
  let v = if f(self.value) { self.value } else { seed }
  let s = new(v)
  self.subscribe_permanent(v => if f(v) { s.update(v) })
  s
}

///|
/// Creates a new signal that maps the values of `self` iff `f(self.get()) == Some(v)`.
/// If `f(self.get())` returns `None`, the new signal will be initialized with the given `seed` value
/// 
/// This combines the functionality of `map` and `filter` into a single operation.
/// 
/// Parameters:
/// - `self`: The original signal.
/// - `f`: The mapping function.
/// - `seed`: The initial value for the new signal if the mapping function returns `None` during creation.
/// 
/// Returns:
/// - A new signal that emits the values from `self` that pass the mapping function `f`.
/// 
/// Example:
/// ```moonbit
/// let mapped_values = []
/// let s = new(1)
/// let s2 = s.filter_map(fn(v) { if v > 5 { Some(v+2) } else { None } }, 0)
/// s2.subscribe_permanent_only(fn(v) { mapped_values.push(v) })
/// s.update(10) // This will trigger the subscriber with value 12.
/// s.update(7) // This will trigger the subscriber with value 9.
/// s.update(3) // This will not trigger the subscriber.
/// inspect(mapped_values, content="[12, 9]")
/// ```
pub fn[T, U] Rsignal::filter_map(
  self : Rsignal[T],
  f : (T) -> U?,
  seed : U,
) -> Rsignal[U] {
  let v = f(self.value).unwrap_or_else(() => seed)
  let s = new(v)
  self.subscribe_permanent(v => match f(v) {
    Some(v) => s.update(v)
    None => ()
  })
  s
}

// -- Combinators ---

///|
/// Creates a new signal which re-emits the latest signal from one of the 
/// signals in `signals` array.
/// 
/// This is useful for combining multiple signals into a single signal.
///
/// The initial value of the new signal will be the default value of `T`.
/// 
/// Parameters:
/// - `signals`: An array of signals to combine.
///
/// Returns:
/// - A new signal that emits the values from all signals in the array.
///
/// Example:
/// ```moonbit
/// let values = []
/// let s1 = new(1)
/// let s2 = new(2)
/// let s3 = new(3)
/// let combined_signal = select_one([s1, s2, s3])
/// combined_signal.subscribe_permanent_only(fn(v) { values.push(v) })
/// s1.update(4) // This will trigger the subscriber with value 4.
/// s2.update(5) // This will trigger the subscriber with value 5.
/// s3.update(6) // This will trigger the subscriber with value 6.
/// inspect(values, content="[4, 5, 6]")
/// ```
pub fn[T : Default] select_one(signals : Array[Rsignal[T]]) -> Rsignal[T] {
  let s = new(T::default())
  for signal in signals {
    signal.subscribe_permanent_only(v => s.update(v))
  }
  s
}

///|
/// Creates a new signal that emits a tuple of the latest values from both signals.
/// 
/// Parameters:
/// - `s1`: The first signal.
/// - `s2`: The second signal.
/// 
/// Returns:
/// - A new signal that emits a tuple of the latest values from `s1` and `s2`.
///
/// Example:
/// ```moonbit
/// let values = []
/// let s1 = new(1)
/// let s2 = new(2)
/// let s3 = combine_pair(s1, s2)
/// s3.subscribe_permanent(fn(v) { values.push(v) })
/// s1.update(3) // This will trigger the subscriber with value (3, 2).
/// s2.update(4) // This will trigger the subscriber with value (3, 4).
/// s1.update(5) // This will trigger the subscriber with value (5, 4).
/// inspect(values, content="[(1, 2), (3, 2), (3, 4), (5, 4)]")
/// ```
pub fn[T, U] combine_pair(s1 : Rsignal[T], s2 : Rsignal[U]) -> Rsignal[(T, U)] {
  let s = new((s1.value, s2.value))
  s1.subscribe_permanent_only(v => s.update((v, s2.value)))
  s2.subscribe_permanent_only(v => s.update((s1.value, v)))
  s
}

///|
/// Creates a new signal that emits a tuple of the latest values from three signals.
///
/// Parameters:
/// - `s1`: The first signal.
/// - `s2`: The second signal.
/// - `s3`: The third signal.
///
/// Returns:
/// - A new signal that emits a tuple of the latest values from `s1`, `s2`, and `s3`.
///
/// Example:
/// ```moonbit
/// let values = []
/// let s1 = new(1)
/// let s2 = new(2)
/// let s3 = new(3)
/// let s4 = combine_triple(s1, s2, s3)
/// s4.subscribe_permanent(fn(v) { values.push(v) })
/// s1.update(4) // This will trigger the subscriber with value (4, 2, 3).
/// s2.update(5) // This will trigger the subscriber with value (4, 5, 3).
/// s3.update(6) // This will trigger the subscriber with value (4, 5, 6).
/// s1.update(7) // This will trigger the subscriber with value (7, 5, 6).
/// inspect(values, content="[(1, 2, 3), (4, 2, 3), (4, 5, 3), (4, 5, 6), (7, 5, 6)]")
/// ```
pub fn[T, U, V] combine_triple(
  s1 : Rsignal[T],
  s2 : Rsignal[U],
  s3 : Rsignal[V],
) -> Rsignal[(T, U, V)] {
  let s = new((s1.value, s2.value, s3.value))
  s1.subscribe_permanent_only(v => s.update((v, s2.value, s3.value)))
  s2.subscribe_permanent_only(v => s.update((s1.value, v, s3.value)))
  s3.subscribe_permanent_only(v => s.update((s1.value, s2.value, v)))
  s
}

///|
/// Combines all signals in `signals` into a single signal `s`. If any of the signals
/// change, `s` will emit an array of the latest values from all signals.
/// 
/// Parameters:
/// - `signals`: An array of signals to combine.
/// 
/// Returns:
/// - A new signal that emits an array of the latest values from all signals.
/// 
/// Example:
/// ```moonbit
/// let values = []
/// let s1 = new(1)
/// let s2 = new(2)
/// let s3 = new(3)
/// let s4 = new(4)
/// let combined_signal = combine_all([s1, s2, s3, s4])
/// combined_signal.subscribe_permanent(fn(v) { values.push(v) })
/// s1.update(5) // This will trigger the subscriber with value [5, 2, 3, 4].
/// s2.update(6) // This will trigger the subscriber with value [5, 6, 3, 4].
/// s3.update(7) // This will trigger the subscriber with value [5, 6, 7, 4].
/// s4.update(8) // This will trigger the subscriber with value [5, 6, 7, 8].
/// inspect(values, content="[[1, 2, 3, 4], [5, 2, 3, 4], [5, 6, 3, 4], [5, 6, 7, 4], [5, 6, 7, 8]]")
/// ```
pub fn[T] combine_all(signals : Array[Rsignal[T]]) -> Rsignal[Array[T]] {
  let latest_values = []
  for signal in signals {
    latest_values.push(signal.value)
  }
  let s = new(latest_values)
  for signal in signals {
    signal.subscribe_permanent_only(new_val => {
      let vals = []
      for s in signals {
        if s.id == signal.id {
          vals.push(new_val)
        } else {
          vals.push(s.value)
        }
      }
      s.update(vals)
    })
  }
  s
}