// A descriptor model for gRPC services and their messages, and the codec that
// turns it into the `FileDescriptorProto` / `FileDescriptorSet` wire bytes of
// `google/protobuf/descriptor.proto`. This is what Server Reflection hands a
// client so `grpcurl` can `list` and `describe` a service: reflection returns the
// serialised `FileDescriptorProto` of the file that defines a symbol. The model
// carries packages, imports, messages with their nested messages, enums, oneofs
// and map entries, and services with their methods; options other than
// `map_entry`, extensions, `json_name` and source info are skipped on decode, so
// a `protoc` set that uses them re-encodes to the modelled subset. Field numbers
// below are the ones fixed by descriptor.proto.

///|
/// The `FieldDescriptorProto.Type` enum (proto3's scalar and composite field
/// types). `TypeGroup` is intentionally absent — groups are removed from proto3.
pub(all) enum FieldType {
  TypeDouble
  TypeFloat
  TypeInt64
  TypeUint64
  TypeInt32
  TypeFixed64
  TypeFixed32
  TypeBool
  TypeString
  TypeMessage
  TypeBytes
  TypeUint32
  TypeEnum
  TypeSfixed32
  TypeSfixed64
  TypeSint32
  TypeSint64
} derive(Eq)

///|
/// The descriptor.proto enum number of a field type.
pub fn FieldType::code(self : FieldType) -> Int32 {
  match self {
    TypeDouble => 1
    TypeFloat => 2
    TypeInt64 => 3
    TypeUint64 => 4
    TypeInt32 => 5
    TypeFixed64 => 6
    TypeFixed32 => 7
    TypeBool => 8
    TypeString => 9
    TypeMessage => 11
    TypeBytes => 12
    TypeUint32 => 13
    TypeEnum => 14
    TypeSfixed32 => 15
    TypeSfixed64 => 16
    TypeSint32 => 17
    TypeSint64 => 18
  }
}

///|
/// The field type for a descriptor.proto enum number; an unrecognised number
/// (including the removed group type `10`) reads as `TypeMessage`.
pub fn FieldType::of(n : Int32) -> FieldType {
  match n {
    1 => TypeDouble
    2 => TypeFloat
    3 => TypeInt64
    4 => TypeUint64
    5 => TypeInt32
    6 => TypeFixed64
    7 => TypeFixed32
    8 => TypeBool
    9 => TypeString
    12 => TypeBytes
    13 => TypeUint32
    14 => TypeEnum
    15 => TypeSfixed32
    16 => TypeSfixed64
    17 => TypeSint32
    18 => TypeSint64
    _ => TypeMessage
  }
}

///|
/// The type's name in `.proto` source, for the scalars; `None` for a message or
/// an enum, which are written by name.
pub fn FieldType::keyword(self : FieldType) -> String? {
  match self {
    TypeDouble => Some("double")
    TypeFloat => Some("float")
    TypeInt64 => Some("int64")
    TypeUint64 => Some("uint64")
    TypeInt32 => Some("int32")
    TypeFixed64 => Some("fixed64")
    TypeFixed32 => Some("fixed32")
    TypeBool => Some("bool")
    TypeString => Some("string")
    TypeBytes => Some("bytes")
    TypeUint32 => Some("uint32")
    TypeSfixed32 => Some("sfixed32")
    TypeSfixed64 => Some("sfixed64")
    TypeSint32 => Some("sint32")
    TypeSint64 => Some("sint64")
    TypeMessage | TypeEnum => None
  }
}

///|
/// The `FieldDescriptorProto.Label`: proto3 fields are `LabelOptional` unless
/// `repeated`.
pub(all) enum FieldLabel {
  LabelOptional
  LabelRequired
  LabelRepeated
} derive(Eq)

///|
/// The descriptor.proto enum number of a label.
pub fn FieldLabel::code(self : FieldLabel) -> Int32 {
  match self {
    LabelOptional => 1
    LabelRequired => 2
    LabelRepeated => 3
  }
}

///|
/// The label for a descriptor.proto enum number; anything else reads as
/// `LabelOptional`.
pub fn FieldLabel::of(n : Int32) -> FieldLabel {
  match n {
    2 => LabelRequired
    3 => LabelRepeated
    _ => LabelOptional
  }
}

///|
/// One field of a message (`FieldDescriptorProto`). `type_name` is the
/// fully-qualified name of the referenced message or enum for `TypeMessage` /
/// `TypeEnum`, and empty for scalars. `oneof_index` places the field in one of
/// its message's `oneofs`; a proto3 `optional` field sits alone in a synthetic
/// one and sets `proto3_optional`.
pub(all) struct FieldDescriptor {
  name : String
  number : Int32
  label : FieldLabel
  type_ : FieldType
  type_name : String
  oneof_index : Int32?
  proto3_optional : Bool
} derive(Eq)

///|
pub fn FieldDescriptor::new(
  name : String,
  number : Int32,
  type_ : FieldType,
  label~ : FieldLabel = LabelOptional,
  type_name~ : String = "",
  oneof_index? : Int32,
  proto3_optional~ : Bool = false,
) -> FieldDescriptor {
  { name, number, label, type_, type_name, oneof_index, proto3_optional, }
}

///|
/// Encode a `FieldDescriptorProto`.
pub fn FieldDescriptor::encode(self : FieldDescriptor) -> Bytes {
  let w = PbWriter::new()
  w.string_(1, self.name)
  w.int32(3, self.number)
  w.enum_(4, self.label.code())
  w.enum_(5, self.type_.code())
  if self.type_name.length() > 0 {
    w.string_(6, self.type_name)
  }
  if self.oneof_index is Some(i) {
    w.int32(9, i)
  }
  if self.proto3_optional {
    w.bool_(17, true)
  }
  w.to_bytes()
}

///|
/// Decode a `FieldDescriptorProto`.
pub fn FieldDescriptor::decode(body : Bytes) -> FieldDescriptor raise PbError {
  let r = PbReader::new(body)
  let mut name = ""
  let mut number = 0
  let mut label = LabelOptional
  let mut type_ = TypeMessage
  let mut type_name = ""
  let mut oneof_index = None
  let mut proto3_optional = false
  while !r.eof() {
    match r.read_tag() {
      (1, LengthDelim) => name = r.read_string()
      (3, Varint) => number = r.read_int32()
      (4, Varint) => label = FieldLabel::of(r.read_int32())
      (5, Varint) => type_ = FieldType::of(r.read_int32())
      (6, LengthDelim) => type_name = r.read_string()
      (9, Varint) => oneof_index = Some(r.read_int32())
      (17, Varint) => proto3_optional = r.read_bool()
      (_, w) => r.skip(w)
    }
  }
  { name, number, label, type_, type_name, oneof_index, proto3_optional, }
}

///|
/// One value of an enum (`EnumValueDescriptorProto`).
pub(all) struct EnumValueDescriptor {
  name : String
  number : Int32
} derive(Eq)

///|
/// Encode an `EnumValueDescriptorProto`.
pub fn EnumValueDescriptor::encode(self : EnumValueDescriptor) -> Bytes {
  let w = PbWriter::new()
  w.string_(1, self.name)
  w.int32(2, self.number)
  w.to_bytes()
}

///|
/// Decode an `EnumValueDescriptorProto`.
pub fn EnumValueDescriptor::decode(
  body : Bytes,
) -> EnumValueDescriptor raise PbError {
  let r = PbReader::new(body)
  let mut name = ""
  let mut number = 0
  while !r.eof() {
    match r.read_tag() {
      (1, LengthDelim) => name = r.read_string()
      (2, Varint) => number = r.read_int32()
      (_, w) => r.skip(w)
    }
  }
  { name, number, }
}

///|
/// An enum type (`EnumDescriptorProto`): its simple name and its values.
pub(all) struct EnumDescriptor {
  name : String
  values : Array[EnumValueDescriptor]
} derive(Eq)

///|
/// Encode an `EnumDescriptorProto`.
pub fn EnumDescriptor::encode(self : EnumDescriptor) -> Bytes {
  let w = PbWriter::new()
  w.string_(1, self.name)
  for v in self.values {
    w.message_(2, v.encode())
  }
  w.to_bytes()
}

///|
/// Decode an `EnumDescriptorProto`.
pub fn EnumDescriptor::decode(body : Bytes) -> EnumDescriptor raise PbError {
  let r = PbReader::new(body)
  let mut name = ""
  let values = []
  while !r.eof() {
    match r.read_tag() {
      (1, LengthDelim) => name = r.read_string()
      (2, LengthDelim) =>
        values.push(EnumValueDescriptor::decode(r.read_bytes()))
      (_, w) => r.skip(w)
    }
  }
  { name, values, }
}

///|
/// A message type (`DescriptorProto`): its simple name, its fields, the
/// messages and enums declared inside it, the names of its oneofs, and whether
/// it is the entry type protoc makes for a `map` field.
pub(all) struct MessageDescriptor {
  name : String
  fields : Array[FieldDescriptor]
  nested : Array[MessageDescriptor]
  enums : Array[EnumDescriptor]
  oneofs : Array[String]
  map_entry : Bool
} derive(Eq)

///|
pub fn MessageDescriptor::new(
  name : String,
  fields : Array[FieldDescriptor],
  nested~ : Array[MessageDescriptor] = [],
  enums~ : Array[EnumDescriptor] = [],
  oneofs~ : Array[String] = [],
  map_entry~ : Bool = false,
) -> MessageDescriptor {
  { name, fields, nested, enums, oneofs, map_entry, }
}

///|
/// Encode a `DescriptorProto`.
pub fn MessageDescriptor::encode(self : MessageDescriptor) -> Bytes {
  let w = PbWriter::new()
  w.string_(1, self.name)
  for f in self.fields {
    w.message_(2, f.encode())
  }
  for m in self.nested {
    w.message_(3, m.encode())
  }
  for e in self.enums {
    w.message_(4, e.encode())
  }
  if self.map_entry {
    let options = PbWriter::new()
    options.bool_(7, true)
    w.message_(7, options.to_bytes())
  }
  for o in self.oneofs {
    let oneof = PbWriter::new()
    oneof.string_(1, o)
    w.message_(8, oneof.to_bytes())
  }
  w.to_bytes()
}

///|
/// Decode a `DescriptorProto`.
pub fn MessageDescriptor::decode(
  body : Bytes,
) -> MessageDescriptor raise PbError {
  let r = PbReader::new(body)
  let mut name = ""
  let fields = []
  let nested = []
  let enums = []
  let oneofs = []
  let mut map_entry = false
  while !r.eof() {
    match r.read_tag() {
      (1, LengthDelim) => name = r.read_string()
      (2, LengthDelim) => fields.push(FieldDescriptor::decode(r.read_bytes()))
      (3, LengthDelim) => nested.push(MessageDescriptor::decode(r.read_bytes()))
      (4, LengthDelim) => enums.push(EnumDescriptor::decode(r.read_bytes()))
      (7, LengthDelim) => {
        let options = PbReader::new(r.read_bytes())
        while !options.eof() {
          match options.read_tag() {
            (7, Varint) => map_entry = options.read_bool()
            (_, w) => options.skip(w)
          }
        }
      }
      (8, LengthDelim) => {
        let oneof = PbReader::new(r.read_bytes())
        let mut oneof_name = ""
        while !oneof.eof() {
          match oneof.read_tag() {
            (1, LengthDelim) => oneof_name = oneof.read_string()
            (_, w) => oneof.skip(w)
          }
        }
        oneofs.push(oneof_name)
      }
      (_, w) => r.skip(w)
    }
  }
  { name, fields, nested, enums, oneofs, map_entry, }
}

///|
/// One RPC method (`MethodDescriptorProto`): its name, the fully-qualified request
/// and response message names, and the two streaming flags that together pick the
/// call cardinality.
pub(all) struct MethodDescriptor {
  name : String
  input_type : String
  output_type : String
  client_streaming : Bool
  server_streaming : Bool
} derive(Eq)

///|
/// Encode a `MethodDescriptorProto`.
pub fn MethodDescriptor::encode(self : MethodDescriptor) -> Bytes {
  let w = PbWriter::new()
  w.string_(1, self.name)
  w.string_(2, self.input_type)
  w.string_(3, self.output_type)
  if self.client_streaming {
    w.bool_(5, true)
  }
  if self.server_streaming {
    w.bool_(6, true)
  }
  w.to_bytes()
}

///|
/// Decode a `MethodDescriptorProto`.
pub fn MethodDescriptor::decode(body : Bytes) -> MethodDescriptor raise PbError {
  let r = PbReader::new(body)
  let mut name = ""
  let mut input_type = ""
  let mut output_type = ""
  let mut client_streaming = false
  let mut server_streaming = false
  while !r.eof() {
    match r.read_tag() {
      (1, LengthDelim) => name = r.read_string()
      (2, LengthDelim) => input_type = r.read_string()
      (3, LengthDelim) => output_type = r.read_string()
      (5, Varint) => client_streaming = r.read_bool()
      (6, Varint) => server_streaming = r.read_bool()
      (_, w) => r.skip(w)
    }
  }
  { name, input_type, output_type, client_streaming, server_streaming, }
}

///|
/// A service (`ServiceDescriptorProto`): its (simple) name and its methods.
pub(all) struct ServiceDescriptor {
  name : String
  methods : Array[MethodDescriptor]
} derive(Eq)

///|
/// Encode a `ServiceDescriptorProto`.
pub fn ServiceDescriptor::encode(self : ServiceDescriptor) -> Bytes {
  let w = PbWriter::new()
  w.string_(1, self.name)
  for m in self.methods {
    w.message_(2, m.encode())
  }
  w.to_bytes()
}

///|
/// Decode a `ServiceDescriptorProto`.
pub fn ServiceDescriptor::decode(
  body : Bytes,
) -> ServiceDescriptor raise PbError {
  let r = PbReader::new(body)
  let mut name = ""
  let methods : Array[MethodDescriptor] = []
  while !r.eof() {
    match r.read_tag() {
      (1, LengthDelim) => name = r.read_string()
      (2, LengthDelim) => methods.push(MethodDescriptor::decode(r.read_bytes()))
      (_, w) => r.skip(w)
    }
  }
  { name, methods, }
}

///|
/// A single `.proto` file (`FileDescriptorProto`): its filename, package, the
/// files it imports, the messages, enums and services it defines, and the
/// syntax level. This is the unit Server Reflection returns.
pub(all) struct FileDescriptor {
  name : String
  package_ : String
  dependencies : Array[String]
  messages : Array[MessageDescriptor]
  enums : Array[EnumDescriptor]
  services : Array[ServiceDescriptor]
  syntax : String
} derive(Eq)

///|
/// A proto3 file with the given filename and package.
pub fn FileDescriptor::new(
  name : String,
  package_ : String,
  dependencies~ : Array[String] = [],
  messages~ : Array[MessageDescriptor] = [],
  enums~ : Array[EnumDescriptor] = [],
  services~ : Array[ServiceDescriptor] = [],
) -> FileDescriptor {
  { name, package_, dependencies, messages, enums, services, syntax: "proto3", }
}

///|
/// Encode a `FileDescriptorProto`.
pub fn FileDescriptor::encode(self : FileDescriptor) -> Bytes {
  let w = PbWriter::new()
  w.string_(1, self.name)
  if self.package_.length() > 0 {
    w.string_(2, self.package_)
  }
  for d in self.dependencies {
    w.string_(3, d)
  }
  for m in self.messages {
    w.message_(4, m.encode())
  }
  for e in self.enums {
    w.message_(5, e.encode())
  }
  for s in self.services {
    w.message_(6, s.encode())
  }
  if self.syntax.length() > 0 {
    w.string_(12, self.syntax)
  }
  w.to_bytes()
}

///|
/// Decode a `FileDescriptorProto`.
pub fn FileDescriptor::decode(body : Bytes) -> FileDescriptor raise PbError {
  let r = PbReader::new(body)
  let mut name = ""
  let mut package_ = ""
  let mut syntax = ""
  let dependencies = []
  let messages = []
  let enums = []
  let services = []
  while !r.eof() {
    match r.read_tag() {
      (1, LengthDelim) => name = r.read_string()
      (2, LengthDelim) => package_ = r.read_string()
      (3, LengthDelim) => dependencies.push(r.read_string())
      (4, LengthDelim) =>
        messages.push(MessageDescriptor::decode(r.read_bytes()))
      (5, LengthDelim) => enums.push(EnumDescriptor::decode(r.read_bytes()))
      (6, LengthDelim) =>
        services.push(ServiceDescriptor::decode(r.read_bytes()))
      (12, LengthDelim) => syntax = r.read_string()
      (_, w) => r.skip(w)
    }
  }
  { name, package_, dependencies, messages, enums, services, syntax, }
}

///|
fn FileDescriptor::prefix(self : FileDescriptor) -> String {
  if self.package_.length() > 0 {
    self.package_ + "."
  } else {
    ""
  }
}

///|
/// The fully-qualified names this file defines: each service and its methods
/// (`package.Service.Method`), each message with the messages and enums inside
/// it, and each enum. These are the symbols a `FileContainingSymbol` reflection
/// request can resolve to this file.
pub fn FileDescriptor::symbols(self : FileDescriptor) -> Array[String] {
  let out = []
  let prefix = self.prefix()
  for s in self.services {
    out.push(prefix + s.name)
    for m in s.methods {
      out.push(prefix + s.name + "." + m.name)
    }
  }
  fn walk(scope : String, m : MessageDescriptor) {
    let name = scope + m.name
    out.push(name)
    for e in m.enums {
      out.push(name + "." + e.name)
    }
    for n in m.nested {
      walk(name + ".", n)
    }
  }

  for m in self.messages {
    walk(prefix, m)
  }
  for e in self.enums {
    out.push(prefix + e.name)
  }
  out
}

///|
/// The fully-qualified names of the services this file defines (`package.Service`).
pub fn FileDescriptor::service_names(self : FileDescriptor) -> Array[String] {
  let prefix = self.prefix()
  self.services.map(s => prefix + s.name)
}

///|
/// The `.proto` source this file describes. Types are written by their
/// fully-qualified names, `map` fields as `map` with their entry types
/// left implicit, and proto3 `optional` fields with the keyword instead of
/// their synthetic oneofs.
pub fn FileDescriptor::source(self : FileDescriptor) -> String {
  let out = StringBuilder()
  let proto3 = self.syntax == "proto3"
  out.write_string(
    "syntax = \"\{if self.syntax == "" { "proto2" } else { self.syntax }}\";\n",
  )
  if self.package_.length() > 0 {
    out.write_string("\npackage \{self.package_};\n")
  }
  if !self.dependencies.is_empty() {
    out.write_string("\n")
    for d in self.dependencies {
      out.write_string("import \"\{d}\";\n")
    }
  }
  for m in self.messages {
    out.write_string("\n")
    write_message(out, m, "", proto3)
  }
  for e in self.enums {
    out.write_string("\n")
    write_enum(out, e, "")
  }
  for s in self.services {
    out.write_string("\nservice \{s.name} {\n")
    for m in s.methods {
      let input = (if m.client_streaming { "stream " } else { "" }) +
        m.input_type
      let output = (if m.server_streaming { "stream " } else { "" }) +
        m.output_type
      out.write_string("  rpc \{m.name}(\{input}) returns (\{output});\n")
    }
    out.write_string("}\n")
  }
  out.to_string()
}

///|
fn write_enum(out : StringBuilder, e : EnumDescriptor, indent : String) -> Unit {
  out.write_string("\{indent}enum \{e.name} {\n")
  for v in e.values {
    out.write_string("\{indent}  \{v.name} = \{v.number};\n")
  }
  out.write_string("\{indent}}\n")
}

///|
fn type_of(f : FieldDescriptor) -> String {
  f.type_.keyword().unwrap_or(f.type_name)
}

///|
fn write_message(
  out : StringBuilder,
  m : MessageDescriptor,
  indent : String,
  proto3 : Bool,
) -> Unit {
  out.write_string("\{indent}message \{m.name} {\n")
  let inner = indent + "  "
  let entries : Map[String, MessageDescriptor] = Map([])
  for n in m.nested {
    if n.map_entry {
      entries[n.name] = n
    }
  }
  let written : Map[Int32, Unit] = Map([])
  for f in m.fields {
    let entry = if f.label is LabelRepeated && f.type_ is TypeMessage {
      let short = match f.type_name.rev_find(".") {
        Some(i) => f.type_name.view(start_offset=i + 1).to_owned()
        None => f.type_name
      }
      entries.get(short)
    } else {
      None
    }
    match (entry, f.oneof_index) {
      (Some(e), _) => {
        let key = e.fields.search_by(x => x.number == 1).map(i => e.fields[i])
        let value = e.fields.search_by(x => x.number == 2).map(i => e.fields[i])
        guard key is Some(k) && value is Some(v) else { continue }
        out.write_string(
          "\{inner}map<\{type_of(k)}, \{type_of(v)}> \{f.name} = \{f.number};\n",
        )
      }
      (None, Some(i)) if !f.proto3_optional => {
        if written.contains(i) {
          continue
        }
        written[i] = ()
        let name = m.oneofs.get(i).unwrap_or("oneof_\{i}")
        out.write_string("\{inner}oneof \{name} {\n")
        for g in m.fields {
          if g.oneof_index == Some(i) {
            out.write_string(
              "\{inner}  \{type_of(g)} \{g.name} = \{g.number};\n",
            )
          }
        }
        out.write_string("\{inner}}\n")
      }
      _ => {
        let label = match f.label {
          LabelRepeated => "repeated "
          LabelRequired => "required "
          LabelOptional if f.proto3_optional || !proto3 => "optional "
          LabelOptional => ""
        }
        out.write_string(
          "\{inner}\{label}\{type_of(f)} \{f.name} = \{f.number};\n",
        )
      }
    }
  }
  for n in m.nested {
    if !n.map_entry {
      write_message(out, n, inner, proto3)
    }
  }
  for e in m.enums {
    write_enum(out, e, inner)
  }
  out.write_string("\{indent}}\n")
}

///|
/// Encode a `FileDescriptorSet` (`protoc --descriptor_set_out`): the concatenation
/// of `FileDescriptorProto`s under repeated field 1.
pub fn encode_file_descriptor_set(files : Array[FileDescriptor]) -> Bytes {
  let w = PbWriter::new()
  for f in files {
    w.message_(1, f.encode())
  }
  w.to_bytes()
}

///|
/// Decode a `FileDescriptorSet` into its files.
pub fn decode_file_descriptor_set(
  body : Bytes,
) -> Array[FileDescriptor] raise PbError {
  let r = PbReader::new(body)
  let files : Array[FileDescriptor] = []
  while !r.eof() {
    match r.read_tag() {
      (1, LengthDelim) => files.push(FileDescriptor::decode(r.read_bytes()))
      (_, w) => r.skip(w)
    }
  }
  files
}

///|
pub extend EnumDescriptor with Eq::{not_equal, equal}

///|
pub extend EnumValueDescriptor with Eq::{not_equal, equal}

///|
pub extend FieldDescriptor with Eq::{not_equal, equal}

///|
pub extend FieldLabel with Eq::{not_equal, equal}

///|
pub extend FieldType with Eq::{not_equal, equal}

///|
pub extend FileDescriptor with Eq::{not_equal, equal}

///|
pub extend MessageDescriptor with Eq::{not_equal, equal}

///|
pub extend MethodDescriptor with Eq::{not_equal, equal}

///|
pub extend ServiceDescriptor with Eq::{not_equal, equal}