///|
fn compare_lines(left : CoverageLine, right : CoverageLine) -> Int {
  left.line.compare(right.line)
}

///|
fn compare_branches(left : CoverageBranch, right : CoverageBranch) -> Int {
  let by_line = left.line.compare(right.line)
  if by_line != 0 {
    return by_line
  }
  let by_block = left.block.compare(right.block)
  if by_block != 0 {
    return by_block
  }
  left.branch.compare(right.branch)
}

///|
fn compare_optional_lines(left : Int?, right : Int?) -> Int {
  match (left, right) {
    (Some(a), Some(b)) => a.compare(b)
    (Some(_), None) => -1
    (None, Some(_)) => 1
    (None, None) => 0
  }
}

///|
fn compare_functions(left : CoverageFunction, right : CoverageFunction) -> Int {
  let by_line = compare_optional_lines(left.line, right.line)
  if by_line != 0 {
    return by_line
  }
  left.name.compare(right.name)
}

///|
fn check_lcov_text(label : String, value : String) -> Unit raise CoverageError {
  if value.contains_char('\n') || value.contains_char('\r') {
    raise InvalidModel("\{label} must not contain a newline")
  }
}

///|
fn write_lcov_file(
  output : StringBuilder,
  file : FileCoverage,
) -> Unit raise CoverageError {
  check_lcov_text("source path", file.path)
  match file.test_name {
    Some(name) => {
      check_lcov_text("test name", name)
      output.write_string("TN:\{name}\n")
    }
    None => output.write_string("TN:\n")
  }
  output.write_string("SF:\{file.path}\n")
  let functions = [ for function in file.functions => function ]
  functions.sort_by(compare_functions)
  for function in functions {
    check_lcov_text("function name", function.name)
    match function.line {
      Some(line) => output.write_string("FN:\{line},\{function.name}\n")
      None => ()
    }
  }
  for function in functions {
    output.write_string("FNDA:\{function.hits},\{function.name}\n")
  }
  let functions_hit = functions.fold(init=0, (count, function) => {
    if function.hits > 0 {
      count + 1
    } else {
      count
    }
  })
  output.write_string("FNF:\{functions.length()}\n")
  output.write_string("FNH:\{functions_hit}\n")
  let lines = [ for line in file.lines => line ]
  lines.sort_by(compare_lines)
  for line in lines {
    output.write_string("DA:\{line.line},\{line.hits}\n")
  }
  let lines_hit = lines.fold(init=0, (count, line) => {
    if line.hits > 0 {
      count + 1
    } else {
      count
    }
  })
  output.write_string("LF:\{lines.length()}\n")
  output.write_string("LH:\{lines_hit}\n")
  let branches = [ for branch in file.branches => branch ]
  branches.sort_by(compare_branches)
  for branch in branches {
    let taken = match branch.taken {
      Some(count) => count.to_string()
      None => "-"
    }
    output.write_string(
      "BRDA:\{branch.line},\{branch.block},\{branch.branch},\{taken}\n",
    )
  }
  let branches_hit = branches.fold(init=0, (count, branch) => {
    if branch.is_covered() {
      count + 1
    } else {
      count
    }
  })
  output.write_string("BRF:\{branches.length()}\n")
  output.write_string("BRH:\{branches_hit}\n")
  output.write_string("end_of_record\n")
}

///|
/// Encode a coverage report as deterministic LCOV text.
///
/// Files retain report order while functions, lines, and branches are sorted
/// within each record. Summary fields are calculated from the actual entries.
pub fn to_lcov(report : CoverageReport) -> String raise CoverageError {
  let output = StringBuilder()
  let canonical = canonicalize_report(report)
  for file in canonical.files {
    write_lcov_file(output, file)
  }
  output.to_string()
}