///|
fn encode_exemplar(exemplar : Exemplar) -> String {
  let out = StringBuilder()
  out.write_string(" # ")
  if exemplar.labels.is_empty() {
    out.write_string("{}")
  } else {
    out.write_string(encode_label_set(exemplar.labels))
  }
  out.write_char(' ')
  out.write_string(format_number(exemplar.value))
  match exemplar.timestamp {
    Some(timestamp) => {
      out.write_char(' ')
      out.write_string(format_number(timestamp))
    }
    None => ()
  }
  out.to_string()
}

///|
fn encode_sample_with_number_label(
  sample : Sample,
  canonical_number_label : StringView,
) -> String {
  let out = StringBuilder()
  out.write_string(sample.name)
  out.write_string(
    encode_metric_label_set(sample.labels, canonical_number_label),
  )
  out.write_char(' ')
  out.write_string(format_number(sample.value))
  match sample.timestamp {
    Some(timestamp) => {
      out.write_char(' ')
      out.write_string(format_number(timestamp))
    }
    None => ()
  }
  match sample.exemplar {
    Some(exemplar) => out.write_string(encode_exemplar(exemplar))
    None => ()
  }
  out.to_string()
}

///|
/// Encode one sample line without assuming a metric-family type.
pub fn encode_sample(sample : Sample) -> String {
  encode_sample_with_number_label(sample, "")
}

///|
fn encode_family(family : MetricFamily, out : StringBuilder) -> Unit {
  out.write_string("# TYPE ")
  out.write_string(family.name)
  out.write_char(' ')
  out.write_string(family.metric_type.to_keyword())
  out.write_char('\n')
  match family.unit {
    Some(unit) =>
      if !unit.is_empty() {
        out.write_string("# UNIT ")
        out.write_string(family.name)
        out.write_char(' ')
        out.write_string(unit)
        out.write_char('\n')
      }
    None => ()
  }
  match family.help {
    Some(help) =>
      if !help.is_empty() {
        out.write_string("# HELP ")
        out.write_string(family.name)
        out.write_char(' ')
        out.write_string(escape_help(help))
        out.write_char('\n')
      }
    None => ()
  }
  let canonical_number_label = match family.metric_type {
    Histogram | GaugeHistogram => "le"
    Summary => "quantile"
    Unknown | Counter | Gauge | Info | StateSet => ""
  }
  for sample in family.samples {
    out.write_string(
      encode_sample_with_number_label(sample, canonical_number_label),
    )
    out.write_char('\n')
  }
}

///|
/// Encode a document in a stable canonical layout.
///
/// Family, sample and label order are preserved. Metadata is reordered as
/// TYPE, UNIT, HELP, then samples. The output ends with `# EOF` by default.
pub fn encode(document : Document, include_eof? : Bool = true) -> String {
  let out = StringBuilder()
  for family in document.families {
    encode_family(family, out)
  }
  if include_eof {
    out.write_string("# EOF\n")
  }
  out.to_string()
}

///|
/// Encode this document in canonical form.
pub fn Document::to_text(self : Document, include_eof? : Bool = true) -> String {
  encode(self, include_eof~)
}

///|
/// Parse and immediately re-encode an exposition document.
pub fn normalize(input : StringView) -> Result[String, ParseError] {
  match parse(input) {
    Ok(document) => Ok(encode(document))
    Err(error) => Err(error)
  }
}