///|
pub struct SignalStatistics {
  samples : Int
  minimum : Double?
  maximum : Double?
  mean : Double?
  variance : Double?
  rms : Double?
  below_range : Int
  above_range : Int
  at_minimum : Int
  at_maximum : Int
} derive(Debug, ToJson)

///|
pub fn Recording::statistics(
  self : Recording,
  signal : Int,
) -> SignalStatistics raise EdfError {
  if signal < 0 ||
    signal >= self.header.signals.length() ||
    self.header.signals[signal].is_annotation() {
    raise Invalid("ordinary signal index required")
  }
  let s = self.header.signals[signal]
  let mut n = 0
  let mut low = 0.0
  let mut high = 0.0
  let mut mean = 0.0
  let mut m2 = 0.0
  let mut squares = 0.0
  let mut below = 0
  let mut above = 0
  let mut at_min = 0
  let mut at_max = 0
  for r = 0; r < self.records; r = r + 1 {
    for j = 0; j < s.samples_per_record; j = j + 1 {
      let digital = self.digital(signal, r, j)
      if digital < s.digital_min {
        below += 1
      } else if digital == s.digital_min {
        at_min += 1
      }
      if digital > s.digital_max {
        above += 1
      } else if digital == s.digital_max {
        at_max += 1
      }
      let v = s.to_physical(digital)
      n += 1
      if n == 1 {
        low = v
        high = v
      } else {
        low = low.min(v)
        high = high.max(v)
      }
      let d = v - mean
      mean += d / n.to_double()
      m2 += d * (v - mean)
      squares += v * v
      if !finite(mean) || !finite(m2) || !finite(squares) {
        raise Limit("signal statistics overflow")
      }
    }
  }
  {
    samples: n,
    minimum: if n == 0 {
      None
    } else {
      Some(low)
    },
    maximum: if n == 0 {
      None
    } else {
      Some(high)
    },
    mean: if n == 0 {
      None
    } else {
      Some(mean)
    },
    variance: if n == 0 {
      None
    } else {
      Some((m2 / n.to_double()).max(0.0))
    },
    rms: if n == 0 {
      None
    } else {
      Some((squares / n.to_double()).sqrt())
    },
    below_range: below,
    above_range: above,
    at_minimum: at_min,
    at_maximum: at_max,
  }
}

///|
pub struct Gap {
  after_record : Int
  start : Double
  end : Double
} derive(Debug, ToJson)

///|
pub fn Recording::gaps(self : Recording) -> Array[Gap] {
  let out = []
  for r = 1; r < self.records; r = r + 1 {
    let start = self.starts[r - 1] + self.header.duration
    let end = self.starts[r]
    if end - start > 1.0e-7 {
      out.push({ after_record: r - 1, start, end, })
    }
  }
  out
}

///|
pub struct FlatRun {
  first_sample : Int
  samples : Int
  digital_value : Int
  start : Double
  end : Double
} derive(Debug, ToJson)

///|
/// Find repeated digital values; runs never bridge a discontinuous time gap.
pub fn Recording::flat_runs(
  self : Recording,
  signal : Int,
  minimum_samples : Int,
) -> Array[FlatRun] raise EdfError {
  if minimum_samples < 2 {
    raise Invalid("flat run threshold must be at least two samples")
  }
  if signal < 0 ||
    signal >= self.header.signals.length() ||
    self.header.signals[signal].is_annotation() {
    raise Invalid("ordinary signal index required")
  }
  let per = self.header.signals[signal].samples_per_record
  let total = self.records * per
  let out = []
  let mut first = 0
  let mut length = 0
  let mut value = 0
  for i = 0; i <= total; i = i + 1 {
    let next = if i < total {
      self.digital(signal, i / per, i % per)
    } else {
      0
    }
    let gap = i > 0 &&
      i < total &&
      i % per == 0 &&
      self.starts[i / per] - (self.starts[i / per - 1] + self.header.duration) >
      1.0e-7
    if i == total || (length > 0 && (next != value || gap)) {
      if length >= minimum_samples {
        if out.length() >= 1000000 {
          raise Limit("too many flat runs")
        }
        out.push({
          first_sample: first,
          samples: length,
          digital_value: value,
          start: self.sample_time(signal, first / per, first % per),
          end: self.sample_time(signal, (i - 1) / per, (i - 1) % per) +
          self.header.duration / per.to_double(),
        })
      }
      first = i
      length = 0
    }
    if i < total {
      value = next
      length += 1
    }
  }
  out
}

///|
/// Long-form CSV respects each channel's sampling rate and discontinuous timestamps.
pub fn Recording::csv(
  self : Recording,
  indices : Array[Int],
  first_record? : Int = 0,
  records? : Int,
  physical? : Bool = true,
) -> String raise EdfError {
  let count = records.unwrap_or(self.records - first_record)
  if first_record < 0 ||
    count < 0 ||
    first_record > self.records ||
    count > self.records - first_record {
    raise Invalid("CSV record range out of bounds")
  }
  let mut rows = 0
  for s in indices {
    if s < 0 ||
      s >= self.header.signals.length() ||
      self.header.signals[s].is_annotation() {
      raise Invalid("CSV requires ordinary signals")
    }
    let n = self.header.signals[s].samples_per_record
    if count > 0 && n > (1000000 - rows) / count {
      raise Limit("CSV exceeds one million rows")
    }
    rows += n * count
  }
  let lines = ["signal,label,record,sample,time_seconds,value,unit"]
  for r = first_record; r < first_record + count; r = r + 1 {
    for s in indices {
      let signal = self.header.signals[s]
      let label = csv_quote(signal.label)
      let unit = csv_quote(if physical { signal.unit } else { "digital" })
      for j = 0; j < signal.samples_per_record; j = j + 1 {
        let value = if physical {
          self.physical(s, r, j).to_string()
        } else {
          self.digital(s, r, j).to_string()
        }
        lines.push(
          "\{s},\{label},\{r},\{j},\{self.sample_time(s,r,j)},\{value},\{unit}",
        )
      }
    }
  }
  lines.join("\n") + "\n"
}

///|
fn csv_quote(s : String) -> String {
  "\"" + s.replace(old="\"", new="\"\"") + "\""
}