///|
/// Options for emitting MoonBit source from a parsed proto file.
pub(all) struct CodegenOptions {
  emit_descriptor_functions : Bool
  emit_runtime_helpers : Bool
} derive(Debug, Eq)

///|
/// Default generator settings used by examples and tests.
pub fn CodegenOptions::default() -> CodegenOptions {
  CodegenOptions::{
    emit_descriptor_functions: true,
    emit_runtime_helpers: true,
  }
}

///|
fn join_strings(parts : Array[String], sep : String) -> String {
  let mut out = ""
  for i = 0; i < parts.length(); i = i + 1 {
    if i > 0 {
      out = out + sep
    }
    out = out + parts[i]
  }
  out
}

///|
fn moonbit_type_name(typ : ScalarType) -> String {
  match typ {
    DoubleType => "Double"
    FloatType => "Float"
    Int32Type => "Int"
    Int64Type => "Int64"
    UInt32Type => "UInt"
    UInt64Type => "UInt64"
    SInt32Type => "Int"
    SInt64Type => "Int64"
    Fixed32Type => "UInt"
    Fixed64Type => "UInt64"
    SFixed32Type => "Int"
    SFixed64Type => "Int64"
    BoolType => "Bool"
    StringType => "String"
    BytesType => "Bytes"
    EnumType(name) => name
    NamedType(name) => name
    MapType(_, _) => "MessageValue"
  }
}

///|
fn moonbit_field_type(field : FieldDescriptor) -> String {
  let base = moonbit_type_name(field.typ)
  match field.label {
    Repeated => "Array[" + base + "]"
    Optional => base + "?"
    Oneof(_) => base + "?"
    Singular => base
  }
}

///|
fn scalar_type_source(typ : ScalarType) -> String {
  match typ {
    DoubleType => "DoubleType"
    FloatType => "FloatType"
    Int32Type => "Int32Type"
    Int64Type => "Int64Type"
    UInt32Type => "UInt32Type"
    UInt64Type => "UInt64Type"
    SInt32Type => "SInt32Type"
    SInt64Type => "SInt64Type"
    Fixed32Type => "Fixed32Type"
    Fixed64Type => "Fixed64Type"
    SFixed32Type => "SFixed32Type"
    SFixed64Type => "SFixed64Type"
    BoolType => "BoolType"
    StringType => "StringType"
    BytesType => "BytesType"
    EnumType(name) => "EnumType(\"" + name + "\")"
    NamedType(name) => "NamedType(\"" + name + "\")"
    MapType(key_typ, value_typ) =>
      "MapType(" +
      scalar_type_source(key_typ) +
      ", " +
      scalar_type_source(value_typ) +
      ")"
  }
}

///|
fn field_label_source(label : FieldLabel) -> String {
  match label {
    Singular => "Singular"
    Optional => "Optional"
    Repeated => "Repeated"
    Oneof(group) => "Oneof(\"" + group + "\")"
  }
}

///|
/// Emit a MoonBit enum declaration for one enum descriptor.
pub fn generate_enum(desc : EnumDescriptor) -> String {
  let lines : Array[String] = []
  lines.push("pub(all) enum " + desc.name + " {")
  for value in desc.values {
    lines.push("  " + value.name)
  }
  lines.push("} derive(Debug, Eq)")
  join_strings(lines, "\n")
}

///|
/// Emit a descriptor function for one enum descriptor.
pub fn generate_enum_descriptor_function(desc : EnumDescriptor) -> String {
  let lines : Array[String] = []
  lines.push("pub fn descriptor_" + desc.name + "() -> EnumDescriptor {")
  lines.push("  EnumDescriptor::{")
  lines.push("    name : \"" + desc.name + "\",")
  lines.push("    allow_alias : " + desc.allow_alias.to_string() + ",")
  lines.push("    values : [")
  for value in desc.values {
    lines.push(
      "      EnumValueDescriptor::{ name : \"" +
      value.name +
      "\", number : " +
      value.number.to_string() +
      " },",
    )
  }
  lines.push("    ],")
  lines.push("  }")
  lines.push("}")
  join_strings(lines, "\n")
}

///|
/// Emit a MoonBit struct declaration for one message descriptor.
pub fn generate_message_struct(desc : MessageDescriptor) -> String {
  let lines : Array[String] = []
  lines.push("pub(all) struct " + desc.name + " {")
  for field in desc.fields {
    lines.push("  " + field.name + " : " + moonbit_field_type(field))
  }
  lines.push("} derive(Debug, Eq)")
  join_strings(lines, "\n")
}

///| Emit a descriptor function that can be compiled into generated MoonBit

///|
/// code and passed back to `encode_message` / `decode_message`.
pub fn generate_descriptor_function(desc : MessageDescriptor) -> String {
  let lines : Array[String] = []
  lines.push("pub fn descriptor_" + desc.name + "() -> MessageDescriptor {")
  lines.push("  MessageDescriptor::{")
  lines.push("    name : \"" + desc.name + "\",")
  lines.push("    fields : [")
  for field in desc.fields {
    lines.push(
      "      FieldDescriptor::{ name : \"" +
      field.name +
      "\", typ : " +
      scalar_type_source(field.typ) +
      ", number : " +
      field.number.to_string() +
      ", label : " +
      field_label_source(field.label) +
      " },",
    )
  }
  lines.push("    ],")
  lines.push("  }")
  lines.push("}")
  join_strings(lines, "\n")
}

///|
/// Emit a registry function used by generated runtime helpers to resolve
/// nested message descriptors.
pub fn generate_message_descriptor_registry(file : ProtoFile) -> String {
  let lines : Array[String] = []
  lines.push("pub fn message_descriptors() -> Array[MessageDescriptor] {")
  lines.push("  [")
  for msg in file.messages {
    lines.push("    descriptor_" + msg.name + "(),")
  }
  lines.push("  ]")
  lines.push("}")
  join_strings(lines, "\n")
}

///|
/// Emit a registry function used by generated JSON helpers to resolve enum
/// descriptors and render protobuf JSON enum names.
pub fn generate_enum_descriptor_registry(file : ProtoFile) -> String {
  let lines : Array[String] = []
  lines.push("pub fn enum_descriptors() -> Array[EnumDescriptor] {")
  lines.push("  [")
  for enum_desc in file.enums {
    lines.push("    descriptor_" + enum_desc.name + "(),")
  }
  lines.push("  ]")
  lines.push("}")
  join_strings(lines, "\n")
}

///|
/// Emit dynamic encode/decode/JSON helper functions for one generated message.
pub fn generate_message_runtime_helpers(desc : MessageDescriptor) -> String {
  let lines : Array[String] = []
  lines.push(
    "pub fn encode_" +
    desc.name +
    "(value : MessageValue) -> EncodeMessageResult {",
  )
  lines.push(
    "  encode_message_with_descriptors(descriptor_" +
    desc.name +
    "(), message_descriptors(), value)",
  )
  lines.push("}")
  lines.push("")
  lines.push(
    "pub fn decode_" + desc.name + "(input : Bytes) -> DecodeMessageResult {",
  )
  lines.push(
    "  decode_message_with_descriptors(descriptor_" +
    desc.name +
    "(), message_descriptors(), input)",
  )
  lines.push("}")
  lines.push("")
  lines.push(
    "pub fn to_json_" +
    desc.name +
    "(value : MessageValue) -> JsonEncodeResult {",
  )
  lines.push(
    "  message_to_json_with_schema(descriptor_" +
    desc.name +
    "(), message_descriptors(), enum_descriptors(), value)",
  )
  lines.push("}")
  lines.push("")
  lines.push(
    "pub fn to_json_lower_camel_" +
    desc.name +
    "(value : MessageValue) -> JsonEncodeResult {",
  )
  lines.push(
    "  message_to_json_lower_camel_with_schema(descriptor_" +
    desc.name +
    "(), message_descriptors(), enum_descriptors(), value)",
  )
  lines.push("}")
  lines.push("")
  lines.push(
    "pub fn from_json_" + desc.name + "(input : String) -> JsonDecodeResult {",
  )
  lines.push(
    "  json_to_message_with_schema(descriptor_" +
    desc.name +
    "(), message_descriptors(), enum_descriptors(), input)",
  )
  lines.push("}")
  join_strings(lines, "\n")
}

///|
/// Generate MoonBit declarations for all messages in a parsed proto file.
pub fn generate_moonbit_source(
  file : ProtoFile,
  options? : CodegenOptions = CodegenOptions::default(),
) -> String {
  let chunks : Array[String] = []
  for enum_desc in file.enums {
    chunks.push(generate_enum(enum_desc))
    if options.emit_descriptor_functions {
      chunks.push(generate_enum_descriptor_function(enum_desc))
    }
  }
  for msg in file.messages {
    chunks.push(generate_message_struct(msg))
    if options.emit_descriptor_functions {
      chunks.push(generate_descriptor_function(msg))
    }
  }
  if options.emit_descriptor_functions &&
    options.emit_runtime_helpers &&
    file.messages.length() > 0 {
    chunks.push(generate_message_descriptor_registry(file))
    chunks.push(generate_enum_descriptor_registry(file))
    for msg in file.messages {
      chunks.push(generate_message_runtime_helpers(msg))
    }
  }
  join_strings(chunks, "\n\n")
}