///|
pub(open) trait AsyncWriter {
  async write(Self, @bytes.View) -> Unit raise
}

///|
pub async fn[T : AsyncWriter] async_write_varint(
  writer : T,
  input : UInt64,
) -> Unit raise {
  let mut v = input
  while v >= 0x80 {
    writer.write([(v.to_byte() & 0x7F) | 0x80])
    v = v >> 7
  }
  writer.write([v.to_byte()])
}

///|
pub async fn[T : AsyncWriter] async_write_tag(
  writer : T,
  tag : (UInt, UInt),
) -> Unit raise {
  let (tag, wire_type) = tag
  writer |> async_write_varint(((tag << 3) | wire_type).to_uint64())
}

///|
pub async fn[T : AsyncWriter] async_write_int32(
  writer : T,
  v : Int,
) -> Unit raise {
  writer |> async_write_varint(v.to_uint64())
}

///|
pub async fn[T : AsyncWriter] async_write_int64(
  writer : T,
  v : Int64,
) -> Unit raise {
  writer |> async_write_varint(v.reinterpret_as_uint64())
}

///|
pub async fn[T : AsyncWriter] async_write_uint32(
  writer : T,
  v : UInt,
) -> Unit raise {
  writer |> async_write_varint(v.to_uint64())
}

///|
pub async fn[T : AsyncWriter] async_write_uint64(
  writer : T,
  v : UInt64,
) -> Unit raise {
  writer |> async_write_varint(v)
}

///|
pub async fn[T : AsyncWriter] async_write_sint32(
  writer : T,
  v : Int,
) -> Unit raise {
  writer |> async_write_varint(((v << 1) ^ (v >> 31)).to_uint64())
}

///|
pub async fn[T : AsyncWriter] async_write_sint64(
  writer : T,
  v : Int64,
) -> Unit raise {
  writer |> async_write_varint(((v << 1) ^ (v >> 63)).reinterpret_as_uint64())
}

///|
pub async fn[T : AsyncWriter] async_write_fixed32(
  writer : T,
  v : UInt,
) -> Unit raise {
  let mut v = v
  for i in 0..<4 {
    writer.write([(v & 0xFF) |> UInt::to_byte()])
    v = v >> 8
  }
}

///|
pub async fn[T : AsyncWriter] async_write_fixed64(
  writer : T,
  v : UInt64,
) -> Unit raise {
  let mut v = v
  for i in 0..<8 {
    writer.write([(v & 0xFF) |> UInt64::to_byte()])
    v = v >> 8
  }
}

///|
pub async fn[T : AsyncWriter] async_write_sfixed32(
  writer : T,
  v : Int,
) -> Unit raise {
  writer |> async_write_fixed32(v.reinterpret_as_uint())
}

///|
pub async fn[T : AsyncWriter] async_write_sfixed64(
  writer : T,
  v : Int64,
) -> Unit raise {
  writer |> async_write_fixed64(v.reinterpret_as_uint64())
}

///|
pub async fn[T : AsyncWriter] async_write_float(
  writer : T,
  v : Float,
) -> Unit raise {
  writer |> async_write_fixed32(v.reinterpret_as_uint())
}

///|
pub async fn[T : AsyncWriter] async_write_double(
  writer : T,
  v : Double,
) -> Unit raise {
  writer |> async_write_fixed64(v.reinterpret_as_uint64())
}

///|
pub async fn[T : AsyncWriter] async_write_bool(
  writer : T,
  v : Bool,
) -> Unit raise {
  writer.write([if v { 1 } else { 0 }])
}

///|
pub async fn[T : AsyncWriter] async_write_enum(
  writer : T,
  v : Enum,
) -> Unit raise {
  writer |> async_write_varint(v.inner().to_uint64())
}

///|
pub async fn[T : AsyncWriter] async_write_bytes(
  writer : T,
  v : Bytes,
) -> Unit raise {
  let length = v.length()
  writer |> async_write_varint(length.to_uint64())
  writer.write(v)
}

///|
pub async fn[T : AsyncWriter] async_write_string(
  writer : T,
  v : String,
) -> Unit raise {
  let utf8_string = @buffer.new()
  for ch in v {
    let ch = ch.to_int()
    if ch < 0x80 {
      utf8_string.write_byte(ch.to_byte())
    } else if ch < 0x800 {
      utf8_string.write_byte((0xC0 | (ch >> 6)).to_byte())
      utf8_string.write_byte((0x80 | (ch & 0x3F)).to_byte())
    } else if ch < 0x10000 {
      utf8_string.write_byte((0xE0 | (ch >> 12)).to_byte())
      utf8_string.write_byte((0x80 | ((ch >> 6) & 0x3F)).to_byte())
      utf8_string.write_byte((0x80 | (ch & 0x3F)).to_byte())
    } else {
      utf8_string.write_byte((0xF0 | (ch >> 18)).to_byte())
      utf8_string.write_byte((0x80 | ((ch >> 12) & 0x3F)).to_byte())
      utf8_string.write_byte((0x80 | ((ch >> 6) & 0x3F)).to_byte())
      utf8_string.write_byte((0x80 | (ch & 0x3F)).to_byte())
    }
  }
  let utf8_string = utf8_string.to_bytes()
  writer
  |> async_write_varint(utf8_string.length().reinterpret_as_uint().to_uint64())
  writer.write(utf8_string)
}