// .proto file parser for proto3 syntax.
// Supports: message, enum, service, rpc, oneof, map fields, nested types,
// block and line comments.

///|
/// Represents a protobuf field type.
pub enum FieldType {
  Int32
  Int64
  Uint32
  Uint64
  Sint32
  Sint64
  Fixed32
  Fixed64
  Sfixed32
  Sfixed64
  Float
  Double
  Bool
  String
  Bytes
  Map(FieldType, FieldType)
  Message(String)
  Enum(String)
} derive(Debug, Eq)

///|
/// Represents a field label.
pub enum FieldLabel {
  Optional
  Required
  Repeated
} derive(Debug, Eq)

///|
/// Represents a single field in a message.
pub struct ProtoField {
  name : String
  number : UInt
  field_type : FieldType
  label : FieldLabel
} derive(Debug, Eq)

///|
/// Represents an enum value.
pub struct ProtoEnumValue {
  name : String
  number : Int
} derive(Debug, Eq)

///|
/// Represents an enum definition.
pub struct ProtoEnum {
  name : String
  values : Array[ProtoEnumValue]
} derive(Debug, Eq)

///|
/// Represents a oneof group inside a message.
pub struct ProtoOneof {
  name : String
  fields : Array[ProtoField]
} derive(Debug, Eq)

///|
/// Represents an RPC method inside a service.
pub struct ProtoRpc {
  name : String
  input_type : String
  output_type : String
  client_streaming : Bool
  server_streaming : Bool
} derive(Debug, Eq)

///|
/// Represents a service definition.
pub struct ProtoService {
  name : String
  rpcs : Array[ProtoRpc]
} derive(Debug, Eq)

///|
/// Represents a message definition.
pub struct ProtoMessage {
  name : String
  fields : Array[ProtoField]
  oneofs : Array[ProtoOneof]
  nested_messages : Array[ProtoMessage]
  nested_enums : Array[ProtoEnum]
} derive(Debug, Eq)

///|
/// A parse error with a line position and message.
pub struct ProtoError {
  line : Int
  message : String
} derive(Debug, Eq)

///|
/// A parsed .proto file containing all definitions.
pub struct ProtoFile {
  messages : Array[ProtoMessage]
  enums : Array[ProtoEnum]
  services : Array[ProtoService]
} derive(Debug, Eq)

///|
/// Format a parse error as a readable string.
pub fn format_proto_error(err : ProtoError) -> String {
  "line \{err.line}: \{err.message}"
}

///|
/// Parse a .proto file content string into a ProtoFile.
pub fn parse_proto(content : String) -> Result[ProtoFile, ProtoError] {
  let cleaned = strip_block_comments(content)
  let lines = split_lines(cleaned)
  let enum_names = collect_enum_names(lines)
  let messages : Array[ProtoMessage] = []
  let enums : Array[ProtoEnum] = []
  let services : Array[ProtoService] = []

  let mut i = 0
  while i < lines.length() {
    let line = trim_line(lines[i])
    if line == "" || is_comment(line) || is_top_level_decl(line) {
      i = i + 1
      continue
    }
    if starts_with(line, "message ") {
      match parse_message(lines, i, enum_names) {
        None => return Err({ line: i + 1, message: "failed to parse message" })
        Some((msg, next_i)) => {
          messages.push(msg)
          i = next_i
        }
      }
    } else if starts_with(line, "enum ") {
      match parse_enum(lines, i) {
        None => return Err({ line: i + 1, message: "failed to parse enum" })
        Some((e, next_i)) => {
          enums.push(e)
          i = next_i
        }
      }
    } else if starts_with(line, "service ") {
      match parse_service(lines, i) {
        None => return Err({ line: i + 1, message: "failed to parse service" })
        Some((s, next_i)) => {
          services.push(s)
          i = next_i
        }
      }
    } else {
      i = i + 1
    }
  }

  Ok({ messages, enums, services })
}

// --- Comment stripping ---

///|
/// Remove `/* ... */` block comments (non-nested) from the source.
fn strip_block_comments(content : String) -> String {
  let out = StringBuilder()
  let mut i = 0
  let mut seg_start = 0
  while i < content.length() {
    if i + 1 < content.length() && content[i:i + 2].to_owned() == "/*" {
      out.write_string(content[seg_start:i].to_owned())
      let close = find_str_from(content, i + 2, "*/")
      if close < 0 {
        return out.to_string()
      }
      i = close + 2
      seg_start = i
    } else {
      i = i + 1
    }
  }
  out.write_string(content[seg_start:].to_owned())
  out.to_string()
}

///|
/// Find the first occurrence of `substr` in `s` at or after `start`.
fn find_str_from(s : String, start : Int, substr : String) -> Int {
  if s.length() - start < substr.length() {
    return -1
  }
  let end = s.length() - substr.length() + 1
  for i in start.. Array[String] {
  let names : Array[String] = []
  for line in lines {
    let t = trim_left(line)
    if starts_with(t, "enum ") {
      let rest = t[5:].to_owned()
      let mut name = trim(rest)
      if ends_with(name, "{") {
        name = trim(name[:name.length() - 1].to_owned())
      }
      if name.length() > 0 && !contains_string(names, name) {
        names.push(name)
      }
    }
  }
  names
}

// --- String helpers ---

///|
fn starts_with(s : String, prefix : String) -> Bool {
  if s.length() < prefix.length() {
    return false
  }
  s[:prefix.length()] == prefix
}

///|
fn ends_with(s : String, suffix : String) -> Bool {
  if s.length() < suffix.length() {
    return false
  }
  s[s.length() - suffix.length():] == suffix
}

///|
fn find_str(s : String, substr : String) -> Int {
  find_str_from(s, 0, substr)
}

///|
fn find_char(s : String, target : Char) -> Int {
  let mut idx = 0
  for c in s {
    if c == target {
      return idx
    }
    idx = idx + 1
  }
  -1
}

///|
fn contains_string(values : Array[String], target : String) -> Bool {
  for value in values {
    if value == target {
      return true
    }
  }
  false
}

///|
fn is_comment(line : String) -> Bool {
  let t = trim_left(line)
  starts_with(t, "//")
}

///|
fn is_top_level_decl(line : String) -> Bool {
  let t = trim_left(line)
  starts_with(t, "syntax ") ||
  starts_with(t, "package ") ||
  starts_with(t, "import ") ||
  starts_with(t, "option ")
}

///|
fn trim_line(line : String) -> String {
  let mut s = line
  let comment_pos = find_str(s, "//")
  if comment_pos >= 0 {
    s = s[:comment_pos].to_owned()
  }
  trim(s)
}

///|
fn trim(s : String) -> String {
  trim_right(trim_left(s))
}

///|
fn trim_left(s : String) -> String {
  let mut start = 0
  while start < s.length() && is_whitespace_char(s, start) {
    start = start + 1
  }
  if start >= s.length() {
    return ""
  }
  s[start:].to_owned()
}

///|
fn trim_right(s : String) -> String {
  let mut end = s.length()
  while end > 0 && is_whitespace_char(s, end - 1) {
    end = end - 1
  }
  if end <= 0 {
    return ""
  }
  s[:end].to_owned()
}

///|
fn is_whitespace_char(s : String, idx : Int) -> Bool {
  let c = s[idx]
  c == ' ' || c == '\t' || c == '\r' || c == '\n'
}

///|
fn split_lines(content : String) -> Array[String] {
  let result : Array[String] = []
  let mut start = 0
  for i in 0.. (ProtoMessage, Int)? {
  let header = trim_line(lines[start])
  if !starts_with(header, "message ") {
    return None
  }
  let rest = header[8:].to_owned()
  let mut msg_name = trim(rest)
  if ends_with(msg_name, "{") {
    msg_name = trim(msg_name[:msg_name.length() - 1].to_owned())
  }

  let mut i = start
  if !ends_with(header, "{") {
    i = i + 1
    while i < lines.length() && trim_line(lines[i]) != "{" {
      i = i + 1
    }
    if i >= lines.length() {
      return None
    }
  }

  i = i + 1 // skip opening brace
  let fields : Array[ProtoField] = []
  let oneofs : Array[ProtoOneof] = []
  let nested_msgs : Array[ProtoMessage] = []
  let nested_enums : Array[ProtoEnum] = []

  while i < lines.length() {
    let line = trim_line(lines[i])
    if line == "" || is_comment(line) {
      i = i + 1
      continue
    }
    if line == "}" {
      i = i + 1
      break
    }
    if starts_with(line, "message ") {
      match parse_message(lines, i, enum_names) {
        None => return None
        Some((nested, next_i)) => {
          nested_msgs.push(nested)
          i = next_i
        }
      }
    } else if starts_with(line, "enum ") {
      match parse_enum(lines, i) {
        None => return None
        Some((e, next_i)) => {
          nested_enums.push(e)
          i = next_i
        }
      }
    } else if starts_with(line, "oneof ") {
      match parse_oneof(lines, i, enum_names) {
        None => return None
        Some((oneof, next_i)) => {
          oneofs.push(oneof)
          i = next_i
        }
      }
    } else if find_str(line, "=") >= 0 {
      match parse_field(line, enum_names) {
        None => i = i + 1
        Some(f) => {
          fields.push(f)
          i = i + 1
        }
      }
    } else {
      i = i + 1
    }
  }

  Some(
    (
      {
        name: msg_name,
        fields,
        oneofs,
        nested_messages: nested_msgs,
        nested_enums,
      },
      i,
    ),
  )
}

///|
fn parse_oneof(
  lines : Array[String],
  start : Int,
  enum_names : Array[String],
) -> (ProtoOneof, Int)? {
  let header = trim_line(lines[start])
  let rest = header[5:].to_owned() // "oneof " is 5 chars
  let mut oneof_name = trim(rest)
  if ends_with(oneof_name, "{") {
    oneof_name = trim(oneof_name[:oneof_name.length() - 1].to_owned())
  }

  let mut i = start
  if !ends_with(header, "{") {
    i = i + 1
    while i < lines.length() && trim_line(lines[i]) != "{" {
      i = i + 1
    }
    if i >= lines.length() {
      return None
    }
  }

  i = i + 1
  let fields : Array[ProtoField] = []
  while i < lines.length() {
    let line = trim_line(lines[i])
    if line == "" || is_comment(line) {
      i = i + 1
      continue
    }
    if line == "}" {
      i = i + 1
      break
    }
    if find_str(line, "=") >= 0 {
      match parse_field(line, enum_names) {
        None => i = i + 1
        Some(f) => {
          fields.push(f)
          i = i + 1
        }
      }
    } else {
      i = i + 1
    }
  }
  Some(({ name: oneof_name, fields }, i))
}

///|
fn parse_enum(lines : Array[String], start : Int) -> (ProtoEnum, Int)? {
  let header = trim_line(lines[start])
  let rest = header[5:].to_owned() // "enum " is 5 chars
  let mut enum_name = trim(rest)
  if ends_with(enum_name, "{") {
    enum_name = trim(enum_name[:enum_name.length() - 1].to_owned())
  }

  let mut i = start
  if !ends_with(header, "{") {
    i = i + 1
    while i < lines.length() && trim_line(lines[i]) != "{" {
      i = i + 1
    }
    if i >= lines.length() {
      return None
    }
  }

  i = i + 1
  let values : Array[ProtoEnumValue] = []

  while i < lines.length() {
    let line = trim_line(lines[i])
    if line == "" || is_comment(line) {
      i = i + 1
      continue
    }
    if line == "}" {
      i = i + 1
      break
    }
    let eq_pos = find_str(line, "=")
    if eq_pos >= 0 {
      let val_name = trim(line[:eq_pos].to_owned())
      let after_eq = trim(line[eq_pos + 1:].to_owned())
      let mut num_str = after_eq
      if ends_with(num_str, ";") {
        num_str = num_str[:num_str.length() - 1].to_owned()
      }
      num_str = trim(num_str)
      match parse_int(num_str) {
        None => i = i + 1
        Some(n) => {
          values.push({ name: val_name, number: n })
          i = i + 1
        }
      }
    } else {
      i = i + 1
    }
  }

  Some(({ name: enum_name, values }, i))
}

///|
fn parse_service(lines : Array[String], start : Int) -> (ProtoService, Int)? {
  let header = trim_line(lines[start])
  let rest = header[8:].to_owned() // "service " is 8 chars
  let mut svc_name = trim(rest)
  if ends_with(svc_name, "{") {
    svc_name = trim(svc_name[:svc_name.length() - 1].to_owned())
  }

  let mut i = start
  if !ends_with(header, "{") {
    i = i + 1
    while i < lines.length() && trim_line(lines[i]) != "{" {
      i = i + 1
    }
    if i >= lines.length() {
      return None
    }
  }

  i = i + 1
  let rpcs : Array[ProtoRpc] = []
  while i < lines.length() {
    let line = trim_line(lines[i])
    if line == "" || is_comment(line) {
      i = i + 1
      continue
    }
    if line == "}" {
      i = i + 1
      break
    }
    if starts_with(line, "rpc ") {
      match parse_rpc(line) {
        None => ()
        Some(rpc) => rpcs.push(rpc)
      }
    }
    i = i + 1
  }
  Some(({ name: svc_name, rpcs }, i))
}

///|
fn parse_rpc(line : String) -> ProtoRpc? {
  let t = trim_left(line)
  let rest = t[4:].to_owned() // "rpc " is 4 chars
  let open_paren = find_char(rest, '(')
  if open_paren < 0 {
    return None
  }
  let name = trim(rest[:open_paren].to_owned())
  let close_paren = find_char(rest, ')')
  if close_paren < 0 {
    return None
  }
  let input_part = trim(rest[open_paren + 1:close_paren].to_owned())
  let (input_type, client_streaming) = parse_rpc_type(input_part)
  let after_close = rest[close_paren + 1:].to_owned()
  let returns_pos = find_str(after_close, "returns")
  if returns_pos < 0 {
    return None
  }
  let ret_part = after_close[returns_pos + 7:].to_owned()
  let ropen = find_char(ret_part, '(')
  let rclose = find_char(ret_part, ')')
  if ropen < 0 || rclose < 0 {
    return None
  }
  let output_part = trim(ret_part[ropen + 1:rclose].to_owned())
  let (output_type, server_streaming) = parse_rpc_type(output_part)
  Some({ name, input_type, output_type, client_streaming, server_streaming })
}

///|
fn parse_rpc_type(part : String) -> (String, Bool) {
  let t = trim_left(part)
  if starts_with(t, "stream ") {
    (trim(t[7:].to_owned()), true)
  } else {
    (t, false)
  }
}

///|
fn parse_field(line : String, enum_names : Array[String]) -> ProtoField? {
  let t = trim_left(line)
  if starts_with(t, "map<") {
    return parse_map_field(t)
  }

  let words = split_words(line)
  let mut idx = 0

  if words.length() < 3 {
    return None
  }

  let mut label = FieldLabel::Optional
  let first = words[0]
  if first == "repeated" {
    label = FieldLabel::Repeated
    idx = 1
  } else if first == "required" {
    label = FieldLabel::Required
    idx = 1
  } else if first == "optional" {
    idx = 1
  }

  if idx + 3 >= words.length() {
    return None
  }

  let type_str = words[idx]
  let name = words[idx + 1]
  if words[idx + 2] != "=" {
    return None
  }
  let num_str = words[idx + 3]

  let number = parse_uint(num_str)
  match number {
    None => None
    Some(n) => {
      let ftype = parse_field_type(type_str, enum_names)
      match ftype {
        None => None
        Some(ft) => Some({ name, number: n, field_type: ft, label })
      }
    }
  }
}

///|
fn parse_map_field(line : String) -> ProtoField? {
  let gt = find_char(line, '>')
  if gt < 0 {
    return None
  }
  let inner = line[4:gt].to_owned() // between "map<" and ">"
  let comma = find_char(inner, ',')
  if comma < 0 {
    return None
  }
  let key_type_str = trim(inner[:comma].to_owned())
  let value_type_str = trim(inner[comma + 1:].to_owned())
  let key_type = parse_scalar_type(key_type_str)
  let value_type = parse_field_type(value_type_str, [])
  if key_type is None || value_type is None {
    return None
  }
  let rest = line[gt + 1:].to_owned()
  let words = split_words(rest)
  if words.length() < 3 {
    return None
  }
  let name = words[0]
  if words[1] != "=" {
    return None
  }
  match parse_uint(words[2]) {
    None => None
    Some(n) => {
      let ft = FieldType::Map(key_type.unwrap(), value_type.unwrap())
      Some({ name, number: n, field_type: ft, label: FieldLabel::Optional })
    }
  }
}

///|
fn split_words(line : String) -> Array[String] {
  let words : Array[String] = []
  let mut start = -1
  for i in 0.. Some(FieldType::Int32)
    "int64" => Some(FieldType::Int64)
    "uint32" => Some(FieldType::Uint32)
    "uint64" => Some(FieldType::Uint64)
    "sint32" => Some(FieldType::Sint32)
    "sint64" => Some(FieldType::Sint64)
    "fixed32" => Some(FieldType::Fixed32)
    "fixed64" => Some(FieldType::Fixed64)
    "sfixed32" => Some(FieldType::Sfixed32)
    "sfixed64" => Some(FieldType::Sfixed64)
    "float" => Some(FieldType::Float)
    "double" => Some(FieldType::Double)
    "bool" => Some(FieldType::Bool)
    "string" => Some(FieldType::String)
    "bytes" => Some(FieldType::Bytes)
    _ => None
  }
}

///|
fn parse_int(s : String) -> Int? {
  if s.length() == 0 {
    return None
  }
  let mut result = 0
  let mut neg = false
  let mut start = 0
  if s[0] == '-' {
    neg = true
    start = 1
  }
  for i in start..= '0' && ch <= '9' {
      result = result * 10 + (ch.to_int() - 48)
    } else {
      break
    }
  }
  Some(if neg { -result } else { result })
}

///|
fn parse_uint(s : String) -> UInt? {
  if s.length() == 0 {
    return None
  }
  let mut result : UInt = 0
  for i in 0..= '0' && ch <= '9' {
      result = result * 10 + (ch.to_int() - 48).reinterpret_as_uint()
    } else {
      break
    }
  }
  Some(result)
}