///|
/// 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 advance_to(Self, DocId) -> 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
}

///|
priv struct TermScorer {
  cursor : PostingCursor
  segment : Segment
  bm25 : Bm25Scorer
}

///|
impl Scorer for TermScorer with fn advance(self) {
  self.cursor.advance()
}

///|
impl Scorer for TermScorer with fn advance_to(self, target) {
  self.cursor.advance_to(target)
}

///|
impl Scorer for TermScorer with fn doc(self) {
  self.cursor.doc()
}

///|
impl Scorer for TermScorer with fn score(self) {
  match self.cursor.posting() {
    Some(posting) => self.bm25.score(posting, self.segment)
    None => 0.0
  }
}

///|
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) {
  TermScorer::{
    cursor: segment.posting_cursor(self.term),
    segment,
    bm25: self.bm25,
  }
  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
}

///|
priv struct BoostScorer {
  child : &Scorer
  boost : Double
}

///|
impl Scorer for BoostScorer with fn advance(self) {
  self.child.advance()
}

///|
impl Scorer for BoostScorer with fn advance_to(self, target) {
  self.child.advance_to(target)
}

///|
impl Scorer for BoostScorer with fn doc(self) {
  self.child.doc()
}

///|
impl Scorer for BoostScorer with fn score(self) {
  self.child.score() * self.boost
}

///|
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) {
  BoostScorer::{ child: self.weight.scorer(segment), boost: self.boost }
  as &Scorer
}

///|
/// Boolean occurrence semantics modeled after Lucene and Tantivy.
pub(all) enum Occur {
  Must
  Filter
  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]
}

///|
priv struct BooleanScorer {
  required : ReadOnlyArray[&Scorer]
  optional : ReadOnlyArray[&Scorer]
  prohibited : ReadOnlyArray[&Scorer]
  mut current_doc : DocId
  mut current_score : Double
}

///|
priv struct FilterScorer {
  child : &Scorer
}

///|
impl Scorer for FilterScorer with fn advance(self) {
  self.child.advance()
}

///|
impl Scorer for FilterScorer with fn advance_to(self, target) {
  self.child.advance_to(target)
}

///|
impl Scorer for FilterScorer with fn doc(self) {
  self.child.doc()
}

///|
impl Scorer for FilterScorer with fn score(_self) {
  0.0
}

///|
fn BooleanScorer::is_prohibited(
  self : BooleanScorer,
  candidate : DocId,
) -> Bool {
  for scorer in self.prohibited {
    if scorer.advance_to(candidate) && scorer.doc() == candidate {
      return true
    }
  }
  false
}

///|
fn BooleanScorer::seek_required(
  self : BooleanScorer,
  initial_target : DocId,
) -> Bool {
  let mut candidate = initial_target
  while true {
    let mut aligned = false
    while !aligned {
      aligned = true
      for scorer in self.required {
        if !scorer.advance_to(candidate) {
          self.current_doc = DocId::new(-1)
          return false
        }
        if scorer.doc().value > candidate.value {
          candidate = scorer.doc()
          aligned = false
        }
      }
    }
    if self.is_prohibited(candidate) {
      candidate = DocId::new(candidate.value + 1)
      continue
    }
    let mut score = 0.0
    for scorer in self.required {
      score += scorer.score()
    }
    for scorer in self.optional {
      if scorer.advance_to(candidate) && scorer.doc() == candidate {
        score += scorer.score()
      }
    }
    self.current_doc = candidate
    self.current_score = score
    return true
  } nobreak {
    false
  }
}

///|
fn BooleanScorer::seek_optional(
  self : BooleanScorer,
  initial_target : DocId,
) -> Bool {
  let mut target = initial_target
  while true {
    let mut found = false
    let mut candidate = DocId::new(-1)
    for scorer in self.optional {
      if scorer.advance_to(target) &&
        (!found || scorer.doc().value < candidate.value) {
        candidate = scorer.doc()
        found = true
      }
    }
    if !found {
      self.current_doc = DocId::new(-1)
      return false
    }
    if self.is_prohibited(candidate) {
      target = DocId::new(candidate.value + 1)
      continue
    }
    let mut score = 0.0
    for scorer in self.optional {
      if scorer.doc() == candidate {
        score += scorer.score()
      }
    }
    self.current_doc = candidate
    self.current_score = score
    return true
  } nobreak {
    false
  }
}

///|
impl Scorer for BooleanScorer with fn advance(self) {
  let target = if self.current_doc.value < 0 {
    DocId::new(0)
  } else {
    DocId::new(self.current_doc.value + 1)
  }
  Scorer::advance_to(self, target)
}

///|
impl Scorer for BooleanScorer with fn advance_to(self, target) {
  if self.current_doc.value >= target.value {
    return true
  }
  if self.required.length() > 0 {
    self.seek_required(target)
  } else if self.optional.length() > 0 {
    self.seek_optional(target)
  } else {
    self.current_doc = DocId::new(-1)
    false
  }
}

///|
impl Scorer for BooleanScorer with fn doc(self) {
  self.current_doc
}

///|
impl Scorer for BooleanScorer with fn score(self) {
  self.current_score
}

///|
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 required : Array[&Scorer] = []
  let optional : Array[&Scorer] = []
  let prohibited : Array[&Scorer] = []
  for clause in self.clauses {
    let child = clause.weight.scorer(segment)
    match clause.occur {
      Occur::Must => required.push(child)
      Occur::Filter => required.push(FilterScorer::{ child, } as &Scorer)
      Occur::Should => optional.push(child)
      Occur::MustNot => prohibited.push(child)
    }
  }
  BooleanScorer::{
    required: ReadOnlyArray::from_array(required),
    optional: ReadOnlyArray::from_array(optional),
    prohibited: ReadOnlyArray::from_array(prohibited),
    current_doc: DocId::new(-1),
    current_score: 0.0,
  }
  as &Scorer
}

///|
/// Exact zero-slop phrase query within one field.
pub struct PhraseQuery {
  field_id : FieldId
  texts : ReadOnlyArray[String]
  positions : ReadOnlyArray[Int]
  slop : 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),
    slop: 0,
  }
}

///|
/// 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),
    slop: 0,
  }
}

///|
/// Allows the ordered phrase to consume up to `slop` positional moves. A
/// value of zero preserves exact phrase semantics.
pub fn PhraseQuery::with_slop(self : PhraseQuery, slop : Int) -> PhraseQuery {
  guard slop >= 0 else { abort("phrase slop must be non-negative") }
  {
    field_id: self.field_id,
    texts: self.texts,
    positions: self.positions,
    slop,
  }
}

///|
pub fn PhraseQuery::slop(self : PhraseQuery) -> Int {
  self.slop
}

///|
priv struct PhraseWeight {
  terms : ReadOnlyArray[Term]
  positions : ReadOnlyArray[Int]
  slop : 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,
    slop: self.slop,
    bm25_scorers: ReadOnlyArray::from_array(bm25_scorers),
  }
  as &Weight
}

///|
priv struct PhraseScorer {
  cursors : ReadOnlyArray[PostingCursor]
  positions : ReadOnlyArray[Int]
  slop : Int
  bm25_scorers : ReadOnlyArray[Bm25Scorer]
  segment : Segment
  mut current_doc : DocId
  mut current_score : Double
}

///|
impl Scorer for PhraseScorer with fn advance(self) {
  let target = if self.current_doc.value < 0 {
    DocId::new(0)
  } else {
    DocId::new(self.current_doc.value + 1)
  }
  Scorer::advance_to(self, target)
}

///|
impl Scorer for PhraseScorer with fn advance_to(self, target) {
  if self.current_doc.value >= target.value {
    return true
  }
  let mut candidate = target
  while true {
    let mut aligned = false
    while !aligned {
      aligned = true
      for cursor in self.cursors {
        if !cursor.advance_to(candidate) {
          self.current_doc = DocId::new(-1)
          return false
        }
        if cursor.doc().value > candidate.value {
          candidate = cursor.doc()
          aligned = false
        }
      }
    }
    let matched_postings : Array[Posting] = []
    for cursor in self.cursors {
      match cursor.posting() {
        Some(posting) => matched_postings.push(posting)
        None => abort("aligned phrase cursor has no posting")
      }
    }
    if phrase_matches(matched_postings, self.positions, self.slop) {
      let mut score = 0.0
      for term_index in 0.. Bool {
  if postings.length() == 0 {
    return false
  }
  for start_position in postings[0].positions {
    let previous_positions : Array[Int] = [start_position]
    let previous_costs : Array[Int] = [0]
    let mut matched_terms = 1
    for term_offset in 1.. 0 && actual_delta == 0) {
            continue
          }
          let move_cost = (actual_delta - expected_delta).abs()
          let total_cost = previous_costs[state_index] + move_cost
          if total_cost <= slop {
            match next_positions.search_by(position => position == candidate) {
              Some(existing) =>
                if total_cost < next_costs[existing] {
                  next_costs[existing] = total_cost
                }
              None => {
                next_positions.push(candidate)
                next_costs.push(total_cost)
              }
            }
          }
        }
      }
      if next_positions.length() == 0 {
        break
      }
      previous_positions.clear()
      previous_costs.clear()
      for position in next_positions {
        previous_positions.push(position)
      }
      for cost in next_costs {
        previous_costs.push(cost)
      }
      matched_terms += 1
    }
    if matched_terms == postings.length() {
      return true
    }
  }
  false
}

///|
impl Weight for PhraseWeight with fn scorer(self, segment) {
  if self.terms.length() == 0 {
    return EmptyScorer::{  } as &Scorer
  }
  let cursors : Array[PostingCursor] = []
  for term in self.terms {
    cursors.push(segment.posting_cursor(term))
  }
  PhraseScorer::{
    cursors: ReadOnlyArray::from_array(cursors),
    positions: self.positions,
    slop: self.slop,
    bm25_scorers: self.bm25_scorers,
    segment,
    current_doc: DocId::new(-1),
    current_score: 0.0,
  }
  as &Scorer
}