///|
pub fn CaliperConfig::new(
  start~ : Point,
  finish~ : Point,
  threshold~ : Double,
  mode? : EdgeMode = Both,
  expected_edges? : Int = 2,
  unit? : String = "px",
) -> CaliperConfig {
  {
    start,
    finish,
    threshold: threshold.max(0.0),
    mode,
    expected_edges: expected_edges.max(1),
    unit,
  }
}

///|
pub fn EdgePolicy::new(
  low_threshold~ : Double,
  high_threshold~ : Double,
  mode? : EdgeMode = Both,
  min_strength? : Double = 0.0,
) -> EdgePolicy {
  {
    low_threshold: low_threshold.max(0.0),
    high_threshold: high_threshold.max(low_threshold),
    mode,
    min_strength: min_strength.max(0.0),
  }
}

///|
fn strongest_edge(edges : Array[EdgePoint]) -> EdgePoint? {
  if edges.is_empty() {
    None
  } else {
    let mut result = edges[0]
    for edge in edges[1:] {
      if edge.strength > result.strength {
        result = edge
      }
    }
    Some(result)
  }
}

///|
pub fn summarize_profile(
  samples : Array[ProfileSample],
  threshold : Double,
  mode? : EdgeMode = Both,
) -> ProfileSummary {
  let edges = ProfileSample::detect_edges(samples, threshold, mode~)
  let gradients = samples.map(fn(sample) { sample.gradient })
  let noise = mean_absolute_deviation(gradients)
  let strongest = strongest_edge(edges)
  {
    samples,
    edges,
    strongest_edge: strongest,
    mean_gradient: mean_value(gradients),
    noise,
    signal_to_noise: match strongest {
      Some(edge) =>
        if noise <= 0.0000000001 {
          edge.strength
        } else {
          edge.strength / noise
        }
      None => 0.0
    },
  }
}

///|
pub fn profile_quality(
  profile : ProfileSummary,
  minimum_signal_to_noise : Double,
) -> QualitySummary {
  let notes : Array[String] = []
  if profile.edges.is_empty() {
    notes.push("no edge candidate")
  }
  if profile.signal_to_noise < minimum_signal_to_noise {
    notes.push("edge signal below threshold")
  }
  if profile.noise > 0.25 {
    notes.push("profile noise is high")
  }
  {
    signal_to_noise: profile.signal_to_noise,
    edge_count: profile.edges.length(),
    confidence: clamp_double(
      profile.signal_to_noise / (minimum_signal_to_noise.max(0.0001) * 2.0),
      0.0,
      1.0,
    ),
    stable: !profile.edges.is_empty() &&
    profile.signal_to_noise >= minimum_signal_to_noise,
    notes,
  }
}

///|
pub fn measure_caliper(
  image : GrayImage,
  config : CaliperConfig,
  tolerance : Tolerance,
) -> PipelineResult {
  let samples = edge_profile(image, config.start, config.finish)
  let profile = summarize_profile(samples, config.threshold, mode=config.mode)
  let quality = profile_quality(profile, 2.0)
  let diagnostics : Array[PipelineDiagnostic] = []
  let measurements : Array[Measurement] = []
  if profile.edges.length() < config.expected_edges {
    diagnostics.push({
      stage: "edge-detection",
      message: "expected edge count was not reached",
      severity: Fail,
    })
  } else {
    let width = edge_pair_width(
      profile.edges,
      Vector2::Vector2(
        x=config.finish.x - config.start.x,
        y=config.finish.y - config.start.y,
      ),
    )
    let measurement = analyze_gap("caliper", width, 1.0, tolerance)
    measurements.push({ ..measurement, unit: config.unit })
  }
  {
    measurements,
    diagnostics,
    accepted: !measurements.is_empty() && measurements[0].decision != Fail,
    confidence: quality.confidence,
  }
}

///|
pub fn measure_caliper_with_scale(
  image : GrayImage,
  config : CaliperConfig,
  scale : Double,
  tolerance : Tolerance,
) -> PipelineResult {
  let result = measure_caliper(image, config, tolerance)
  if result.measurements.is_empty() {
    result
  } else {
    let measurement = result.measurements[0]
    let scaled = {
      ..measurement,
      value: measurement.value * scale,
      deviation: measurement.value * scale - tolerance.nominal,
      decision: tolerance.judge(measurement.value * scale),
    }
    { ..result, measurements: [scaled], accepted: scaled.decision != Fail }
  }
}

///|
pub fn pipeline_has_error(result : PipelineResult) -> Bool {
  !result.diagnostics.is_empty() || !result.accepted
}

///|
pub fn pipeline_decision(result : PipelineResult) -> MeasurementDecision {
  if result.measurements.is_empty() {
    Fail
  } else {
    let mut decision = Pass
    for measurement in result.measurements {
      match measurement.decision {
        Fail => decision = Fail
        Warn => if decision == Pass { decision = Warn }
        Pass => ()
      }
    }
    decision
  }
}

///|
pub fn pipeline_notes(result : PipelineResult) -> Array[String] {
  result.diagnostics.map(fn(diagnostic) {
    diagnostic.stage + ": " + diagnostic.message
  })
}

///|
pub fn repeat_caliper(
  image : GrayImage,
  config : CaliperConfig,
  tolerance : Tolerance,
  repetitions : Int,
) -> StatsSummary {
  let values : Array[Double] = []
  for _ in 0.. Array[Point] {
  profile.edges.map(fn(edge) { edge.point })
}

///|
pub fn profile_edge_strengths(profile : ProfileSummary) -> Array[Double] {
  profile.edges.map(fn(edge) { edge.strength })
}

///|
pub fn edge_polarity_count(profile : ProfileSummary, polarity : Int) -> Int {
  let mut result = 0
  for edge in profile.edges {
    if edge.polarity == polarity {
      result += 1
    }
  }
  result
}

///|
pub fn choose_edge_pair(
  profile : ProfileSummary,
  minimum_spacing : Double,
) -> Array[EdgePoint] {
  if profile.edges.length() < 2 {
    []
  } else {
    let mut first = 0
    let mut second = 1
    let mut best = -1.0
    for i in 0..= minimum_spacing {
          profile.edges[i].strength + profile.edges[j].strength
        } else {
          -1.0
        }
        if score > best {
          best = score
          first = i
          second = j
        }
      }
    }
    if best < 0.0 {
      []
    } else {
      [profile.edges[first], profile.edges[second]]
    }
  }
}

///|
pub fn edge_pair_midpoint(edges : Array[EdgePoint]) -> Point {
  if edges.is_empty() {
    Point::Point(x=0.0, y=0.0)
  } else {
    let mut x = 0.0
    let mut y = 0.0
    for edge in edges {
      x += edge.point.x
      y += edge.point.y
    }
    Point::Point(
      x=x / Double::from_int(edges.length()),
      y=y / Double::from_int(edges.length()),
    )
  }
}

///|
pub fn edge_pair_orientation(edges : Array[EdgePoint]) -> Double {
  if edges.length() < 2 {
    0.0
  } else {
    measure_angle(edges[0].point, edges[edges.length() - 1].point)
  }
}

///|
pub fn profile_baseline(samples : Array[ProfileSample]) -> Double {
  if samples.is_empty() {
    0.0
  } else {
    (samples[0].value + samples[samples.length() - 1].value) / 2.0
  }
}

///|
pub fn subtract_profile_baseline(
  samples : Array[ProfileSample],
) -> Array[ProfileSample] {
  let baseline = profile_baseline(samples)
  samples.map(fn(sample) { { ..sample, value: sample.value - baseline } })
}

///|
pub fn profile_dynamic_range(samples : Array[ProfileSample]) -> Double {
  let values = samples.map(fn(sample) { sample.value })
  maximum_value(values) - minimum_value(values)
}

///|
pub fn profile_gradient_range(samples : Array[ProfileSample]) -> Double {
  let gradients = samples.map(fn(sample) { sample.gradient })
  maximum_value(gradients) - minimum_value(gradients)
}

///|
pub fn profile_is_monotonic(samples : Array[ProfileSample]) -> Bool {
  if samples.length() < 2 {
    true
  } else {
    let mut direction = 0
    let mut valid = true
    for sample in samples[1:] {
      let current = if sample.gradient > 0.0 {
        1
      } else if sample.gradient < 0.0 {
        -1
      } else {
        0
      }
      if direction == 0 {
        direction = current
      } else if current != 0 && current != direction {
        valid = false
      }
    }
    valid
  }
}