///|
pub fn process_frame(
  image : GrayImage,
  config : CaliperConfig,
  tolerance : Tolerance,
) -> PipelineResult {
  let validation = validate_image(image)
  let config_validation = validate_caliper_config(config)
  let result = measure_caliper(image, config, tolerance)
  let diagnostics = validation
  diagnostics.append(config_validation)
  diagnostics.append(result.diagnostics)
  {
    ..result,
    diagnostics,
    accepted: result.accepted && diagnostics_ok(diagnostics),
  }
}

///|
pub fn process_frames(
  images : Array[GrayImage],
  config : CaliperConfig,
  tolerance : Tolerance,
) -> BatchRun {
  let batch = BatchRun::new("frames")
  for image in images {
    let result = process_frame(image, config, tolerance)
    for measurement in result.measurements {
      batch.add(measurement)
    }
  }
  batch
}

///|
pub fn process_pyramid(
  image : GrayImage,
  config : CaliperConfig,
  tolerance : Tolerance,
  levels : Int,
) -> PipelineResult {
  let pyramid = build_pyramid(image, levels)
  if pyramid.is_empty() {
    {
      measurements: [],
      diagnostics: [diagnostic_error("pyramid", "no pyramid levels")],
      accepted: false,
      confidence: 0.0,
    }
  } else {
    let level = pyramid[0]
    process_frame(level.image, config, tolerance)
  }
}

///|
pub fn frame_quality(image : GrayImage) -> QualitySummary {
  let profile = summarize_profile(
    edge_profile(
      image,
      Point::Point(x=0.0, y=Double::from_int(image.height / 2)),
      Point::Point(
        x=Double::from_int((image.width - 1).max(0)),
        y=Double::from_int(image.height / 2),
      ),
    ),
    0.05,
  )
  profile_quality(profile, 2.0)
}

///|
pub fn frame_diagnostics(
  image : GrayImage,
  config : CaliperConfig,
  tolerance : Tolerance,
) -> Array[DiagnosticEvent] {
  let result = diagnostics_for_image(image)
  let profile = summarize_profile(
    edge_profile(image, config.start, config.finish),
    config.threshold,
    mode=config.mode,
  )
  result.append(quality_diagnostics(image, profile))
  let tolerance_events = [
    diagnostic_event("LOWER", "tolerance", "lower bound", tolerance.lower),
    diagnostic_event("UPPER", "tolerance", "upper bound", tolerance.upper),
  ]
  result.append(tolerance_events)
  result
}

///|
pub fn sliding_window(
  values : Array[Double],
  width : Int,
  step : Int,
) -> Array[Array[Double]] {
  let result : Array[Array[Double]] = []
  let safe_width = width.max(1)
  let safe_step = step.max(1)
  let mut start = 0
  while start < values.length() {
    let window : Array[Double] = []
    for i in start..<(start + safe_width).min(values.length()) {
      window.push(values[i])
    }
    if !window.is_empty() {
      result.push(window)
    }
    start += safe_step
  }
  result
}

///|
pub fn window_statistics(
  values : Array[Double],
  width : Int,
  step : Int,
) -> Array[StatsSummary] {
  sliding_window(values, width, step).map(fn(window) {
    describe_samples(window)
  })
}

///|
pub fn rolling_decisions(
  values : Array[Double],
  tolerance : Tolerance,
  width : Int,
) -> Array[MeasurementDecision] {
  let result : Array[MeasurementDecision] = []
  for window in sliding_window(values, width, 1) {
    result.push(tolerance.judge(mean_value(window)))
  }
  result
}

///|
pub fn first_failure(decisions : Array[MeasurementDecision]) -> Int? {
  for i in 0.. Int {
  let mut best = 0
  let mut current = 0
  for decision in decisions {
    if decision == target {
      current += 1
      best = best.max(current)
    } else {
      current = 0
    }
  }
  best
}

///|
pub fn frame_acceptance_rate(results : Array[PipelineResult]) -> Double {
  if results.is_empty() {
    0.0
  } else {
    let mut accepted = 0
    for result in results {
      if result.accepted {
        accepted += 1
      }
    }
    Double::from_int(accepted) / Double::from_int(results.length())
  }
}

///|
pub fn merge_pipeline_results(
  results : Array[PipelineResult],
) -> PipelineResult {
  if results.is_empty() {
    { measurements: [], diagnostics: [], accepted: false, confidence: 0.0 }
  } else {
    let mut result = results[0]
    for item in results[1:] {
      result = suite_merge(result, item)
    }
    result
  }
}

///|
pub fn pipeline_timeline(results : Array[PipelineResult]) -> Array[Double] {
  results.map(fn(result) { result.confidence })
}

///|
pub fn timeline_summary(results : Array[PipelineResult]) -> StatsSummary {
  describe_samples(pipeline_timeline(results))
}

///|
pub fn frame_report(results : Array[PipelineResult]) -> String {
  let out = StringBuilder()
  out.write_string("frame,accepted,confidence,decision\n")
  for i in 0..