///|
/// Features computed for one timestamped signal window.
pub struct WindowFeature {
  start_idx : Int
  end_idx : Int
  duration_s : Double
  summary : SignalSummary
  area : Double
  peak_index : Int
  time_to_peak_s : Double
  slope_to_peak : Double
  zero_crossings : Int
} derive(Debug, Eq)

///|
fn count_crossings(values : Array[Double], center : Double) -> Int {
  let mut previous = 0
  let mut count = 0
  for value in values {
    if !value.is_nan() && !value.is_inf() {
      let sign = if value > center {
        1
      } else if value < center {
        -1
      } else {
        0
      }
      if sign != 0 {
        if previous != 0 && sign != previous {
          count = count + 1
        }
        previous = sign
      }
    }
  }
  count
}

///|
/// Computes integral, extrema, timing, slope, and crossing features safely.
pub fn compute_window_features(
  timestamps : Array[Double],
  values : Array[Double],
  start_idx : Int,
  end_idx : Int,
) -> WindowFeature {
  let common = if timestamps.length() < values.length() {
    timestamps.length()
  } else {
    values.length()
  }
  let range = clamp_range(common, start_idx, end_idx)
  let summary = summarize_signal(copy_range(values, range.start, range.end))
  if range.start >= range.end {
    return {
      start_idx: range.start,
      end_idx: range.end,
      duration_s: 0.0,
      summary,
      area: 0.0,
      peak_index: -1,
      time_to_peak_s: 0.0,
      slope_to_peak: 0.0,
      zero_crossings: 0,
    }
  }
  let end_time = if range.end < timestamps.length() {
    timestamps[range.end]
  } else {
    timestamps[timestamps.length() - 1]
  }
  let duration = end_time - timestamps[range.start]
  let mut area = 0.0
  for i = range.start; i < range.end - 1; i = i + 1 {
    let dt = timestamps[i + 1] - timestamps[i]
    if dt > 0.0 &&
      !dt.is_nan() &&
      !dt.is_inf() &&
      !values[i].is_nan() &&
      !values[i + 1].is_nan() &&
      !values[i].is_inf() &&
      !values[i + 1].is_inf() {
      area = area + (values[i] + values[i + 1]) * dt / 2.0
    }
  }
  let mut peak_index = range.start
  for i = range.start + 1; i < range.end; i = i + 1 {
    if values[i] > values[peak_index] {
      peak_index = i
    }
  }
  let peak_time = timestamps[peak_index] - timestamps[range.start]
  let slope = if peak_time > 0.0 {
    (values[peak_index] - values[range.start]) / peak_time
  } else {
    0.0
  }
  let window = copy_range(values, range.start, range.end)
  {
    start_idx: range.start,
    end_idx: range.end,
    duration_s: if duration > 0.0 {
      duration
    } else {
      0.0
    },
    summary,
    area,
    peak_index,
    time_to_peak_s: if peak_time > 0.0 {
      peak_time
    } else {
      0.0
    },
    slope_to_peak: slope,
    zero_crossings: count_crossings(window, summary.mean),
  }
}

///|
/// Computes overlapping signal windows with deterministic tail handling.
pub fn compute_sliding_signal_features(
  timestamps : Array[Double],
  values : Array[Double],
  window_samples : Int,
  step_samples : Int,
) -> Array[WindowFeature] {
  let common = if timestamps.length() < values.length() {
    timestamps.length()
  } else {
    values.length()
  }
  if common < 2 || window_samples <= 0 || step_samples <= 0 {
    return []
  }
  let result : Array[WindowFeature] = []
  let mut start = 0
  while start < common {
    let end = if start + window_samples > common {
      common
    } else {
      start + window_samples
    }
    if end - start < 2 {
      break
    }
    result.push(compute_window_features(timestamps, values, start, end))
    start = start + step_samples
  }
  result
}