///|
/// Retention policy for historical telemetry samples.
pub struct ArchivePolicy {
  maximum_samples : Int
  maximum_age : Int
  bucket_width : Int
  reject_invalid : Bool
} derive(Eq, Debug)

///|
pub fn ArchivePolicy::new(
  maximum_samples? : Int = 100000,
  maximum_age? : Int = 86400 * 30,
  bucket_width? : Int = 60,
  reject_invalid? : Bool = false,
) -> Result[ArchivePolicy, String] {
  if maximum_samples < 1 || maximum_age < 1 || bucket_width < 1 {
    Err("archive retention values must be positive")
  } else {
    Ok({ maximum_samples, maximum_age, bucket_width, reject_invalid })
  }
}

///|
pub fn ArchivePolicy::maximum_samples(self : ArchivePolicy) -> Int {
  self.maximum_samples
}

///|
pub fn ArchivePolicy::maximum_age(self : ArchivePolicy) -> Int {
  self.maximum_age
}

///|
pub fn ArchivePolicy::bucket_width(self : ArchivePolicy) -> Int {
  self.bucket_width
}

///|
pub fn ArchivePolicy::reject_invalid(self : ArchivePolicy) -> Bool {
  self.reject_invalid
}

///|
/// A durable-shaped historical sample retained in memory by the core.
pub struct ArchivedSample {
  station : CommonAddress
  address : InformationAddress
  type_id : ApplicationType
  value : Float
  quality : QualityDescriptor
  timestamp : Int
  sequence : Int
  source : String
} derive(Debug)

///|
pub fn ArchivedSample::station(self : ArchivedSample) -> CommonAddress {
  self.station
}

///|
pub fn ArchivedSample::address(self : ArchivedSample) -> InformationAddress {
  self.address
}

///|
pub fn ArchivedSample::type_id(self : ArchivedSample) -> ApplicationType {
  self.type_id
}

///|
pub fn ArchivedSample::value(self : ArchivedSample) -> Float {
  self.value
}

///|
pub fn ArchivedSample::quality(self : ArchivedSample) -> QualityDescriptor {
  self.quality
}

///|
pub fn ArchivedSample::timestamp(self : ArchivedSample) -> Int {
  self.timestamp
}

///|
pub fn ArchivedSample::sequence(self : ArchivedSample) -> Int {
  self.sequence
}

///|
pub fn ArchivedSample::source(self : ArchivedSample) -> String {
  self.source
}

///|
pub struct ArchiveBucket {
  station : CommonAddress
  address : InformationAddress
  start : Int
  end : Int
  mut count : Int
  mut minimum : Float
  mut maximum : Float
  mut sum : Float
  mut invalid : Int
} derive(Eq, Debug)

///|
pub fn ArchiveBucket::station(self : ArchiveBucket) -> CommonAddress {
  self.station
}

///|
pub fn ArchiveBucket::address(self : ArchiveBucket) -> InformationAddress {
  self.address
}

///|
pub fn ArchiveBucket::start(self : ArchiveBucket) -> Int {
  self.start
}

///|
pub fn ArchiveBucket::end(self : ArchiveBucket) -> Int {
  self.end
}

///|
pub fn ArchiveBucket::count(self : ArchiveBucket) -> Int {
  self.count
}

///|
pub fn ArchiveBucket::minimum(self : ArchiveBucket) -> Float {
  self.minimum
}

///|
pub fn ArchiveBucket::maximum(self : ArchiveBucket) -> Float {
  self.maximum
}

///|
pub fn ArchiveBucket::sum(self : ArchiveBucket) -> Float {
  self.sum
}

///|
pub fn ArchiveBucket::invalid(self : ArchiveBucket) -> Int {
  self.invalid
}

///|
pub fn ArchiveBucket::average(self : ArchiveBucket) -> Float {
  if self.count == 0 {
    Float::from_int(0)
  } else {
    Float::div(self.sum, Float::from_int(self.count))
  }
}

///|
/// Query over a station/address/time range.
pub struct ArchiveQuery {
  station : CommonAddress?
  first_address : InformationAddress?
  last_address : InformationAddress?
  start : Int?
  end : Int?
  source : String?
  include_invalid : Bool
} derive(Eq, Debug)

///|
pub fn ArchiveQuery::all() -> ArchiveQuery {
  {
    station: None,
    first_address: None,
    last_address: None,
    start: None,
    end: None,
    source: None,
    include_invalid: false,
  }
}

///|
pub fn ArchiveQuery::station(
  self : ArchiveQuery,
  value : CommonAddress,
) -> ArchiveQuery {
  {
    station: Some(value),
    first_address: self.first_address,
    last_address: self.last_address,
    start: self.start,
    end: self.end,
    source: self.source,
    include_invalid: self.include_invalid,
  }
}

///|
pub fn ArchiveQuery::address_range(
  self : ArchiveQuery,
  first : InformationAddress,
  last : InformationAddress,
) -> ArchiveQuery {
  {
    station: self.station,
    first_address: Some(first),
    last_address: Some(last),
    start: self.start,
    end: self.end,
    source: self.source,
    include_invalid: self.include_invalid,
  }
}

///|
pub fn ArchiveQuery::time_range(
  self : ArchiveQuery,
  start : Int,
  end : Int,
) -> ArchiveQuery {
  {
    station: self.station,
    first_address: self.first_address,
    last_address: self.last_address,
    start: Some(start),
    end: Some(end),
    source: self.source,
    include_invalid: self.include_invalid,
  }
}

///|
pub fn ArchiveQuery::from_source(
  self : ArchiveQuery,
  source : String,
) -> ArchiveQuery {
  {
    station: self.station,
    first_address: self.first_address,
    last_address: self.last_address,
    start: self.start,
    end: self.end,
    source: Some(source),
    include_invalid: self.include_invalid,
  }
}

///|
pub fn ArchiveQuery::with_invalid(
  self : ArchiveQuery,
  include_invalid_value : Bool,
) -> ArchiveQuery {
  {
    station: self.station,
    first_address: self.first_address,
    last_address: self.last_address,
    start: self.start,
    end: self.end,
    source: self.source,
    include_invalid: include_invalid_value,
  }
}

///|
fn archive_matches(sample : ArchivedSample, query : ArchiveQuery) -> Bool {
  let station_ok = match query.station {
    Some(value) => value == sample.station()
    None => true
  }
  let first_ok = match query.first_address {
    Some(value) => sample.address().number() >= value.number()
    None => true
  }
  let last_ok = match query.last_address {
    Some(value) => sample.address().number() <= value.number()
    None => true
  }
  let start_ok = match query.start {
    Some(value) => sample.timestamp() >= value
    None => true
  }
  let end_ok = match query.end {
    Some(value) => sample.timestamp() <= value
    None => true
  }
  let source_ok = match query.source {
    Some(value) => sample.source() == value
    None => true
  }
  station_ok &&
  first_ok &&
  last_ok &&
  start_ok &&
  end_ok &&
  source_ok &&
  (query.include_invalid || sample.quality().is_usable())
}

///|
pub struct ArchiveStore {
  samples : Array[ArchivedSample]
  policy : ArchivePolicy
  mut next_sequence : Int
  mut evicted : Int
} derive(Debug)

///|
pub fn ArchiveStore::new(
  policy? : ArchivePolicy = ArchivePolicy::new().unwrap(),
) -> ArchiveStore {
  { samples: [], policy, next_sequence: 1, evicted: 0 }
}

///|
pub fn ArchiveStore::len(self : ArchiveStore) -> Int {
  self.samples.length()
}

///|
pub fn ArchiveStore::evicted(self : ArchiveStore) -> Int {
  self.evicted
}

///|
pub fn ArchiveStore::policy(self : ArchiveStore) -> ArchivePolicy {
  self.policy
}

///|
pub fn ArchiveStore::append(
  self : ArchiveStore,
  sample : TelemetrySample,
) -> Result[Unit, String] {
  if self.policy.reject_invalid() && !sample.quality().is_usable() {
    Err("invalid telemetry is excluded by archive policy")
  } else {
    self.samples.push({
      station: sample.station(),
      address: sample.address(),
      type_id: sample.type_id(),
      value: sample.value(),
      quality: sample.quality(),
      timestamp: sample.timestamp(),
      sequence: self.next_sequence,
      source: sample.source(),
    })
    self.next_sequence += 1
    ignore(self.prune(sample.timestamp()))
    Ok(())
  }
}

///|
pub fn ArchiveStore::append_batch(
  self : ArchiveStore,
  samples : Array[TelemetrySample],
) -> Int {
  let mut accepted = 0
  for sample in samples {
    match self.append(sample) {
      Ok(_) => accepted += 1
      Err(_) => ()
    }
  }
  accepted
}

///|
pub fn ArchiveStore::prune(self : ArchiveStore, now : Int) -> Int {
  let cutoff = now - self.policy.maximum_age()
  let mut removed = 0
  while self.samples.length() > 0 &&
        (
          self.samples.length() > self.policy.maximum_samples() ||
          self.samples[0].timestamp() < cutoff
        ) {
    ignore(self.samples.remove(0))
    removed += 1
    self.evicted += 1
  }
  removed
}

///|
pub fn ArchiveStore::query(
  self : ArchiveStore,
  query : ArchiveQuery,
) -> Array[ArchivedSample] {
  let result : Array[ArchivedSample] = []
  for sample in self.samples {
    if archive_matches(sample, query) {
      result.push(sample)
    }
  }
  result
}

///|
pub fn ArchiveStore::buckets(
  self : ArchiveStore,
  query : ArchiveQuery,
) -> Array[ArchiveBucket] {
  let selected = self.query(query)
  let buckets : Map[(Int, Int), ArchiveBucket] = Map([])
  for sample in selected {
    let start = sample.timestamp() /
      self.policy.bucket_width() *
      self.policy.bucket_width()
    let key = (sample.address().number(), start)
    let bucket = match buckets.get(key) {
      Some(value) => value
      None =>
        {
          station: sample.station(),
          address: sample.address(),
          start,
          end: start + self.policy.bucket_width() - 1,
          count: 0,
          minimum: sample.value(),
          maximum: sample.value(),
          sum: Float::from_int(0),
          invalid: 0,
        }
    }
    if sample.value() < bucket.minimum {
      bucket.minimum = sample.value()
    }
    if sample.value() > bucket.maximum {
      bucket.maximum = sample.value()
    }
    bucket.count += 1
    bucket.sum = Float::add(bucket.sum, sample.value())
    if !sample.quality().is_usable() {
      bucket.invalid += 1
    }
    buckets[key] = bucket
  }
  let result : Array[ArchiveBucket] = []
  for _, bucket in buckets {
    result.push(bucket)
  }
  result.sort_by((left, right) => {
    if left.start() != right.start() {
      left.start() - right.start()
    } else {
      left.address().number() - right.address().number()
    }
  })
  result
}

///|
pub fn ArchiveStore::latest(
  self : ArchiveStore,
  station : CommonAddress,
  address : InformationAddress,
) -> ArchivedSample? {
  let mut result : ArchivedSample? = None
  for sample in self.samples {
    if sample.station() == station && sample.address() == address {
      result = Some(sample)
    }
  }
  result
}

///|
pub fn ArchiveStore::range_count(
  self : ArchiveStore,
  query : ArchiveQuery,
) -> Int {
  self.query(query).length()
}

///|
pub fn archive_policy_examples() -> Array[ArchivePolicy] {
  [
    ArchivePolicy::new().unwrap(),
    ArchivePolicy::new(maximum_samples=10, maximum_age=60, bucket_width=5).unwrap(),
  ]
}