///|
fn find_value_frequency(
  frequencies : Array[RomanIndexValueFrequency],
  value : Int,
) -> Int {
  for index = 0; index < frequencies.length(); index = index + 1 {
    if frequencies[index].value == value {
      return index
    }
  }
  -1
}

///|
/// Count values in first-occurrence order using Int64 accumulators.
pub fn roman_index_value_frequencies(
  index : RomanDocumentIndex,
) -> Array[RomanIndexValueFrequency] {
  let frequencies : Array[RomanIndexValueFrequency] = []
  for entry in index.entries {
    let position = find_value_frequency(frequencies, entry.value)
    if position < 0 {
      frequencies.push({ value: entry.value, count: 1L })
    } else {
      frequencies[position] = {
        value: entry.value,
        count: frequencies[position].count + 1L,
      }
    }
  }
  frequencies
}

///|
fn find_canonical_frequency(
  frequencies : Array[RomanIndexCanonicalFrequency],
  canonical : String,
) -> Int {
  for index = 0; index < frequencies.length(); index = index + 1 {
    if frequencies[index].canonical == canonical {
      return index
    }
  }
  -1
}

///|
/// Count canonical spellings in first-occurrence order.
pub fn roman_index_canonical_frequencies(
  index : RomanDocumentIndex,
) -> Array[RomanIndexCanonicalFrequency] {
  let frequencies : Array[RomanIndexCanonicalFrequency] = []
  for entry in index.entries {
    let position = find_canonical_frequency(frequencies, entry.canonical)
    if position < 0 {
      frequencies.push({ canonical: entry.canonical, count: 1L })
    } else {
      frequencies[position] = {
        canonical: entry.canonical,
        count: frequencies[position].count + 1L,
      }
    }
  }
  frequencies
}

///|
fn find_mode_frequency(
  frequencies : Array[RomanIndexModeFrequency],
  mode : RomanMode,
) -> Int {
  for index = 0; index < frequencies.length(); index = index + 1 {
    if frequencies[index].mode == mode {
      return index
    }
  }
  -1
}

///|
/// Count notation modes in first-occurrence order.
pub fn roman_index_mode_frequencies(
  index : RomanDocumentIndex,
) -> Array[RomanIndexModeFrequency] {
  let frequencies : Array[RomanIndexModeFrequency] = []
  for entry in index.entries {
    let position = find_mode_frequency(frequencies, entry.mode)
    if position < 0 {
      frequencies.push({ mode: entry.mode, count: 1L })
    } else {
      frequencies[position] = {
        mode: entry.mode,
        count: frequencies[position].count + 1L,
      }
    }
  }
  frequencies
}

///|
fn count_entries_for_document(
  entries : Array[RomanIndexEntry],
  document_id : String,
) -> Int64 {
  let mut count = 0L
  for entry in entries {
    if entry.document_id == document_id {
      count = count + 1L
    }
  }
  count
}

///|
/// Count accepted entries for every document, including empty documents.
pub fn roman_index_document_frequencies(
  index : RomanDocumentIndex,
) -> Array[RomanIndexDocumentFrequency] {
  let frequencies : Array[RomanIndexDocumentFrequency] = []
  for document in index.documents {
    frequencies.push({
      document_id: document.id,
      count: count_entries_for_document(index.entries, document.id),
    })
  }
  frequencies
}

///|
fn find_source_frequency(
  frequencies : Array[RomanIndexSourceFrequency],
  source_text : String,
) -> Int {
  for index = 0; index < frequencies.length(); index = index + 1 {
    if frequencies[index].source_text == source_text {
      return index
    }
  }
  -1
}

///|
/// Count exact source spellings in first-occurrence order.
pub fn roman_index_source_frequencies(
  index : RomanDocumentIndex,
) -> Array[RomanIndexSourceFrequency] {
  let frequencies : Array[RomanIndexSourceFrequency] = []
  for entry in index.entries {
    let position = find_source_frequency(frequencies, entry.source_text)
    if position < 0 {
      frequencies.push({ source_text: entry.source_text, count: 1L })
    } else {
      frequencies[position] = {
        source_text: entry.source_text,
        count: frequencies[position].count + 1L,
      }
    }
  }
  frequencies
}

///|
fn find_normalized_frequency(
  frequencies : Array[RomanIndexNormalizedFrequency],
  normalized_text : String,
) -> Int {
  for index = 0; index < frequencies.length(); index = index + 1 {
    if frequencies[index].normalized_text == normalized_text {
      return index
    }
  }
  -1
}

///|
/// Count normalized spellings in first-occurrence order.
pub fn roman_index_normalized_frequencies(
  index : RomanDocumentIndex,
) -> Array[RomanIndexNormalizedFrequency] {
  let frequencies : Array[RomanIndexNormalizedFrequency] = []
  for entry in index.entries {
    let position = find_normalized_frequency(frequencies, entry.normalized_text)
    if position < 0 {
      frequencies.push({ normalized_text: entry.normalized_text, count: 1L })
    } else {
      frequencies[position] = {
        normalized_text: entry.normalized_text,
        count: frequencies[position].count + 1L,
      }
    }
  }
  frequencies
}

///|
fn find_token_count_frequency(
  frequencies : Array[RomanIndexTokenCountFrequency],
  token_count : Int,
) -> Int {
  for index = 0; index < frequencies.length(); index = index + 1 {
    if frequencies[index].token_count == token_count {
      return index
    }
  }
  -1
}

///|
/// Count parser token lengths in first-occurrence order.
pub fn roman_index_token_count_frequencies(
  index : RomanDocumentIndex,
) -> Array[RomanIndexTokenCountFrequency] {
  let frequencies : Array[RomanIndexTokenCountFrequency] = []
  for entry in index.entries {
    let position = find_token_count_frequency(frequencies, entry.token_count)
    if position < 0 {
      frequencies.push({ token_count: entry.token_count, count: 1L })
    } else {
      frequencies[position] = {
        token_count: entry.token_count,
        count: frequencies[position].count + 1L,
      }
    }
  }
  frequencies
}

///|
fn count_retained_rejections_for_document(
  rejections : Array[RomanIndexRejectionSummary],
  document_id : String,
) -> Int64 {
  let mut count = 0L
  for rejection in rejections {
    if rejection.document_id == document_id {
      count = count + 1L
    }
  }
  count
}

///|
fn document_entry_extrema(
  entries : Array[RomanIndexEntry],
  document_id : String,
) -> (Int?, Int?) {
  let mut minimum : Int? = None
  let mut maximum : Int? = None
  for entry in entries {
    if entry.document_id == document_id {
      match minimum {
        None => minimum = Some(entry.value)
        Some(current) if entry.value < current => minimum = Some(entry.value)
        _ => ()
      }
      match maximum {
        None => maximum = Some(entry.value)
        Some(current) if entry.value > current => maximum = Some(entry.value)
        _ => ()
      }
    }
  }
  (minimum, maximum)
}

///|
fn document_distinct_value_count(
  entries : Array[RomanIndexEntry],
  document_id : String,
) -> Int64 {
  let values : Array[Int] = []
  for entry in entries {
    if entry.document_id == document_id {
      let mut seen = false
      for value in values {
        if value == entry.value {
          seen = true
          break
        }
      }
      if !seen {
        values.push(entry.value)
      }
    }
  }
  values.length().to_int64()
}

///|
fn count_document_unicode_entries(
  entries : Array[RomanIndexEntry],
  document_id : String,
) -> Int64 {
  let mut count = 0L
  for entry in entries {
    if entry.document_id == document_id && entry.used_unicode_compatibility {
      count = count + 1L
    }
  }
  count
}

///|
fn count_document_normalized_entries(
  entries : Array[RomanIndexEntry],
  document_id : String,
) -> Int64 {
  let mut count = 0L
  for entry in entries {
    if entry.document_id == document_id && entry.normalized_input {
      count = count + 1L
    }
  }
  count
}

///|
fn summarize_index_document(
  index : RomanDocumentIndex,
  document : RomanIndexDocumentMetadata,
) -> RomanIndexDocumentSummary {
  let (minimum, maximum) = document_entry_extrema(index.entries, document.id)
  {
    document_id: document.id,
    parse_mode: document.parse_mode,
    source_length: document.source_length,
    candidates_examined: document.candidates_examined.to_int64(),
    accepted: count_entries_for_document(index.entries, document.id),
    rejected: document.rejected_count.to_int64(),
    retained_rejections: count_retained_rejections_for_document(
      index.rejections,
      document.id,
    ),
    first_ordinal: document.first_ordinal,
    min_value: minimum,
    max_value: maximum,
    distinct_values: document_distinct_value_count(index.entries, document.id),
    used_unicode: count_document_unicode_entries(index.entries, document.id),
    normalized_inputs: count_document_normalized_entries(
      index.entries,
      document.id,
    ),
  }
}

///|
fn index_entry_extrema(entries : Array[RomanIndexEntry]) -> (Int?, Int?) {
  let mut minimum : Int? = None
  let mut maximum : Int? = None
  for entry in entries {
    match minimum {
      None => minimum = Some(entry.value)
      Some(current) if entry.value < current => minimum = Some(entry.value)
      _ => ()
    }
    match maximum {
      None => maximum = Some(entry.value)
      Some(current) if entry.value > current => maximum = Some(entry.value)
      _ => ()
    }
  }
  (minimum, maximum)
}

///|
fn count_index_rejections(
  documents : Array[RomanIndexDocumentMetadata],
) -> Int64 {
  let mut count = 0L
  for document in documents {
    count = count + document.rejected_count.to_int64()
  }
  count
}

///|
fn count_index_unicode_entries(entries : Array[RomanIndexEntry]) -> Int64 {
  let mut count = 0L
  for entry in entries {
    if entry.used_unicode_compatibility {
      count = count + 1L
    }
  }
  count
}

///|
fn count_index_normalized_entries(entries : Array[RomanIndexEntry]) -> Int64 {
  let mut count = 0L
  for entry in entries {
    if entry.normalized_input {
      count = count + 1L
    }
  }
  count
}

///|
/// Recompute deterministic index statistics from retained entries and metadata.
pub fn summarize_roman_document_index(
  index : RomanDocumentIndex,
) -> RomanIndexSummary {
  let documents : Array[RomanIndexDocumentSummary] = []
  for document in index.documents {
    documents.push(summarize_index_document(index, document))
  }
  let (minimum, maximum) = index_entry_extrema(index.entries)
  {
    document_count: index.documents.length().to_int64(),
    entry_count: index.entries.length().to_int64(),
    rejected_count: count_index_rejections(index.documents),
    min_value: minimum,
    max_value: maximum,
    distinct_values: roman_index_value_frequencies(index).length().to_int64(),
    used_unicode: count_index_unicode_entries(index.entries),
    normalized_inputs: count_index_normalized_entries(index.entries),
    documents,
  }
}