///|
/// Aggregation used when reducing samples for a dashboard or export.
pub(all) enum Aggregation {
  LastValue
  MinimumValue
  MaximumValue
  SumValue
  AverageValue
  CountValue
} derive(Debug, Eq)

///|
pub fn aggregation_name(value : Aggregation) -> String {
  match value {
    LastValue => "last"
    MinimumValue => "min"
    MaximumValue => "max"
    SumValue => "sum"
    AverageValue => "average"
    CountValue => "count"
  }
}

///|
/// One immutable historian sample.
pub(all) struct TagSample {
  point_id : String
  timestamp : Int
  value : Int
  quality : ReadingQuality
}

///|
pub fn TagSample::new(
  point_id : String,
  timestamp : Int,
  value : Int,
  quality? : ReadingQuality = Good,
) -> Result[TagSample, ModbusError] {
  if point_id == "" || timestamp < 0 {
    Err(InvalidData)
  } else {
    Ok({ point_id, timestamp, value, quality })
  }
}

///|
pub fn TagSample::point_id(self : TagSample) -> String {
  self.point_id
}

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

///|
pub fn TagSample::value(self : TagSample) -> Int {
  self.value
}

///|
pub fn TagSample::quality(self : TagSample) -> ReadingQuality {
  self.quality
}

///|
pub fn TagSample::is_good(self : TagSample) -> Bool {
  self.quality == Good
}

///|
pub fn TagSample::summary(self : TagSample) -> String {
  self.point_id +
  " t=" +
  self.timestamp.to_string() +
  " value=" +
  self.value.to_string() +
  " quality=" +
  reading_quality_name(self.quality)
}

///|
/// A time range with samples belonging to one point.
pub(all) struct TagSeries {
  point_id : String
  samples : Array[TagSample]
  max_samples : Int
  mut dropped : Int
}

///|
pub fn TagSeries::new(
  point_id : String,
  max_samples? : Int = 8192,
) -> Result[TagSeries, ModbusError] {
  if point_id == "" || max_samples < 1 {
    Err(InvalidData)
  } else {
    Ok({ point_id, samples: [], max_samples, dropped: 0 })
  }
}

///|
pub fn TagSeries::point_id(self : TagSeries) -> String {
  self.point_id
}

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

///|
pub fn TagSeries::dropped(self : TagSeries) -> Int {
  self.dropped
}

///|
pub fn TagSeries::append(
  self : TagSeries,
  sample : TagSample,
) -> Result[Unit, ModbusError] {
  if sample.point_id() != self.point_id {
    return Err(UnitMismatch)
  }
  if self.samples.length() > 0 &&
    sample.timestamp() < self.samples[self.samples.length() - 1].timestamp() {
    return Err(InvalidData)
  }
  if self.samples.length() >= self.max_samples {
    ignore(self.samples.remove(0))
    self.dropped += 1
  }
  self.samples.push(sample)
  Ok(())
}

///|
pub fn TagSeries::at(
  self : TagSeries,
  index : Int,
) -> Result[TagSample, ModbusError] {
  if index < 0 || index >= self.samples.length() {
    Err(InvalidAddress)
  } else {
    Ok(self.samples[index])
  }
}

///|
pub fn TagSeries::first(self : TagSeries) -> TagSample? {
  if self.samples.length() == 0 {
    None
  } else {
    Some(self.samples[0])
  }
}

///|
pub fn TagSeries::last(self : TagSeries) -> TagSample? {
  if self.samples.length() == 0 {
    None
  } else {
    Some(self.samples[self.samples.length() - 1])
  }
}

///|
pub fn TagSeries::between(
  self : TagSeries,
  start : Int,
  end : Int,
) -> Result[Array[TagSample], ModbusError] {
  if start < 0 || end < start {
    return Err(InvalidData)
  }
  let result : Array[TagSample] = []
  for sample in self.samples {
    if sample.timestamp() >= start && sample.timestamp() <= end {
      result.push(sample)
    }
  }
  Ok(result)
}

///|
pub fn TagSeries::good_count(self : TagSeries) -> Int {
  let mut count = 0
  for sample in self.samples {
    if sample.is_good() {
      count += 1
    }
  }
  count
}

///|
pub fn TagSeries::average(
  self : TagSeries,
  start : Int,
  end : Int,
) -> Result[Int, ModbusError] {
  let samples = match self.between(start, end) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  if samples.length() == 0 {
    return Err(Incomplete)
  }
  let mut sum = 0
  let mut count = 0
  for sample in samples {
    if sample.is_good() {
      sum += sample.value()
      count += 1
    }
  }
  if count == 0 {
    Err(Incomplete)
  } else {
    Ok(sum / count)
  }
}

///|
pub fn TagSeries::aggregate(
  self : TagSeries,
  start : Int,
  end : Int,
  aggregation : Aggregation,
) -> Result[Int, ModbusError] {
  let samples = match self.between(start, end) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  if samples.length() == 0 {
    return Err(Incomplete)
  }
  match aggregation {
    LastValue => Ok(samples[samples.length() - 1].value())
    CountValue => Ok(samples.length())
    AverageValue => {
      let mut sum = 0
      for sample in samples {
        sum += sample.value()
      }
      Ok(sum / samples.length())
    }
    MinimumValue => {
      let mut value = samples[0].value()
      for sample in samples {
        if sample.value() < value {
          value = sample.value()
        }
      }
      Ok(value)
    }
    MaximumValue => {
      let mut value = samples[0].value()
      for sample in samples {
        if sample.value() > value {
          value = sample.value()
        }
      }
      Ok(value)
    }
    SumValue => {
      let mut value = 0
      for sample in samples {
        value += sample.value()
      }
      Ok(value)
    }
  }
}

///|
pub fn TagSeries::snapshot(self : TagSeries) -> Array[TagSample] {
  let result : Array[TagSample] = []
  for sample in self.samples {
    result.push(sample)
  }
  result
}

///|
pub fn TagSeries::clear(self : TagSeries) -> Unit {
  self.samples.clear()
  self.dropped = 0
}

///|
pub fn TagSeries::summary(self : TagSeries) -> String {
  self.point_id +
  " samples=" +
  self.length().to_string() +
  " good=" +
  self.good_count().to_string() +
  " dropped=" +
  self.dropped.to_string()
}

///|
/// A bounded multi-point historian.
pub(all) struct Historian {
  series : Array[TagSeries]
  max_points : Int
  per_point_capacity : Int
}

///|
pub fn Historian::new(
  max_points? : Int = 256,
  per_point_capacity? : Int = 8192,
) -> Result[Historian, ModbusError] {
  if max_points < 1 || per_point_capacity < 1 {
    Err(CapacityExceeded)
  } else {
    Ok({ series: [], max_points, per_point_capacity })
  }
}

///|
pub fn Historian::point_count(self : Historian) -> Int {
  self.series.length()
}

///|
pub fn Historian::ensure_point(
  self : Historian,
  point_id : String,
) -> Result[Unit, ModbusError] {
  if point_id == "" {
    return Err(InvalidData)
  }
  for current in self.series {
    if current.point_id() == point_id {
      return Ok(())
    }
  }
  if self.series.length() >= self.max_points {
    return Err(CapacityExceeded)
  }
  self.series.push(
    TagSeries::new(point_id, max_samples=self.per_point_capacity).unwrap(),
  )
  Ok(())
}

///|
pub fn Historian::append(
  self : Historian,
  sample : TagSample,
) -> Result[Unit, ModbusError] {
  match self.ensure_point(sample.point_id()) {
    Ok(_) => ()
    Err(error) => return Err(error)
  }
  for series in self.series {
    if series.point_id() == sample.point_id() {
      return series.append(sample)
    }
  }
  Err(InvalidData)
}

///|
pub fn Historian::get(self : Historian, point_id : String) -> TagSeries? {
  for series in self.series {
    if series.point_id() == point_id {
      return Some(series)
    }
  }
  None
}

///|
pub fn Historian::query(
  self : Historian,
  point_id : String,
  start : Int,
  end : Int,
) -> Result[Array[TagSample], ModbusError] {
  match self.get(point_id) {
    Some(series) => series.between(start, end)
    None => Err(InvalidData)
  }
}

///|
pub fn Historian::aggregate(
  self : Historian,
  point_id : String,
  start : Int,
  end : Int,
  aggregation : Aggregation,
) -> Result[Int, ModbusError] {
  match self.get(point_id) {
    Some(series) => series.aggregate(start, end, aggregation)
    None => Err(InvalidData)
  }
}

///|
pub fn Historian::series_snapshot(self : Historian) -> Array[TagSeries] {
  let result : Array[TagSeries] = []
  for series in self.series {
    result.push(series)
  }
  result
}

///|
pub fn Historian::remove(
  self : Historian,
  point_id : String,
) -> Result[TagSeries, ModbusError] {
  for index, series in self.series {
    if series.point_id() == point_id {
      ignore(self.series.remove(index))
      return Ok(series)
    }
  }
  Err(InvalidData)
}

///|
pub fn Historian::clear(self : Historian) -> Unit {
  for series in self.series {
    series.clear()
  }
}

///|
pub fn Historian::total_samples(self : Historian) -> Int {
  let mut result = 0
  for series in self.series {
    result += series.length()
  }
  result
}

///|
pub fn Historian::summary(self : Historian) -> String {
  "points=" +
  self.point_count().to_string() +
  " samples=" +
  self.total_samples().to_string()
}

///|
/// A fixed interval bucket for downsampled exports.
pub(all) struct HistoryBucket {
  start : Int
  end : Int
  mut count : Int
  mut minimum : Int
  mut maximum : Int
  mut sum : Int
}

///|
pub fn HistoryBucket::new(
  start : Int,
  width : Int,
  first : Int,
) -> Result[HistoryBucket, ModbusError] {
  if start < 0 || width < 1 {
    Err(InvalidData)
  } else {
    Ok({
      start,
      end: start + width,
      count: 1,
      minimum: first,
      maximum: first,
      sum: first,
    })
  }
}

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

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

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

///|
pub fn HistoryBucket::minimum(self : HistoryBucket) -> Int {
  self.minimum
}

///|
pub fn HistoryBucket::maximum(self : HistoryBucket) -> Int {
  self.maximum
}

///|
pub fn HistoryBucket::average(self : HistoryBucket) -> Int {
  self.sum / self.count
}

///|
pub fn HistoryBucket::contains(self : HistoryBucket, timestamp : Int) -> Bool {
  timestamp >= self.start && timestamp < self.end
}

///|
pub fn HistoryBucket::add(self : HistoryBucket, value : Int) -> Unit {
  self.count += 1
  self.sum += value
  if value < self.minimum {
    self.minimum = value
  }
  if value > self.maximum {
    self.maximum = value
  }
}

///|
pub fn bucket_samples(
  samples : Array[TagSample],
  start : Int,
  width : Int,
) -> Result[Array[HistoryBucket], ModbusError] {
  if start < 0 || width < 1 {
    return Err(InvalidData)
  }
  let buckets : Array[HistoryBucket] = []
  for sample in samples {
    let index = (sample.timestamp() - start) / width
    if sample.timestamp() >= start {
      let bucket_start = start + index * width
      let mut found = false
      for bucket in buckets {
        if bucket.start() == bucket_start {
          bucket.add(sample.value())
          found = true
        }
      }
      if !found {
        buckets.push(
          HistoryBucket::new(bucket_start, width, sample.value()).unwrap(),
        )
      }
    }
  }
  Ok(buckets)
}