///|
struct Buffer {
  mut buf : FixedArray[Byte]
  mut len : Int
}

///|
pub fn Buffer::new(capacity? : Int = 0) -> Buffer {
  let cap = if capacity < 0 { 0 } else { capacity }
  Buffer::{ buf: FixedArray::make(cap, 0x00), len: 0 }
}

///|
pub impl Default for Buffer with default() {
  Buffer::new()
}

///|
pub fn buffer(capacity? : Int = 0) -> Buffer {
  Buffer::new(capacity~)
}

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

///|
pub fn Buffer::capacity(self : Buffer) -> Int {
  self.buf.length()
}

///|
fn Buffer::grow(self : Buffer, by~ : Int) -> Unit {
  let new_len = self.len + by
  let mut cap = self.buf.length()
  if new_len <= cap {
    return
  }
  if cap == 0 {
    cap = 16
  }
  while cap < new_len {
    cap *= 2
  }
  let new_buf : FixedArray[Byte] = FixedArray::make(cap, 0x00)
  new_buf.unsafe_blit(0, self.buf, 0, self.len)
  self.buf = new_buf
}

///|
fn Buffer::write_byte_unchecked(self : Buffer, b : Byte) -> Unit {
  self.grow(by=1)
  self.buf[self.len] = b
  self.len = self.len + 1
}

///|
pub fn Buffer::write_byte(self : Buffer, b : Byte) -> Unit raise Malformed {
  self.grow(by=1)
  self.buf[self.len] = b
  self.check(self.len + 1)
  self.len = self.len + 1
}

///|
fn Buffer::check(self : Buffer, length : Int) -> Unit raise Malformed {
  match self.buf.unsafe_reinterpret_as_bytes()[:length] {
    [.., 0x00..=0x7F] => ()
    [.., 0xC2..=0xDF] => ()
    [.., 0xC2..=0xDF, 0x80..=0xBF] => ()
    [.., 0xE0] => ()
    [.., 0xE0, 0xA0..=0xBF] => ()
    [.., 0xE0, 0xA0..=0xBF, 0x80..=0xBF] => ()
    [.. prev, 0xED, 0xA0..=0xBF, 0x80..=0xBF, 0xED, 0xB0..=0xBF, 0x80..=0xBF] =>
      raise Malformed(prev.length())
    [.., 0xE1..=0xEF] => ()
    [.., 0xE1..=0xEF, 0x80..=0xBF] => ()
    [.., 0xE1..=0xEF, 0x80..=0xBF, 0x80..=0xBF] => ()
    [.., 0xF0] => ()
    [.., 0xF0, 0x90..=0xBf] => ()
    [.., 0xF0, 0x90..=0xBf, 0x80..=0xBF] => ()
    [.., 0xF0, 0x90..=0xBf, 0x80..=0xBF, 0x80..=0xBF] => ()
    [.., 0xF1..=0xF3] => ()
    [.., 0xF1..=0xF3, 0x80..=0xBF] => ()
    [.., 0xF1..=0xF3, 0x80..=0xBF, 0x80..=0xBF] => ()
    [.., 0xF1..=0xF3, 0x80..=0xBF, 0x80..=0xBF, 0x80..=0xBF] => ()
    [.., 0xF4] => ()
    [.., 0xF4, 0x80..=0x8F] => ()
    [.., 0xF4, 0x80..=0x8F, 0x80..=0xBF] => ()
    [.., 0xF4, 0x80..=0x8F, 0x80..=0xBF, 0x80..=0xBF] => ()
    [..] as view => raise Malformed(view.start_offset())
  }
}

///|
pub fn Buffer::write_bytesview(
  self : Buffer,
  bytesview : BytesView,
) -> Unit raise Malformed {
  let n = bytesview.length()
  let len = self.len
  self.grow(by=n)
  self.buf.blit_from_bytes(len, bytesview.data(), bytesview.start_offset(), n)
  self.check(len + n)
  self.len = self.len + 1
}

///|
pub fn Buffer::write_bytes(
  self : Buffer,
  bytes : Bytes,
) -> Unit raise Malformed {
  let n = bytes.length()
  let len = self.len
  self.grow(by=n)
  self.buf.blit_from_bytes(len, bytes, 0, n)
  self.check(len + n)
  self.len = len + n
}

///|
pub fn Buffer::to_wtf8(self : Buffer) -> Wtf8 {
  let buf = self.buf
  let len = self.len
  self.len = 0
  if len < buf.length() {
    let bytes : FixedArray[Byte] = FixedArray::make(len, 0)
    bytes.unsafe_blit(0, buf, 0, len)
    bytes.unsafe_reinterpret_as_bytes()
  } else {
    buf.unsafe_reinterpret_as_bytes()
  }
}

///|
pub fn Buffer::write_codepoint(
  self : Buffer,
  codepoint : @codepoint.CodePoint,
) -> Unit {
  let cp = codepoint.to_int()
  if cp <= 0x7F {
    self.write_byte_unchecked(cp.to_byte())
  } else if cp <= 0x7FF {
    self.write_byte_unchecked((0xC0 | ((cp >> 6) & 0x1F)).to_byte())
    self.write_byte_unchecked((0x80 | (cp & 0x3F)).to_byte())
  } else if cp <= 0xFFFF {
    self.write_byte_unchecked((0xE0 | ((cp >> 12) & 0x0F)).to_byte())
    self.write_byte_unchecked((0x80 | ((cp >> 6) & 0x3F)).to_byte())
    self.write_byte_unchecked((0x80 | (cp & 0x3F)).to_byte())
  } else {
    self.write_byte_unchecked((0xF0 | ((cp >> 18) & 0x07)).to_byte())
    self.write_byte_unchecked((0x80 | ((cp >> 12) & 0x3F)).to_byte())
    self.write_byte_unchecked((0x80 | ((cp >> 6) & 0x3F)).to_byte())
    self.write_byte_unchecked((0x80 | (cp & 0x3F)).to_byte())
  }
}