///|
/// Explicit timestamp/summary association, replacing Rust's associated Summary type.
/// Functions must obey Timely's monotonicity, identity and composition laws.
pub struct SummaryDomain[T, S] {
  priv identity : S
  priv apply : (S, T) -> T?
  priv compose : (S, S) -> S?
  priv less_equal : (S, S) -> Bool
}

///|
pub fn[T, S] SummaryDomain::SummaryDomain(
  identity : S,
  apply~ : (S, T) -> T?,
  compose~ : (S, S) -> S?,
  less_equal~ : (S, S) -> Bool,
) -> SummaryDomain[T, S] {
  { identity, apply, compose, less_equal, }
}

///|
fn checked_add(a : Int64, b : Int64) -> Int64? {
  let sum = a + b
  if (b > 0 && sum < a) || (b < 0 && sum > a) {
    None
  } else {
    Some(sum)
  }
}

///|
pub fn SummaryDomain::int64() -> SummaryDomain[Int64, Int64] {
  SummaryDomain(0, apply=checked_add, compose=checked_add, less_equal=fn(a, b) {
    a <= b
  })
}

///|
pub fn[A, B, S, U] SummaryDomain::product(
  outer : SummaryDomain[A, S],
  inner : SummaryDomain[B, U],
) -> SummaryDomain[Product[A, B], Product[S, U]] {
  SummaryDomain(
    Product(outer.identity, inner.identity),
    apply=fn(summary, time) {
      guard (outer.apply)(summary.outer, time.outer) is Some(a) else {
        return None
      }
      guard (inner.apply)(summary.inner, time.inner) is Some(b) else {
        return None
      }
      Some(Product(a, b))
    },
    compose=fn(first, second) {
      guard (outer.compose)(first.outer, second.outer) is Some(a) else {
        return None
      }
      guard (inner.compose)(first.inner, second.inner) is Some(b) else {
        return None
      }
      Some(Product(a, b))
    },
    less_equal=fn(a, b) {
      (outer.less_equal)(a.outer, b.outer) &&
      (inner.less_equal)(a.inner, b.inner)
    },
  )
}

///|
pub fn[T, S] SummaryDomain::results_in(
  self : SummaryDomain[T, S],
  summary : S,
  time : T,
) -> T? {
  (self.apply)(summary, time)
}

///|
pub fn[T, S] SummaryDomain::followed_by(
  self : SummaryDomain[T, S],
  first : S,
  second : S,
) -> S? {
  (self.compose)(first, second)
}

///|
pub fn[T, S] SummaryDomain::identity(self : SummaryDomain[T, S]) -> S {
  self.identity
}