///|
/// Return a query with no evidence constraints or page limit.
pub fn default_roman_index_query() -> RomanIndexQuery {
  {
    document_id: None,
    source_text: None,
    normalized_text: None,
    canonical_text: None,
    mode: None,
    min_value: None,
    max_value: None,
    used_unicode: None,
    normalized_input: None,
    offset: 0,
    limit: None,
  }
}

///|
fn validate_roman_index_query(
  query : RomanIndexQuery,
) -> Result[Unit, RomanIndexQueryError] {
  match (query.min_value, query.max_value) {
    (Some(minimum), Some(maximum)) if minimum > maximum =>
      return Err(IndexQueryMinimumAboveMaximum(minimum, maximum))
    _ => ()
  }
  if query.offset < 0 {
    return Err(NegativeIndexQueryOffset(query.offset))
  }
  match query.limit {
    Some(limit) if limit <= 0 => Err(NonPositiveIndexQueryLimit(limit))
    _ => Ok(())
  }
}

///|
fn index_entry_matches_document(
  entry : RomanIndexEntry,
  expected : String?,
) -> Bool {
  match expected {
    Some(document_id) => entry.document_id == document_id
    None => true
  }
}

///|
fn index_entry_matches_source(
  entry : RomanIndexEntry,
  expected : String?,
) -> Bool {
  match expected {
    Some(source_text) => entry.source_text == source_text
    None => true
  }
}

///|
fn index_entry_matches_normalized(
  entry : RomanIndexEntry,
  expected : String?,
) -> Bool {
  match expected {
    Some(normalized_text) => entry.normalized_text == normalized_text
    None => true
  }
}

///|
fn index_entry_matches_canonical(
  entry : RomanIndexEntry,
  expected : String?,
) -> Bool {
  match expected {
    Some(canonical_text) => entry.canonical == canonical_text
    None => true
  }
}

///|
fn index_entry_matches_mode(
  entry : RomanIndexEntry,
  expected : RomanMode?,
) -> Bool {
  match expected {
    Some(mode) => entry.mode == mode
    None => true
  }
}

///|
fn index_entry_matches_minimum(entry : RomanIndexEntry, minimum : Int?) -> Bool {
  match minimum {
    Some(value) => entry.value >= value
    None => true
  }
}

///|
fn index_entry_matches_maximum(entry : RomanIndexEntry, maximum : Int?) -> Bool {
  match maximum {
    Some(value) => entry.value <= value
    None => true
  }
}

///|
fn index_entry_matches_unicode(
  entry : RomanIndexEntry,
  expected : Bool?,
) -> Bool {
  match expected {
    Some(used_unicode) => entry.used_unicode_compatibility == used_unicode
    None => true
  }
}

///|
fn index_entry_matches_normalization(
  entry : RomanIndexEntry,
  expected : Bool?,
) -> Bool {
  match expected {
    Some(normalized_input) => entry.normalized_input == normalized_input
    None => true
  }
}

///|
fn roman_index_entry_matches_query(
  entry : RomanIndexEntry,
  query : RomanIndexQuery,
) -> Bool {
  index_entry_matches_document(entry, query.document_id) &&
  index_entry_matches_source(entry, query.source_text) &&
  index_entry_matches_normalized(entry, query.normalized_text) &&
  index_entry_matches_canonical(entry, query.canonical_text) &&
  index_entry_matches_mode(entry, query.mode) &&
  index_entry_matches_minimum(entry, query.min_value) &&
  index_entry_matches_maximum(entry, query.max_value) &&
  index_entry_matches_unicode(entry, query.used_unicode) &&
  index_entry_matches_normalization(entry, query.normalized_input)
}

///|
fn index_query_page_has_capacity(page_length : Int, limit : Int?) -> Bool {
  match limit {
    Some(maximum) => page_length < maximum
    None => true
  }
}

///|
/// Filter before paginating and report the complete filtered count.
pub fn query_roman_index_page(
  index : RomanDocumentIndex,
  query : RomanIndexQuery,
) -> Result[RomanIndexQueryPage, RomanIndexQueryError] {
  match validate_roman_index_query(query) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  let entries : Array[RomanIndexEntry] = []
  let mut total_matches = 0L
  let mut matched_position = 0
  for entry in index.entries {
    if roman_index_entry_matches_query(entry, query) {
      if matched_position >= query.offset &&
        index_query_page_has_capacity(entries.length(), query.limit) {
        entries.push(entry)
      }
      matched_position = matched_position + 1
      total_matches = total_matches + 1L
    }
  }
  Ok({ entries, total_matches, offset: query.offset, limit: query.limit })
}

///|
/// Filter and paginate entries while preserving complete index order.
pub fn query_roman_index(
  index : RomanDocumentIndex,
  query : RomanIndexQuery,
) -> Result[Array[RomanIndexEntry], RomanIndexQueryError] {
  match query_roman_index_page(index, query) {
    Ok(page) => Ok(page.entries)
    Err(error) => Err(error)
  }
}

///|
/// Count all filtered entries without applying query pagination.
pub fn count_roman_index_matches(
  index : RomanDocumentIndex,
  query : RomanIndexQuery,
) -> Result[Int64, RomanIndexQueryError] {
  match validate_roman_index_query(query) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  match query_roman_index_page(index, { ..query, offset: 0, limit: None }) {
    Ok(page) => Ok(page.total_matches)
    Err(error) => Err(error)
  }
}

///|
/// Find an entry by its stable global ordinal.
pub fn find_roman_index_entry_by_ordinal(
  index : RomanDocumentIndex,
  ordinal : Int,
) -> RomanIndexEntry? {
  for entry in index.entries {
    if entry.ordinal == ordinal {
      return Some(entry)
    }
  }
  None
}

///|
/// Find retained document metadata by exact ID.
pub fn find_roman_index_document(
  index : RomanDocumentIndex,
  document_id : String,
) -> RomanIndexDocumentMetadata? {
  for document in index.documents {
    if document.id == document_id {
      return Some(document)
    }
  }
  None
}

///|
/// Return all entries for an exact document ID in source order.
pub fn roman_index_entries_for_document(
  index : RomanDocumentIndex,
  document_id : String,
) -> Array[RomanIndexEntry] {
  let entries : Array[RomanIndexEntry] = []
  for entry in index.entries {
    if entry.document_id == document_id {
      entries.push(entry)
    }
  }
  entries
}

///|
/// Return retained rejection details for one document in source order.
pub fn roman_index_rejections_for_document(
  index : RomanDocumentIndex,
  document_id : String,
) -> Array[RomanIndexRejectionSummary] {
  let rejections : Array[RomanIndexRejectionSummary] = []
  for rejection in index.rejections {
    if rejection.document_id == document_id {
      rejections.push(rejection)
    }
  }
  rejections
}

///|
/// Return the first entry having an exact numeric value.
pub fn first_roman_index_entry_for_value(
  index : RomanDocumentIndex,
  value : Int,
) -> RomanIndexEntry? {
  for entry in index.entries {
    if entry.value == value {
      return Some(entry)
    }
  }
  None
}

///|
/// Return all entries having an exact numeric value.
pub fn roman_index_entries_for_value(
  index : RomanDocumentIndex,
  value : Int,
) -> Array[RomanIndexEntry] {
  let entries : Array[RomanIndexEntry] = []
  for entry in index.entries {
    if entry.value == value {
      entries.push(entry)
    }
  }
  entries
}

///|
/// Return entries inside an inclusive numeric range.
pub fn roman_index_entries_in_value_range(
  index : RomanDocumentIndex,
  minimum : Int,
  maximum : Int,
) -> Result[Array[RomanIndexEntry], RomanIndexQueryError] {
  query_roman_index(index, {
    ..default_roman_index_query(),
    min_value: Some(minimum),
    max_value: Some(maximum),
  })
}

///|
/// Return entries having exact source spelling.
pub fn roman_index_entries_for_source_text(
  index : RomanDocumentIndex,
  source_text : String,
) -> Array[RomanIndexEntry] {
  let entries : Array[RomanIndexEntry] = []
  for entry in index.entries {
    if entry.source_text == source_text {
      entries.push(entry)
    }
  }
  entries
}

///|
/// Return entries having exact normalized spelling.
pub fn roman_index_entries_for_normalized_text(
  index : RomanDocumentIndex,
  normalized_text : String,
) -> Array[RomanIndexEntry] {
  let entries : Array[RomanIndexEntry] = []
  for entry in index.entries {
    if entry.normalized_text == normalized_text {
      entries.push(entry)
    }
  }
  entries
}

///|
/// Return entries having exact profile-canonical spelling.
pub fn roman_index_entries_for_canonical_text(
  index : RomanDocumentIndex,
  canonical_text : String,
) -> Array[RomanIndexEntry] {
  let entries : Array[RomanIndexEntry] = []
  for entry in index.entries {
    if entry.canonical == canonical_text {
      entries.push(entry)
    }
  }
  entries
}

///|
/// Group equal values in order of the first matching entry.
pub fn group_roman_index_entries_by_value(
  index : RomanDocumentIndex,
) -> Array[RomanIndexValueGroup] {
  let groups : Array[RomanIndexValueGroup] = []
  for entry in index.entries {
    let mut group_index = -1
    for position = 0; position < groups.length(); position = position + 1 {
      if groups[position].value == entry.value {
        group_index = position
        break
      }
    }
    if group_index < 0 {
      groups.push({ value: entry.value, entries: [entry] })
    } else {
      groups[group_index].entries.push(entry)
    }
  }
  groups
}

///|
/// Group equal canonical spellings in first occurrence order.
pub fn group_roman_index_entries_by_canonical(
  index : RomanDocumentIndex,
) -> Array[RomanIndexCanonicalGroup] {
  let groups : Array[RomanIndexCanonicalGroup] = []
  for entry in index.entries {
    let mut group_index = -1
    for position = 0; position < groups.length(); position = position + 1 {
      if groups[position].canonical == entry.canonical {
        group_index = position
        break
      }
    }
    if group_index < 0 {
      groups.push({ canonical: entry.canonical, entries: [entry] })
    } else {
      groups[group_index].entries.push(entry)
    }
  }
  groups
}

///|
/// Group entries by document according to the first accepted entry.
pub fn group_roman_index_entries_by_document(
  index : RomanDocumentIndex,
) -> Array[RomanIndexDocumentGroup] {
  let groups : Array[RomanIndexDocumentGroup] = []
  for entry in index.entries {
    let mut group_index = -1
    for position = 0; position < groups.length(); position = position + 1 {
      if groups[position].document_id == entry.document_id {
        group_index = position
        break
      }
    }
    if group_index < 0 {
      groups.push({ document_id: entry.document_id, entries: [entry] })
    } else {
      groups[group_index].entries.push(entry)
    }
  }
  groups
}

///|
/// Group entries by notation mode in first occurrence order.
pub fn group_roman_index_entries_by_mode(
  index : RomanDocumentIndex,
) -> Array[RomanIndexModeGroup] {
  let groups : Array[RomanIndexModeGroup] = []
  for entry in index.entries {
    let mut group_index = -1
    for position = 0; position < groups.length(); position = position + 1 {
      if groups[position].mode == entry.mode {
        group_index = position
        break
      }
    }
    if group_index < 0 {
      groups.push({ mode: entry.mode, entries: [entry] })
    } else {
      groups[group_index].entries.push(entry)
    }
  }
  groups
}

///|
/// Group entries by Unicode compatibility evidence.
pub fn group_roman_index_entries_by_unicode(
  index : RomanDocumentIndex,
) -> Array[RomanIndexUnicodeGroup] {
  let groups : Array[RomanIndexUnicodeGroup] = []
  for entry in index.entries {
    let mut group_index = -1
    for position = 0; position < groups.length(); position = position + 1 {
      if groups[position].used_unicode == entry.used_unicode_compatibility {
        group_index = position
        break
      }
    }
    if group_index < 0 {
      groups.push({
        used_unicode: entry.used_unicode_compatibility,
        entries: [entry],
      })
    } else {
      groups[group_index].entries.push(entry)
    }
  }
  groups
}

///|
/// Group entries by whether parsing changed their input spelling.
pub fn group_roman_index_entries_by_normalization(
  index : RomanDocumentIndex,
) -> Array[RomanIndexNormalizationGroup] {
  let groups : Array[RomanIndexNormalizationGroup] = []
  for entry in index.entries {
    let mut group_index = -1
    for position = 0; position < groups.length(); position = position + 1 {
      if groups[position].normalized_input == entry.normalized_input {
        group_index = position
        break
      }
    }
    if group_index < 0 {
      groups.push({ normalized_input: entry.normalized_input, entries: [entry] })
    } else {
      groups[group_index].entries.push(entry)
    }
  }
  groups
}