// Price transforms and rolling utilities (TA-Lib "Statistic Functions" and
// "Price Transform" groups).

///|
/// Applies a per-bar transform of (open, high, low, close); any NaN
/// component maps to NaN.
fn transform(
  candles : @base.Candles,
  f : (Double, Double, Double, Double) -> Double,
) -> Array[Double] raise @base.TaError {
  @base.validate_candles(candles)
  let n = candles.close.length()
  let out : Array[Double] = []
  for i in 0.. Array[Double] raise @base.TaError {
  transform(candles, fn(o : Double, h : Double, l : Double, c : Double) {
    (o + h + l + c) / 4.0
  })
}

///|
/// Median price: `(high + low) / 2`.
pub fn med_price(candles : @base.Candles) -> Array[Double] raise @base.TaError {
  transform(candles, fn(o : Double, h : Double, l : Double, c : Double) {
    let _o = o
    let _c = c
    (h + l) / 2.0
  })
}

///|
/// Typical price (used by CCI and MFI): `(high + low + close) / 3`.
pub fn typ_price(candles : @base.Candles) -> Array[Double] raise @base.TaError {
  transform(candles, fn(o : Double, h : Double, l : Double, c : Double) {
    let _o = o
    (h + l + c) / 3.0
  })
}

///|
/// Weighted close price (TA-Lib `WCLPRICE`): `(high + low + 2 * close) / 4`.
pub fn wcl_price(candles : @base.Candles) -> Array[Double] raise @base.TaError {
  transform(candles, fn(o : Double, h : Double, l : Double, c : Double) {
    let _o = o
    (h + l + 2.0 * c) / 4.0
  })
}

///|
/// Number of leading NaN values produced by `max`/`min`/`sum` for the given
/// period.
pub fn window_lookback(period : Int32) -> Int32 {
  period - 1
}

///|
/// Rolling maximum over a window of `period` values. A NaN inside the window
/// yields NaN.
pub fn max(
  data : Array[Double],
  period : Int32,
) -> Array[Double] raise @base.TaError {
  @base.validate_series(data, period)
  @base.rolling_max(data, period)
}

///|
/// Rolling minimum over a window of `period` values. A NaN inside the window
/// yields NaN.
pub fn min(
  data : Array[Double],
  period : Int32,
) -> Array[Double] raise @base.TaError {
  @base.validate_series(data, period)
  @base.rolling_min(data, period)
}

///|
/// Rolling sum over a window of `period` values. A NaN inside the window
/// yields NaN.
pub fn sum(
  data : Array[Double],
  period : Int32,
) -> Array[Double] raise @base.TaError {
  @base.validate_series(data, period)
  @base.rolling_sum(data, period)
}

///|
/// Rolling extremes over a window: min and max arrays of equal length.
/// A NaN inside the window yields NaN for both.
pub struct MinMaxResult {
  min : Array[Double]
  max : Array[Double]
}

///|
/// Number of leading NaN values produced by `minmax` for the given period.
pub fn minmax_lookback(period : Int32) -> Int32 {
  period - 1
}

///|
/// Rolling minimum and maximum over a window of `period` values, computed in
/// a single pass.
pub fn minmax(
  data : Array[Double],
  period : Int32,
) -> MinMaxResult raise @base.TaError {
  @base.validate_series(data, period)
  let min = @base.rolling_min(data, period)
  let max = @base.rolling_max(data, period)
  { min, max, }
}

///|
/// Number of leading NaN values produced by `avgdev` for the given period.
pub fn avgdev_lookback(period : Int32) -> Int32 {
  period - 1
}

///|
/// Mean absolute deviation over a rolling window:
/// `sum(|x - mean|) / period`. A flat window reads 0; a NaN inside the
/// window yields NaN.
pub fn avgdev(
  data : Array[Double],
  period : Int32,
) -> Array[Double] raise @base.TaError {
  @base.validate_series(data, period)
  let s1 = @base.rolling_sum(data, period)
  let n = data.length()
  let out : Array[Double] = []
  for i in 0..