///|
fn error_message(code : String, args : Array[String?]) -> String {
  let arg = if args.is_empty() { "None" } else { args[0].unwrap_or("None") }
  match code {
    "E0001" => "Generic error"
    "E0002" => "Expected an entry start"
    "E0003" => "Expected token: \"" + arg + "\""
    "E0004" => "Expected a character from range: \"" + arg + "\""
    "E0005" => "Expected message \"" + arg + "\" to have a value or attributes"
    "E0006" => "Expected term \"-" + arg + "\" to have a value"
    "E0007" => "Keyword cannot end with a whitespace"
    "E0008" => "The callee has to be an upper-case identifier or a term"
    "E0009" => "The argument name has to be a simple identifier"
    "E0010" => "Expected one of the variants to be marked as default (*)"
    "E0011" => "Expected at least one variant after \"->\""
    "E0012" => "Expected value"
    "E0013" => "Expected variant key"
    "E0014" => "Expected literal"
    "E0015" => "Only one variant can be marked as default (*)"
    "E0016" => "Message references cannot be used as selectors"
    "E0017" => "Terms cannot be used as selectors"
    "E0018" => "Attributes of messages cannot be used as selectors"
    "E0019" => "Attributes of terms cannot be used as placeables"
    "E0020" => "Unterminated string expression"
    "E0021" => "Positional arguments must not follow named arguments"
    "E0022" => "Named arguments must be unique"
    "E0024" => "Cannot access variants of a message."
    "E0025" => "Unknown escape sequence: \\" + arg + "."
    "E0026" => "Invalid Unicode escape sequence: " + arg + "."
    "E0027" => "Unbalanced closing brace in TextElement."
    "E0028" => "Expected an inline expression"
    "E0029" => "Expected simple expression as selector"
    "E9001" => "Syntax nesting exceeds configured depth limit"
    _ => code
  }
}

///|
fn Cursor::attribute(self : Cursor) -> Node raise ParseFailure {
  let start = self.index
  self.expect('.')
  let id = self.identifier()
  ignore(self.skip_blank_inline())
  self.expect('=')
  let value = match self.maybe_pattern() {
    Some(v) => v
    None => raise ParseFailure("E0012", [])
  }
  Node::with_span(Attribute(id, value), start, self.index)
}

///|
fn Cursor::attributes(self : Cursor) -> Array[Node] raise ParseFailure {
  let attrs : Array[Node] = []
  self.peek_blank()
  while self.current_peek() == Some('.') {
    self.skip_to_peek()
    attrs.push(self.attribute())
    self.peek_blank()
  }
  attrs
}

///|
fn Cursor::message(self : Cursor, term : Bool) -> Node raise ParseFailure {
  let start = self.index
  if term {
    self.expect('-')
  }
  let id = self.identifier()
  ignore(self.skip_blank_inline())
  self.expect('=')
  let value = self.maybe_pattern()
  if term && value == None {
    raise ParseFailure("E0006", [Some(identifier_name(id))])
  }
  let attrs = self.attributes()
  let kind = if term {
    Term(id, value.unwrap(), attrs, None)
  } else {
    if value == None && attrs.is_empty() {
      raise ParseFailure("E0005", [Some(identifier_name(id))])
    }
    Message(id, value, attrs, None)
  }
  Node::with_span(kind, start, self.index)
}

///|
fn Cursor::entry(self : Cursor) -> Node raise ParseFailure {
  match self.current() {
    Some('#') => self.comment()
    Some('-') => self.message(true)
    ch if is_id_start(ch) => self.message(false)
    _ => raise ParseFailure("E0002", [])
  }
}

///|
fn Cursor::entry_or_junk(self : Cursor) -> Node {
  let start = self.index
  try {
    let entry = self.entry()
    self.expect_line_end()
    entry
  } catch {
    ParseFailure(code, args) => {
      let mut error_index = self.index
      self.recover(start)
      if self.index < error_index {
        error_index = self.index
      }
      let annotation = Node::with_span(
        Annotation(code, args, error_message(code, args)),
        error_index,
        error_index,
      )
      Node::with_span(
        Junk(self.slice(start, self.index), [annotation]),
        start,
        self.index,
      )
    }
  }
}

///|
fn attach_comment(entry : Node, comment : Node) -> Node {
  let kind = match entry.kind {
    Message(id, value, attrs, _) => Message(id, value, attrs, Some(comment))
    Term(id, value, attrs, _) => Term(id, value, attrs, Some(comment))
    _ => entry.kind
  }
  { kind, span: { start: comment.span.start, end: entry.span.end, }, }
}

///|
/// Parse an FTL resource, preserving invalid entries as Junk with annotations.
/// Default nesting is limited to 64, returning recoverable E9001 on excess.
pub fn parse(source : String) -> Node {
  let c = Cursor::new(source)
  ignore(c.skip_blank_block())
  let entries : Array[Node] = []
  let mut pending : Node? = None
  while c.current() != None {
    let mut entry = c.entry_or_junk()
    let blank_lines = c.skip_blank_block()
    match entry.kind {
      Comment(_) if blank_lines.is_empty() && c.current() != None => {
        pending = Some(entry)
        continue
      }
      _ => ()
    }
    if pending is Some(comment) {
      match entry.kind {
        Message(_, _, _, _) | Term(_, _, _, _) =>
          entry = attach_comment(entry, comment)
        _ => entries.push(comment)
      }
      pending = None
    }
    entries.push(entry)
  }
  Node::with_span(Resource(entries), 0, c.index)
}

///|
/// Parse the first entry, ignoring preceding valid comments as upstream does.
pub fn parse_entry(source : String) -> Node {
  let c = Cursor::new(source)
  ignore(c.skip_blank_block())
  while c.current() == Some('#') {
    let skipped = c.entry_or_junk()
    match skipped.kind {
      Junk(_, _) => return skipped
      _ => ()
    }
    ignore(c.skip_blank_block())
  }
  c.entry_or_junk()
}