///|
pub fn profile_values(samples : Array[ProfileSample]) -> Array[Double] {
  samples.map(fn(sample) { sample.value })
}

///|
pub fn profile_gradients(samples : Array[ProfileSample]) -> Array[Double] {
  samples.map(fn(sample) { sample.gradient })
}

///|
pub fn profile_from_values(
  values : Array[Double],
  start : Point,
  finish : Point,
) -> Array[ProfileSample] {
  let result : Array[ProfileSample] = []
  let denominator = Double::from_int((values.length() - 1).max(1))
  for i in 0.. Array[ProfileSample] {
  if samples.is_empty() || count <= 0 {
    []
  } else {
    let values = profile_values(samples)
    let start = samples[0].point
    let finish = samples[samples.length() - 1].point
    let result_values : Array[Double] = []
    for i in 0.. Array[ProfileSample] {
  profile_from_values(
    moving_average(profile_values(samples), window),
    if samples.is_empty() {
      Point::Point(x=0.0, y=0.0)
    } else {
      samples[0].point
    },
    if samples.is_empty() {
      Point::Point(x=0.0, y=0.0)
    } else {
      samples[samples.length() - 1].point
    },
  )
}

///|
pub fn profile_exponential_smooth(
  samples : Array[ProfileSample],
  alpha : Double,
) -> Array[ProfileSample] {
  let values = exponential_smooth(profile_values(samples), alpha)
  profile_from_values(
    values,
    if samples.is_empty() {
      Point::Point(x=0.0, y=0.0)
    } else {
      samples[0].point
    },
    if samples.is_empty() {
      Point::Point(x=0.0, y=0.0)
    } else {
      samples[samples.length() - 1].point
    },
  )
}

///|
pub fn profile_normalize(
  samples : Array[ProfileSample],
) -> Array[ProfileSample] {
  let values = profile_values(samples)
  let low = minimum_value(values)
  let high = maximum_value(values)
  let span = high - low
  samples.map(fn(sample) {
    {
      ..sample,
      value: if span <= 0.0 {
        0.0
      } else {
        (sample.value - low) / span
      },
    }
  })
}

///|
pub fn profile_derivative(
  samples : Array[ProfileSample],
) -> Array[ProfileSample] {
  let result : Array[ProfileSample] = []
  for i in 0.. Array[ProfileSample] {
  profile_derivative(profile_derivative(samples))
}

///|
pub fn profile_peaks(
  samples : Array[ProfileSample],
  threshold : Double,
) -> Array[EdgePoint] {
  let result : Array[EdgePoint] = []
  if samples.length() >= 3 {
    for i in 1..<(samples.length() - 1) {
      let current = samples[i].gradient
      if current.abs() >= threshold &&
        current.abs() >= samples[i - 1].gradient.abs() &&
        current.abs() >= samples[i + 1].gradient.abs() {
        result.push({
          point: samples[i].point,
          strength: current.abs(),
          polarity: if current >= 0.0 {
            1
          } else {
            -1
          },
        })
      }
    }
  }
  result
}

///|
pub fn profile_valleys(
  samples : Array[ProfileSample],
  threshold : Double,
) -> Array[EdgePoint] {
  let inverted = samples.map(fn(sample) {
    { ..sample, gradient: -sample.gradient }
  })
  profile_peaks(inverted, threshold)
}

///|
pub fn profile_zero_crossings(samples : Array[ProfileSample]) -> Array[Point] {
  let result : Array[Point] = []
  for i in 1.. Double {
  let mut result = 0.0
  for sample in samples {
    result += sample.value * sample.value
  }
  result
}

///|
pub fn profile_gradient_energy(samples : Array[ProfileSample]) -> Double {
  let mut result = 0.0
  for sample in samples {
    result += sample.gradient * sample.gradient
  }
  result
}

///|
pub fn profile_cross_correlation(
  first : Array[ProfileSample],
  second : Array[ProfileSample],
) -> Double {
  let count = first.length().min(second.length())
  if count == 0 {
    0.0
  } else {
    let first_values : Array[Double] = []
    let second_values : Array[Double] = []
    for i in 0.. Int {
  let limit = maximum_shift.max(0)
  let mut best_shift = 0
  let mut best_score = -2.0
  for shift in -limit..<=limit {
    let left : Array[Double] = []
    let right : Array[Double] = []
    for i in 0..= 0 && j < second.length() {
        left.push(first[i].value)
        right.push(second[j].value)
      }
    }
    let score = correlation_values(left, right)
    if score > best_score {
      best_score = score
      best_shift = shift
    }
  }
  best_shift
}

///|
pub fn profile_quantized(
  samples : Array[ProfileSample],
  levels : Int,
) -> Array[ProfileSample] {
  let count = levels.max(2)
  samples.map(fn(sample) {
    let index = (sample.value.clamp(min=0.0, max=1.0) *
      Double::from_int(count - 1))
      .round()
      .to_int()
    { ..sample, value: Double::from_int(index) / Double::from_int(count - 1) }
  })
}

///|
pub fn profile_outlier_repaired(
  samples : Array[ProfileSample],
  z_limit : Double,
) -> Array[ProfileSample] {
  let values = profile_values(samples)
  let repaired = reject_outliers(values, z_limit)
  if repaired.length() == values.length() {
    samples
  } else {
    profile_from_values(
      repaired,
      samples[0].point,
      samples[samples.length() - 1].point,
    )
  }
}

///|
pub fn profile_window(
  samples : Array[ProfileSample],
  first : Int,
  last : Int,
) -> Array[ProfileSample] {
  let start = first.max(0).min(samples.length())
  let finish = last.max(start).min(samples.length())
  let result : Array[ProfileSample] = []
  for i in start.. Double {
  if samples.length() < 2 {
    0.0
  } else {
    distance(samples[0].point, samples[samples.length() - 1].point)
  }
}

///|
pub fn profile_sample_spacing(samples : Array[ProfileSample]) -> Double {
  if samples.length() < 2 {
    0.0
  } else {
    profile_distance(samples) / Double::from_int(samples.length() - 1)
  }
}