///|
/// Cost-sensitive decision rules for imbalanced streaming classification.
pub struct ClassCost {
  false_positive : Double
  false_negative : Double
  true_positive : Double
  true_negative : Double
}

///|
pub fn ClassCost::new(
  false_positive? : Double = 1.0,
  false_negative? : Double = 1.0,
  true_positive? : Double = 0.0,
  true_negative? : Double = 0.0,
) -> ClassCost {
  {
    false_positive: if false_positive < 0.0 {
      0.0
    } else {
      false_positive
    },
    false_negative: if false_negative < 0.0 {
      0.0
    } else {
      false_negative
    },
    true_positive,
    true_negative,
  }
}

///|
pub fn ClassCost::cost(
  self : ClassCost,
  prediction : Double,
  label : Double,
  threshold : Double,
) -> Double {
  let predicted_positive = prediction >= threshold
  let actual_positive = label >= 0.5
  if predicted_positive && actual_positive {
    self.true_positive
  } else if predicted_positive {
    self.false_positive
  } else if actual_positive {
    self.false_negative
  } else {
    self.true_negative
  }
}

///|
pub fn ClassCost::expected_cost(
  self : ClassCost,
  probability : Double,
  threshold : Double,
) -> Double {
  probability * self.cost(1.0, 1.0, threshold) +
  (1.0 - probability) * self.cost(1.0, 0.0, threshold)
}

///|
pub fn ClassCost::positive_weight(self : ClassCost) -> Double {
  self.false_negative + self.true_positive.abs()
}

///|
pub fn ClassCost::negative_weight(self : ClassCost) -> Double {
  self.false_positive + self.true_negative.abs()
}

///|
pub fn ClassCost::balanced_threshold(self : ClassCost) -> Double {
  let positive = self.positive_weight()
  let negative = self.negative_weight()
  if positive + negative <= 0.0 {
    0.5
  } else {
    clamp(negative / (positive + negative), 0.0, 1.0)
  }
}

///|
pub fn cost_sensitive_decision(probability : Double, costs : ClassCost) -> Bool {
  probability >= costs.balanced_threshold()
}

///|
pub fn cost_matrix(costs : ClassCost) -> Array[Array[Double]] {
  [
    [costs.true_negative, costs.false_positive],
    [costs.false_negative, costs.true_positive],
  ]
}

///|
pub fn expected_binary_cost(
  probability : Double,
  positive_cost : Double,
  negative_cost : Double,
) -> Double {
  let p = clamp_probability(probability)
  p * positive_cost + (1.0 - p) * negative_cost
}

///|
pub fn threshold_is_valid(threshold : Double) -> Bool {
  threshold >= 0.0 && threshold <= 1.0
}

///|
pub fn safe_threshold(threshold : Double) -> Double {
  clamp(threshold, 0.0, 1.0)
}

///|
pub struct CostSensitiveEvaluator {
  costs : ClassCost
  mut total : Double
  mut count : Int
}

///|
pub fn CostSensitiveEvaluator::new(
  costs? : ClassCost = ClassCost::new(),
) -> CostSensitiveEvaluator {
  { costs, total: 0.0, count: 0 }
}

///|
pub fn CostSensitiveEvaluator::observe(
  self : CostSensitiveEvaluator,
  prediction : Double,
  label : Double,
  threshold : Double,
) -> Double {
  let value = self.costs.cost(prediction, label, safe_threshold(threshold))
  self.total += value
  self.count += 1
  value
}

///|
pub fn CostSensitiveEvaluator::mean(self : CostSensitiveEvaluator) -> Double {
  if self.count == 0 {
    0.0
  } else {
    self.total / self.count.to_double()
  }
}

///|
pub fn CostSensitiveEvaluator::count(self : CostSensitiveEvaluator) -> Int {
  self.count
}

///|
pub fn CostSensitiveEvaluator::total(self : CostSensitiveEvaluator) -> Double {
  self.total
}

///|
pub fn CostSensitiveEvaluator::reset(self : CostSensitiveEvaluator) -> Unit {
  self.total = 0.0
  self.count = 0
}

///|
pub struct ThresholdOptimizer {
  costs : ClassCost
  minimum : Double
  maximum : Double
  steps : Int
  mut best_threshold : Double
  mut best_cost : Double
}

///|
pub fn ThresholdOptimizer::new(
  costs? : ClassCost = ClassCost::new(),
  minimum? : Double = 0.01,
  maximum? : Double = 0.99,
  steps? : Int = 99,
) -> ThresholdOptimizer {
  {
    costs,
    minimum: clamp(minimum, 0.0, 1.0),
    maximum: clamp(maximum, 0.0, 1.0),
    steps: if steps < 1 {
      1
    } else {
      steps
    },
    best_threshold: 0.5,
    best_cost: 0.0,
  }
}

///|
pub fn ThresholdOptimizer::fit(
  self : ThresholdOptimizer,
  predictions : Array[Double],
  labels : Array[Double],
) -> Double {
  let count = if predictions.length() < labels.length() {
    predictions.length()
  } else {
    labels.length()
  }
  let mut best = 0.5
  let mut cost = 1.0e30
  for step in 0..<=self.steps {
    let threshold = self.minimum +
      (self.maximum - self.minimum) * step.to_double() / self.steps.to_double()
    let mut candidate = 0.0
    for i in 0.. Double {
  self.best_threshold
}

///|
pub fn ThresholdOptimizer::cost(self : ThresholdOptimizer) -> Double {
  self.best_cost
}

///|
pub fn ThresholdOptimizer::reset(self : ThresholdOptimizer) -> Unit {
  self.best_threshold = 0.5
  self.best_cost = 0.0
}