///|
/// Stateful feature transforms for production event pipelines.
///
/// Transforms are kept separate from models so the same preprocessing state
/// can be persisted, monitored, and reused by training and serving paths.
pub(all) enum ScalarTransformKind {
  Identity
  Clip(lower~ : Double, upper~ : Double)
  Log1p
  SignedLog
  Sqrt
  Abs
} derive(Debug, Eq)

///|
pub fn scalar_transform_catalog() -> Array[ScalarTransformKind] {
  [Identity, Log1p, SignedLog, Sqrt, Abs]
}

///|
pub fn apply_scalar_transform(
  kind : ScalarTransformKind,
  value : Double,
) -> Double {
  let safe = if value.is_nan() || value.is_inf() { 0.0 } else { value }
  match kind {
    Identity => safe
    Clip(lower~, upper~) => clamp(safe, lower, upper)
    Log1p => if safe <= -1.0 { 0.0 } else { @math.ln(1.0 + safe) }
    SignedLog =>
      if safe >= 0.0 {
        @math.ln(1.0 + safe)
      } else {
        -@math.ln(1.0 - safe)
      }
    Sqrt => if safe <= 0.0 { 0.0 } else { safe.sqrt() }
    Abs => safe.abs()
  }
}

///|
pub fn invert_scalar_transform(
  kind : ScalarTransformKind,
  value : Double,
) -> Double {
  match kind {
    Identity => value
    Clip(lower~, upper~) => clamp(value, lower, upper)
    Log1p => @math.exp(value) - 1.0
    SignedLog =>
      if value >= 0.0 {
        @math.exp(value) - 1.0
      } else {
        1.0 - @math.exp(-value)
      }
    Sqrt => if value <= 0.0 { 0.0 } else { value * value }
    Abs => value.abs()
  }
}

///|
pub struct ScalarTransform {
  kind : ScalarTransformKind
  mut applied : Int
  mut invalid : Int
}

///|
pub fn ScalarTransform::new(kind : ScalarTransformKind) -> ScalarTransform {
  { kind, applied: 0, invalid: 0 }
}

///|
pub fn ScalarTransform::kind(self : ScalarTransform) -> ScalarTransformKind {
  self.kind
}

///|
pub fn ScalarTransform::apply(self : ScalarTransform, value : Double) -> Double {
  if value.is_nan() || value.is_inf() {
    self.invalid += 1
  } else {
    self.applied += 1
  }
  apply_scalar_transform(self.kind, value)
}

///|
pub fn ScalarTransform::apply_array(
  self : ScalarTransform,
  values : Array[Double],
) -> Array[Double] {
  values.map(value => self.apply(value))
}

///|
pub fn ScalarTransform::inverse(
  self : ScalarTransform,
  value : Double,
) -> Double {
  invert_scalar_transform(self.kind, value)
}

///|
pub fn ScalarTransform::applied(self : ScalarTransform) -> Int {
  self.applied
}

///|
pub fn ScalarTransform::invalid(self : ScalarTransform) -> Int {
  self.invalid
}

///|
pub fn ScalarTransform::reset(self : ScalarTransform) -> Unit {
  self.applied = 0
  self.invalid = 0
}

///|
pub struct VectorTransformPipeline {
  transforms : Array[ScalarTransform]
  expected_dimension : Int
  mut rows : Int
  mut rejected : Int
}

///|
pub fn VectorTransformPipeline::new(
  expected_dimension : Int,
) -> VectorTransformPipeline {
  {
    transforms: [],
    expected_dimension: if expected_dimension < 0 {
      0
    } else {
      expected_dimension
    },
    rows: 0,
    rejected: 0,
  }
}

///|
pub fn VectorTransformPipeline::add(
  self : VectorTransformPipeline,
  transform : ScalarTransform,
) -> Unit {
  self.transforms.push(transform)
}

///|
pub fn VectorTransformPipeline::transform(
  self : VectorTransformPipeline,
  values : Array[Double],
) -> Array[Double] {
  if values.length() != self.expected_dimension {
    self.rejected += 1
    []
  } else {
    let output = copy_vector(values)
    for transform in self.transforms {
      for i in 0.. Array[Double] {
  let output = copy_vector(values)
  for transform in self.transforms {
    for i in 0.. Int {
  self.expected_dimension
}

///|
pub fn VectorTransformPipeline::transform_count(
  self : VectorTransformPipeline,
) -> Int {
  self.transforms.length()
}

///|
pub fn VectorTransformPipeline::rows(self : VectorTransformPipeline) -> Int {
  self.rows
}

///|
pub fn VectorTransformPipeline::rejected(self : VectorTransformPipeline) -> Int {
  self.rejected
}

///|
pub fn VectorTransformPipeline::reset(self : VectorTransformPipeline) -> Unit {
  for transform in self.transforms {
    transform.reset()
  }
  self.rows = 0
  self.rejected = 0
}

///|
pub struct OnlineMinMaxScaler {
  minimum : Array[Double]
  maximum : Array[Double]
  initialized : Array[Bool]
  mut observations : Int
}

///|
pub fn OnlineMinMaxScaler::new(dimension : Int) -> OnlineMinMaxScaler {
  let size = if dimension < 0 { 0 } else { dimension }
  {
    minimum: Array::make(size, 0.0),
    maximum: Array::make(size, 0.0),
    initialized: Array::make(size, false),
    observations: 0,
  }
}

///|
pub fn OnlineMinMaxScaler::update(
  self : OnlineMinMaxScaler,
  values : Array[Double],
) -> Unit {
  let limit = if values.length() < self.minimum.length() {
    values.length()
  } else {
    self.minimum.length()
  }
  for i in 0.. self.maximum[i] {
        self.maximum[i] = value
      }
    }
  }
  self.observations += 1
}

///|
pub fn OnlineMinMaxScaler::transform(
  self : OnlineMinMaxScaler,
  values : Array[Double],
  lower? : Double = 0.0,
  upper? : Double = 1.0,
) -> Array[Double] {
  let output = Array::makei(self.minimum.length(), i => {
    if i >= values.length() || !self.initialized[i] {
      lower
    } else {
      let span = self.maximum[i] - self.minimum[i]
      if span.abs() <= 1.0e-12 {
        (lower + upper) / 2.0
      } else {
        lower + (values[i] - self.minimum[i]) / span * (upper - lower)
      }
    }
  })
  output.map(value => {
    clamp(
      value,
      if lower < upper {
        lower
      } else {
        upper
      },
      if lower < upper {
        upper
      } else {
        lower
      },
    )
  })
}

///|
pub fn OnlineMinMaxScaler::inverse(
  self : OnlineMinMaxScaler,
  values : Array[Double],
  lower? : Double = 0.0,
  upper? : Double = 1.0,
) -> Array[Double] {
  Array::makei(self.minimum.length(), i => {
    if i >= values.length() || !self.initialized[i] {
      0.0
    } else {
      let span = self.maximum[i] - self.minimum[i]
      if span.abs() <= 1.0e-12 {
        self.minimum[i]
      } else {
        self.minimum[i] + (values[i] - lower) / (upper - lower) * span
      }
    }
  })
}

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

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

///|
pub fn OnlineMinMaxScaler::observations(self : OnlineMinMaxScaler) -> Int {
  self.observations
}

///|
pub fn OnlineMinMaxScaler::reset(self : OnlineMinMaxScaler) -> Unit {
  self.minimum.fill(0.0)
  self.maximum.fill(0.0)
  self.initialized.fill(false)
  self.observations = 0
}

///|
pub struct OnlineRobustScaler {
  sketches : Array[OnlineQuantileSketch]
  mut observations : Int
}

///|
pub fn OnlineRobustScaler::new(
  dimension : Int,
  capacity? : Int = 256,
) -> OnlineRobustScaler {
  let size = if dimension < 0 { 0 } else { dimension }
  {
    sketches: Array::makei(size, _ => OnlineQuantileSketch::new(capacity~)),
    observations: 0,
  }
}

///|
pub fn OnlineRobustScaler::update(
  self : OnlineRobustScaler,
  values : Array[Double],
) -> Unit {
  let limit = if values.length() < self.sketches.length() {
    values.length()
  } else {
    self.sketches.length()
  }
  for i in 0.. Array[Double] {
  Array::makei(self.sketches.length(), i => {
    let median = self.sketches[i].quantile(0.5).unwrap_or(0.0)
    let low = self.sketches[i].quantile(0.25).unwrap_or(median)
    let high = self.sketches[i].quantile(0.75).unwrap_or(median)
    let span = high - low
    if i >= values.length() || span.abs() <= 1.0e-12 {
      0.0
    } else {
      (values[i] - median) / span
    }
  })
}

///|
pub fn OnlineRobustScaler::quantile(
  self : OnlineRobustScaler,
  feature : Int,
  probability : Double,
) -> Double? {
  self.sketches
  .get(feature)
  .map(sketch => sketch.quantile(probability).unwrap_or(0.0))
}

///|
pub fn OnlineRobustScaler::observations(self : OnlineRobustScaler) -> Int {
  self.observations
}

///|
pub fn OnlineRobustScaler::reset(self : OnlineRobustScaler) -> Unit {
  for sketch in self.sketches {
    sketch.reset()
  }
  self.observations = 0
}

///|
pub struct OneHotEncoder {
  values : Map[String, Int]
  unknown_index : Int?
  mut next_index : Int
  mut unknowns : Int
}

///|
pub fn OneHotEncoder::new(unknown_index? : Int) -> OneHotEncoder {
  { values: {}, unknown_index, next_index: 0, unknowns: 0 }
}

///|
pub fn OneHotEncoder::fit(self : OneHotEncoder, value : String) -> Int {
  self.values.get_or_init(value, () => {
    let index = self.next_index
    self.next_index += 1
    index
  })
}

///|
pub fn OneHotEncoder::encode(self : OneHotEncoder, value : String) -> Int {
  match self.values.get(value) {
    Some(index) => index
    None => {
      self.unknowns += 1
      self.unknown_index.unwrap_or(self.next_index)
    }
  }
}

///|
pub fn OneHotEncoder::vector(
  self : OneHotEncoder,
  value : String,
) -> Array[Double] {
  let output = Array::make(self.dimension(), 0.0)
  let index = self.encode(value)
  if index >= 0 && index < output.length() {
    output[index] = 1.0
  }
  output
}

///|
pub fn OneHotEncoder::dimension(self : OneHotEncoder) -> Int {
  match self.unknown_index {
    Some(index) =>
      if index + 1 > self.next_index {
        index + 1
      } else {
        self.next_index
      }
    None => self.next_index
  }
}

///|
pub fn OneHotEncoder::contains(self : OneHotEncoder, value : String) -> Bool {
  self.values.contains(value)
}

///|
pub fn OneHotEncoder::unknowns(self : OneHotEncoder) -> Int {
  self.unknowns
}

///|
pub fn OneHotEncoder::entries(self : OneHotEncoder) -> Array[(String, Int)] {
  self.values.to_array()
}

///|
pub fn OneHotEncoder::reset(self : OneHotEncoder) -> Unit {
  self.values.clear()
  self.next_index = 0
  self.unknowns = 0
}

///|
pub struct FeatureCrossEncoder {
  hasher : FeatureHasher
  separator : String
  mut crosses : Int
}

///|
pub fn FeatureCrossEncoder::new(
  buckets : Int,
  separator? : String = "=",
) -> FeatureCrossEncoder {
  { hasher: FeatureHasher::new(buckets), separator, crosses: 0 }
}

///|
pub fn FeatureCrossEncoder::encode(
  self : FeatureCrossEncoder,
  values : Array[String],
) -> SparseVector {
  let features : Array[String] = []
  for i in 0.. SparseVector {
  let features = generate_feature_crosses(values, order~)
  self.crosses += features.length()
  self.hasher.encode(features)
}

///|
pub fn FeatureCrossEncoder::buckets(self : FeatureCrossEncoder) -> Int {
  self.hasher.buckets()
}

///|
pub fn FeatureCrossEncoder::crosses(self : FeatureCrossEncoder) -> Int {
  self.crosses
}

///|
pub fn FeatureCrossEncoder::reset(self : FeatureCrossEncoder) -> Unit {
  self.crosses = 0
}

///|
pub struct MissingValueImputer {
  policy : MissingValuePolicy
  means : Array[Double]
  counts : Array[Int]
  last : Array[Double]
  mut imputed : Int
  mut rejected : Int
}

///|
pub fn MissingValueImputer::new(
  dimension : Int,
  policy? : MissingValuePolicy = Zero,
) -> MissingValueImputer {
  let size = if dimension < 0 { 0 } else { dimension }
  {
    policy,
    means: Array::make(size, 0.0),
    counts: Array::make(size, 0),
    last: Array::make(size, 0.0),
    imputed: 0,
    rejected: 0,
  }
}

///|
pub fn MissingValueImputer::observe(
  self : MissingValueImputer,
  values : Array[Double],
) -> Unit {
  let limit = if values.length() < self.means.length() {
    values.length()
  } else {
    self.means.length()
  }
  for i in 0.. Array[Double]? {
  let mut invalid = values.length() != self.means.length()
  let output = Array::make(self.means.length(), 0.0)
  for i in 0..= values.length() ||
      values[i].is_nan() ||
      values[i].is_inf()
    if missing {
      invalid = true
      match self.policy {
        Reject => {
          self.rejected += 1
          output[i] = 0.0
        }
        Zero => {
          self.imputed += 1
          output[i] = 0.0
        }
        Mean => {
          self.imputed += 1
          output[i] = self.means[i]
        }
        CarryForward => {
          self.imputed += 1
          output[i] = self.last[i]
        }
      }
    } else {
      output[i] = values[i]
    }
  }
  match self.policy {
    Reject => if invalid { None } else { Some(output) }
    _ => Some(output)
  }
}

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

///|
pub fn MissingValueImputer::imputed(self : MissingValueImputer) -> Int {
  self.imputed
}

///|
pub fn MissingValueImputer::rejected(self : MissingValueImputer) -> Int {
  self.rejected
}

///|
pub fn MissingValueImputer::reset(self : MissingValueImputer) -> Unit {
  self.means.fill(0.0)
  self.counts.fill(0)
  self.last.fill(0.0)
  self.imputed = 0
  self.rejected = 0
}

///|
pub struct TimeFeatureEncoder {
  period : Int64
  include_sine : Bool
  include_cosine : Bool
  include_elapsed : Bool
  mut origin : Int64?
}

///|
pub fn TimeFeatureEncoder::new(
  period : Int64,
  include_sine? : Bool = true,
  include_cosine? : Bool = true,
  include_elapsed? : Bool = false,
) -> TimeFeatureEncoder {
  {
    period: if period <= 0 {
      1
    } else {
      period
    },
    include_sine,
    include_cosine,
    include_elapsed,
    origin: None,
  }
}

///|
pub fn TimeFeatureEncoder::transform(
  self : TimeFeatureEncoder,
  timestamp : Int64,
) -> Array[Double] {
  let safe = timestamp % self.period
  let phase = 2.0 * @math.PI * safe.to_double() / self.period.to_double()
  let output : Array[Double] = []
  if self.include_sine {
    output.push(@math.sin(phase))
  }
  if self.include_cosine {
    output.push(@math.cos(phase))
  }
  if self.include_elapsed {
    let start = self.origin.unwrap_or(timestamp)
    output.push((timestamp - start).to_double() / self.period.to_double())
  }
  if self.origin is None {
    self.origin = Some(timestamp)
  }
  output
}

///|
pub fn TimeFeatureEncoder::feature_count(self : TimeFeatureEncoder) -> Int {
  (if self.include_sine { 1 } else { 0 }) +
  (if self.include_cosine { 1 } else { 0 }) +
  (if self.include_elapsed { 1 } else { 0 })
}

///|
pub fn TimeFeatureEncoder::period(self : TimeFeatureEncoder) -> Int64 {
  self.period
}

///|
pub fn TimeFeatureEncoder::reset(self : TimeFeatureEncoder) -> Unit {
  self.origin = None
}

///|
pub struct TextVocabulary {
  counts : Map[String, Int]
  maximum : Int
  mut documents : Int
  mut unknowns : Int
}

///|
pub fn TextVocabulary::new(maximum? : Int = 10000) -> TextVocabulary {
  {
    counts: {},
    maximum: if maximum < 1 {
      1
    } else {
      maximum
    },
    documents: 0,
    unknowns: 0,
  }
}

///|
pub fn TextVocabulary::observe(
  self : TextVocabulary,
  tokens : Array[String],
) -> Unit {
  self.documents += 1
  for token in tokens {
    if token != "" {
      self.counts.update_or_default(token, 0, previous => previous + 1)
    }
  }
  if self.counts.length() > self.maximum {
    let entries = self.counts.to_array()
    entries.sort_by((left, right) => left.1 - right.1)
    let remove_count = self.counts.length() - self.maximum
    for i in 0.. Int? {
  let entries = self.counts.keys().to_array()
  for i in 0.. Int {
  self.counts.length()
}

///|
pub fn TextVocabulary::documents(self : TextVocabulary) -> Int {
  self.documents
}

///|
pub fn TextVocabulary::unknowns(self : TextVocabulary) -> Int {
  self.unknowns
}

///|
pub fn TextVocabulary::tokens(self : TextVocabulary) -> Array[String] {
  self.counts.keys().to_array()
}

///|
pub fn TextVocabulary::reset(self : TextVocabulary) -> Unit {
  self.counts.clear()
  self.documents = 0
  self.unknowns = 0
}