///|
/// A categorical defect count with a stable category label.
pub(all) struct CategoryCount {
  category : String
  count : Int
  proportion : Double
} derive(Debug, ToJson)

///|
/// Pareto row with cumulative contribution.
pub(all) struct ParetoRow {
  category : String
  count : Int
  proportion : Double
  cumulative : Double
} derive(Debug, ToJson)

///|
/// Calculate defect rates from counts and exposure values.
pub fn attribute_rate(
  counts : Array[Int],
  exposure : Array[Int],
) -> Array[Double]? {
  if counts.length() == 0 || counts.length() != exposure.length() {
    None
  } else {
    let result : Array[Double] = []
    for index in 0.. exposure[index] {
        return None
      }
      result.push(counts[index].to_double() / exposure[index].to_double())
    }
    Some(result)
  }
}

///|
/// Calculate average defects per opportunity with varying exposure.
pub fn defects_per_opportunity(
  defects : Array[Int],
  opportunities : Array[Int],
) -> Double? {
  if defects.length() == 0 || defects.length() != opportunities.length() {
    None
  } else {
    let mut total_defects = 0
    let mut total_opportunities = 0
    for index in 0.. Array[CategoryCount] {
  let result : Array[CategoryCount] = []
  for category in categories {
    let mut found : Int? = None
    for index, row in result {
      if row.category == category {
        found = Some(index)
      }
    }
    match found {
      Some(index) =>
        result[index] = {
          category: result[index].category,
          count: result[index].count + 1,
          proportion: 0.0,
        }
      None => result.push({ category, count: 1, proportion: 0.0 })
    }
  }
  let total = categories.length().to_double()
  for index in 0.. Array[CategoryCount] {
  let result = rows.copy()
  for index in 1.. 0 && result[cursor - 1].count < value.count {
      result[cursor] = result[cursor - 1]
      cursor -= 1
    }
    result[cursor] = value
  }
  result
}

///|
/// Build a Pareto ranking from categorical observations.
pub fn pareto_categories(categories : Array[String]) -> Array[ParetoRow] {
  let sorted = sort_category_counts(category_counts(categories))
  let result : Array[ParetoRow] = []
  let mut cumulative = 0.0
  for row in sorted {
    cumulative += row.proportion
    result.push({
      category: row.category,
      count: row.count,
      proportion: row.proportion,
      cumulative,
    })
  }
  result
}

///|
/// A binomial attribute-chart point.
pub(all) struct AttributePoint {
  index : Int
  rate : Double
  center : Double
  lower : Double
  upper : Double
  alarm : Bool
} derive(Debug, ToJson)

///|
/// Create a p-chart series from variable exposures.
pub fn p_chart_points(
  counts : Array[Int],
  exposure : Array[Int],
) -> Array[AttributePoint]? {
  match attribute_rate(counts, exposure) {
    None => None
    Some(rates) => {
      let center = mean_of(rates)
      let result : Array[AttributePoint] = []
      for index, rate in rates {
        let variance = center * (1.0 - center) / exposure[index].to_double()
        let margin = 3.0 * variance.max(0.0).sqrt()
        let lower = (center - margin).max(0.0)
        let upper = (center + margin).min(1.0)
        result.push({
          index,
          rate,
          center,
          lower,
          upper,
          alarm: rate < lower || rate > upper,
        })
      }
      Some(result)
    }
  }
}

///|
/// A c-chart point for counts at a constant opportunity count.
pub(all) struct CountPoint {
  index : Int
  count : Int
  center : Double
  lower : Double
  upper : Double
  alarm : Bool
} derive(Debug, ToJson)

///|
/// Create a count chart with Poisson three-sigma limits.
pub fn count_chart_points(counts : Array[Int]) -> Array[CountPoint]? {
  if counts.length() == 0 {
    None
  } else {
    let mut total = 0
    for count in counts {
      if count < 0 {
        return None
      }
      total += count
    }
    let center = total.to_double() / counts.length().to_double()
    let margin = 3.0 * center.sqrt()
    let lower = (center - margin).max(0.0)
    let upper = center + margin
    let result : Array[CountPoint] = []
    for index, count in counts {
      result.push({
        index,
        count,
        center,
        lower,
        upper,
        alarm: count.to_double() < lower || count.to_double() > upper,
      })
    }
    Some(result)
  }
}

///|
/// Return the number of attribute alarms.
pub fn attribute_alarm_count(points : Array[AttributePoint]) -> Int {
  let mut count = 0
  for point in points {
    if point.alarm {
      count += 1
    }
  }
  count
}

///|
/// Return the number of count-chart alarms.
pub fn count_alarm_count(points : Array[CountPoint]) -> Int {
  let mut count = 0
  for point in points {
    if point.alarm {
      count += 1
    }
  }
  count
}

///|
/// Compare two categorical defect distributions.
pub fn category_delta(
  first : Array[CategoryCount],
  second : Array[CategoryCount],
) -> Array[CategoryCount] {
  let result : Array[CategoryCount] = []
  for row in second {
    let mut old = 0
    for previous in first {
      if previous.category == row.category {
        old = previous.count
      }
    }
    result.push({
      category: row.category,
      count: row.count - old,
      proportion: row.proportion,
    })
  }
  result
}

///|
/// Return categories whose counts increased.
pub fn increased_categories(
  first : Array[CategoryCount],
  second : Array[CategoryCount],
) -> Array[CategoryCount] {
  let result : Array[CategoryCount] = []
  for row in category_delta(first, second) {
    if row.count > 0 {
      result.push(row)
    }
  }
  result
}

///|
/// Return categories whose counts disappeared from the second sample.
pub fn removed_categories(
  first : Array[CategoryCount],
  second : Array[CategoryCount],
) -> Array[String] {
  let result : Array[String] = []
  for row in first {
    let mut exists = false
    for current in second {
      if current.category == row.category {
        exists = true
      }
    }
    if !exists {
      result.push(row.category)
    }
  }
  result
}

///|
/// Weighted severity score for defect categories.
pub fn weighted_defect_score(
  categories : Array[String],
  weights : Array[(String, Double)],
) -> Double {
  let mut total = 0.0
  for category in categories {
    for pair in weights {
      if pair.0 == category {
        total += pair.1
      }
    }
  }
  total
}

///|
/// Convert defect categories to a binary presence vector.
pub fn category_presence(
  categories : Array[String],
  vocabulary : Array[String],
) -> Array[Int] {
  let result : Array[Int] = []
  for word in vocabulary {
    let mut present = 0
    for category in categories {
      if category == word {
        present = 1
      }
    }
    result.push(present)
  }
  result
}

///|
/// Stable category vocabulary with first-seen order.
pub fn category_vocabulary(categories : Array[String]) -> Array[String] {
  let result : Array[String] = []
  for category in categories {
    let mut found = false
    for current in result {
      if current == category {
        found = true
      }
    }
    if !found {
      result.push(category)
    }
  }
  result
}