///|
fn package_name_from_line(line : StringView) -> String? {
  let trimmed = line.trim()
  guard trimmed.has_prefix("package ") else { return None }
  let quoted = trimmed["package ".length():]
  let start = find_char(quoted, '"', 0)
  match start {
    Some(start) =>
      match find_char(quoted, '"', start + 1) {
        Some(end) => Some(substring(quoted, start + 1, end))
        None => None
      }
    None => None
  }
}

///|
fn public_declaration_kind(line : StringView) -> SymbolKind? {
  if line.has_prefix("pub fn") {
    Some(Function)
  } else if line.has_prefix("pub type ") {
    Some(TypeAlias)
  } else if line.has_prefix("pub struct ") ||
    line.has_prefix("pub(all) struct ") ||
    line.has_prefix("pub(open) struct ") {
    Some(Struct)
  } else if line.has_prefix("pub enum ") ||
    line.has_prefix("pub(all) enum ") ||
    line.has_prefix("pub(open) enum ") {
    Some(Enum)
  } else if line.has_prefix("pub trait ") || line.has_prefix("pub(open) trait ") {
    Some(Trait)
  } else if line.has_prefix("pub let ") || line.has_prefix("pub const ") {
    Some(Value)
  } else {
    None
  }
}

///|
fn read_identifier(text : StringView, start : Int32) -> (String, Int32) {
  let builder = StringBuilder()
  let mut index = start
  while index < text.length() {
    let ch = text.get_char(index).unwrap()
    if is_identifier_char(ch) {
      builder.write_char(ch)
      index += 1
    } else {
      break
    }
  }
  (builder.to_string(), index)
}

///|
fn skip_space(text : StringView, start : Int32) -> Int32 {
  let mut index = start
  while index < text.length() {
    let ch = text.get_char(index).unwrap()
    if ch.is_whitespace() {
      index += 1
    } else {
      break
    }
  }
  index
}

///|
fn symbol_name_and_rest(
  text : StringView,
  marker : String,
) -> (String, String, Int32)? {
  let marker_index = match text.find(marker) {
    Some(index) => index
    None => return None
  }
  let mut index = skip_space(text, marker_index + marker.length())
  if marker == "fn" && index < text.length() {
    let ch = text.get_char(index).unwrap()
    if ch == '[' {
      index = skip_balanced(text, index, '[', ']')
      index = skip_space(text, index)
    }
  }
  let (name, name_end) = read_identifier(text, index)
  guard !name.is_empty() else { return None }
  let rest = substring(text, name_end, text.length()).trim().to_owned()
  Some((name, rest, name_end))
}

///|
fn type_body(symbol : ApiSymbol) -> String {
  match symbol.kind {
    TypeAlias =>
      match find_char(symbol.signature, '=', 0) {
        Some(index) =>
          substring(symbol.signature, index + 1, symbol.signature.length())
          .trim()
          .to_owned()
        None => ""
      }
    Struct | Enum | Trait =>
      match find_char(symbol.signature, '{', 0) {
        Some(index) =>
          substring(symbol.signature, index, symbol.signature.length())
          .trim()
          .to_owned()
        None => ""
      }
    _ => ""
  }
}

///|
fn strip_mode_suffix(text : StringView) -> (String, ParameterMode) {
  let trimmed = text.trim()
  if trimmed.has_suffix("?") {
    (substring(trimmed, 0, trimmed.length() - 1).trim().to_owned(), Optional)
  } else if trimmed.has_suffix("~") {
    (substring(trimmed, 0, trimmed.length() - 1).trim().to_owned(), Labelled)
  } else if trimmed == "self" {
    ("self", Receiver)
  } else {
    (trimmed.to_owned(), Positional)
  }
}

///|
fn parse_parameter(
  raw : StringView,
  index : Int32,
  is_method : Bool,
) -> Parameter? {
  let text = raw.trim()
  if text.is_empty() {
    return None
  }
  if text == "self" {
    return Some(Parameter::{
      name: "self",
      type_text: "",
      mode: Receiver,
      default_value: None,
    })
  }
  match find_char(text, ':', 0) {
    Some(colon) => {
      let name_view = text[0:colon].trim()
      let type_view = text[colon + 1:].trim()
      let (name, mode) = strip_mode_suffix(name_view)
      let (type_text, default_value) = match
        top_level_assignment_index(type_view) {
        Some(index) =>
          (
            substring(type_view, 0, index).trim().to_owned(),
            Some(
              substring(type_view, index + 1, type_view.length())
              .trim()
              .to_owned(),
            ),
          )
        None => (type_view.to_owned(), None)
      }
      Some(Parameter::{ name, type_text, mode, default_value, })
    }
    None =>
      if is_method && index == 0 && text == "Self" {
        Some(Parameter::{
          name: "self",
          type_text: "Self",
          mode: Receiver,
          default_value: None,
        })
      } else {
        Some(Parameter::{
          name: "arg\{index}",
          type_text: text.to_owned(),
          mode: Positional,
          default_value: None,
        })
      }
  }
}

///|
fn function_paren_range(
  signature : StringView,
  name : StringView,
) -> (Int32, Int32)? {
  let name_index = match signature.find(name) {
    Some(index) => index
    None => return None
  }
  let open_index = match find_char(signature, '(', name_index + name.length()) {
    Some(index) => index
    None => return None
  }
  let close_index = match find_matching_paren(signature, open_index) {
    Some(index) => index
    None => return None
  }
  Some((open_index, close_index))
}

///|
fn function_parameters(
  signature : StringView,
  name : StringView,
) -> Array[Parameter] {
  let (open_index, close_index) = match function_paren_range(signature, name) {
    Some(range) => range
    None => return []
  }
  let inner = signature[open_index + 1:close_index]
  let result = []
  let is_method = name.contains("::")
  for index, raw in split_top_level(inner) {
    if parse_parameter(raw, index, is_method) is Some(parameter) {
      result.push(parameter)
    }
  }
  result
}

///|
fn function_result(signature : StringView, name : StringView) -> String {
  let (_open_index, close_index) = match function_paren_range(signature, name) {
    Some(range) => range
    None => return ""
  }
  substring(signature, close_index + 1, signature.length()).trim().to_owned()
}

///|
fn parse_symbol(declaration : StringView) -> ApiSymbol? {
  let line = declaration.trim()
  let kind = match public_declaration_kind(line) {
    Some(kind) => kind
    None => return None
  }
  let marker = match kind {
    Function => "fn"
    TypeAlias => "type"
    Struct => "struct"
    Enum => "enum"
    Trait => "trait"
    Value => if line.has_prefix("pub let") { "let" } else { "const" }
  }
  let (name, _rest, _name_end) = match symbol_name_and_rest(line, marker) {
    Some(value) => value
    None => return None
  }
  let signature = normalize_whitespace(line)
  let parameters = match kind {
    Function => function_parameters(signature, name)
    _ => []
  }
  let result_text = match kind {
    Function => function_result(signature, name)
    _ => ""
  }
  Some(ApiSymbol::{ kind, name, signature, parameters, result_text, })
}

///|
/// Parses the stable declaration subset emitted by `moon info`.
///
/// Only public functions, type aliases, structs, enums, traits and values are
/// retained. Documentation and implementation details are intentionally omitted.
pub fn parse_interface(source : StringView) -> ApiSnapshot {
  let lines = source.split("\n").to_array()
  let mut package_name = ""
  let symbols = []
  let current = StringBuilder()
  let mut depth = 0
  let mut collecting = false
  for raw_line in lines {
    let line = raw_line.trim()
    if package_name.is_empty() {
      if package_name_from_line(line) is Some(name) {
        package_name = name
      }
    }
    if !collecting {
      if public_declaration_kind(line) is Some(_) {
        current.write_stringview(normalize_whitespace(line))
        depth = declaration_depth_delta(line)
        collecting = true
        if depth <= 0 {
          if parse_symbol(current.to_string()) is Some(symbol) {
            symbols.push(symbol)
          }
          current.reset()
          collecting = false
          depth = 0
        }
      }
    } else {
      current.write_char(' ')
      current.write_stringview(normalize_whitespace(line))
      depth += declaration_depth_delta(line)
      if depth <= 0 {
        if parse_symbol(current.to_string()) is Some(symbol) {
          symbols.push(symbol)
        }
        current.reset()
        collecting = false
        depth = 0
      }
    }
  }
  if collecting && parse_symbol(current.to_string()) is Some(symbol) {
    symbols.push(symbol)
  }
  ApiSnapshot::{ package_name, symbols, }
}

///|
pub fn find_symbol(
  snapshot : ApiSnapshot,
  kind : SymbolKind,
  name : StringView,
) -> ApiSymbol? {
  for symbol in snapshot.symbols {
    if same_symbol_kind(symbol.kind, kind) && symbol.name == name.to_owned() {
      return Some(symbol)
    }
  }
  None
}