///|
/// Describes matching intent and builds query-and-snapshot-specific state.
///
/// The trait is open so applications can add query types without changing
/// Searcher. Concrete queries create one Weight from global SearchStatistics,
/// and each Weight creates a comparable segment-local Scorer.
pub(open) trait Query {
  fn weight(Self, SearchStatistics) -> &Weight
}

///|
/// Query state that can create a Scorer for an immutable segment.
pub(open) trait Weight {
  fn scorer(Self, Segment) -> &Scorer
}

///|
/// Ordered iterator over matching segment-local documents and their scores.
pub(open) trait Scorer {
  fn advance(Self) -> Bool
  fn doc(Self) -> DocId
  fn score(Self) -> Double
}

///|
/// Exact field-qualified term query.
///
/// The term is already analyzed. QueryParser is the higher-level entry point
/// for raw field text; callers can keep using TermQuery for exact control.
pub struct TermQuery {
  term : Term
}

///|
pub fn TermQuery::new(term : Term) -> TermQuery {
  { term, }
}

///|
pub fn TermQuery::term(self : TermQuery) -> Term {
  self.term
}

///|
priv struct TermWeight {
  term : Term
  bm25 : Bm25Scorer
}

///|
pub impl Query for TermQuery with fn weight(self, statistics) {
  TermWeight::{
    term: self.term,
    bm25: Bm25Scorer::from_statistics(statistics, self.term),
  }
  as &Weight
}

///|
impl Weight for TermWeight with fn scorer(self, segment) {
  let postings = segment.postings_for(self.term)
  let scored_docs : Array[ScoredDoc] = []
  for posting in postings {
    scored_docs.push({
      doc_id: posting.doc_id,
      score: self.bm25.score(posting, segment),
    })
  }
  ArrayScorer::new(scored_docs) as &Scorer
}

///|
/// Multiplies every score produced by a child query.
pub struct BoostQuery {
  query : &Query
  boost : Double
}

///|
pub fn BoostQuery::new(query : &Query, boost : Double) -> BoostQuery {
  { query, boost }
}

///|
priv struct BoostWeight {
  weight : &Weight
  boost : Double
}

///|
pub impl Query for BoostQuery with fn weight(self, statistics) {
  BoostWeight::{ weight: self.query.weight(statistics), boost: self.boost }
  as &Weight
}

///|
impl Weight for BoostWeight with fn scorer(self, segment) {
  let child = self.weight.scorer(segment)
  let scored_docs : Array[ScoredDoc] = []
  while child.advance() {
    scored_docs.push({ doc_id: child.doc(), score: child.score() * self.boost })
  }
  ArrayScorer::new(scored_docs) as &Scorer
}

///|
/// Boolean occurrence semantics modeled after Lucene and Tantivy.
pub(all) enum Occur {
  Must
  Should
  MustNot
} derive(Eq, @debug.Debug)

///|
/// One query and its occurrence rule in a BooleanQuery.
pub struct BooleanClause {
  occur : Occur
  query : &Query
}

///|
pub fn BooleanClause::new(occur : Occur, query : &Query) -> BooleanClause {
  { occur, query }
}

///|
pub struct BooleanQuery {
  clauses : ReadOnlyArray[BooleanClause]
}

///|
pub fn BooleanQuery::new(clauses : Array[BooleanClause]) -> BooleanQuery {
  let frozen : Array[BooleanClause] = []
  for clause in clauses {
    frozen.push(clause)
  }
  { clauses: ReadOnlyArray::from_array(frozen) }
}

///|
priv struct WeightedClause {
  occur : Occur
  weight : &Weight
}

///|
priv struct BooleanWeight {
  clauses : ReadOnlyArray[WeightedClause]
}

///|
pub impl Query for BooleanQuery with fn weight(self, statistics) {
  let clauses : Array[WeightedClause] = []
  for clause in self.clauses {
    clauses.push({
      occur: clause.occur,
      weight: clause.query.weight(statistics),
    })
  }
  BooleanWeight::{ clauses: ReadOnlyArray::from_array(clauses) } as &Weight
}

///|
impl Weight for BooleanWeight with fn scorer(self, segment) {
  let document_count = segment.doc_count()
  let must_matches = Array::make(document_count, 0)
  let should_matches = Array::make(document_count, false)
  let prohibited = Array::make(document_count, false)
  let scores = Array::make(document_count, 0.0)
  let mut must_clause_count = 0
  for clause in self.clauses {
    if clause.occur == Occur::Must {
      must_clause_count += 1
    }
    let child = clause.weight.scorer(segment)
    while child.advance() {
      let doc_index = child.doc().value
      if doc_index < 0 || doc_index >= document_count {
        continue
      }
      match clause.occur {
        Occur::Must => {
          must_matches[doc_index] += 1
          scores[doc_index] += child.score()
        }
        Occur::Should => {
          should_matches[doc_index] = true
          scores[doc_index] += child.score()
        }
        Occur::MustNot => prohibited[doc_index] = true
      }
    }
  }
  let scored_docs : Array[ScoredDoc] = []
  for doc_index in 0.. 0 {
      true
    } else {
      should_matches[doc_index]
    }
    if has_required && has_positive && !prohibited[doc_index] {
      scored_docs.push({
        doc_id: DocId::new(doc_index),
        score: scores[doc_index],
      })
    }
  }
  ArrayScorer::new(scored_docs) as &Scorer
}

///|
/// Exact zero-slop phrase query within one field.
pub struct PhraseQuery {
  field_id : FieldId
  texts : ReadOnlyArray[String]
  positions : ReadOnlyArray[Int]
}

///|
pub fn PhraseQuery::new(
  field_id : FieldId,
  texts : Array[String],
) -> PhraseQuery {
  let frozen : Array[String] = []
  let positions : Array[Int] = []
  for text in texts {
    frozen.push(text)
    positions.push(positions.length())
  }
  {
    field_id,
    texts: ReadOnlyArray::from_array(frozen),
    positions: ReadOnlyArray::from_array(positions),
  }
}

///|
/// Creates a phrase with explicit relative token positions, preserving gaps
/// introduced during query-time analysis.
pub fn PhraseQuery::with_positions(
  field_id : FieldId,
  texts : Array[String],
  positions : Array[Int],
) -> PhraseQuery {
  guard texts.length() == positions.length() else {
    abort("phrase texts and positions must have equal length")
  }
  let frozen_texts : Array[String] = []
  let frozen_positions : Array[Int] = []
  let mut previous_position = -1
  for index in 0..= previous_position else {
      abort("phrase positions must be non-decreasing")
    }
    frozen_texts.push(texts[index])
    frozen_positions.push(positions[index])
    previous_position = positions[index]
  }
  {
    field_id,
    texts: ReadOnlyArray::from_array(frozen_texts),
    positions: ReadOnlyArray::from_array(frozen_positions),
  }
}

///|
priv struct PhraseWeight {
  terms : ReadOnlyArray[Term]
  positions : ReadOnlyArray[Int]
  bm25_scorers : ReadOnlyArray[Bm25Scorer]
}

///|
pub impl Query for PhraseQuery with fn weight(self, statistics) {
  let terms : Array[Term] = []
  let bm25_scorers : Array[Bm25Scorer] = []
  for text in self.texts {
    let term = Term::new(self.field_id, text)
    terms.push(term)
    bm25_scorers.push(Bm25Scorer::from_statistics(statistics, term))
  }
  PhraseWeight::{
    terms: ReadOnlyArray::from_array(terms),
    positions: self.positions,
    bm25_scorers: ReadOnlyArray::from_array(bm25_scorers),
  }
  as &Weight
}

///|
fn posting_for_doc(
  postings : ReadOnlyArray[Posting],
  doc_id : DocId,
) -> Posting? {
  match postings.search_by(posting => posting.doc_id == doc_id) {
    Some(index) => Some(postings[index])
    None => None
  }
}

///|
fn contains_position(positions : ReadOnlyArray[Int], expected : Int) -> Bool {
  positions.search_by(position => position == expected) is Some(_)
}

///|
fn phrase_matches(
  postings : Array[Posting],
  query_positions : ReadOnlyArray[Int],
) -> Bool {
  if postings.length() == 0 {
    return false
  }
  let base_position = query_positions[0]
  for start_position in postings[0].positions {
    let mut matched = true
    for term_offset in 1.. matched_postings.push(posting)
        None => {
          all_terms_match = false
          break
        }
      }
    }
    if all_terms_match && phrase_matches(matched_postings, self.positions) {
      let mut score = 0.0
      for term_index in 0..