///|
/// The binariser of the features, the port of the `FeatureBinarizer` of the
/// upstream.
///
/// Every feature is turned into a set of literals, and every literal holds for
/// the rows whose value of the feature satisfies it. A feature with at most two
/// distinct values becomes one literal and, with the negations, its negation.
/// A feature with more distinct values becomes one literal per threshold, and
/// the thresholds are the quantiles of the values.
pub struct FeatureBinarizer {
  names : Array[String]
  negations : Bool
  num_thresholds : Int32
  kind : Array[FeatureKind]
  levels : Array[Array[Double]]
  thresholds : Array[Array[Double]]
}

///|
/// A binariser of the features that is not fitted yet.
pub fn FeatureBinarizer::new(
  names : Array[String],
  num_thresholds : Int32,
  negations : Bool,
) -> FeatureBinarizer {
  {
    names: names.copy(),
    negations,
    num_thresholds,
    kind: [],
    levels: [],
    thresholds: [],
  }
}

///|
/// Learn the literals of the features from the given rows.
///
/// The binariser keeps the distinct values of the binary features and the
/// thresholds of the ordinal ones, so that it can transform other rows with
/// the same literals.
pub fn FeatureBinarizer::fit(
  self : FeatureBinarizer,
  columns : Array[Array[Double]],
) -> Unit {
  for c = 0; c < columns.length(); c = c + 1 {
    let uniq = distinct_values(columns[c])
    if uniq.length() <= 2 {
      self.kind.push(FeatureKind::Binary)
      self.levels.push(uniq)
      self.thresholds.push([])
    } else {
      self.kind.push(FeatureKind::Ordinal)
      self.levels.push([])
      self.thresholds.push(self.thresholds_of(uniq))
    }
  }
}

///|
/// The thresholds of an ordinal feature.
///
/// A feature with few distinct values is cut at every value but the largest
/// one, and a feature with many distinct values is cut at the quantiles of the
/// values, the way the upstream does it.
fn FeatureBinarizer::thresholds_of(
  self : FeatureBinarizer,
  uniq : Array[Double],
) -> Array[Double] {
  if uniq.length() <= self.num_thresholds + 1 {
    let out : Array[Double] = []
    for i = 0; i < uniq.length() - 1; i = i + 1 {
      out.push(uniq[i])
    }
    return out
  }
  let out : Array[Double] = []
  for k = 1; k <= self.num_thresholds; k = k + 1 {
    let probability = k.to_double() / (self.num_thresholds + 1).to_double()
    let quantile = @utils.percentile(uniq, probability * 100.0)
    if out.length() == 0 || out[out.length() - 1] != quantile {
      out.push(quantile)
    }
  }
  out
}

///|
/// Turn the given rows into the literals of the features.
pub fn FeatureBinarizer::transform(
  self : FeatureBinarizer,
  columns : Array[Array[Double]],
) -> BinarizedData {
  let literals : Array[Literal] = []
  let values : Array[Array[Double]] = []
  let feature_of : Array[Int32] = []
  let n_rows = if columns.length() == 0 { 0 } else { columns[0].length() }
  for c = 0; c < columns.length(); c = c + 1 {
    let binary = match self.kind[c] {
      FeatureKind::Binary => true
      FeatureKind::Ordinal => false
    }
    if binary {
      // The values are renamed to zero and one, in the order of their size.
      let levels = self.levels[c]
      let mapped : Array[Double] = []
      for value in columns[c] {
        let index = index_of_value(levels, value)
        mapped.push(if index < 0 { 0.0 } else { index.to_double() })
      }
      literals.push({
        feature: self.names[c],
        operation: LiteralOperation::Binary,
        value: 0.0,
      })
      values.push(mapped)
      feature_of.push(c)
      if self.negations {
        let negated : Array[Double] = []
        for value in mapped {
          negated.push(1.0 - value)
        }
        literals.push({
          feature: self.names[c],
          operation: LiteralOperation::Negation,
          value: 0.0,
        })
        values.push(negated)
        feature_of.push(c)
      }
    } else {
      let thresholds = self.thresholds[c]
      let less : Array[Array[Double]] = []
      for threshold in thresholds {
        let column : Array[Double] = []
        for value in columns[c] {
          column.push(if value <= threshold { 1.0 } else { 0.0 })
        }
        less.push(column)
      }
      for k = 0; k < thresholds.length(); k = k + 1 {
        literals.push({
          feature: self.names[c],
          operation: LiteralOperation::LessEqual,
          value: thresholds[k],
        })
        values.push(less[k])
        feature_of.push(c)
      }
      if self.negations {
        for k = 0; k < thresholds.length(); k = k + 1 {
          let column : Array[Double] = []
          for value in less[k] {
            column.push(1.0 - value)
          }
          literals.push({
            feature: self.names[c],
            operation: LiteralOperation::Greater,
            value: thresholds[k],
          })
          values.push(column)
          feature_of.push(c)
        }
      }
    }
  }
  { literals, columns: values, feature_of, n_rows, }
}

///|
/// Draw the reference samples of the upstream, the port of
/// `sample_reference`.
///
/// The reference samples are drawn uniformly over the range of every column,
/// and they are the background the rules are learned against. A column with
/// two distinct values is drawn from its two values, and a column with more
/// distinct values is drawn uniformly between its smallest and its largest
/// value, the way the upstream does it with a generator of its own for every
/// column.
pub fn sample_reference(
  columns : Array[Array[Double]],
  n : Int32,
  random : @utils.Random,
  seed : Int32,
) -> Array[Array[Double]] {
  let out : Array[Array[Double]] = []
  let mut counter = seed
  for c = 0; c < columns.length(); c = c + 1 {
    let uniq = distinct_values(columns[c])
    let column : Array[Double] = []
    if uniq.length() < 2 {
      for i = 0; i < n; i = i + 1 {
        column.push(uniq[0])
      }
    } else if uniq.length() == 2 {
      for i = 0; i < n; i = i + 1 {
        column.push(uniq[random.next_int(2)])
      }
    } else {
      let low = uniq[0]
      let high = uniq[uniq.length() - 1]
      let generator = @utils.Random::new(counter.to_uint64())
      for i = 0; i < n; i = i + 1 {
        column.push(low + (high - low) * generator.next_double())
      }
      counter = counter + 1
    }
    out.push(column)
  }
  out
}