///|
/// A timestamp lattice with a unique global minimum. SummaryDomain carries the
/// associated path-summary operations separately, rather than as a Rust type.
pub(open) trait Timestamp: Lattice {
  fn minimum() -> Self
}

///|
pub impl Timestamp for Int8 with fn minimum() {
  Int8(-128)
}

///|
pub impl Timestamp for Byte with fn minimum() {
  0
}

///|
pub impl Timestamp for Int16 with fn minimum() {
  -32768
}

///|
pub impl Timestamp for UInt16 with fn minimum() {
  0
}

///|
pub impl Timestamp for Int32 with fn minimum() {
  -2147483648
}

///|
pub impl Timestamp for UInt32 with fn minimum() {
  0U
}

///|
pub impl Timestamp for Int64 with fn minimum() {
  -9223372036854775808L
}

///|
pub impl Timestamp for UInt64 with fn minimum() {
  0UL
}

///|
pub impl Timestamp for Int128 with fn minimum() {
  Int128::from_words(-9223372036854775808L, 0UL)
}

///|
pub impl Timestamp for UInt128 with fn minimum() {
  UInt128(0UL)
}

///|
pub impl Timestamp for Duration with fn minimum() {
  Duration(0UL)
}

///|
pub impl Timestamp for Unit with fn minimum() {
  ()
}

///|
pub impl[A : Timestamp, B : Timestamp] Timestamp for Product[A, B] with fn minimum() {
  Product(A::minimum(), B::minimum())
}

///|
/// A runtime-length product timestamp. Missing coordinates are Timestamp.minimum;
/// construction removes trailing minimum coordinates and copies the input array.
pub struct PointStamp[T] {
  priv vector : Array[T]
} derive(Eq, Debug)

///|
pub fn[T : Timestamp] PointStamp::PointStamp(
  vector : Array[T],
) -> PointStamp[T] {
  let vector = vector.copy()
  while !vector.is_empty() && vector[vector.length() - 1] == T::minimum() {
    let _ = vector.pop()
  }
  { vector, }
}

///|
pub fn[T] PointStamp::to_array(self : PointStamp[T]) -> Array[T] {
  self.vector.copy()
}

///|
pub impl[T : Compare] Compare for PointStamp[T] with fn compare(self, other) {
  for i in 0..= self.vector.length() {
        other.vector[i]
      } else if i >= other.vector.length() {
        self.vector[i]
      } else {
        Lattice::join(self.vector[i], other.vector[i])
      }
    },
  )
  PointStamp(coordinates)
}

///|
pub impl[T : Timestamp] Lattice for PointStamp[T] with fn meet(self, other) {
  PointStamp(
    Array::makei(Int32::min(self.vector.length(), other.vector.length()), fn(
      i,
    ) {
      Lattice::meet(self.vector[i], other.vector[i])
    }),
  )
}

///|
pub impl[T : Timestamp] Timestamp for PointStamp[T] with fn minimum() {
  PointStamp([])
}

///|
/// Truncate to retain coordinates (None retains all), then apply per-coordinate
/// actions, extending missing coordinates from the timestamp minimum as needed.
pub struct PointStampSummary[S] {
  priv retain : Int32?
  priv actions : Array[S]
} derive(Eq, Debug)

///|
pub fn[S] PointStampSummary::PointStampSummary(
  actions : Array[S],
  retain? : Int32,
) -> PointStampSummary[S] {
  if retain is Some(n) {
    guard n >= 0 else { abort("negative pointstamp retention") }
  }
  { retain, actions: actions.copy(), }
}

///|
pub fn[S] PointStampSummary::retain(self : PointStampSummary[S]) -> Int32? {
  self.retain
}

///|
pub fn[S] PointStampSummary::actions(self : PointStampSummary[S]) -> Array[S] {
  self.actions.copy()
}

///|
pub impl[S : Compare] Compare for PointStampSummary[S] with fn compare(
  self,
  other,
) {
  let retain = self.retain.compare(other.retain)
  if retain != 0 {
    return retain
  }
  for i in 0.. SummaryDomain[PointStamp[T], PointStampSummary[S]] {
  SummaryDomain(
    PointStampSummary([]),
    apply=fn(summary, time) {
      let length = match summary.retain {
        None => time.vector.length()
        Some(n) => Int32::min(n, time.vector.length())
      }
      let output = []
      for i in 0.. Some(Int32::min(a, b))
        (None, b) => b
        (a, None) => a
      }
      let length = match second.retain {
        None => first.actions.length()
        Some(n) => Int32::min(n, first.actions.length())
      }
      let actions = []
      for i in 0.. b.actions.length() {
        return false
      }
      for i, action in a.actions {
        if !(inner.less_equal)(action, b.actions[i]) {
          return false
        }
      }
      true
    },
  )
}

///|
/// Feedback at a one-based runtime scope level; earlier coordinates are unchanged.
pub fn[T, S] SummaryDomain::feedback_summary(
  self : SummaryDomain[T, S],
  level : Int32,
  action : S,
) -> PointStampSummary[S] {
  guard level > 0 else { abort("dynamic scope levels start at one") }
  let actions = Array::make(level - 1, self.identity)
  actions.push(action)
  PointStampSummary(actions)
}

///|
/// Dynamic entry requires no physical coordinate: absent coordinates are minimum.
pub fn[D, A, B, R] Collection::enter_dynamic(
  self : Collection[D, Product[A, PointStamp[B]], R],
  level : Int32,
) -> Collection[D, Product[A, PointStamp[B]], R] {
  guard level >= 0 else { abort("negative dynamic scope level") }
  self
}

///|
/// Leave the one-based dynamic level, discarding that and deeper coordinates.
pub fn[D, A : Lattice, B : Timestamp, R, S, U] Scope::leave_dynamic(
  self : Scope[Product[A, PointStamp[B]], Product[S, PointStampSummary[U]]],
  source : Collection[D, Product[A, PointStamp[B]], R],
  level : Int32,
) -> Collection[D, Product[A, PointStamp[B]], R] {
  guard level > 0 else { abort("dynamic scope levels start at one") }
  let summary = Product(
    self.domain.identity.outer,
    PointStampSummary([], retain=level - 1),
  )
  let (edge, result) = self.feedback(summary)
  edge.connect(source)
  result
}

///|
pub extend Int8 with Timestamp::{minimum}

///|
pub extend Int128 with Timestamp::{minimum}

///|
pub extend UInt128 with Timestamp::{minimum}

///|
pub extend Duration with Timestamp::{minimum}

///|
pub extend Product with Timestamp::{minimum}

///|
pub extend PointStamp with Timestamp::{minimum}

///|
pub extend PointStamp with Eq::{equal, not_equal}

///|
pub extend PointStamp with Compare::{compare, op_lt, op_le, op_gt, op_ge}

///|
pub extend PointStamp with Debug::{to_repr}

///|
pub extend PointStampSummary with Eq::{equal, not_equal}

///|
pub extend PointStampSummary with Compare::{compare, op_lt, op_le, op_gt, op_ge}

///|
pub extend PointStampSummary with Debug::{to_repr}

///|
pub extend PointStamp with Lattice::{less_equal, join, meet}

///|
/// Encode runtime timestamp coordinates using an explicit scalar codec.
pub fn[T : Timestamp] WireCodec::pointstamp(
  scalar : WireCodec[T],
) -> WireCodec[PointStamp[T]] {
  WireCodec(
    encode=fn(value) { Json(value.to_array().map(fn(x) { scalar.to_json(x) })) },
    decode=fn(value) {
      guard value is Array(values) else { return None }
      let coordinates = []
      for value in values {
        guard scalar.from_json(value) is Some(value) else { return None }
        coordinates.push(value)
      }
      Some(PointStamp(coordinates))
    },
  )
}

///|
/// Encode truncation and coordinate actions for distributed dynamic scopes.
pub fn[S] WireCodec::pointstamp_summary(
  scalar : WireCodec[S],
) -> WireCodec[PointStampSummary[S]] {
  WireCodec(
    encode=fn(value) {
      [
        Json(value.retain()),
        Json(value.actions().map(fn(x) { scalar.to_json(x) })),
      ]
    },
    decode=fn(value) {
      guard value is [retain, Array(values)] else { return None }
      let retain : Int32? = @json.from_json(retain) catch { _ => return None }
      if retain is Some(n) && n < 0 {
        return None
      }
      let actions = []
      for value in values {
        guard scalar.from_json(value) is Some(value) else { return None }
        actions.push(value)
      }
      Some(PointStampSummary(actions, retain?))
    },
  )
}