///|
fn protobuf_float(value : Double) -> Json {
  if value.is_nan() {
    Json::string("NaN")
  } else if value.is_inf() {
    Json::string(if value < 0 { "-Infinity" } else { "Infinity" })
  } else {
    Json::number(value)
  }
}

///|
fn ProtobufDecoder::integer64(
  self : ProtobufDecoder,
  text : String,
  number : Double,
) -> Json {
  if self.options.int64_as_string || self.options.protojson {
    Json::string(text)
  } else {
    Json::number(number, repr=text)
  }
}

///|
fn ProtobufDecoder::scalar(
  self : ProtobufDecoder,
  field : ProtoField,
  value : ProtoValue,
) -> Json raise McapError {
  match (field.kind, value) {
    (1, PFixed64(value)) => protobuf_float(value.reinterpret_as_double())
    (2, PFixed32(value)) => {
      let number = Float::reinterpret_from_uint(value)
      let converted = number.to_double()
      if converted.is_inf() || converted.is_nan() {
        protobuf_float(converted)
      } else {
        Json::number(converted, repr=number.to_string())
      }
    }
    (3, PVarint(value)) | (16, PFixed64(value)) =>
      self.integer64(value.to_int64().to_string(), value.to_int64().to_double())
    (4, PVarint(value)) | (6, PFixed64(value)) =>
      self.integer64(value.to_string(), value.to_double())
    (5, PVarint(value)) => Json::number(value.to_uint().to_int().to_double())
    (7, PFixed32(value)) => Json::number(value.to_double())
    (8, PVarint(value)) => Json::boolean(value != 0UL)
    (9, PBytes(bytes)) =>
      Json::string(
        @utf8.decode(bytes[:]) catch {
          _ => raise Invalid("invalid protobuf string UTF-8")
        },
      )
    (12, PBytes(bytes)) => Json::string(@base64.encode(bytes[:]))
    (13, PVarint(value)) => Json::number(value.to_uint().to_double())
    (14, PVarint(_)) if self.options.protojson &&
      field.type_name == "google.protobuf.NullValue" => Json::null()
    (14, PVarint(value)) =>
      match self.enums[field.type_name].values.get(value.to_uint().to_int()) {
        Some(name) => Json::string(name)
        None => Json::number(value.to_uint().to_int().to_double())
      }
    (15, PFixed32(value)) => Json::number(value.to_int().to_double())
    (17, PVarint(value)) => {
      let number = value.to_uint()
      Json::number(((number >> 1) ^ (0U - (number & 1U))).to_int().to_double())
    }
    (18, PVarint(value)) => {
      let decoded = ((value >> 1) ^ (0UL - (value & 1UL))).to_int64()
      self.integer64(decoded.to_string(), decoded.to_double())
    }
    _ => raise Invalid("protobuf field/wire type mismatch")
  }
}

///|
fn ProtobufDecoder::default_value(
  self : ProtobufDecoder,
  field : ProtoField,
) -> Json raise McapError {
  match field.kind {
    9 | 12 => Json::string("")
    3 | 4 | 6 | 16 | 18 => self.integer64("0", 0)
    8 => Json::boolean(false)
    11 => Json::object(Map([]))
    14 if self.options.protojson &&
      field.type_name == "google.protobuf.NullValue" => Json::null()
    14 =>
      Json::string(
        self.enums[field.type_name].values[self.enums[field.type_name].first],
      )
    _ => Json::number(0)
  }
}

///|
fn protobuf_map_key(value : Json) -> String raise McapError {
  match value {
    String(value) => value
    Number(_, ..) => value.stringify()
    True => "true"
    False => "false"
    _ => raise Invalid("invalid protobuf map key")
  }
}

///|
fn protobuf_packed(kind : Int, wire : ProtoWire) -> ProtoValue raise McapError {
  if wire.budget[0] <= 0 {
    raise Invalid("protobuf packed item budget exceeded")
  }
  wire.budget[0] -= 1
  match kind {
    1 | 6 | 16 => PFixed64(cursor(wire.take(8), default_limits()).u64())
    2 | 7 | 15 => PFixed32(cursor(wire.take(4), default_limits()).u32())
    _ => PVarint(wire.varint())
  }
}

///|
fn ProtobufDecoder::message(
  self : ProtobufDecoder,
  name : String,
  bytes : Bytes,
  budget : Array[Int],
  depth : Int,
  keep_fields? : Array[String],
) -> Json raise McapError {
  if depth > self.options.depth {
    raise Invalid("protobuf message depth budget exceeded")
  }
  let model = match self.messages.get(name) {
    Some(model) => model
    None => raise Invalid("unknown protobuf message type: \{name}")
  }
  let wire = proto_wire(bytes, budget)
  let values : Map[Int, Array[ProtoValue]] = Map([])
  let oneofs : Map[Int, Int] = Map([])
  let present : Map[Int, Bool] = Map([])
  while wire.next() is Some(item) {
    guard model.fields.get(item.number) is Some(field) else {
      wire.skip(item, self.options.depth - depth)
      continue
    }
    present[field.number] = true
    if field.oneof is Some(id) {
      if oneofs.get(id) is Some(old) && old != field.number {
        values.remove(old)
      }
      oneofs[id] = field.number
    }
    if keep_fields is Some(names) && !names.contains(field.json_name) {
      wire.skip(item, self.options.depth - depth)
      continue
    }
    if field.label == 3 &&
      ![9, 11, 12].contains(field.kind) &&
      item.value is PBytes(data) {
      let packed = proto_wire(data, budget)
      let list = values.get_or_init(field.number, fn() { [] })
      while packed.position < data.length() {
        list.push(protobuf_packed(field.kind, packed))
      }
    } else if field.label == 3 || field.kind == 11 {
      values.get_or_init(field.number, fn() { [] }).push(item.value)
    } else {
      values[field.number] = [item.value]
    }
  }
  let result : Map[String, Json] = Map([])
  for field in model.fields.values() {
    if keep_fields is Some(names) && !names.contains(field.json_name) {
      if field.label == 2 && !present.contains(field.number) {
        raise Invalid("required protobuf field missing: \{field.name}")
      }
      continue
    }
    guard values.get(field.number) is Some(items) else {
      if field.label == 2 {
        raise Invalid("required protobuf field missing: \{field.name}")
      }
      if model.map_entry {
        result[field.json_name] = if field.kind == 11 {
          self.message(field.type_name, b"", budget, depth + 1)
        } else {
          self.default_value(field)
        }
      } else if self.options.protojson &&
        self.options.emit_defaults &&
        (
          field.label == 3 ||
          (
            model.proto3 &&
            field.kind != 11 &&
            field.oneof is None &&
            !field.optional
          )
        ) {
        result[field.json_name] = if field.label == 3 {
          if field.kind == 11 && self.messages[field.type_name].map_entry {
            Json::object(Map([]))
          } else {
            Json::array([])
          }
        } else {
          self.default_value(field)
        }
      }
      continue
    }
    let value = if field.kind == 11 {
      let target = self.messages[field.type_name]
      if field.label == 3 && target.map_entry {
        let entries : Map[String, Json] = Map([])
        for item in items {
          guard item is PBytes(data) else {
            raise Invalid("protobuf map requires byte entries")
          }
          let decoded = self.message(field.type_name, data, budget, depth + 1)
          guard decoded is Object(fields) else {
            raise Invalid("protobuf map entry did not decode to object")
          }
          entries[protobuf_map_key(fields[target.fields[1].json_name])] = fields[target.fields[2].json_name]
        }
        Json::object(entries)
      } else if field.label == 3 {
        let list = []
        for item in items {
          guard item is PBytes(data) else {
            raise Invalid("protobuf nested message wire mismatch")
          }
          list.push(self.message(field.type_name, data, budget, depth + 1))
        }
        Json::array(list)
      } else {
        let payload = @buffer.Buffer::Buffer()
        for item in items {
          guard item is PBytes(data) else {
            raise Invalid("protobuf nested message wire mismatch")
          }
          if data.length() > self.options.message_bytes - payload.length() {
            raise Invalid("protobuf merged message exceeds byte budget")
          }
          payload.write_bytes(data[:])
        }
        self.message(field.type_name, payload.to_bytes(), budget, depth + 1)
      }
    } else if field.label == 3 {
      Json::array(items.map(fn(item) { self.scalar(field, item) }))
    } else {
      self.scalar(field, items[items.length() - 1])
    }
    let empty_repeated = field.label == 3 &&
      value is Array(items) &&
      items.is_empty()
    if self.options.protojson && !self.options.emit_defaults && empty_repeated {
      continue
    }
    if !self.options.protojson ||
      self.options.emit_defaults ||
      !model.proto3 ||
      model.map_entry ||
      field.label == 3 ||
      field.kind == 11 ||
      field.oneof is Some(_) ||
      field.optional ||
      value != self.default_value(field) {
      result[field.json_name] = value
    }
  }
  if self.options.protojson {
    self.well_known(name, result, budget, depth)
  } else {
    Json::object(result)
  }
}

///|
pub fn ProtobufDecoder::decode(
  self : ProtobufDecoder,
  name : String,
  payload : Bytes,
) -> Json raise McapError {
  if payload.length() > self.options.message_bytes {
    raise Invalid("protobuf message byte budget exceeded")
  }
  let name = if name.has_prefix(".") { name[1:].to_string() } else { name }
  self.message(name, payload, [self.options.items], 1)
}

///|
/// Skips materializing unrequested top-level fields. Selected nested messages
/// retain normal merge/map/oneof semantics before the final nested projection.
pub fn ProtobufDecoder::decode_selected(
  self : ProtobufDecoder,
  name : String,
  payload : Bytes,
  selection : DecodeSelection,
) -> Json raise McapError {
  let paths = selection_paths(selection)
  if payload.length() > self.options.message_bytes {
    raise Invalid("protobuf message byte budget exceeded")
  }
  if self.options.protojson ||
    paths.is_empty() ||
    paths.any(fn(p) { p.is_empty() }) {
    return selected_json(self.decode(name, payload), paths, [])
  }
  let names = []
  for p in paths {
    if !names.contains(p[0]) {
      names.push(p[0])
    }
  }
  let name = if name.has_prefix(".") { name[1:].to_string() } else { name }
  selected_json(
    self.message(name, payload, [selection.scan_items], 1, keep_fields=names),
    paths,
    [],
  )
}