///| Version 1: little-endian 64-bit fields, IEEE doubles, and FNV-1a checksum.

///|
/// This checksum detects accidental corruption; it is not authentication.
fn checksum(bytes : Bytes, end : Int32) -> UInt32 {
  let mut h = 2166136261U
  for i = 0; i < end; i = i + 1 {
    h = (h ^ bytes[i].to_uint()) * 16777619U
  }
  h
}

///|
pub fn Store::snapshot(self : Store) -> Bytes {
  let b = @buffer.Buffer()
  b.write_int64_le(0x4152524DL)
  b.write_int64_le(1L)
  b.write_int64_le(self.step.to_int64())
  b.write_int64_le(self.latest)
  b.write_int64_le(self.archives.length().to_int64())
  for a in self.archives {
    b.write_int64_le(a.spec.factor.to_int64())
    b.write_int64_le(a.spec.rows.to_int64())
    b.write_int64_le(
      match a.spec.aggregation {
        Average => 0L
        Minimum => 1L
        Maximum => 2L
        Last => 3L
      },
    )
    b.write_int64_le(a.spec.min_known.to_int64())
    b.write_int64_le(a.bucket)
    b.write_int64_le(a.count.to_int64())
    b.write_double_le(a.mean)
    b.write_double_le(a.low)
    b.write_double_le(a.high)
    b.write_double_le(a.last)
    b.write_int64_le(a.data.length().to_int64())
    for i = 0; i < a.data.length(); i = i + 1 {
      let p = a.data[(a.head + i) % a.data.length()]
      b.write_int64_le(p.timestamp)
      match p.value {
        None => {
          b.write_int64_le(0L)
          b.write_double_le(0.0)
        }
        Some(v) => {
          b.write_int64_le(1L)
          b.write_double_le(v)
        }
      }
    }
  }
  let body = b.to_bytes()
  b.write_int64_le(checksum(body, body.length()).to_int64())
  b.to_bytes()
}

///|
priv struct Reader {
  data : Bytes
  mut pos : Int32
}

///|
fn Reader::word(self : Reader) -> Int64 raise ArchiveError {
  if self.pos > self.data.length() - 8 {
    raise InvalidSnapshot("truncated word")
  }
  let mut n = 0L
  for i = 0; i < 8; i = i + 1 {
    n = n | (self.data[self.pos + i].to_int64() << (i * 8))
  }
  self.pos += 8
  n
}

///|
fn Reader::bounded(
  self : Reader,
  low : Int32,
  high : Int32,
) -> Int32 raise ArchiveError {
  let n = self.word()
  if n < low.to_int64() || n > high.to_int64() {
    raise InvalidSnapshot("integer out of bounds")
  }
  n.to_int()
}

///|
fn Reader::number(self : Reader) -> Double raise ArchiveError {
  let n = self.word().reinterpret_as_double()
  if !finite(n) {
    raise InvalidSnapshot("non-finite number")
  }
  n
}

///|
/// Decoding is bounded to one million retained points and 32 archives.
pub fn Store::restore(bytes : Bytes) -> Store raise ArchiveError {
  if bytes.length() < 48 || bytes.length() > 24004000 {
    raise InvalidSnapshot("snapshot size out of bounds")
  }
  let trailer = { data: bytes, pos: bytes.length() - 8, }
  if trailer.word() != checksum(bytes, bytes.length() - 8).to_int64() {
    raise InvalidSnapshot("checksum mismatch")
  }
  let r = { data: bytes, pos: 0, }
  if r.word() != 0x4152524DL || r.word() != 1L {
    raise InvalidSnapshot("unsupported format/version")
  }
  let step = r.bounded(1, 86400)
  let latest = r.word()
  if latest < 0L || latest > 9000000000000000L || latest % step.to_int64() != 0L {
    raise InvalidSnapshot("invalid latest timestamp")
  }
  let size = r.bounded(1, 32)

  let archives = []
  let mut capacity = 0
  for index = 0; index < size; index = index + 1 {
    let factor = r.bounded(1, 1000000)
    let rows = r.bounded(1, 1000000)
    capacity += rows
    if capacity > 1000000 {
      raise InvalidSnapshot("capacity limit")
    }
    let aggregation = match r.bounded(0, 3) {
      0 => Average
      1 => Minimum
      2 => Maximum
      _ => Last
    }
    let min_known = r.bounded(1, factor)
    let spec = Spec::new(factor, rows, aggregation, min_known~)

    let a = empty_archive(spec)
    a.bucket = r.word()
    let tick = latest / step.to_int64()
    if a.bucket != tick / factor.to_int64() {
      raise InvalidSnapshot("invalid pending bucket")
    }
    a.count = r.bounded(0, (tick % factor.to_int64()).to_int())
    a.mean = r.number()
    a.low = r.number()
    a.high = r.number()
    a.last = r.number()
    if a.count == 0 {
      if a.mean != 0.0 || a.low != 0.0 || a.high != 0.0 || a.last != 0.0 {
        raise InvalidSnapshot("invalid empty accumulator")
      }
    } else if a.low > a.high || a.last < a.low || a.last > a.high {
      raise InvalidSnapshot("invalid accumulator bounds")
    }
    let n = r.bounded(0, rows)
    if n.to_int64() > a.bucket {
      raise InvalidSnapshot("too many historical buckets")
    }
    for i = 0; i < n; i = i + 1 {
      let timestamp = r.word()
      let expected = (a.bucket - n.to_int64() + i.to_int64() + 1L) *
        factor.to_int64() *
        step.to_int64()
      if timestamp != expected || timestamp > latest {
        raise InvalidSnapshot("invalid bucket timestamp")
      }
      let tag = r.bounded(0, 1)
      let v = r.number()
      if tag == 0 && v != 0.0 {
        raise InvalidSnapshot("noncanonical missing value")
      }
      a.data.push({ timestamp, value: if tag == 0 { None } else { Some(v) }, })
    }
    archives.push(a)
  }
  if r.pos != bytes.length() - 8 {
    raise InvalidSnapshot("trailing data")
  }
  { step, latest, archives, }
}