// `.proto` source in, descriptors out. The parsing is `moonbitstack/moonproto`'s — a language
// with editions, groups, reserved ranges and custom options is not something to write twice —
// and what happens here is the conversion into the descriptor model the rest of this library
// carries, plus the name resolution a parser cannot do on its own: a field writes the type it
// holds as a name relative to where it stands, and only the set of all declared names says which
// one that is.
//
// Every name in the model is absolute — written with the leading dot — so that a lookup never
// has to ask which convention the writer of a descriptor used.

///|
/// The files `sources` describe, one per entry: the name the file is known by — which is what an
/// `import` in another file writes — and its text. Order does not matter: a name resolves against
/// every file given, not against the ones already read.
pub fn compile_proto(
  sources : Array[(String, String)],
) -> Array[FileDescriptor] raise PbError {
  let parsed = []
  for source in sources {
    let proto = @mpparser.parse(source.0, source.1) catch {
      e => raise BadSchema(e.to_string())
    }
    parsed.push(proto)
  }
  // Every name any file declares, which is what a relative reference is resolved against, and
  // the ones that name an enum — a field that holds a named type says only which name, and
  // whether that name is an enum is what decides how its value is written.
  let symbols = []
  let enums = []
  for proto in parsed {
    let prefix = proto_prefix(proto.package_name)
    for message in proto.message_type {
      proto_collect_symbols(prefix, message.name, message, symbols, enums)
    }
    for e in proto.enum_type {
      symbols.push(prefix + "." + e.name)
      enums.push(prefix + "." + e.name)
    }
  }
  let files = []
  for proto in parsed {
    files.push(proto_file_of(proto, symbols, enums))
  }
  files
}

///|
/// The scope a file's declarations stand in, with the leading dot, or the empty string for a
/// file with no package.
fn proto_prefix(package_name : String) -> String {
  if package_name.length() > 0 {
    "." + package_name
  } else {
    ""
  }
}

///|
fn proto_collect_symbols(
  prefix : String,
  name : String,
  message : @mpdesc.DescriptorProto,
  out : Array[String],
  enums : Array[String],
) -> Unit {
  let full = prefix + "." + name
  out.push(full)
  for nested in message.nested_type {
    proto_collect_symbols(full, nested.name, nested, out, enums)
  }
  for e in message.enum_type {
    out.push(full + "." + e.name)
    enums.push(full + "." + e.name)
  }
}

///|
/// One file, its type names resolved.
pub fn proto_file_of(
  proto : @mpdesc.FileDescriptorProto,
  symbols : Array[String],
  enum_names : Array[String],
) -> FileDescriptor {
  let prefix = proto_prefix(proto.package_name)
  let messages = []
  for message in proto.message_type {
    messages.push(
      proto_message_of(prefix, proto.package_name, message, symbols, enum_names),
    )
  }
  let enums = []
  for e in proto.enum_type {
    enums.push(proto_enum_of(e))
  }
  let services = []
  for s in proto.service {
    services.push(proto_service_of(s, prefix, proto.package_name, symbols))
  }
  {
    name: proto.name,
    package_: proto.package_name,
    dependencies: proto.dependency,
    messages,
    enums,
    services,
    syntax: if proto.syntax.length() > 0 {
      proto.syntax
    } else {
      "proto3"
    },
  }
}

///|
fn proto_message_of(
  prefix : String,
  package_ : String,
  proto : @mpdesc.DescriptorProto,
  symbols : Array[String],
  enum_names : Array[String],
) -> MessageDescriptor {
  let full = prefix + "." + proto.name
  let fields = []
  for f in proto.field {
    fields.push(proto_field_of(f, full, package_, symbols, enum_names))
  }
  let nested = []
  for inner in proto.nested_type {
    nested.push(proto_message_of(full, package_, inner, symbols, enum_names))
  }
  let enums = []
  for e in proto.enum_type {
    enums.push(proto_enum_of(e))
  }
  let oneofs = []
  for oneof in proto.oneof_decl {
    oneofs.push(oneof.name)
  }
  // A proto3 `optional` field is only marked present if it sits in a oneof of its own, which
  // protoc writes and a parser may leave out. If it is missing, it is added here: a descriptor
  // that says `proto3_optional` without a oneof is one a strict reader refuses.
  for i = 0; i < fields.length(); i = i + 1 {
    let f = fields[i]
    if f.proto3_optional && f.oneof_index is None {
      fields[i] = {
        name: f.name,
        number: f.number,
        label: f.label,
        type_: f.type_,
        type_name: f.type_name,
        oneof_index: Some(oneofs.length()),
        proto3_optional: true,
      }
      oneofs.push("_" + f.name)
    }
  }
  {
    name: proto.name,
    fields,
    nested,
    enums,
    oneofs,
    map_entry: proto.map_entry,
  }
}

///|
fn proto_enum_of(proto : @mpdesc.EnumDescriptorProto) -> EnumDescriptor {
  let values = []
  for v in proto.value {
    values.push({ name: v.name, number: v.number, })
  }
  { name: proto.name, values, }
}

///|
fn proto_service_of(
  proto : @mpdesc.ServiceDescriptorProto,
  scope : String,
  package_ : String,
  symbols : Array[String],
) -> ServiceDescriptor {
  let methods = []
  for m in proto.methods {
    methods.push({
      name: m.name,
      // A method names its request and response the same way a field names its type: relative
      // to the file it is declared in.
      input_type: proto_resolve(m.input_type, scope, package_, symbols),
      output_type: proto_resolve(m.output_type, scope, package_, symbols),
      client_streaming: m.client_streaming,
      server_streaming: m.server_streaming,
    })
  }
  { name: proto.name, methods, }
}

///|
fn proto_field_of(
  proto : @mpdesc.FieldDescriptorProto,
  scope : String,
  package_ : String,
  symbols : Array[String],
  enums : Array[String],
) -> FieldDescriptor {
  let type_name = match proto.type_name {
    Some(name) => proto_resolve(name, scope, package_, symbols)
    None => ""
  }
  // A field that holds a named type leaves `type` unset and says only the name; whether that
  // name is an enum decides how the value is written, so it has to be looked up here.
  let type_ = if type_name.length() > 0 && enums.contains(type_name) {
    // The parser reports every named type as a message, so a name that the file set says is an
    // enum is the only thing that tells the two apart — and an enum written as a message is a
    // value the JSON mapping drops on the floor.
    FieldType::TypeEnum
  } else if proto.type_code == 0 && type_name.length() > 0 {
    FieldType::TypeMessage
  } else {
    FieldType::of(proto.type_code)
  }
  {
    name: proto.name,
    number: proto.number,
    label: FieldLabel::of(proto.label),
    type_,
    type_name,
    oneof_index: proto.oneof_index,
    proto3_optional: proto.proto3_optional,
  }
}

///|
/// The absolute spelling of a name a field holds. A name that starts with a dot is absolute
/// already; anything else is tried against the scope it stands in and then against each enclosing
/// scope outward, which is how the language resolves a relative name.
fn proto_resolve(
  name : String,
  scope : String,
  package_ : String,
  symbols : Array[String],
) -> String {
  if name.length() == 0 {
    return ""
  }
  if name[0] == '.' {
    return name
  }
  let mut at = scope
  while true {
    let candidate = at + "." + name
    if symbols.contains(candidate) {
      return candidate
    }
    if !at.contains(".") {
      break
    }
    at = proto_parent(at)
  }
  // Nothing here declares it, so the file it names was not passed in: keep the name as written,
  // anchored to its own package rather than to the file that refers to it.
  if name.contains(".") || package_.length() == 0 {
    "." + name
  } else {
    "." + package_ + "." + name
  }
}

///|
/// The scope a name declared inside `name` stands in once you step out of it: `.a.b.C` is `.a.b`,
/// and `.a` is the root.
fn proto_parent(name : String) -> String {
  let mut cut = 0
  let mut at = 0
  for ch in name {
    if ch == '.' {
      cut = at
    }
    at = at + 1
  }
  name.exact_view(start=0, end=cut).to_owned()
}

///|
/// A schema compiled from `.proto` source.
pub fn Schema::compile(
  sources : Array[(String, String)],
) -> Schema raise PbError {
  Schema::of(compile_proto(sources))
}