///|
priv struct Subscription {
  peer : String
  process : UInt32
  object : ObjectId
  mut expires : UInt64?
  mut baseline : Array[Tag]?
}

///|
pub(all) struct CovEnvelope {
  peer : String
  notification : CovNotification
} derive(Debug)

///|
pub struct CovManager {
  priv device : Device
  priv subscriptions : Array[Subscription]
  priv mut now : UInt64
}

///|
pub fn CovManager::new(device : Device) -> CovManager {
  { device, subscriptions: [], now: 0UL, }
}

///|
fn CovManager::advance(
  self : CovManager,
  now : UInt64,
) -> Unit raise BacnetError {
  if now < self.now {
    raise State("host clock moved backwards")
  }
  self.now = now
  for i = self.subscriptions.length() - 1; i >= 0; i = i - 1 {
    if self.subscriptions[i].expires is Some(deadline) && deadline <= now {
      self.subscriptions.remove(i) |> ignore
    }
  }
}

///|
pub fn CovManager::count(
  self : CovManager,
  now : UInt64,
) -> Int32 raise BacnetError {
  self.advance(now)
  self.subscriptions.length()
}

///|
pub fn CovManager::subscribe(
  self : CovManager,
  peer : String,
  request : SubscribeCov,
  now : UInt64,
) -> Unit raise BacnetError {
  self.advance(now)
  ignore(encode_subscribe_cov(request, limits=self.device.limits))
  if peer.is_empty() || text_bytes(peer, 256).length() > 256 {
    raise Invalid("subscription peer", 0)
  }
  let object = self.device.find(request.object)
  ignore(self.device.point_domain(object))
  ignore(object.find(85U))
  ignore(self.device.cov_values(request.object))
  if request.confirmed == Some(true) {
    raise Property(5U, 45U)
  }
  let mut found = -1
  for i = 0; i < self.subscriptions.length(); i = i + 1 {
    let s = self.subscriptions[i]
    if s.peer == peer &&
      s.process == request.process &&
      s.object == request.object {
      found = i
      break
    }
  }
  if request.confirmed == None {
    if found >= 0 {
      self.subscriptions.remove(found) |> ignore
    }
    return
  }
  let lifetime = request.lifetime.unwrap_or(0U).to_uint64()
  if lifetime > 18446744073709551615UL - now {
    raise State("COV deadline overflow")
  }
  let expires = if lifetime == 0UL { None } else { Some(now + lifetime) }
  if found >= 0 {
    self.subscriptions[found].expires = expires
    self.subscriptions[found].baseline = None
  } else {
    if self.subscriptions.length() >= self.device.limits.max_subscriptions {
      raise Limit("subscription count")
    }
    self.subscriptions.push({
      peer,
      process: request.process,
      object: request.object,
      expires,
      baseline: None,
    })
  }
}

///|
fn Device::cov_values(
  self : Device,
  id : ObjectId,
) -> Array[Tag] raise BacnetError {
  let value = self.read({ object: id, property: 85U, index: None, })
  let status = self.read({ object: id, property: 111U, index: None, })
  match value[:] {
    [Application(v)] => domain_validate(self.point_domain(self.find(id)), v)
    _ => raise Property(2U, 9U)
  }
  match status[:] {
    [Application(Bits(4, data))] if data.length() == 1 => ()
    _ => raise Property(2U, 9U)
  }
  if id.kind == 0 || id.kind == 1 || id.kind == 2 {
    match self.find(id).find(22U).values[:] {
      [Application(Real(v))] if v >= 0.0F &&
        (v.reinterpret_as_uint() & 0x7f800000U) != 0x7f800000U => ()
      _ => raise Property(2U, 37U)
    }
  }
  [Constructed(0, value), Constructed(1, status)]
}

///|
fn Device::cov_changed(
  self : Device,
  id : ObjectId,
  baseline : Array[Tag],
  current : Array[Tag],
) -> Bool raise BacnetError {
  if encode_tags(baseline, limits=self.limits) ==
    encode_tags(current, limits=self.limits) {
    return false
  }
  match (baseline[:], current[:]) {
    (
      [Constructed(0, [Application(Real(old))]), Constructed(1, old_status)],
      [Constructed(0, [Application(Real(new))]), Constructed(1, new_status)],
    ) => {
      if encode_tags(old_status, limits=self.limits) !=
        encode_tags(new_status, limits=self.limits) {
        return true
      }
      let increment = match self.find(id).find(22U).values[:] {
        [Application(Real(v))] => v
        _ => raise State("missing analog COV increment")
      }
      let delta = new.to_double() - old.to_double()
      (if delta < 0.0 { -delta } else { delta }) >= increment.to_double()
    }
    _ => true
  }
}

///|
pub fn CovManager::poll(
  self : CovManager,
  now : UInt64,
) -> Array[CovEnvelope] raise BacnetError {
  self.advance(now)
  let output : Array[CovEnvelope] = []
  let updates : Array[(Subscription, Array[Tag])] = []
  let mut bytes = 0
  for subscription in self.subscriptions {
    let current = self.device.cov_values(subscription.object)
    let changed = match subscription.baseline {
      None => true
      Some(old) => self.device.cov_changed(subscription.object, old, current)
    }
    if !changed {
      continue
    }
    let values : Array[CovProperty] = match current[:] {
      [Constructed(0, value), Constructed(1, status)] =>
        [
          { property: 85U, index: None, value, priority: None, },
          { property: 111U, index: None, value: status, priority: None, },
        ]
      _ => raise State("COV snapshot shape")
    }
    let remaining = match subscription.expires {
      None => 0U
      Some(deadline) => (deadline - now).to_uint()
    }
    let notification : CovNotification = {
      process: subscription.process,
      device: self.device.id,
      object: subscription.object,
      remaining,
      values,
    }
    let wire = encode_cov(notification, limits=self.device.limits)
    if wire.length() > self.device.limits.max_bytes - bytes {
      raise Limit("notification batch bytes")
    }
    bytes += wire.length()
    output.push({ peer: subscription.peer, notification, })
    updates.push((subscription, clone_tags(current, self.device.limits)))
  }
  for (subscription, snapshot) in updates {
    subscription.baseline = Some(snapshot)
  }
  output
}

///|
pub extend CovEnvelope with @debug.Debug::{to_repr}