///|
pub fn z_scores(data : Array[Double]) -> Array[Double] {
  let center = mean(data)
  let scale = population_stddev(data)
  let result = []
  for value in data {
    result.push(if scale == 0.0 { 0.0 } else { (value - center) / scale })
  }
  result
}

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

///|
pub fn robust_standardize(data : Array[Double]) -> Array[Double] {
  let center = median(data)
  let scale = mad(data)
  let result = []
  for value in data {
    result.push(robust_z_score(value, center, scale))
  }
  result
}

///|
pub fn min_max_scale(
  data : Array[Double],
  lower? : Double = 0.0,
  upper? : Double = 1.0,
) -> Array[Double] {
  if lower >= upper {
    abort("lower bound must be less than upper bound")
  }
  if data.length() == 0 {
    return []
  }
  let minimum = min_value(data)
  let maximum = max_value(data)
  let result = []
  for value in data {
    if maximum == minimum {
      result.push((lower + upper) / 2.0)
    } else {
      result.push(
        lower + (value - minimum) / (maximum - minimum) * (upper - lower),
      )
    }
  }
  result
}

///|
pub fn robust_min_max_scale(
  data : Array[Double],
  lower? : Double = 0.0,
  upper? : Double = 1.0,
) -> Array[Double] {
  if lower >= upper {
    abort("lower bound must be less than upper bound")
  }
  if data.length() == 0 {
    return []
  }
  let low = quantile(data, 0.05)
  let high = quantile(data, 0.95)
  let clipped = []
  for value in data {
    clipped.push(clamp_double(value, low, high))
  }
  min_max_scale(clipped, lower~, upper~)
}

///|
pub fn l1_normalize(data : Array[Double]) -> Array[Double] {
  let scale = sum_absolute(data)
  let result = []
  for value in data {
    result.push(if scale == 0.0 { 0.0 } else { value / scale })
  }
  result
}

///|
pub fn l2_normalize(data : Array[Double]) -> Array[Double] {
  let scale = sum_squared(data).sqrt()
  let result = []
  for value in data {
    result.push(if scale == 0.0 { 0.0 } else { value / scale })
  }
  result
}

///|
pub fn center_by_median(data : Array[Double]) -> Array[Double] {
  let center = median(data)
  let result = []
  for value in data {
    result.push(value - center)
  }
  result
}

///|
pub fn center_by_mean(data : Array[Double]) -> Array[Double] {
  let center = mean(data)
  let result = []
  for value in data {
    result.push(value - center)
  }
  result
}

///|
pub fn scale_by_mad(data : Array[Double]) -> Array[Double] {
  let scale = mad(data)
  let result = []
  for value in data {
    result.push(if scale == 0.0 { 0.0 } else { value / scale })
  }
  result
}

///|
pub fn robust_center_scale(data : Array[Double]) -> Array[Double] {
  [median(data), mad(data)]
}

///|
pub fn quantile_transform(
  data : Array[Double],
  output_lower? : Double = 0.0,
  output_upper? : Double = 1.0,
) -> Array[Double] {
  if output_lower >= output_upper {
    abort("output_lower must be less than output_upper")
  }
  let result = []
  for value in data {
    let probability = empirical_cdf(data, value)
    result.push(output_lower + probability * (output_upper - output_lower))
  }
  result
}

///|
pub fn rank_normalize(data : Array[Double]) -> Array[Double] {
  if data.length() == 0 {
    return []
  }
  let denominator = if data.length() == 1 {
    1.0
  } else {
    (data.length() - 1).to_double()
  }
  let result = []
  for rank in ranks(data) {
    result.push((rank - 1.0) / denominator)
  }
  result
}

///|
pub fn clip_range(
  data : Array[Double],
  lower : Double,
  upper : Double,
) -> Array[Double] {
  if lower > upper {
    abort("lower must not exceed upper")
  }
  let result = []
  for value in data {
    result.push(clamp_double(value, lower, upper))
  }
  result
}

///|
pub fn soft_clip(
  data : Array[Double],
  center : Double,
  scale : Double,
  threshold : Double,
) -> Array[Double] {
  if scale <= 0.0 || threshold <= 0.0 {
    abort("scale and threshold must be positive")
  }
  let limit = scale * threshold
  let result = []
  for value in data {
    result.push(center + clamp_double(value - center, -limit, limit))
  }
  result
}

///|
pub fn normalize_against_reference(
  data : Array[Double],
  reference : Array[Double],
) -> Array[Double] {
  let center = median(reference)
  let scale = mad(reference)
  let result = []
  for value in data {
    result.push(robust_z_score(value, center, scale))
  }
  result
}

///|
pub fn quantile_map(
  data : Array[Double],
  reference : Array[Double],
) -> Array[Double] {
  if reference.length() == 0 {
    return []
  }
  let result = []
  for value in data {
    result.push(quantile(reference, empirical_cdf(data, value)))
  }
  result
}

///|
pub fn difference_from_baseline(
  data : Array[Double],
  baseline : Double,
) -> Array[Double] {
  let result = []
  for value in data {
    result.push(value - baseline)
  }
  result
}

///|
pub fn percent_change(data : Array[Double], baseline : Double) -> Array[Double] {
  if baseline == 0.0 {
    abort("baseline must not be zero")
  }
  let result = []
  for value in data {
    result.push((value - baseline) / abs_double(baseline))
  }
  result
}

///|
pub fn robust_percent_change(
  data : Array[Double],
  reference : Array[Double],
) -> Array[Double] {
  let baseline = median(reference)
  percent_change(data, if baseline == 0.0 { 1.0 } else { baseline })
}

///|
pub fn winsorized_standardize(
  data : Array[Double],
  trim_percent : Double,
) -> Array[Double] {
  z_scores(winsorize(data, trim_percent))
}

///|
pub fn robust_clip_and_center(
  data : Array[Double],
  lower_probability : Double,
  upper_probability : Double,
) -> Array[Double] {
  center_by_median(
    clip_by_quantiles(data, lower_probability, upper_probability),
  )
}

///|
pub fn signed_log_scale(data : Array[Double]) -> Array[Double] {
  let result = []
  for value in data {
    let magnitude = abs_double(value)
    let transformed = if magnitude == 0.0 { 0.0 } else { magnitude.sqrt() }
    result.push(sign_double(value) * transformed)
  }
  result
}

///|
pub fn rank_centered(data : Array[Double]) -> Array[Double] {
  let result = []
  let center = (data.length().to_double() + 1.0) / 2.0
  for rank in ranks(data) {
    result.push(rank - center)
  }
  result
}

///|
pub fn monotone_scale(data : Array[Double], factor : Double) -> Array[Double] {
  if factor <= 0.0 {
    abort("factor must be positive")
  }
  let result = []
  for value in data {
    result.push(value * factor)
  }
  result
}

///|
pub fn affine_transform(
  data : Array[Double],
  offset : Double,
  factor : Double,
) -> Array[Double] {
  let result = []
  for value in data {
    result.push(offset + factor * value)
  }
  result
}