///|
/// The configuration of a ruleset estimator, the port of the parameters of
/// the `BCSRulesetEstimator` of the upstream.
pub struct RulesetConfig {
  /// The multiplier of the number of the reference samples.
  n_ref_multiplier : Double
  /// The share of the positive samples the rules have to cover.
  alpha : Double
  /// The regularisation of the number of the rules.
  lambda0 : Double
  /// The regularisation of the number of the literals.
  lambda1 : Double
  /// The number of the results of the beam search.
  k : Int32
  /// The number of the extra rules per iteration of the beam search.
  d : Int32
  /// The width of the beam search.
  b : Int32
  /// The number of the iterations of the column generation.
  iter_max : Int32
  /// The tolerance of the comparisons.
  eps : Double
  /// The number of the bins of a continuous feature.
  num_thresholds : Int32
  /// Whether the negations of the literals are added.
  negations : Bool
  /// The strategy of the rounding.
  rounding : RoundingStrategy
  /// The seed of the reference samples.
  seed : Int32
}

///|
/// The estimator of a boolean rule set, the port of the
/// `BCSRulesetEstimator` of the upstream.
///
/// The estimator learns the rules of a region of the covariates: the support
/// of the data or the region where the treatment and the control overlap.
pub struct RulesetEstimator {
  config : RulesetConfig
  mut binarizer : FeatureBinarizer
  mut literals : Array[Literal]
  mut rule : BooleanRule
  mut fitted : Bool
}

///|
/// An estimator of a boolean rule set.
pub fn RulesetEstimator::new(config : RulesetConfig) -> RulesetEstimator {
  {
    config,
    binarizer: FeatureBinarizer::new([], 9, true),
    literals: [],
    rule: BooleanRule::new(
      0.95,
      0.001,
      0.001,
      20,
      20,
      10,
      10,
      0.000001,
      RoundingStrategy::GreedySweep,
    ),
    fitted: false,
  }
}

///|
/// Learn the rules of the given rows.
///
/// The labels of the rows are one for the rows the rules have to cover and
/// zero for the rows they have to avoid. The reference samples are the
/// background the rules are learned against.
pub fn RulesetEstimator::fit(
  self : RulesetEstimator,
  columns : Array[Array[Double]],
  names : Array[String],
  labels : Array[Double],
) -> Unit {
  let n = if columns.length() == 0 { 0 } else { columns[0].length() }
  let dim = columns.length()
  let num_reference = (n.to_double() *
  dim.to_double() *
  self.config.n_ref_multiplier).to_int()
  let random = @utils.Random::new(self.config.seed.to_uint64())
  let reference = sample_reference(
    columns,
    num_reference,
    random,
    self.config.seed,
  )
  let all_columns : Array[Array[Double]] = []
  for c = 0; c < dim; c = c + 1 {
    let values = columns[c].copy()
    for i = 0; i < num_reference; i = i + 1 {
      values.push(reference[c][i])
    }
    all_columns.push(values)
  }
  let all_labels = labels.copy()
  for i = 0; i < num_reference; i = i + 1 {
    all_labels.push(-1.0)
  }
  let binarizer = FeatureBinarizer::new(
    names,
    self.config.num_thresholds,
    self.config.negations,
  )
  // The binariser is fitted on the rows of the data, and not on the reference
  // samples, the way the upstream does it.
  let fitted_columns : Array[Array[Double]] = []
  for c = 0; c < dim; c = c + 1 {
    let head : Array[Double] = []
    for i = 0; i < n; i = i + 1 {
      head.push(all_columns[c][i])
    }
    fitted_columns.push(head)
  }
  binarizer.fit(fitted_columns)
  let binarized = binarizer.transform(all_columns)
  let rule = BooleanRule::new(
    self.config.alpha,
    self.config.lambda0,
    self.config.lambda1,
    self.config.k,
    self.config.d,
    self.config.b,
    self.config.iter_max,
    self.config.eps,
    self.config.rounding,
  )
  rule.fit(binarized, all_labels)
  self.binarizer = binarizer
  self.literals = binarized.literals
  self.rule = rule
  self.fitted = true
}

///|
/// Whether the rules cover every row of the given covariates.
pub fn RulesetEstimator::predict(
  self : RulesetEstimator,
  columns : Array[Array[Double]],
) -> Array[Bool] {
  let binarized = self.binarizer.transform(columns)
  self.rule.predict(binarized)
}

///|
/// The rules of the estimator.
pub fn RulesetEstimator::rules(self : RulesetEstimator) -> RuleSet {
  self.rule.rules(self.literals)
}

///|
/// The value of the program of the rules.
pub fn RulesetEstimator::objective(self : RulesetEstimator) -> Double {
  self.rule.lp_value()
}