///|
/// Read-only collection statistics for one Searcher snapshot.
///
/// Query implementations use these global values while constructing Weight.
/// A Weight can then create comparable Scorers for every immutable Segment.
pub struct SearchStatistics {
  segments : ReadOnlyArray[SnapshotSegment]
}

///|
fn SearchStatistics::from_segments(
  segments : ReadOnlyArray[SnapshotSegment],
) -> SearchStatistics {
  { segments, }
}

///|
pub fn SearchStatistics::segment_count(self : SearchStatistics) -> Int {
  self.segments.length()
}

///|
pub fn SearchStatistics::doc_count(self : SearchStatistics) -> Int {
  let mut count = 0
  for snapshot in self.segments {
    count += snapshot.live_doc_count()
  }
  count
}

///|
pub fn SearchStatistics::field_doc_count(
  self : SearchStatistics,
  field_id : FieldId,
) -> Int {
  let mut count = 0
  for snapshot in self.segments {
    for doc_index in 0.. Int {
  let mut count = 0
  for snapshot in self.segments {
    for posting in snapshot.segment.postings_for(term) {
      if !snapshot.is_deleted(posting.doc_id) {
        count += 1
      }
    }
  }
  count
}

///|
pub fn SearchStatistics::average_field_length(
  self : SearchStatistics,
  field_id : FieldId,
) -> Double {
  let mut document_count = 0
  let mut total_length = 0
  for snapshot in self.segments {
    for doc_index in 0.. ArrayScorer {
  { documents: ReadOnlyArray::from_array(documents), cursor: -1 }
}

///|
impl Scorer for ArrayScorer with fn advance(self) {
  if self.cursor + 1 < self.documents.length() {
    self.cursor += 1
    true
  } else {
    false
  }
}

///|
impl Scorer for ArrayScorer with fn doc(self) {
  if self.cursor < 0 || self.cursor >= self.documents.length() {
    DocId::new(-1)
  } else {
    self.documents[self.cursor].doc_id
  }
}

///|
impl Scorer for ArrayScorer with fn score(self) {
  if self.cursor < 0 || self.cursor >= self.documents.length() {
    0.0
  } else {
    self.documents[self.cursor].score
  }
}

///|
/// Builds single-Segment BM25 state. Retained for M1-M3 API compatibility.
pub fn Bm25Scorer::new(segment : Segment, term : Term) -> Bm25Scorer {
  Bm25Scorer::from_statistics(
    SearchStatistics::from_segments(
      ReadOnlyArray::from_array([live_snapshot(segment)]),
    ),
    term,
  )
}

///|
/// Builds query state from the complete Searcher snapshot.
pub fn Bm25Scorer::from_statistics(
  statistics : SearchStatistics,
  term : Term,
) -> Bm25Scorer {
  let document_count = statistics.field_doc_count(term.field_id)
  let document_frequency = statistics.document_frequency(term)
  let idf = if document_count <= 0 || document_frequency <= 0 {
    0.0
  } else {
    let n = document_count.to_double()
    let df = document_frequency.to_double()
    @math.ln(1.0 + (n - df + 0.5) / (df + 0.5))
  }
  {
    field_id: term.field_id,
    idf,
    average_length: statistics.average_field_length(term.field_id),
    k1: 1.2,
    b: 0.75,
  }
}

///|
pub fn Bm25Scorer::score(
  self : Bm25Scorer,
  posting : Posting,
  segment : Segment,
) -> Double {
  let field_length = segment.field_length(posting.doc_id, self.field_id)
  if self.idf <= 0.0 || self.average_length <= 0.0 || field_length <= 0 {
    return 0.0
  }
  let term_frequency = posting.term_freq.to_double()
  let length_normalization = 1.0 -
    self.b +
    self.b * field_length.to_double() / self.average_length
  let denominator = term_frequency + self.k1 * length_normalization
  self.idf * term_frequency * (self.k1 + 1.0) / denominator
}