///|
pub(all) struct ProtobufOptions {
  descriptor_bytes : Int
  message_bytes : Int
  depth : Int
  items : Int
  fields : Int
  int64_as_string : Bool
  protojson : Bool
  emit_defaults : Bool
}

///|
pub fn protobuf_options() -> ProtobufOptions {
  {
    descriptor_bytes: 4 * 1024 * 1024,
    message_bytes: 1024 * 1024,
    depth: 64,
    items: 100000,
    fields: 10000,
    int64_as_string: false,
    protojson: false,
    emit_defaults: false,
  }
}

///|
enum ProtoValue {
  PVarint(UInt64)
  PFixed32(UInt)
  PFixed64(UInt64)
  PBytes(Bytes)
  PStart
  PEnd
}

///|
struct ProtoItem {
  number : Int
  value : ProtoValue
}

///|
struct ProtoWire {
  data : Bytes
  budget : Array[Int]
  mut position : Int
}

///|
fn proto_wire(bytes : Bytes, budget : Array[Int]) -> ProtoWire {
  { data: bytes, budget, position: 0, }
}

///|
fn ProtoWire::take(self : ProtoWire, length : Int) -> Bytes raise McapError {
  if length < 0 || length > self.data.length() - self.position {
    raise Invalid("truncated protobuf value")
  }
  let bytes = self.data[self.position:self.position + length].to_owned()
  self.position += length
  bytes
}

///|
fn ProtoWire::varint(self : ProtoWire) -> UInt64 raise McapError {
  let mut value = 0UL
  for i = 0; i < 10; i = i + 1 {
    let byte = self.take(1)[0]
    if i == 9 && byte > 0x01 {
      raise Invalid("protobuf varint exceeds UInt64")
    }
    value = value | ((byte & 0x7f).to_uint64() << (i * 7))
    if (byte & 0x80) == 0x00 {
      return value
    }
  }
  raise Invalid("unterminated protobuf varint")
}

///|
fn ProtoWire::next(self : ProtoWire) -> ProtoItem? raise McapError {
  if self.position == self.data.length() {
    return None
  }
  if self.budget[0] <= 0 {
    raise Invalid("protobuf item budget exceeded")
  }
  self.budget[0] -= 1
  let key = self.varint()
  if key >> 3 == 0UL || key >> 3 > 536870911UL {
    raise Invalid("invalid protobuf field number")
  }
  let value = match (key & 7UL).to_int() {
    0 => PVarint(self.varint())
    1 => PFixed64(cursor(self.take(8), default_limits()).u64())
    2 =>
      PBytes(
        self.take(
          bounded_length(
            self.varint(),
            self.data.length() - self.position,
            "protobuf value",
          ),
        ),
      )
    3 => PStart
    4 => PEnd
    5 => PFixed32(cursor(self.take(4), default_limits()).u32())
    _ => raise Invalid("invalid protobuf wire type")
  }
  Some({ number: (key >> 3).to_int(), value, })
}

///|
fn ProtoWire::skip(
  self : ProtoWire,
  item : ProtoItem,
  depth : Int,
) -> Unit raise McapError {
  match item.value {
    PEnd => raise Invalid("unexpected protobuf end group")
    PStart => {
      if depth <= 0 {
        raise Invalid("protobuf group depth budget exceeded")
      }
      while self.next() is Some(child) {
        if child.value is PEnd {
          if child.number != item.number {
            raise Invalid("protobuf end group mismatch")
          }
          return
        }
        self.skip(child, depth - 1)
      }
      raise Invalid("unterminated protobuf group")
    }
    _ => ()
  }
}

///|
fn ProtoItem::bytes(self : ProtoItem) -> Bytes raise McapError {
  match self.value {
    PBytes(bytes) => bytes
    _ => raise Invalid("protobuf descriptor wire mismatch")
  }
}

///|
fn ProtoItem::text(self : ProtoItem) -> String raise McapError {
  @utf8.decode(self.bytes()[:]) catch {
    _ => raise Invalid("invalid protobuf UTF-8")
  }
}

///|
fn ProtoItem::number(self : ProtoItem) -> Int raise McapError {
  match self.value {
    PVarint(value) => value.to_uint().to_int()
    _ => raise Invalid("protobuf descriptor number mismatch")
  }
}