///|
/// Emit fresh IDs even for names that themselves look like `(123)` references.
fn write_name(
  out : StringBuilder,
  key : String,
  value : String,
  id : Int32,
) -> Unit {
  out.write_string(key + "=")
  if value != "" {
    out.write_string("(\{id}) " + value)
  }
  out.write_string("\n")
}

///|
fn write_positions(out : StringBuilder, values : Array[UInt64]) -> Unit {
  for i = 0; i < values.length(); i = i + 1 {
    if i > 0 {
      out.write_string(" ")
    }
    out.write_string(values[i].to_string())
  }
}

///| Normalize parsed text to absolute positions and fresh name IDs. Comments and

///|
/// original whitespace are not retained. Counters and all supported records are.
pub fn normalize(text : String) -> String raise ParseError {
  let profile = parse(text)
  let mut budget = 0
  for part in profile.parts {
    budget = budget + 32
    for h in part.headers {
      let size = h.key.length() + h.value.length() + 4
      if size > 16777216 - budget {
        raise Resource(h.line, "normalized text exceeds size budget")
      }
      budget = budget + size
    }
  }
  for record in profile.records {
    let size = record_size(record)
    if size > 16777216 - budget {
      raise Resource(record.line, "normalized text exceeds size budget")
    }
    budget = budget + size
  }
  let out = StringBuilder()
  let mut index = 0
  let mut id = 0
  for part_index = 0
      part_index < profile.parts.length()
      part_index = part_index + 1 {
    let part = profile.parts[part_index]
    if !part.headers.any(h => h.key == "part") {
      out.write_string("part: \{part_index + 1}\n")
    }
    for h in part.headers {
      if h.key != "totals" {
        out.write_string(h.key + ": " + h.value + "\n")
      }
    }
    while index < profile.records.length() &&
          profile.records[index].part == part_index {
      let record = profile.records[index]
      let c = record.context
      id = id + 1
      write_name(out, "ob", c.object, id)
      write_name(out, "fl", c.file, id)
      write_name(out, "fn", c.function_name, id)
      write_name(out, "fi", c.cost_file, id + 1)
      // fi uses another file ID so the function file remains available to consumers.
      id = id + 1
      match record.kind {
        SelfCost => ()
        Call(count, target, target_pos) => {
          id = id + 1
          write_name(out, "cob", target.object, id)
          write_name(out, "cfi", target.file, id)
          write_name(out, "cfn", target.function_name, id)
          out.write_string("calls=" + count.to_string() + " ")
          write_positions(out, target_pos)
          out.write_string("\n")
        }
      }
      write_positions(out, record.positions)
      for value in record.values {
        out.write_string(" " + value.to_string())
      }
      out.write_string("\n")
      index = index + 1
    }
    for h in part.headers {
      if h.key == "totals" {
        out.write_string(h.key + ": " + h.value + "\n")
      }
    }
    out.write_string("\n")
  }
  out.to_string()
}