///|
/// Stateless numeric feature engineering helpers.
pub fn polynomial_features(
  values : Array[Double],
  degree : Int,
) -> Array[Double] {
  let safe_degree = if degree < 1 { 1 } else { degree }
  let result = Array::make(0, 1.0)
  for value in values {
    let mut power = 1.0
    for _ in 1..<=safe_degree {
      power *= value
      result.push(power)
    }
  }
  result
}

///|
pub fn pairwise_interactions(
  values : Array[Double],
  include_diagonal? : Bool = false,
) -> Array[Double] {
  let result = Array::make(0, 0.0)
  for i in 0.. Array[Double] {
  let result = copy_vector(values)
  result.append(pairwise_interactions(values, include_diagonal~)[:])
  result
}

///|
pub struct QuantileBinner {
  boundaries : Array[Double]
}

///|
pub fn QuantileBinner::new(boundaries : Array[Double]) -> QuantileBinner {
  let sorted = copy_vector(boundaries)
  sorted.sort()
  { boundaries: sorted }
}

///|
pub fn QuantileBinner::bin(self : QuantileBinner, value : Double) -> Int {
  let mut low = 0
  let mut high = self.boundaries.length()
  while low < high {
    let middle = low + (high - low) / 2
    if value <= self.boundaries[middle] {
      high = middle
    } else {
      low = middle + 1
    }
  }
  low
}

///|
pub fn QuantileBinner::bins(self : QuantileBinner) -> Int {
  self.boundaries.length() + 1
}

///|
pub fn QuantileBinner::one_hot(
  self : QuantileBinner,
  value : Double,
) -> Array[Double] {
  one_hot(self.bin(value), self.bins())
}

///|
pub fn QuantileBinner::boundaries(self : QuantileBinner) -> Array[Double] {
  copy_vector(self.boundaries)
}

///|
pub struct OnlineQuantileSketch {
  capacity : Int
  values : Array[Double]
  mut seen : Int
}

///|
pub fn OnlineQuantileSketch::new(capacity? : Int = 256) -> OnlineQuantileSketch {
  { capacity: if capacity < 2 { 2 } else { capacity }, values: [], seen: 0 }
}

///|
pub fn OnlineQuantileSketch::update(
  self : OnlineQuantileSketch,
  value : Double,
) -> Unit {
  self.seen += 1
  self.values.push(value)
  if self.values.length() > self.capacity {
    self.values.sort()
    let stride = self.values.length() / self.capacity
    let compressed = Array::makei(self.capacity, i => self.values[i * stride])
    self.values.clear()
    self.values.append(compressed[:])
  }
}

///|
pub fn OnlineQuantileSketch::quantile(
  self : OnlineQuantileSketch,
  probability : Double,
) -> Double? {
  if self.values.is_empty() {
    None
  } else {
    let sorted = copy_vector(self.values)
    sorted.sort()
    let index = (clamp(probability, 0.0, 1.0) *
    (sorted.length() - 1).to_double()).to_int()
    Some(sorted[index])
  }
}

///|
pub fn OnlineQuantileSketch::median(self : OnlineQuantileSketch) -> Double? {
  self.quantile(0.5)
}

///|
pub fn OnlineQuantileSketch::seen(self : OnlineQuantileSketch) -> Int {
  self.seen
}

///|
pub fn OnlineQuantileSketch::reset(self : OnlineQuantileSketch) -> Unit {
  self.values.clear()
  self.seen = 0
}

///|
pub struct TargetEncoder {
  sums : Map[String, Double]
  counts : Map[String, Double]
  mut prior : RunningMean
  smoothing : Double
}

///|
pub fn TargetEncoder::new(smoothing? : Double = 10.0) -> TargetEncoder {
  {
    sums: {},
    counts: {},
    prior: RunningMean::new(),
    smoothing: if smoothing < 0.0 {
      0.0
    } else {
      smoothing
    },
  }
}

///|
pub fn TargetEncoder::update(
  self : TargetEncoder,
  category : String,
  target : Double,
  weight? : Double = 1.0,
) -> Unit {
  self.sums.update_or_default(category, 0.0, previous => {
    previous + target * weight
  })
  self.counts.update_or_default(category, 0.0, previous => previous + weight)
  self.prior.update(target)
}

///|
pub fn TargetEncoder::encode(self : TargetEncoder, category : String) -> Double {
  let prior = self.prior.value()
  let count = self.counts.get(category).unwrap_or(0.0)
  let sum = self.sums.get(category).unwrap_or(0.0)
  if count + self.smoothing <= 0.0 {
    prior
  } else {
    (sum + self.smoothing * prior) / (count + self.smoothing)
  }
}

///|
pub fn TargetEncoder::known_categories(self : TargetEncoder) -> Int {
  self.counts.length()
}

///|
pub fn TargetEncoder::reset(self : TargetEncoder) -> Unit {
  self.sums.clear()
  self.counts.clear()
  self.prior = RunningMean::new()
}