///|
pub fn binary_labels_from_threshold(
  values : Array[Double],
  threshold : Double,
) -> Array[Bool] {
  let result = []
  for value in values {
    result.push(value > threshold)
  }
  result
}

///|
pub fn binary_labels_from_median(values : Array[Double]) -> Array[Bool] {
  binary_labels_from_threshold(values, median(values))
}

///|
pub fn accuracy(matrix : ConfusionMatrix) -> Double {
  let total = matrix.true_positive +
    matrix.false_positive +
    matrix.true_negative +
    matrix.false_negative
  if total == 0 {
    0.0
  } else {
    (matrix.true_positive + matrix.true_negative).to_double() /
    total.to_double()
  }
}

///|
pub fn false_positive_rate(matrix : ConfusionMatrix) -> Double {
  let denominator = matrix.false_positive + matrix.true_negative
  if denominator == 0 {
    0.0
  } else {
    matrix.false_positive.to_double() / denominator.to_double()
  }
}

///|
pub fn false_negative_rate(matrix : ConfusionMatrix) -> Double {
  let denominator = matrix.false_negative + matrix.true_positive
  if denominator == 0 {
    0.0
  } else {
    matrix.false_negative.to_double() / denominator.to_double()
  }
}

///|
pub fn negative_predictive_value(matrix : ConfusionMatrix) -> Double {
  let denominator = matrix.true_negative + matrix.false_negative
  if denominator == 0 {
    0.0
  } else {
    matrix.true_negative.to_double() / denominator.to_double()
  }
}

///|
pub fn diagnostic_likelihood_positive(matrix : ConfusionMatrix) -> Double {
  let fpr = false_positive_rate(matrix)
  if fpr == 0.0 {
    0.0
  } else {
    recall(matrix) / fpr
  }
}

///|
pub fn diagnostic_likelihood_negative(matrix : ConfusionMatrix) -> Double {
  let fpr = false_positive_rate(matrix)
  let tnr = specificity(matrix)
  if tnr == 0.0 {
    0.0
  } else {
    fpr / tnr
  }
}

///|
pub fn roc_points(
  scores : Array[Double],
  actual : Array[Bool],
  thresholds : Array[Double],
) -> Array[Array[Double]] {
  let result = []
  let sweep = threshold_sweep(scores, actual, thresholds)
  for row in sweep {
    let threshold = row[0]
    let predicted = binary_labels_from_threshold(scores, threshold)
    let matrix = confusion_matrix(actual, predicted)
    result.push([false_positive_rate(matrix), recall(matrix), threshold])
  }
  result
}

///|
pub fn auc_from_roc(points : Array[Array[Double]]) -> Double {
  if points.length() <= 1 {
    return 0.0
  }
  let sorted = []
  for point in points {
    sorted.push(point)
  }
  sorted.sort_by((left, right) => {
    if left[0] < right[0] {
      -1
    } else if left[0] > right[0] {
      1
    } else {
      0
    }
  })
  let mut area = 0.0
  for index = 1; index < sorted.length(); index = index + 1 {
    let width = sorted[index][0] - sorted[index - 1][0]
    let height = (sorted[index][1] + sorted[index - 1][1]) / 2.0
    area += width * height
  }
  area
}

///|
pub fn average_precision(points : Array[Array[Double]]) -> Double {
  if points.length() == 0 {
    return 0.0
  }
  let sorted = []
  for point in points {
    sorted.push(point)
  }
  sorted.sort_by((left, right) => {
    if left[0] > right[0] {
      -1
    } else if left[0] < right[0] {
      1
    } else {
      0
    }
  })
  let mut area = 0.0
  for index = 1; index < sorted.length(); index = index + 1 {
    let recall_width = abs_double(sorted[index][0] - sorted[index - 1][0])
    area += recall_width * sorted[index][1]
  }
  area
}

///|
pub fn brier_score(
  actual : Array[Bool],
  probabilities : Array[Double],
) -> Double {
  if actual.length() == 0 || actual.length() != probabilities.length() {
    return 0.0
  }
  let mut total = 0.0
  for index = 0; index < actual.length(); index = index + 1 {
    let target = if actual[index] { 1.0 } else { 0.0 }
    let error = probabilities[index] - target
    total += error * error
  }
  total / actual.length().to_double()
}

///|
pub fn calibration_bins(
  actual : Array[Bool],
  probabilities : Array[Double],
  bins : Int,
) -> Array[Array[Double]] {
  if bins <= 0 {
    abort("bins must be positive")
  }
  if actual.length() != probabilities.length() {
    return []
  }
  let result = []
  for bin = 0; bin < bins; bin = bin + 1 {
    let lower = bin.to_double() / bins.to_double()
    let upper = (bin + 1).to_double() / bins.to_double()
    let mut count = 0
    let mut positive = 0
    let mut probability_total = 0.0
    for index = 0; index < probabilities.length(); index = index + 1 {
      if probabilities[index] >= lower &&
        (probabilities[index] < upper || bin == bins - 1) {
        count += 1
        if actual[index] {
          positive += 1
        }
        probability_total += probabilities[index]
      }
    }
    if count == 0 {
      result.push([lower, upper, 0.0, 0.0, 0.0])
    } else {
      result.push([
        lower,
        upper,
        count.to_double(),
        positive.to_double() / count.to_double(),
        probability_total / count.to_double(),
      ])
    }
  }
  result
}

///|
pub fn threshold_metrics(
  scores : Array[Double],
  actual : Array[Bool],
  threshold : Double,
) -> Array[Double] {
  let predicted = binary_labels_from_threshold(scores, threshold)
  let matrix = confusion_matrix(actual, predicted)
  [
    accuracy(matrix),
    precision(matrix),
    recall(matrix),
    specificity(matrix),
    f1_score(matrix),
    matthews_correlation(matrix),
  ]
}

///|
pub fn robust_classification_report(
  scores : Array[Double],
  actual : Array[Bool],
  thresholds : Array[Double],
) -> Array[Array[Double]] {
  let result = []
  for threshold in thresholds {
    result.push(threshold_metrics(scores, actual, threshold))
  }
  result
}

///|
pub fn agreement_rate(first : Array[Bool], second : Array[Bool]) -> Double {
  if first.length() == 0 || first.length() != second.length() {
    return 0.0
  }
  let mut agreement = 0
  for index = 0; index < first.length(); index = index + 1 {
    if first[index] == second[index] {
      agreement += 1
    }
  }
  agreement.to_double() / first.length().to_double()
}

///|
pub fn robust_rank_agreement(
  first : Array[Double],
  second : Array[Double],
) -> Double {
  spearman_correlation(first, second)
}