///|
/// Return a Hann-weighted dot product for two same-length windows.
pub fn weighted_dot(left : Array[Double], right : Array[Double]) -> Double {
  let count = if left.length() < right.length() {
    left.length()
  } else {
    right.length()
  }
  if count == 0 {
    return 0.0
  }
  let mut total = 0.0
  for index = 0; index < count; index = index + 1 {
    let weight = 0.5 -
      0.5 * @math.cos(2.0 * @math.PI * index.to_double() / count.to_double())
    total += weight * left[index] * right[index]
  }
  total
}

///|
/// Return the normalized weighted mean-square error.
pub fn weighted_mse(left : Array[Double], right : Array[Double]) -> Double {
  let count = if left.length() < right.length() {
    left.length()
  } else {
    right.length()
  }
  if count == 0 {
    return 0.0
  }
  let mut total = 0.0
  let mut weights = 0.0
  for index = 0; index < count; index = index + 1 {
    let weight = 0.5 -
      0.5 * @math.cos(2.0 * @math.PI * index.to_double() / count.to_double())
    let difference = left[index] - right[index]
    total += weight * difference * difference
    weights += weight
  }
  if weights == 0.0 {
    0.0
  } else {
    total / weights
  }
}

///|
/// Return a normalized root-mean-square error.
pub fn normalized_rmse(
  reference : Array[Double],
  candidate : Array[Double],
) -> Double {
  let denominator = rms_samples(reference)
  if denominator == 0.0 {
    rms_samples(candidate)
  } else {
    weighted_mse(reference, candidate).sqrt() / denominator
  }
}

///|
/// Apply a polynomial baseline of degree one to a copy.
pub fn remove_polynomial_baseline(
  data : Array[Double],
  sample_rate : Double,
) -> Array[Double] {
  let result = data.copy()
  if data.length() < 2 || sample_rate <= 0.0 {
    return result
  }
  let slope = baseline_slope(data)
  let intercept = mean_samples(data) -
    slope * (data.length() - 1).to_double() / 2.0
  for index = 0; index < result.length(); index = index + 1 {
    result[index] = result[index] - (intercept + slope * index.to_double())
  }
  result
}

///|
/// Return a stable sample index for a fractional position.
pub fn fractional_index(length : Int, fraction : Double) -> Int {
  if length <= 0 {
    return 0
  }
  let p = fraction.max(0.0).min(1.0)
  (p * (length - 1).to_double()).round().to_int()
}

///|
/// Interpolate a sample at a fractional index.
pub fn sample_at_fraction(data : Array[Double], fraction : Double) -> Double {
  if data.is_empty() {
    return 0.0
  }
  if data.length() == 1 {
    return data[0]
  }
  let p = fraction.max(0.0).min(1.0) * (data.length() - 1).to_double()
  let left = p.to_int()
  if left >= data.length() - 1 {
    data[data.length() - 1]
  } else {
    let t = p - left.to_double()
    data[left] + t * (data[left + 1] - data[left])
  }
}

///|
/// Compute a normalized crossfade between two waveforms.
pub fn crossfade(
  left : Array[Double],
  right : Array[Double],
  fraction : Double,
) -> Array[Double] {
  let count = if left.length() < right.length() {
    left.length()
  } else {
    right.length()
  }
  let result = Array::make(count, 0.0)
  let t = fraction.max(0.0).min(1.0)
  for index = 0; index < count; index = index + 1 {
    result[index] = (1.0 - t) * left[index] + t * right[index]
  }
  result
}

///|
/// Return a normalized triangular weighting vector.
pub fn triangular_weights(length : Int) -> Array[Double] {
  let result = Array::make(if length < 0 { 0 } else { length }, 0.0)
  if length <= 0 {
    return result
  }
  let center = (length - 1).to_double() / 2.0
  for index = 0; index < length; index = index + 1 {
    result[index] = if center == 0.0 {
      1.0
    } else {
      1.0 - (index.to_double() - center).abs() / (center + 1.0)
    }
  }
  result
}

///|
/// Apply arbitrary weights and return a weighted average.
pub fn weighted_average(
  data : Array[Double],
  weights : Array[Double],
) -> Double {
  let count = if data.length() < weights.length() {
    data.length()
  } else {
    weights.length()
  }
  let mut numerator = 0.0
  let mut denominator = 0.0
  for index = 0; index < count; index = index + 1 {
    numerator += data[index] * weights[index]
    denominator += weights[index]
  }
  if denominator == 0.0 {
    0.0
  } else {
    numerator / denominator
  }
}