///|
/// PP-OCR character dictionary. Class 0 is the CTC blank and is not stored.
/// Class `k` is line `k - 1`.
pub struct Charset {
  chars : Array[String]
} derive(Debug, Eq)

///|
pub extend Charset with Eq::{not_equal, equal}

///|
pub extend Charset with @debug.Debug::{to_repr}

///|
fn strip_cr(line : String) -> String {
  if line.length() > 0 && line[line.length() - 1] == '\r' {
    line[:line.length() - 1].to_owned()
  } else {
    line
  }
}

///|
/// Parse a dictionary file. A trailing newline does not add an empty class.
pub fn Charset::from_text(text : String) -> Charset {
  let collected : Array[String] = []
  for part in text.split("\n") {
    collected.push(strip_cr(part.to_owned()))
  }
  let mut limit = collected.length()
  if limit > 0 && collected[limit - 1].length() == 0 {
    limit = limit - 1
  }
  let chars : Array[String] = []
  for index in 0.. Charset {
  let parsed = Charset::from_text(text)
  let count = parsed.chars.length()
  if count > 0 && parsed.chars[count - 1] == " " {
    parsed
  } else {
    parsed.chars.push(" ")
    parsed
  }
}

///|
/// Map a CTC class to its character. Blank and out-of-range classes are `None`.
pub fn Charset::glyph(self : Charset, class : Int32) -> String? {
  if class <= 0 {
    None
  } else {
    let index = class - 1
    if index >= self.chars.length() {
      None
    } else {
      Some(self.chars[index])
    }
  }
}

///|
/// Greedy CTC text and the mean probability of the kept characters.
pub(all) struct CtcText {
  text : String
  confidence : Double
} derive(Debug, Eq, ToJson)

///|
pub extend CtcText with Eq::{not_equal, equal}

///|
pub extend CtcText with ToJson::{to_json}

///|
pub extend CtcText with @debug.Debug::{to_repr}

///|
fn step_confidence(
  logits : ArrayView[Float],
  offset : Int32,
  classes : Int32,
  best : Int32,
) -> Double {
  let mut sum = 0.0
  let mut negative = false
  let mut peak = logits[offset].to_double()
  for class in 0.. peak {
      peak = value
    }
    sum = sum + value
  }
  let chosen = logits[offset + best].to_double()
  if !negative && (if sum > 1.0 { sum - 1.0 } else { 1.0 - sum }) <= 0.05 {
    chosen
  } else {
    let mut denom = 0.0
    for class in 0.. String raise OcrError {
  ctc_decode(logits, time~, classes~, charset~).text
}

///|
/// Greedy CTC decode with a confidence in `0.0..=1.0` when the row is a
/// probability distribution or a softmax of logits.
pub fn ctc_decode(
  logits : ArrayView[Float],
  time~ : Int32,
  classes~ : Int32,
  charset~ : Charset,
) -> CtcText raise OcrError {
  if time < 0 || classes <= 0 {
    raise OcrError::Model("ctc shape is invalid")
  }
  if logits.length() != time * classes {
    raise OcrError::Model("ctc logits length does not match time*classes")
  }
  let builder = StringBuilder()
  let mut previous = -1
  let mut score_sum = 0.0
  let mut score_count = 0
  for step in 0..