///|
fn csv_escape(value : String) -> String {
  if value.contains(",") || value.contains("\"") || value.contains("\n") {
    "\"" + value.replace(old="\"", new="\"\"") + "\""
  } else {
    value
  }
}

///|
pub fn render_batch_csv(batch : BatchRun) -> String {
  render_measurements_csv(batch.records)
}

///|
pub fn render_batch_markdown(batch : BatchRun) -> String {
  let summary = batch.summary()
  let out = StringBuilder()
  out.write_string("# Batch Measurement Report\n\n")
  out.write_string("Batch: ")
  out.write_string(batch.id)
  out.write_string("\n\n")
  out.write_string("- Total: ")
  out.write_string(summary.total.to_string())
  out.write_string("\n- Accepted: ")
  out.write_string((summary.pass + summary.warn).to_string())
  out.write_string("\n- Rejected: ")
  out.write_string(summary.fail.to_string())
  out.write_string("\n- Acceptance rate: ")
  out.write_string(summary.acceptance_rate.to_string())
  out.write_string(
    "\n\n| Name | Value | Unit | Decision | Deviation |\n| --- | ---: | --- | --- | ---: |\n",
  )
  for measurement in batch.records {
    out.write_string("| ")
    out.write_string(csv_escape(measurement.name))
    out.write_string(" | ")
    out.write_string(measurement.value.to_string())
    out.write_string(" | ")
    out.write_string(measurement.unit)
    out.write_string(" | ")
    out.write_string(decision_name(measurement.decision))
    out.write_string(" | ")
    out.write_string(measurement.deviation.to_string())
    out.write_string(" |\n")
  }
  out.to_string()
}

///|
fn decision_name(decision : MeasurementDecision) -> String {
  match decision {
    Pass => "PASS"
    Warn => "WARN"
    Fail => "FAIL"
  }
}

///|
pub fn render_pipeline_markdown(
  result : PipelineResult,
  part_id : String,
) -> String {
  let out = StringBuilder()
  out.write_string("# Pipeline Result\n\nPart: ")
  out.write_string(part_id)
  out.write_string("\n\nStatus: ")
  out.write_string(decision_name(pipeline_decision(result)))
  out.write_string("\nConfidence: ")
  out.write_string(result.confidence.to_string())
  out.write_string("\n\n")
  if result.measurements.is_empty() {
    out.write_string("No measurements were produced.\n")
  } else {
    out.write_string(
      "| Item | Value | Unit | Decision |\n| --- | ---: | --- | --- |\n",
    )
    for measurement in result.measurements {
      out.write_string("| ")
      out.write_string(measurement.name)
      out.write_string(" | ")
      out.write_string(measurement.value.to_string())
      out.write_string(" | ")
      out.write_string(measurement.unit)
      out.write_string(" | ")
      out.write_string(decision_name(measurement.decision))
      out.write_string(" |\n")
    }
  }
  if !result.diagnostics.is_empty() {
    out.write_string("\n## Diagnostics\n")
    for diagnostic in result.diagnostics {
      out.write_string("- [")
      out.write_string(decision_name(diagnostic.severity))
      out.write_string("] ")
      out.write_string(diagnostic.stage)
      out.write_string(": ")
      out.write_string(diagnostic.message)
      out.write_string("\n")
    }
  }
  out.to_string()
}

///|
pub fn render_quality_markdown(summary : QualitySummary) -> String {
  let out = StringBuilder()
  out.write_string("## Quality\n\n")
  out.write_string("| Metric | Value |\n| --- | ---: |\n")
  out.write_string("| Signal to noise | ")
  out.write_string(summary.signal_to_noise.to_string())
  out.write_string(" |\n| Edge count | ")
  out.write_string(summary.edge_count.to_string())
  out.write_string(" |\n| Confidence | ")
  out.write_string(summary.confidence.to_string())
  out.write_string(" |\n| Stable | ")
  out.write_string(summary.stable.to_string())
  out.write_string(" |\n")
  for note in summary.notes {
    out.write_string("\n> ")
    out.write_string(note)
  }
  out.to_string()
}

///|
pub fn render_diagnostics_csv(events : Array[DiagnosticEvent]) -> String {
  let out = StringBuilder()
  out.write_string("code,stage,detail,value\n")
  for event in events {
    out.write_string(csv_escape(event.code))
    out.write_string(",")
    out.write_string(csv_escape(event.stage))
    out.write_string(",")
    out.write_string(csv_escape(event.detail))
    out.write_string(",")
    out.write_string(event.value.to_string())
    out.write_string("\n")
  }
  out.to_string()
}

///|
pub fn render_calibration_markdown(fit : CalibrationFit) -> String {
  let summary = residual_summary(fit)
  let out = StringBuilder()
  out.write_string("## Calibration\n\n")
  out.write_string("- RMS error: ")
  out.write_string(fit.rms_error.to_string())
  out.write_string("\n")
  out.write_string("- Maximum error: ")
  out.write_string(fit.max_error.to_string())
  out.write_string("\n")
  out.write_string("- Residual median: ")
  out.write_string(summary.median.to_string())
  out.write_string("\n")
  out.write_string("- Residual P95: ")
  out.write_string(summary.percentile(0.95).to_string())
  out.write_string("\n")
  out.to_string()
}

///|
pub fn render_component_table(components : Array[Component]) -> String {
  let out = StringBuilder()
  out.write_string(
    "| Label | Area | Perimeter | Circularity | Centroid |\n| ---: | ---: | ---: | ---: | --- |\n",
  )
  for component in components {
    out.write_string("| ")
    out.write_string(component.label.to_string())
    out.write_string(" | ")
    out.write_string(component.area.to_string())
    out.write_string(" | ")
    out.write_string(component.perimeter.to_string())
    out.write_string(" | ")
    out.write_string(component.circularity.to_string())
    out.write_string(" | (")
    out.write_string(component.centroid.x.to_string())
    out.write_string(", ")
    out.write_string(component.centroid.y.to_string())
    out.write_string(") |\n")
  }
  out.to_string()
}

///|
pub fn render_histogram_text(histogram : Histogram) -> String {
  let out = StringBuilder()
  let span = histogram.maximum - histogram.minimum
  for i in 0..