///|
priv enum ParsedDirective {
  HelpDirective(String, String)
  TypeDirective(String, MetricType)
  UnitDirective(String, String)
  EofDirective
  IgnoredComment
}

///|
fn directive_error(
  line : Int,
  message : String,
) -> Result[ParsedDirective, ParseError] {
  Err(ParseError::new(line, 1, message))
}

///|
fn parse_directive_head(
  payload : StringView,
  line : Int,
  preserve_value_prefix? : Bool = false,
  allow_empty? : Bool = false,
) -> Result[(String, String), ParseError] {
  let cursor = TextCursor::new(payload)
  let name = cursor.read_metric_name()
  if !is_valid_metric_name(name) {
    return Err(ParseError::new(line, 1, "invalid directive metric name"))
  }
  let has_separator = if preserve_value_prefix {
    match cursor.next() {
      Some(' ' | '\t') => true
      _ => false
    }
  } else {
    cursor.take_horizontal_space()
  }
  if !has_separator {
    return Err(
      ParseError::new(line, 1, "directive is missing its value after '\{name}'"),
    )
  }
  let value = cursor.remaining()
  if value == "" && !allow_empty {
    return Err(ParseError::new(line, 1, "directive value must not be empty"))
  }
  Ok((name, value))
}

///|
fn parse_directive(
  line_text : StringView,
  line : Int,
) -> Result[ParsedDirective, ParseError] {
  let text = line_text.trim_start()
  if text == "# EOF" {
    return Ok(EofDirective)
  }
  if text.has_prefix("# HELP ") {
    let payload = text.sub(start=7)
    let (name, encoded) = match
      parse_directive_head(
        payload,
        line,
        preserve_value_prefix=true,
        allow_empty=true,
      ) {
      Ok(pair) => pair
      Err(error) => return Err(error)
    }
    if encoded == "" {
      return Ok(IgnoredComment)
    }
    let help = match unescape_help(encoded) {
      Ok(value) => value
      Err(error) => return directive_error(line, error)
    }
    return Ok(HelpDirective(name, help))
  }
  if text.has_prefix("# TYPE ") {
    let payload = text.sub(start=7)
    let (name, keyword) = match parse_directive_head(payload, line) {
      Ok(pair) => pair
      Err(error) => return Err(error)
    }
    if keyword.contains(" ") || keyword.contains("\t") {
      return directive_error(line, "TYPE directive has extra fields")
    }
    let metric_type = match MetricType::from_keyword(keyword) {
      Ok(value) => value
      Err(error) => return directive_error(line, error)
    }
    return Ok(TypeDirective(name, metric_type))
  }
  if text.has_prefix("# UNIT ") {
    let payload = text.sub(start=7)
    let (name, unit) = match
      parse_directive_head(payload, line, allow_empty=true) {
      Ok(pair) => pair
      Err(error) => return Err(error)
    }
    if unit == "" {
      return Ok(IgnoredComment)
    }
    if unit.contains(" ") || unit.contains("\t") {
      return directive_error(line, "UNIT directive has extra fields")
    }
    return Ok(UnitDirective(name, unit))
  }
  Ok(IgnoredComment)
}

///|
fn string_array_contains(values : Array[String], value : StringView) -> Bool {
  for item in values {
    if value.equal_to_string(item) {
      return true
    }
  }
  false
}

///|
fn enter_family_sequence(
  active : String?,
  closed : Array[String],
  name : String,
  line : Int,
) -> Result[String, ParseError] {
  match active {
    None => Ok(name)
    Some(current) =>
      if current == name {
        Ok(current)
      } else if string_array_contains(closed, name) {
        Err(
          ParseError::new(
            line,
            1,
            "metric family '\{name}' is interleaved with another family",
          ),
        )
      } else {
        closed.push(current)
        Ok(name)
      }
  }
}

///|
fn find_family_index(families : Array[MetricFamily], name : StringView) -> Int {
  for index in 0.. Int {
  let found = find_family_index(families, name)
  if found >= 0 {
    found
  } else {
    families.push(MetricFamily::new(name))
    families.length() - 1
  }
}

///|
fn sample_matches_family(family : MetricFamily, sample_name : String) -> Bool {
  match family.metric_type {
    Counter =>
      sample_name == "\{family.name}_total" ||
      sample_name == "\{family.name}_created"
    Histogram =>
      sample_name == "\{family.name}_bucket" ||
      sample_name == "\{family.name}_sum" ||
      sample_name == "\{family.name}_count" ||
      sample_name == "\{family.name}_created"
    GaugeHistogram =>
      sample_name == "\{family.name}_bucket" ||
      sample_name == "\{family.name}_gsum" ||
      sample_name == "\{family.name}_gcount"
    Summary =>
      sample_name == family.name ||
      sample_name == "\{family.name}_sum" ||
      sample_name == "\{family.name}_count" ||
      sample_name == "\{family.name}_created"
    Info => sample_name == "\{family.name}_info"
    Unknown | Gauge | StateSet => sample_name == family.name
  }
}

///|
fn find_sample_family(
  families : Array[MetricFamily],
  sample : Sample,
  active_family : String?,
) -> Int {
  match active_family {
    Some(name) => {
      let active = find_family_index(families, name)
      if active >= 0 && sample_matches_family(families[active], sample.name) {
        return active
      }
    }
    None => ()
  }
  let exact = find_family_index(families, sample.name)
  if exact >= 0 && sample_matches_family(families[exact], sample.name) {
    return exact
  }
  for index in 0.. Result[Unit, ParseError] {
  if family.samples.is_empty() {
    Ok(())
  } else {
    Err(
      ParseError::new(
        line,
        1,
        "metadata for family '\{family.name}' must precede its samples",
      ),
    )
  }
}

///|
/// Parse an OpenMetrics text exposition document.
///
/// Unknown `#` comments are ignored. Missing `# EOF` is preserved as
/// `Document.has_eof = false` and reported by `validate`, allowing callers to
/// inspect partially written scrape output.
pub fn parse(input : StringView) -> Result[Document, ParseError] {
  let families : Array[MetricFamily] = []
  let closed_families : Array[String] = []
  let seen_type_names : Array[String] = []
  let mut active_family : String? = None
  let mut line_number = 0
  let mut has_eof = false
  for raw_line in input.split("\n") {
    line_number += 1
    let line_text = raw_line.trim_end(chars="\r")
    if line_text.trim().is_empty() {
      continue
    }
    if has_eof {
      return Err(
        ParseError::new(
          line_number, 1, "non-empty content is not allowed after # EOF",
        ),
      )
    }
    if line_text.trim_start().has_prefix("#") {
      let directive = match parse_directive(line_text, line_number) {
        Ok(value) => value
        Err(error) => return Err(error)
      }
      match directive {
        IgnoredComment => ()
        EofDirective => has_eof = true
        HelpDirective(name, help) => {
          active_family = Some(
            match
              enter_family_sequence(
                active_family, closed_families, name, line_number,
              ) {
              Ok(value) => value
              Err(error) => return Err(error)
            },
          )
          let index = ensure_family(families, name)
          let family = families[index]
          match reject_late_metadata(family, line_number) {
            Err(error) => return Err(error)
            Ok(_) => ()
          }
          if family.help is Some(_) {
            return Err(
              ParseError::new(
                line_number,
                1,
                "duplicate HELP directive for '\{family.name}'",
              ),
            )
          }
          families[index] = { ..family, help: Some(help) }
        }
        TypeDirective(name, metric_type) => {
          active_family = Some(
            match
              enter_family_sequence(
                active_family, closed_families, name, line_number,
              ) {
              Ok(value) => value
              Err(error) => return Err(error)
            },
          )
          let index = ensure_family(families, name)
          let family = families[index]
          match reject_late_metadata(family, line_number) {
            Err(error) => return Err(error)
            Ok(_) => ()
          }
          if string_array_contains(seen_type_names, name) {
            return Err(
              ParseError::new(
                line_number,
                1,
                "duplicate TYPE directive for '\{family.name}'",
              ),
            )
          }
          seen_type_names.push(name)
          families[index] = { ..family, metric_type, }
        }
        UnitDirective(name, unit) => {
          active_family = Some(
            match
              enter_family_sequence(
                active_family, closed_families, name, line_number,
              ) {
              Ok(value) => value
              Err(error) => return Err(error)
            },
          )
          let index = ensure_family(families, name)
          let family = families[index]
          match reject_late_metadata(family, line_number) {
            Err(error) => return Err(error)
            Ok(_) => ()
          }
          if family.unit is Some(_) {
            return Err(
              ParseError::new(
                line_number,
                1,
                "duplicate UNIT directive for '\{family.name}'",
              ),
            )
          }
          families[index] = { ..family, unit: Some(unit) }
        }
      }
    } else {
      let sample = match parse_sample_line(line_text, line=line_number) {
        Ok(value) => value
        Err(error) => return Err(error)
      }
      let found = find_sample_family(families, sample, active_family)
      let index = if found >= 0 {
        found
      } else {
        ensure_family(families, sample.name)
      }
      let family = families[index]
      active_family = Some(
        match
          enter_family_sequence(
            active_family,
            closed_families,
            family.name,
            line_number,
          ) {
          Ok(value) => value
          Err(error) => return Err(error)
        },
      )
      families[index] = family.add_sample(sample)
    }
  }
  Ok({ families, has_eof })
}

///|
/// Descriptive alias for `parse`.
pub fn parse_document(input : StringView) -> Result[Document, ParseError] {
  parse(input)
}