///|
pub suberror CodecError {
  Invalid(String)
} derive(Debug)

///|
pub(all) struct Sample {
  timestamp : Int64
  bits : UInt64
} derive(Debug, Eq)

///|
priv struct Writer {
  bytes : Array[Byte]
  mut partial : Int
  mut used : Int
}

///|
fn Writer::put(self : Writer, n : UInt64, width : Int) -> Unit {
  let mut remaining = width
  while remaining > 0 {
    let take = Int::min(remaining, 8 - self.used)
    let bits = ((n >> (remaining - take)) & ((1UL << take) - 1UL)).to_int()
    self.partial = (self.partial << take) | bits
    self.used += take
    remaining -= take
    if self.used == 8 {
      self.bytes.push(self.partial.to_byte())
      self.partial = 0
      self.used = 0
    }
  }
}

///|
fn Writer::snapshot(self : Writer, count : Int) -> Bytes {
  let data = self.bytes.copy()
  if self.used > 0 {
    data.push((self.partial << (8 - self.used)).to_byte())
  }
  for i in 0..<4 {
    data[4 + i] = ((count >> ((3 - i) * 8)) & 255).to_byte()
  }
  Bytes::from_array(data)
}

///|
priv struct Reader {
  bytes : Bytes
  mut pos : Int
}

///|
fn Reader::get(self : Reader, n : Int) -> UInt64 raise CodecError {
  if n < 0 || self.pos + n > self.bytes.length() * 8 {
    raise Invalid("truncated bitstream")
  }
  let mut out = 0UL
  let mut remaining = n
  while remaining > 0 {
    let available = 8 - self.pos % 8
    let take = Int::min(remaining, available)
    let bits = (self.bytes[self.pos / 8].to_int() >> (available - take)) &
      ((1 << take) - 1)
    out = (out << take) | bits.to_uint64()
    self.pos += take
    remaining -= take
  }
  out
}

///|
fn zeros(x : UInt64) -> (Int, Int) {
  (x.clz(), x.ctz())
}

///|
pub fn encode(samples : Array[Sample]) -> Bytes raise CodecError {
  if samples.length() > 100000 {
    raise Invalid("sample limit")
  }
  let encoder = Encoder::new()
  for sample in samples {
    encoder.append(sample)
  }
  encoder.finish()
}

///|
pub fn decode(data : Bytes) -> Array[Sample] raise CodecError {
  let decoder = Decoder::new(data)
  let out = []
  while decoder.next() is Some(sample) {
    out.push(sample)
  }
  out
}