///|
/// Deterministic feature selection diagnostics for numeric feature matrices.
pub struct FeatureScore {
  index : Int
  relevance : Double
  redundancy : Double
  stability : Double
  selected : Bool
}

///|
pub struct FeatureSelection {
  scores : Array[FeatureScore]
  selected_indices : Array[Int]
  threshold : Double
  objective : Double
}

///|
pub fn feature_score(
  index : Int,
  relevance : Double,
  redundancy : Double,
  stability : Double,
) -> FeatureScore {
  { index, relevance, redundancy, stability, selected: false }
}

///|
pub fn feature_relevance(
  feature : Array[Double],
  target : Array[Double],
) -> Double {
  if feature.length() != target.length() || feature.length() < 2 {
    0.0
  } else {
    abs_double(pearson_correlation(feature, target))
  }
}

///|
pub fn feature_redundancy(
  feature : Array[Double],
  others : Array[Array[Double]],
) -> Double {
  if others.length() == 0 {
    return 0.0
  }
  let mut maximum = 0.0
  for other in others {
    if other.length() == feature.length() {
      let value = abs_double(pearson_correlation(feature, other))
      if value > maximum {
        maximum = value
      }
    }
  }
  maximum
}

///|
pub fn feature_stability(feature : Array[Double], folds : Int) -> Double {
  let count = if folds < 2 { 2 } else { folds }
  let chunks = sampling_kfold(feature, count, 0)
  if chunks.length() == 0 {
    0.0
  } else {
    let means = []
    for chunk in chunks {
      means.push(mean(chunk.holdout))
    }
    1.0 / (1.0 + sample_stddev(means))
  }
}

///|
pub fn feature_selection_scores(
  features : Array[Array[Double]],
  target : Array[Double],
  folds : Int,
) -> Array[FeatureScore] {
  let result = []
  for index = 0; index < features.length(); index = index + 1 {
    let others = []
    for other_index = 0
        other_index < features.length()
        other_index = other_index + 1 {
      if other_index != index {
        others.push(features[other_index])
      }
    }
    result.push(
      feature_score(
        index,
        feature_relevance(features[index], target),
        feature_redundancy(features[index], others),
        feature_stability(features[index], folds),
      ),
    )
  }
  result
}

///|
pub fn feature_selection_rank(
  scores : Array[FeatureScore],
) -> Array[FeatureScore] {
  let result = scores.copy()
  result.sort_by((left, right) => {
    let left_score = left.relevance * left.stability - left.redundancy
    let right_score = right.relevance * right.stability - right.redundancy
    if left_score > right_score {
      -1
    } else if left_score < right_score {
      1
    } else {
      0
    }
  })
  result
}

///|
pub fn feature_selection_run(
  features : Array[Array[Double]],
  target : Array[Double],
  threshold : Double,
  folds : Int,
) -> FeatureSelection {
  let scores = feature_selection_rank(
    feature_selection_scores(features, target, folds),
  )
  let selected = []
  let safe_threshold = if threshold < 0.0 { 0.0 } else { threshold }
  let mut objective = 0.0
  for index = 0; index < scores.length(); index = index + 1 {
    let score = scores[index]
    let value = score.relevance * score.stability - score.redundancy
    let keep = value >= safe_threshold
    if keep {
      selected.push(score.index)
      objective += value
    }
    scores[index] = { ..score, selected: keep }
  }
  { scores, selected_indices: selected, threshold: safe_threshold, objective }
}

///|
pub fn feature_selection_project(
  features : Array[Array[Double]],
  selection : FeatureSelection,
) -> Array[Array[Double]] {
  let result = []
  let rows = if features.length() == 0 { 0 } else { features[0].length() }
  for row_index = 0; row_index < rows; row_index = row_index + 1 {
    let row = []
    for index in selection.selected_indices {
      if index < features.length() && row_index < features[index].length() {
        row.push(features[index][row_index])
      }
    }
    result.push(row)
  }
  result
}

///|
pub fn feature_selection_indices(selection : FeatureSelection) -> Array[Int] {
  selection.selected_indices.copy()
}

///|
pub fn feature_selection_scores_vector(
  selection : FeatureSelection,
) -> Array[Double] {
  let result = []
  for score in selection.scores {
    result.push(score.relevance * score.stability - score.redundancy)
  }
  result
}

///|
pub fn feature_selection_relevance(
  selection : FeatureSelection,
) -> Array[Double] {
  let result = []
  for score in selection.scores {
    result.push(score.relevance)
  }
  result
}

///|
pub fn feature_selection_redundancy(
  selection : FeatureSelection,
) -> Array[Double] {
  let result = []
  for score in selection.scores {
    result.push(score.redundancy)
  }
  result
}

///|
pub fn feature_selection_is_selected(
  selection : FeatureSelection,
  index : Int,
) -> Bool {
  selection.selected_indices.contains(index)
}

///|
pub fn feature_selection_count(selection : FeatureSelection) -> Int {
  selection.selected_indices.length()
}

///|
pub fn feature_selection_quality(selection : FeatureSelection) -> Double {
  if selection.scores.length() == 0 {
    1.0
  } else {
    selection.objective / selection.scores.length().to_double()
  }
}

///|
pub fn feature_selection_lines(selection : FeatureSelection) -> Array[String] {
  let lines = [
    "selected=" + selection.selected_indices.length().to_string(),
    "threshold=" + selection.threshold.to_string(),
    "objective=" + selection.objective.to_string(),
  ]
  for score in selection.scores {
    lines.push(
      score.index.to_string() +
      "|" +
      score.relevance.to_string() +
      "|" +
      score.redundancy.to_string() +
      "|" +
      score.stability.to_string() +
      "|" +
      score.selected.to_string(),
    )
  }
  lines
}

///|
pub fn feature_selection_string(selection : FeatureSelection) -> String {
  feature_selection_lines(selection).join("\n")
}

///|
pub fn feature_selection_compare(
  left : FeatureSelection,
  right : FeatureSelection,
) -> Array[Double] {
  [
    feature_selection_count(left).to_double(),
    feature_selection_count(right).to_double(),
    feature_selection_quality(left),
    feature_selection_quality(right),
    right.objective - left.objective,
  ]
}

///|
pub fn feature_selection_stable(
  left : FeatureSelection,
  right : FeatureSelection,
) -> Bool {
  left.selected_indices == right.selected_indices
}

///|
pub fn feature_selection_batch(
  features : Array[Array[Array[Double]]],
  target : Array[Double],
  threshold : Double,
  folds : Int,
) -> Array[Double] {
  let result = []
  for matrix in features {
    result.push(
      feature_selection_quality(
        feature_selection_run(matrix, target, threshold, folds),
      ),
    )
  }
  result
}

///|
pub fn feature_selection_summary(selection : FeatureSelection) -> Array[Double] {
  [
    selection.selected_indices.length().to_double(),
    selection.scores.length().to_double(),
    selection.threshold,
    selection.objective,
    feature_selection_quality(selection),
  ]
}