///|
/// A column-oriented feature matrix for one numeric signal.
pub struct FeatureFrame {
  source : Array[Double]
  columns : Array[Array[Double]]
  names : Array[String]
  row_count : Int
}

///|
/// Configuration for deterministic feature construction.
pub struct FeatureConfig {
  lags : Array[Int]
  windows : Array[Int]
  include_differences : Bool
  include_ratios : Bool
  include_robust_scale : Bool
}

///|
pub fn feature_default_config() -> FeatureConfig {
  {
    lags: [1, 2, 7],
    windows: [3, 7, 14],
    include_differences: true,
    include_ratios: true,
    include_robust_scale: true,
  }
}

///|
pub fn feature_config(
  lags : Array[Int],
  windows : Array[Int],
  include_differences : Bool,
  include_ratios : Bool,
  include_robust_scale : Bool,
) -> FeatureConfig {
  { lags, windows, include_differences, include_ratios, include_robust_scale }
}

///|
pub fn feature_lag(
  data : Array[Double],
  lag : Int,
  fill : Double,
) -> Array[Double] {
  let result = []
  let distance = if lag < 0 { 0 } else { lag }
  for index = 0; index < data.length(); index = index + 1 {
    if index < distance {
      result.push(fill)
    } else {
      result.push(data[index - distance])
    }
  }
  result
}

///|
pub fn feature_lead(
  data : Array[Double],
  lead : Int,
  fill : Double,
) -> Array[Double] {
  let result = []
  let distance = if lead < 0 { 0 } else { lead }
  for index = 0; index < data.length(); index = index + 1 {
    let source = index + distance
    if source >= data.length() {
      result.push(fill)
    } else {
      result.push(data[source])
    }
  }
  result
}

///|
pub fn feature_difference(data : Array[Double], lag : Int) -> Array[Double] {
  let result = []
  let distance = if lag <= 0 { 1 } else { lag }
  for index = 0; index < data.length(); index = index + 1 {
    if index < distance {
      result.push(0.0)
    } else {
      result.push(data[index] - data[index - distance])
    }
  }
  result
}

///|
pub fn feature_absolute_difference(
  data : Array[Double],
  lag : Int,
) -> Array[Double] {
  let differences = feature_difference(data, lag)
  let result = []
  for value in differences {
    result.push(abs_double(value))
  }
  result
}

///|
pub fn feature_second_difference(
  data : Array[Double],
  lag : Int,
) -> Array[Double] {
  feature_difference(feature_difference(data, lag), lag)
}

///|
pub fn feature_percent_change(data : Array[Double], lag : Int) -> Array[Double] {
  let result = []
  let distance = if lag <= 0 { 1 } else { lag }
  for index = 0; index < data.length(); index = index + 1 {
    if index < distance || abs_double(data[index - distance]) <= 1.0e-12 {
      result.push(0.0)
    } else {
      result.push(
        (data[index] - data[index - distance]) /
        abs_double(data[index - distance]),
      )
    }
  }
  result
}

///|
pub fn feature_log_change(data : Array[Double], lag : Int) -> Array[Double] {
  let result = []
  let distance = if lag <= 0 { 1 } else { lag }
  for index = 0; index < data.length(); index = index + 1 {
    if index < distance || data[index] <= 0.0 || data[index - distance] <= 0.0 {
      result.push(0.0)
    } else {
      result.push(drift_log(data[index]) - drift_log(data[index - distance]))
    }
  }
  result
}

///|
pub fn feature_ratio(
  data : Array[Double],
  lag : Int,
  fallback : Double,
) -> Array[Double] {
  let result = []
  let distance = if lag <= 0 { 1 } else { lag }
  for index = 0; index < data.length(); index = index + 1 {
    if index < distance || abs_double(data[index - distance]) <= 1.0e-12 {
      result.push(fallback)
    } else {
      result.push(data[index] / data[index - distance])
    }
  }
  result
}

///|
pub fn feature_rolling_mean(
  data : Array[Double],
  window : Int,
) -> Array[Double] {
  rolling_mean(data, if window <= 0 { 1 } else { window })
}

///|
pub fn feature_rolling_median(
  data : Array[Double],
  window : Int,
) -> Array[Double] {
  rolling_median(data, if window <= 0 { 1 } else { window })
}

///|
pub fn feature_rolling_mad(data : Array[Double], window : Int) -> Array[Double] {
  rolling_mad(data, if window <= 0 { 1 } else { window })
}

///|
pub fn feature_rolling_iqr(data : Array[Double], window : Int) -> Array[Double] {
  rolling_iqr(data, if window <= 0 { 1 } else { window })
}

///|
pub fn feature_rolling_min(data : Array[Double], window : Int) -> Array[Double] {
  rolling_min(data, if window <= 0 { 1 } else { window })
}

///|
pub fn feature_rolling_max(data : Array[Double], window : Int) -> Array[Double] {
  rolling_max(data, if window <= 0 { 1 } else { window })
}

///|
pub fn feature_rolling_range(
  data : Array[Double],
  window : Int,
) -> Array[Double] {
  rolling_range(data, if window <= 0 { 1 } else { window })
}

///|
pub fn feature_rolling_slope(
  data : Array[Double],
  window : Int,
) -> Array[Double] {
  let result = []
  let width = if window <= 1 { 2 } else { window }
  for index = 0; index < data.length(); index = index + 1 {
    let start = if index + 1 < width { 0 } else { index + 1 - width }
    let values = []
    let coordinates = []
    for cursor = start; cursor <= index; cursor = cursor + 1 {
      values.push(data[cursor])
      coordinates.push((cursor - start).to_double())
    }
    if values.length() < 2 {
      result.push(0.0)
    } else {
      result.push(linear_regression(coordinates, values).slope)
    }
  }
  result
}

///|
pub fn feature_rolling_signal_to_noise(
  data : Array[Double],
  window : Int,
) -> Array[Double] {
  robust_moving_signal_to_noise(data, if window <= 0 { 1 } else { window })
}

///|
pub fn feature_rolling_z(data : Array[Double], window : Int) -> Array[Double] {
  rolling_z_scores(data, if window <= 0 { 1 } else { window })
}

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

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

///|
pub fn feature_min_max(
  data : Array[Double],
  lower : Double,
  upper : Double,
) -> Array[Double] {
  robust_min_max_scale(data, lower~, upper~)
}

///|
pub fn feature_clip(
  data : Array[Double],
  lower : Double,
  upper : Double,
) -> Array[Double] {
  clip_range(data, lower, upper)
}

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

///|
pub fn feature_residual_from_rolling(
  data : Array[Double],
  window : Int,
) -> Array[Double] {
  let baseline = feature_rolling_median(data, window)
  difference_from_baseline(data, median(baseline))
}

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

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

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

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

///|
pub fn feature_is_zero(data : Array[Double], tolerance : Double) -> Array[Bool] {
  let result = []
  let limit = if tolerance < 0.0 { -tolerance } else { tolerance }
  for value in data {
    result.push(abs_double(value) <= limit)
  }
  result
}

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

///|
pub fn feature_threshold_flag(
  data : Array[Double],
  lower : Double,
  upper : Double,
) -> Array[Bool] {
  let result = []
  for value in data {
    result.push(value < lower || value > upper)
  }
  result
}

///|
pub fn feature_quantile_distance(
  data : Array[Double],
  probability : Double,
) -> Array[Double] {
  let level = quantile(data, probability)
  feature_deviation_from_level(data, level)
}

///|
pub fn feature_quantile_flag(
  data : Array[Double],
  probability : Double,
  multiplier : Double,
) -> Array[Bool] {
  let level = quantile(data, probability)
  let scale = mad(data)
  let result = []
  for value in data {
    result.push(abs_double(value - level) > multiplier * scale)
  }
  result
}

///|
pub fn feature_autocorrelation(data : Array[Double], lag : Int) -> Double {
  robust_autocorrelation(data, if lag <= 0 { 1 } else { lag })
}

///|
pub fn feature_autocorrelation_series(
  data : Array[Double],
  max_lag : Int,
) -> Array[Double] {
  autocorrelation_series(data, if max_lag < 0 { 0 } else { max_lag })
}

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

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

///|
pub fn feature_mean_abs_change(data : Array[Double]) -> Double {
  mean_absolute_error(data, feature_lag(data, 1, 0.0))
}

///|
pub fn feature_median_abs_change(data : Array[Double]) -> Double {
  median_absolute_error(data, feature_lag(data, 1, 0.0))
}

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

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

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

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

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

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

///|
pub fn feature_entropy(data : Array[Double], bins : Int) -> Double {
  let snapshot = drift_snapshot_with_bins(data, data, bins)
  let mut result = 0.0
  for probability in snapshot.probabilities {
    if probability > 0.0 {
      result -= probability * drift_log(probability)
    }
  }
  result
}

///|
pub fn feature_missing_fraction(data : Array[Double]) -> Double {
  if data.length() == 0 {
    0.0
  } else {
    quality_missing_indices(data).length().to_double() /
    data.length().to_double()
  }
}

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

///|
pub fn feature_outlier_fraction(
  data : Array[Double],
  threshold : Double,
) -> Double {
  outlier_fraction(outlier_indices_z(data, threshold~), data.length())
}

///|
pub fn feature_quality_score(
  data : Array[Double],
  rule : QualityRule,
) -> Double {
  quality_report(data, rule).quality_score
}

///|
pub fn feature_frame(
  data : Array[Double],
  config : FeatureConfig,
) -> FeatureFrame {
  let columns = []
  let names = []
  for lag in config.lags {
    columns.push(feature_lag(data, lag, 0.0))
    names.push("lag_" + lag.to_string())
  }
  for window in config.windows {
    columns.push(feature_rolling_median(data, window))
    names.push("rolling_median_" + window.to_string())
    columns.push(feature_rolling_mad(data, window))
    names.push("rolling_mad_" + window.to_string())
    columns.push(feature_rolling_slope(data, window))
    names.push("rolling_slope_" + window.to_string())
  }
  if config.include_differences {
    for lag in config.lags {
      columns.push(feature_difference(data, lag))
      names.push("difference_" + lag.to_string())
    }
  }
  if config.include_ratios {
    for lag in config.lags {
      columns.push(feature_ratio(data, lag, 1.0))
      names.push("ratio_" + lag.to_string())
    }
  }
  if config.include_robust_scale {
    columns.push(feature_robust_standardize(data))
    names.push("robust_standardized")
  }
  { source: data, columns, names, row_count: data.length() }
}

///|
pub fn FeatureFrame::column_count(self : FeatureFrame) -> Int {
  self.columns.length()
}

///|
pub fn FeatureFrame::row_count(self : FeatureFrame) -> Int {
  self.row_count
}

///|
pub fn FeatureFrame::names(self : FeatureFrame) -> Array[String] {
  self.names.copy()
}

///|
pub fn FeatureFrame::source(self : FeatureFrame) -> Array[Double] {
  self.source.copy()
}

///|
pub fn FeatureFrame::column(self : FeatureFrame, index : Int) -> Array[Double] {
  if index < 0 || index >= self.columns.length() {
    []
  } else {
    self.columns[index].copy()
  }
}

///|
pub fn FeatureFrame::column_by_name(
  self : FeatureFrame,
  name : String,
) -> Array[Double] {
  for index = 0; index < self.names.length(); index = index + 1 {
    if self.names[index] == name {
      return self.columns[index].copy()
    }
  }
  []
}

///|
pub fn FeatureFrame::row(self : FeatureFrame, index : Int) -> Array[Double] {
  let result = []
  if index < 0 || index >= self.row_count {
    return result
  }
  for column in self.columns {
    result.push(column[index])
  }
  result
}

///|
pub fn FeatureFrame::column_means(self : FeatureFrame) -> Array[Double] {
  let result = []
  for column in self.columns {
    result.push(mean(column))
  }
  result
}

///|
pub fn FeatureFrame::column_mads(self : FeatureFrame) -> Array[Double] {
  let result = []
  for column in self.columns {
    result.push(mad(column))
  }
  result
}

///|
pub fn FeatureFrame::quality(
  self : FeatureFrame,
  rule : QualityRule,
) -> Array[Double] {
  let result = []
  for column in self.columns {
    result.push(quality_report(column, rule).quality_score)
  }
  result
}

///|
pub fn FeatureFrame::standardize(self : FeatureFrame) -> Array[Array[Double]] {
  let result = []
  for column in self.columns {
    result.push(robust_standardize(column))
  }
  result
}

///|
pub fn feature_flatten(frame : FeatureFrame) -> Array[Double] {
  let result = []
  for column in frame.columns {
    for value in column {
      result.push(value)
    }
  }
  result
}

///|
pub fn feature_column_ranges(frame : FeatureFrame) -> Array[Double] {
  let result = []
  for column in frame.columns {
    result.push(range(column))
  }
  result
}

///|
pub fn feature_column_scores(frame : FeatureFrame) -> Array[Double] {
  let result = []
  for column in frame.columns {
    result.push(robust_summary_score(column))
  }
  result
}

///|
pub fn feature_frame_score(frame : FeatureFrame) -> Double {
  if frame.columns.length() == 0 {
    0.0
  } else {
    mean(feature_column_scores(frame))
  }
}