///|
/// Readings attached to one Unicode scalar value.
pub struct ReadingSet {
  code_value : Int
  readings_value : Array[String]
} derive(Eq, Debug)

///|
/// Counts and boundaries proven by a full generated-data validation pass.
pub struct DatasetStats {
  code_point_count : Int
  reading_count : Int
  ambiguous_count : Int
  maximum_reading_count : Int
  first_code_value : Int
  last_code_value : Int
} derive(Eq, Debug)

///|
fn reading_copy(values : Array[String]) -> Array[String] {
  let output : Array[String] = []
  for value in values {
    output.push(value)
  }
  output
}

///|
fn split_reading_values(value : String) -> Array[String] {
  let output : Array[String] = []
  let mut start = 0
  for index = 0; index < value.length(); index = index + 1 {
    if value[index].to_int() == 44 {
      if index > start {
        output.push(syllable_ascii_slice(value, start, index))
      }
      start = index + 1
    }
  }
  if start < value.length() {
    output.push(syllable_ascii_slice(value, start, value.length()))
  }
  output
}

///|
fn readings_are_unique(values : Array[String]) -> Bool {
  for left = 0; left < values.length(); left = left + 1 {
    for right = left + 1; right < values.length(); right = right + 1 {
      if values[left] == values[right] {
        return false
      }
    }
  }
  true
}

///|
/// Validates every bundled code point and reading, then returns dataset facts.
pub fn validate_unihan_dataset() -> Result[DatasetStats, PinyinError] {
  if unihan_code_points.length() != unihan_readings.length() {
    return Err(InvalidGeneratedData("code_reading_length_mismatch"))
  }
  if unihan_code_points.length() == 0 {
    return Err(InvalidGeneratedData("empty_dataset"))
  }
  let mut reading_count = 0
  let mut ambiguous_count = 0
  let mut maximum_readings = 0
  for index = 0; index < unihan_code_points.length(); index = index + 1 {
    let code = unihan_code_points[index]
    if code < 0 || code > 0x10FFFF || (code >= 0xD800 && code <= 0xDFFF) {
      return Err(InvalidGeneratedData("invalid_code_point:" + code.to_string()))
    }
    if index > 0 && unihan_code_points[index - 1] >= code {
      return Err(InvalidGeneratedData("code_points_not_strictly_sorted"))
    }
    let values = split_reading_values(unihan_readings[index])
    if values.length() == 0 {
      return Err(InvalidGeneratedData("empty_reading_list:" + code.to_string()))
    }
    if !readings_are_unique(values) {
      return Err(InvalidGeneratedData("duplicate_reading:" + code.to_string()))
    }
    for value in values {
      match parse_syllable(value) {
        Err(_) =>
          return Err(
            InvalidGeneratedData(
              "invalid_reading:" + code.to_string() + ":" + value,
            ),
          )
        Ok(_) => ()
      }
    }
    reading_count = reading_count + values.length()
    if values.length() > 1 {
      ambiguous_count = ambiguous_count + 1
    }
    if values.length() > maximum_readings {
      maximum_readings = values.length()
    }
  }
  Ok({
    code_point_count: unihan_code_points.length(),
    reading_count,
    ambiguous_count,
    maximum_reading_count: maximum_readings,
    first_code_value: unihan_code_points[0],
    last_code_value: unihan_code_points[unihan_code_points.length() - 1],
  })
}

///|
/// Finds a sorted code point in the generated Unicode table.
pub fn lookup_readings(code_point : Int) -> ReadingSet? {
  if code_point < 0 {
    return None
  }
  let mut low = 0
  let mut high = unihan_code_points.length() - 1
  while low <= high {
    let middle = low + (high - low) / 2
    let candidate = unihan_code_points[middle]
    if candidate == code_point {
      return Some({
        code_value: code_point,
        readings_value: split_reading_values(unihan_readings[middle]),
      })
    }
    if candidate < code_point {
      low = middle + 1
    } else {
      high = middle - 1
    }
  }
  None
}

///|
pub fn ReadingSet::code_point(self : ReadingSet) -> Int {
  self.code_value
}

///|
pub fn ReadingSet::readings(self : ReadingSet) -> Array[String] {
  reading_copy(self.readings_value)
}

///|
pub fn ReadingSet::is_ambiguous(self : ReadingSet) -> Bool {
  self.readings_value.length() > 1
}

///|
pub fn DatasetStats::code_points(self : DatasetStats) -> Int {
  self.code_point_count
}

///|
pub fn DatasetStats::readings(self : DatasetStats) -> Int {
  self.reading_count
}

///|
pub fn DatasetStats::ambiguous_code_points(self : DatasetStats) -> Int {
  self.ambiguous_count
}

///|
pub fn DatasetStats::maximum_readings_per_code_point(
  self : DatasetStats,
) -> Int {
  self.maximum_reading_count
}

///|
pub fn DatasetStats::first_code_point(self : DatasetStats) -> Int {
  self.first_code_value
}

///|
pub fn DatasetStats::last_code_point(self : DatasetStats) -> Int {
  self.last_code_value
}

///|
/// Selects a reading while making the ambiguity policy observable.
pub fn select_reading(
  code_point : Int,
  policy : AmbiguityPolicy,
) -> Result[String, PinyinError] {
  let result = match lookup_readings(code_point) {
    None => return Err(UnknownCharacter(code_point))
    Some(value) => value
  }
  if result.readings_value.length() == 0 {
    return Err(UnknownCharacter(code_point))
  }
  if result.readings_value.length() > 1 {
    match policy {
      RejectAmbiguous =>
        return Err(
          AmbiguousCharacter(code_point, result.readings_value.length()),
        )
      PreserveAllReadings => {
        let mut joined = ""
        for index = 0; index < result.readings_value.length(); index = index + 1 {
          if index > 0 {
            joined = joined + "|"
          }
          joined = joined + result.readings_value[index]
        }
        return Ok(joined)
      }
      SelectFirstReading => ()
    }
  }
  Ok(result.readings_value[0])
}