///|
/// Parameters for robust one-dimensional signal processing.
pub struct SignalRule {
  window : Int
  threshold : Double
  smoothing : Double
  minimum_gap : Int
}

///|
pub struct SignalPeak {
  index : Int
  value : Double
  prominence : Double
  left : Double
  right : Double
}

///|
pub struct SignalChange {
  index : Int
  before : Double
  after : Double
  score : Double
}

///|
pub fn signal_rule(
  window : Int,
  threshold : Double,
  smoothing : Double,
  minimum_gap : Int,
) -> SignalRule {
  {
    window: if window < 1 {
      1
    } else {
      window
    },
    threshold: if threshold < 0.0 {
      -threshold
    } else {
      threshold
    },
    smoothing: if smoothing < 0.0 {
      0.0
    } else if smoothing > 1.0 {
      1.0
    } else {
      smoothing
    },
    minimum_gap: if minimum_gap < 1 {
      1
    } else {
      minimum_gap
    },
  }
}

///|
pub fn signal_identity(data : Array[Double]) -> Array[Double] {
  data.copy()
}

///|
pub fn signal_convolution(
  data : Array[Double],
  kernel : Array[Double],
) -> Array[Double] {
  let result = []
  if data.length() == 0 || kernel.length() == 0 {
    return result
  }
  let radius = kernel.length() / 2
  for index = 0; index < data.length(); index = index + 1 {
    let mut total = 0.0
    let mut weight = 0.0
    for offset = 0; offset < kernel.length(); offset = offset + 1 {
      let source = index + offset - radius
      let bounded = if source < 0 {
        0
      } else if source >= data.length() {
        data.length() - 1
      } else {
        source
      }
      total += data[bounded] * kernel[offset]
      weight += kernel[offset]
    }
    result.push(if weight == 0.0 { 0.0 } else { total / weight })
  }
  result
}

///|
pub fn signal_box_kernel(window : Int) -> Array[Double] {
  let width = if window < 1 { 1 } else { window }
  let result = []
  for _ in 0.. Array[Double] {
  signal_convolution(data, signal_box_kernel(window))
}

///|
pub fn signal_moving_median(
  data : Array[Double],
  window : Int,
) -> Array[Double] {
  let result = []
  let width = if window < 1 { 1 } else { window }
  let radius = width / 2
  for index = 0; index < data.length(); index = index + 1 {
    let values = []
    let start = if index < radius { 0 } else { index - radius }
    let end = if index + radius + 1 > data.length() {
      data.length()
    } else {
      index + radius + 1
    }
    for cursor = start; cursor < end; cursor = cursor + 1 {
      values.push(data[cursor])
    }
    result.push(median(values))
  }
  result
}

///|
pub fn signal_exponential_smooth(
  data : Array[Double],
  alpha : Double,
) -> Array[Double] {
  streaming_ewma(data, alpha)
}

///|
pub fn signal_double_smooth(
  data : Array[Double],
  alpha : Double,
) -> Array[Double] {
  signal_exponential_smooth(signal_exponential_smooth(data, alpha), alpha)
}

///|
pub fn signal_first_difference(data : Array[Double]) -> Array[Double] {
  transform_difference(data, 1)
}

///|
pub fn signal_second_difference(data : Array[Double]) -> Array[Double] {
  signal_first_difference(signal_first_difference(data))
}

///|
pub fn signal_gradient(data : Array[Double]) -> Array[Double] {
  let result = []
  if data.length() == 0 {
    return result
  }
  result.push(0.0)
  for index = 1; index < data.length(); index = index + 1 {
    result.push(data[index] - data[index - 1])
  }
  result
}

///|
pub fn signal_total_variation(data : Array[Double]) -> Double {
  sum_absolute(signal_first_difference(data))
}

///|
pub fn signal_energy(data : Array[Double]) -> Double {
  sum_squared(data)
}

///|
pub fn signal_root_mean_square(data : Array[Double]) -> Double {
  if data.length() == 0 {
    0.0
  } else {
    (signal_energy(data) / data.length().to_double()).sqrt()
  }
}

///|
pub fn signal_zero_crossings(data : Array[Double]) -> Int {
  if data.length() < 2 {
    return 0
  }
  let mut count = 0
  for index = 1; index < data.length(); index = index + 1 {
    if (data[index] < 0.0 && data[index - 1] >= 0.0) ||
      (data[index] >= 0.0 && data[index - 1] < 0.0) {
      count += 1
    }
  }
  count
}

///|
pub fn signal_autocorrelation(data : Array[Double], lag : Int) -> Double {
  let safe_lag = if lag < 0 { 0 } else { lag }
  if data.length() <= safe_lag || data.length() == 0 {
    return 0.0
  }
  let center = mean(data)
  let mut numerator = 0.0
  let mut denominator = 0.0
  for index = 0; index < data.length(); index = index + 1 {
    let deviation = data[index] - center
    denominator += deviation * deviation
    if index + safe_lag < data.length() {
      numerator += deviation * (data[index + safe_lag] - center)
    }
  }
  if denominator == 0.0 {
    0.0
  } else {
    numerator / denominator
  }
}

///|
pub fn signal_autocorrelation_path(
  data : Array[Double],
  maximum_lag : Int,
) -> Array[Double] {
  let result = []
  let count = if maximum_lag < 0 { 0 } else { maximum_lag }
  for lag = 0; lag <= count; lag = lag + 1 {
    result.push(signal_autocorrelation(data, lag))
  }
  result
}

///|
pub fn signal_peak_candidates(
  data : Array[Double],
  threshold : Double,
) -> Array[SignalPeak] {
  let result = []
  if data.length() < 3 {
    return result
  }
  let baseline = median(data)
  let scale = mad(data)
  let safe_threshold = if threshold < 0.0 { -threshold } else { threshold }
  for index = 1; index < data.length() - 1; index = index + 1 {
    let left = data[index - 1]
    let value = data[index]
    let right = data[index + 1]
    let prominence = value - (left + right) / 2.0
    if value >= left &&
      value >= right &&
      prominence > safe_threshold * (if scale == 0.0 { 1.0 } else { scale }) &&
      value > baseline {
      result.push({ index, value, prominence, left, right })
    }
  }
  result
}

///|
pub fn signal_trough_candidates(
  data : Array[Double],
  threshold : Double,
) -> Array[SignalPeak] {
  let inverted = []
  for value in data {
    inverted.push(-value)
  }
  let peaks = signal_peak_candidates(inverted, threshold)
  let result = []
  for peak in peaks {
    result.push({
      ..peak,
      value: -peak.value,
      left: -peak.left,
      right: -peak.right,
      prominence: peak.prominence,
    })
  }
  result
}

///|
pub fn signal_peak_indices(peaks : Array[SignalPeak]) -> Array[Int] {
  let result = []
  for peak in peaks {
    result.push(peak.index)
  }
  result
}

///|
pub fn signal_peak_values(peaks : Array[SignalPeak]) -> Array[Double] {
  let result = []
  for peak in peaks {
    result.push(peak.value)
  }
  result
}

///|
pub fn signal_local_range(data : Array[Double], window : Int) -> Array[Double] {
  let result = []
  let width = if window < 1 { 1 } else { window }
  for index = 0; index < data.length(); index = index + 1 {
    let start = if index + 1 > width { index + 1 - width } else { 0 }
    let values = []
    for cursor = start; cursor <= index; cursor = cursor + 1 {
      values.push(data[cursor])
    }
    result.push(range(values))
  }
  result
}

///|
pub fn signal_local_scale(data : Array[Double], window : Int) -> Array[Double] {
  let result = []
  let width = if window < 1 { 1 } else { window }
  for index = 0; index < data.length(); index = index + 1 {
    let start = if index + 1 > width { index + 1 - width } else { 0 }
    let values = []
    for cursor = start; cursor <= index; cursor = cursor + 1 {
      values.push(data[cursor])
    }
    result.push(mad(values))
  }
  result
}

///|
pub fn signal_local_z(data : Array[Double], window : Int) -> Array[Double] {
  transform_rolling_z(data, window)
}

///|
pub fn signal_change_candidates(
  data : Array[Double],
  window : Int,
  threshold : Double,
) -> Array[SignalChange] {
  let result = []
  let width = if window < 1 { 1 } else { window }
  if data.length() <= width * 2 {
    return result
  }
  let safe_threshold = if threshold < 0.0 { -threshold } else { threshold }
  for index = width; index < data.length() - width; index = index + 1 {
    let before = []
    let after = []
    for cursor = index - width; cursor < index; cursor = cursor + 1 {
      before.push(data[cursor])
    }
    for cursor = index; cursor < index + width; cursor = cursor + 1 {
      after.push(data[cursor])
    }
    let difference = abs_double(mean(after) - mean(before))
    let scale = (mad(before) + mad(after)) / 2.0
    let score = if scale == 0.0 { difference } else { difference / scale }
    if score >= safe_threshold {
      result.push({ index, before: mean(before), after: mean(after), score })
    }
  }
  result
}

///|
pub fn signal_change_indices(changes : Array[SignalChange]) -> Array[Int] {
  let result = []
  for change in changes {
    result.push(change.index)
  }
  result
}

///|
pub fn signal_change_scores(changes : Array[SignalChange]) -> Array[Double] {
  let result = []
  for change in changes {
    result.push(change.score)
  }
  result
}

///|
pub fn signal_denoise(data : Array[Double], rule : SignalRule) -> Array[Double] {
  let median_smoothed = signal_moving_median(data, rule.window)
  let average_smoothed = signal_moving_average(median_smoothed, rule.window)
  let result = []
  for index = 0; index < data.length(); index = index + 1 {
    result.push(
      rule.smoothing * average_smoothed[index] +
      (1.0 - rule.smoothing) * data[index],
    )
  }
  result
}

///|
pub fn signal_noise_estimate(data : Array[Double]) -> Double {
  mad(signal_first_difference(data)) / 1.4826
}

///|
pub fn signal_snr(data : Array[Double]) -> Double {
  let noise = signal_noise_estimate(data)
  if noise == 0.0 {
    0.0
  } else {
    signal_root_mean_square(data) / noise
  }
}

///|
pub fn signal_outlier_repaired(
  data : Array[Double],
  threshold : Double,
) -> Array[Double] {
  let smoothed = signal_moving_median(data, 3)
  let scale = signal_noise_estimate(data)
  let safe_scale = if scale == 0.0 { 1.0 } else { scale }
  let result = []
  for index = 0; index < data.length(); index = index + 1 {
    if abs_double(data[index] - smoothed[index]) > threshold * safe_scale {
      result.push(smoothed[index])
    } else {
      result.push(data[index])
    }
  }
  result
}

///|
pub fn signal_resample_linear(
  data : Array[Double],
  target_size : Int,
) -> Array[Double] {
  let result = []
  let count = if target_size < 0 { 0 } else { target_size }
  if count == 0 || data.length() == 0 {
    return result
  }
  if data.length() == 1 {
    for _ in 0..= data.length() { left } else { left + 1 }
    let fraction = position - left.to_double()
    result.push(data[left] + fraction * (data[right] - data[left]))
  }
  result
}

///|
pub fn signal_quantize(data : Array[Double], levels : Int) -> Array[Double] {
  let result = []
  let count = if levels < 2 { 2 } else { levels }
  let minimum = min_value(data)
  let maximum = max_value(data)
  let width = maximum - minimum
  for value in data {
    if width == 0.0 {
      result.push(value)
    } else {
      let index = ((value - minimum) / width * (count - 1).to_double()).to_int()
      result.push(minimum + index.to_double() / (count - 1).to_double() * width)
    }
  }
  result
}

///|
pub fn signal_run_lengths(
  data : Array[Double],
  tolerance : Double,
) -> Array[Int] {
  let result = []
  if data.length() == 0 {
    return result
  }
  let mut run = 1
  for index = 1; index < data.length(); index = index + 1 {
    if abs_double(data[index] - data[index - 1]) <= tolerance {
      run += 1
    } else {
      result.push(run)
      run = 1
    }
  }
  result.push(run)
  result
}

///|
pub fn signal_flatline_fraction(
  data : Array[Double],
  tolerance : Double,
) -> Double {
  let runs = signal_run_lengths(data, tolerance)
  let mut repeated = 0
  for run in runs {
    if run > 1 {
      repeated += run
    }
  }
  if data.length() == 0 {
    0.0
  } else {
    repeated.to_double() / data.length().to_double()
  }
}

///|
pub fn signal_summary(data : Array[Double], window : Int) -> Array[Double] {
  [
    signal_energy(data),
    signal_root_mean_square(data),
    signal_total_variation(data),
    signal_noise_estimate(data),
    signal_snr(data),
    signal_flatline_fraction(data, 0.0),
    signal_autocorrelation(data, 1),
    signal_autocorrelation(data, window),
  ]
}