///|
/// Threshold schedules used when a service changes its noise profile over time.
pub struct ThresholdPoint {
  index : Int
  threshold : Double
  score : Double
  accepted : Bool
}

///|
pub fn ThresholdPoint::new(
  index : Int,
  score : Double,
  threshold : Double,
) -> ThresholdPoint {
  { index, threshold, score, accepted: score >= threshold }
}

///|
pub fn threshold_from_quantile(
  scores : Array[Double],
  false_positive_rate : Double,
) -> Double {
  quantile(scores, 1.0 - clamp_probability(false_positive_rate))
}

///|
pub fn threshold_from_mad(
  scores : Array[Double],
  multiplier? : Double = 3.0,
) -> Double {
  median(scores) + multiplier * median_absolute_deviation(scores) * 1.4826
}

///|
pub fn threshold_from_mean(
  scores : Array[Double],
  standard_deviations? : Double = 3.0,
) -> Double {
  mean(scores) + standard_deviations * standard_deviation(scores)
}

///|
pub fn threshold_grid(
  minimum : Double,
  maximum : Double,
  steps : Int,
) -> Array[Double] {
  let result : Array[Double] = []
  if steps <= 0 {
    return result
  }
  if steps == 1 {
    result.push(minimum)
    return result
  }
  for i in 0.. Double {
  let length = if scores.length() < labels.length() {
    scores.length()
  } else {
    labels.length()
  }
  let mut true_positive = 0
  let mut predicted_positive = 0
  for i in 0..= threshold
    if predicted {
      predicted_positive += 1
    }
    if predicted && labels[i] {
      true_positive += 1
    }
  }
  if predicted_positive == 0 {
    0.0
  } else {
    true_positive.to_double() / predicted_positive.to_double()
  }
}

///|
pub fn score_recall(
  scores : Array[Double],
  labels : Array[Bool],
  threshold : Double,
) -> Double {
  let length = if scores.length() < labels.length() {
    scores.length()
  } else {
    labels.length()
  }
  let mut true_positive = 0
  let mut actual_positive = 0
  for i in 0..= threshold {
      true_positive += 1
    }
  }
  if actual_positive == 0 {
    1.0
  } else {
    true_positive.to_double() / actual_positive.to_double()
  }
}

///|
pub fn score_f1(
  scores : Array[Double],
  labels : Array[Bool],
  threshold : Double,
) -> Double {
  let precision = score_precision(scores, labels, threshold)
  let recall = score_recall(scores, labels, threshold)
  if precision + recall == 0.0 {
    0.0
  } else {
    2.0 * precision * recall / (precision + recall)
  }
}

///|
pub fn best_f1_threshold(
  scores : Array[Double],
  labels : Array[Bool],
  candidates : Array[Double],
) -> ThresholdPoint {
  if candidates.length() == 0 {
    return ThresholdPoint::new(0, 0.0, 0.0)
  }
  let mut best = ThresholdPoint::new(
    0,
    score_f1(scores, labels, candidates[0]),
    candidates[0],
  )
  for i in 1.. best.score {
      best = { index: i, threshold: candidate, score: f1, accepted: true }
    }
  }
  best
}

///|
pub struct AdaptiveThreshold {
  mut threshold : Double
  rate : Double
  minimum : Double
  maximum : Double
  mut count : Int
}

///|
pub fn AdaptiveThreshold::new(
  initial? : Double = 1.0,
  rate? : Double = 0.05,
  minimum? : Double = 0.0,
  maximum? : Double = 1.7976931348623157e308,
) -> AdaptiveThreshold {
  {
    threshold: if initial < minimum {
      minimum
    } else if initial > maximum {
      maximum
    } else {
      initial
    },
    rate: clamp_probability(rate),
    minimum,
    maximum,
    count: 0,
  }
}

///|
pub fn AdaptiveThreshold::observe(
  self : AdaptiveThreshold,
  score : Double,
  positive : Bool,
) -> Double {
  self.count += 1
  let error = if positive { 1.0 - score } else { score }
  self.threshold += self.rate * (if positive { -error } else { error })
  if self.threshold < self.minimum {
    self.threshold = self.minimum
  }
  if self.threshold > self.maximum {
    self.threshold = self.maximum
  }
  self.threshold
}

///|
pub fn AdaptiveThreshold::value(self : AdaptiveThreshold) -> Double {
  self.threshold
}

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

///|
pub fn threshold_points(
  scores : Array[Double],
  threshold : Double,
) -> Array[ThresholdPoint] {
  let result : Array[ThresholdPoint] = []
  for i, score in scores {
    result.push(ThresholdPoint::new(i, score, threshold))
  }
  result
}