///|
/// Linearly maps sensor values from an input range to an engineering range.
pub fn scale_signal(
  signal : ArrayView[Double],
  input_min~ : Double,
  input_max~ : Double,
  output_min~ : Double,
  output_max~ : Double,
) -> Array[Double] raise SpectrumError {
  if signal.length() == 0 {
    raise EmptySignal
  }
  if input_min >= input_max {
    raise InvalidArgument(message="input range must have positive width")
  }
  let width = input_max - input_min
  let output_width = output_max - output_min
  signal.map(value => output_min + (value - input_min) * output_width / width)
}

///|
/// Subtracts a known sensor baseline without changing the input array.
pub fn remove_baseline(
  signal : ArrayView[Double],
  baseline : Double,
) -> Array[Double] raise SpectrumError {
  if signal.length() == 0 {
    raise EmptySignal
  }
  signal.map(value => value - baseline)
}

///|
/// Computes robust z-scores using median and median absolute deviation.
pub fn robust_z_scores(
  signal : ArrayView[Double],
) -> Array[Double] raise SpectrumError {
  if signal.length() == 0 {
    raise EmptySignal
  }
  let center = median(signal)
  let deviations = signal.map(value => (value - center).abs())
  let mad = median(deviations)
  if mad == 0.0 {
    return Array::make(signal.length(), 0.0)
  }
  let scale = 1.4826 * mad
  signal.map(value => (value - center) / scale)
}

///|
/// Returns sample indices whose robust z-score exceeds the given magnitude.
pub fn detect_outliers(
  signal : ArrayView[Double],
  z_threshold~ : Double,
) -> Array[Int] raise SpectrumError {
  if signal.length() == 0 {
    raise EmptySignal
  }
  if z_threshold <= 0.0 {
    raise InvalidArgument(message="z_threshold must be positive")
  }
  let scores = robust_z_scores(signal)
  let outliers : Array[Int] = []
  for i in 0.. z_threshold {
      outliers.push(i)
    }
  }
  outliers
}

///|
/// Returns the baseline-corrected signal and its estimated offset.
pub fn baseline_corrected(
  signal : ArrayView[Double],
) -> (Double, Array[Double]) raise SpectrumError {
  if signal.length() == 0 {
    raise EmptySignal
  }
  let baseline = median(signal)
  (baseline, remove_baseline(signal, baseline))
}