///|
pub fn radial_caliper_profile(
  image : GrayImage,
  center : Point,
  angle : Double,
  length : Double,
  samples : Int,
) -> Array[ProfileSample] {
  let direction = Vector2::Vector2(x=@math.cos(angle), y=@math.sin(angle))
  edge_profile(
    image,
    center,
    Point::Point(
      x=center.x + direction.x * length,
      y=center.y + direction.y * length,
    ),
    steps=samples,
  )
}

///|
pub fn radial_edge_points(
  image : GrayImage,
  center : Point,
  angles : Array[Double],
  length : Double,
  threshold : Double,
) -> Array[EdgePoint] {
  let result : Array[EdgePoint] = []
  for angle in angles {
    let profile = radial_caliper_profile(image, center, angle, length, 128)
    let edges = ProfileSample::detect_edges(profile, threshold)
    if !edges.is_empty() {
      let strongest = edges[0]
      let mut best = strongest
      for edge in edges[1:] {
        if edge.strength > best.strength {
          best = edge
        }
      }
      result.push(best)
    }
  }
  result
}

///|
pub fn radial_circle_measurement(
  image : GrayImage,
  center : Point,
  angles : Array[Double],
  length : Double,
  threshold : Double,
  _unit : String,
) -> Circle {
  let edges = radial_edge_points(image, center, angles, length, threshold)
  let points = edges.map(fn(edge) { edge.point })
  if points.length() < 3 {
    Circle::Circle(center~, radius=0.0, rms_error=0.0)
  } else {
    let circle = detect_circle_from_points(points)
    { ..circle, radius: circle.radius, rms_error: circle.rms_error }
  }
}

///|
pub fn multi_angle_widths(
  image : GrayImage,
  center : Point,
  angles : Array[Double],
  length : Double,
  threshold : Double,
) -> Array[Double] {
  let result : Array[Double] = []
  for angle in angles {
    let direction = Vector2::Vector2(x=@math.cos(angle), y=@math.sin(angle))
    let start = Point::Point(
      x=center.x - direction.x * length,
      y=center.y - direction.y * length,
    )
    let finish = Point::Point(
      x=center.x + direction.x * length,
      y=center.y + direction.y * length,
    )
    let profile = summarize_profile(
      edge_profile(image, start, finish),
      threshold,
    )
    if profile.edges.length() >= 2 {
      result.push(edge_pair_width(profile.edges, direction))
    }
  }
  result
}

///|
pub fn width_statistics_by_angle(
  image : GrayImage,
  center : Point,
  angle_count : Int,
  length : Double,
  threshold : Double,
) -> StatsSummary {
  let angles : Array[Double] = []
  for i in 0.. Array[CaliperConfig] {
  let result : Array[CaliperConfig] = []
  for i in 0.. PipelineResult {
  let measurements : Array[Measurement] = []
  let diagnostics : Array[PipelineDiagnostic] = []
  let mut accepted = true
  let mut confidence = 1.0
  for config in configs {
    let current = measure_caliper(image, config, tolerance)
    measurements.append(current.measurements)
    diagnostics.append(current.diagnostics)
    accepted = accepted && current.accepted
    confidence *= current.confidence
  }
  { measurements, diagnostics, accepted, confidence }
}

///|
pub fn caliper_suite_widths(result : PipelineResult) -> Array[Double] {
  measurement_values(result.measurements)
}

///|
pub fn caliper_suite_spread(result : PipelineResult) -> Double {
  repeatability(caliper_suite_widths(result))
}

///|
pub fn caliper_suite_report(
  result : PipelineResult,
  part_id : String,
) -> String {
  render_pipeline_markdown(result, part_id) +
  "\nRepeatability: " +
  caliper_suite_spread(result).to_string()
}

///|
pub fn edge_distance_along_ray(
  edge : EdgePoint,
  center : Point,
  direction : Vector2,
) -> Double {
  Vector2::Vector2(x=edge.point.x - center.x, y=edge.point.y - center.y).dot(
    direction.normalized(),
  )
}

///|
pub fn sort_edges_by_distance(
  edges : Array[EdgePoint],
  center : Point,
  direction : Vector2,
) -> Array[EdgePoint] {
  let result : Array[EdgePoint] = []
  for edge in edges {
    let distance_value = edge_distance_along_ray(edge, center, direction)
    let mut index = 0
    while index < result.length() &&
          edge_distance_along_ray(result[index], center, direction) <=
          distance_value {
      index += 1
    }
    result.insert(index, edge)
  }
  result
}

///|
pub fn nearest_edge(edges : Array[EdgePoint], point : Point) -> EdgePoint? {
  if edges.is_empty() {
    None
  } else {
    let mut result = edges[0]
    let mut best = distance(result.point, point)
    for edge in edges[1:] {
      let current = distance(edge.point, point)
      if current < best {
        result = edge
        best = current
      }
    }
    Some(result)
  }
}

///|
pub fn strongest_edges(
  edges : Array[EdgePoint],
  minimum_strength : Double,
) -> Array[EdgePoint] {
  edges.filter(fn(edge) { edge.strength >= minimum_strength })
}

///|
pub fn merge_nearby_edges(
  edges : Array[EdgePoint],
  distance_limit : Double,
) -> Array[EdgePoint] {
  let result : Array[EdgePoint] = []
  for edge in edges {
    let nearby = nearest_edge(result, edge.point)
    match nearby {
      Some(existing) =>
        if distance(existing.point, edge.point) <= distance_limit {
          if edge.strength > existing.strength {
            for i in 0.. result.push(edge)
    }
  }
  result
}

///|
pub fn edge_confidence(
  edge : EdgePoint,
  profile_noise : Double,
  threshold : Double,
) -> Double {
  let denominator = profile_noise.max(0.0000001) + threshold.max(0.0)
  clamp_double(edge.strength / denominator, 0.0, 1.0)
}

///|
pub fn caliper_direction(config : CaliperConfig) -> Vector2 {
  Vector2::Vector2(
    x=config.finish.x - config.start.x,
    y=config.finish.y - config.start.y,
  ).normalized()
}

///|
pub fn caliper_length(config : CaliperConfig) -> Double {
  distance(config.start, config.finish)
}

///|
pub fn caliper_midpoint(config : CaliperConfig) -> Point {
  Point::Point(
    x=(config.start.x + config.finish.x) / 2.0,
    y=(config.start.y + config.finish.y) / 2.0,
  )
}

///|
pub fn caliper_is_valid(config : CaliperConfig) -> Bool {
  caliper_length(config) > 0.0 &&
  config.expected_edges > 0 &&
  config.threshold >= 0.0
}

///|
pub fn caliper_config_report(config : CaliperConfig) -> String {
  "start=(" +
  config.start.x.to_string() +
  "," +
  config.start.y.to_string() +
  "); finish=(" +
  config.finish.x.to_string() +
  "," +
  config.finish.y.to_string() +
  "); threshold=" +
  config.threshold.to_string()
}