///|
/// A disjunction of scored terms with an optimized WAND Top-K entry point.
pub struct BlockMaxWandQuery {
  terms : ReadOnlyArray[Term]
}

///|
pub fn BlockMaxWandQuery::new(terms : Array[Term]) -> BlockMaxWandQuery {
  let unique : Array[Term] = []
  for term in terms {
    if unique.search_by(existing => existing == term) is None {
      unique.push(term)
    }
  }
  { terms: ReadOnlyArray::from_array(unique) }
}

///|
pub(all) struct WandSearchResult {
  hits : ReadOnlyArray[SearchHit]
  evaluated_candidates : Int
  skipped_candidates : Int
} derive(@debug.Debug)

///|
priv struct WandWeight {
  child : &Weight
}

///|
pub impl Query for BlockMaxWandQuery with fn weight(self, statistics) {
  let clauses : Array[BooleanClause] = []
  for term in self.terms {
    clauses.push(
      BooleanClause::new(Occur::Should, TermQuery::new(term) as &Query),
    )
  }
  WandWeight::{ child: Query::weight(BooleanQuery::new(clauses), statistics) }
  as &Weight
}

///|
impl Weight for WandWeight with fn scorer(self, segment) {
  self.child.scorer(segment)
}

///|
priv struct WandTermState {
  cursor : PostingCursor
  bm25 : Bm25Scorer
  upper_bound : Double
}

///|
fn wand_term_state(
  segment : Segment,
  term : Term,
  statistics : SearchStatistics,
) -> WandTermState? {
  let postings = segment.postings_for(term)
  if postings.length() == 0 {
    return None
  }
  let bm25 = Bm25Scorer::from_statistics(statistics, term)
  let mut upper_bound = 0.0
  for posting in postings {
    let score = bm25.score(posting, segment)
    if score > upper_bound {
      upper_bound = score
    }
  }
  let cursor = segment.posting_cursor(term)
  if !cursor.advance() {
    return None
  }
  Some({ cursor, bm25, upper_bound })
}

///|
/// Runs safe WAND pruning using exact per-term upper bounds. Returned hits are
/// identical to the ordinary disjunction and deterministic Top-K tie-break.
pub fn Searcher::search_wand(
  self : Searcher,
  query : BlockMaxWandQuery,
  collector : TopKCollector,
) -> WandSearchResult {
  let heap = TopKHeap::new(collector.limit)
  let mut evaluated_candidates = 0
  let mut skipped_candidates = 0
  for segment_ord in 0.. states.push(state)
        None => ()
      }
    }
    while true {
      let active : Array[Int] = []
      for index in 0..= 0 {
          active.push(index)
        }
      }
      if active.length() == 0 {
        break
      }
      active.sort_by((left, right) => {
        states[left].cursor.doc().value.compare(
          states[right].cursor.doc().value,
        )
      })
      let threshold = match heap.competitive_score() {
        Some(score) => score
        None => -1.0
      }
      let mut upper_sum = 0.0
      let mut pivot_offset = -1
      for offset in 0.. threshold {
          pivot_offset = offset
          break
        }
      }
      if pivot_offset < 0 {
        break
      }
      let pivot_doc = states[active[pivot_offset]].cursor.doc()
      let first_doc = states[active[0]].cursor.doc()
      if first_doc == pivot_doc {
        let mut score = 0.0
        for state_index in active {
          if states[state_index].cursor.doc() == pivot_doc {
            match states[state_index].cursor.posting() {
              Some(posting) =>
                score += states[state_index].bm25.score(
                  posting,
                  snapshot.segment,
                )
              None => ()
            }
          }
        }
        evaluated_candidates += 1
        if !snapshot.is_deleted(pivot_doc) {
          heap.offer({ address: DocAddress::new(segment_ord, pivot_doc), score })
        }
        for state_index in active {
          if states[state_index].cursor.doc() == pivot_doc {
            ignore(states[state_index].cursor.advance())
          }
        }
      } else {
        for offset in 0.. before + 1 {
            skipped_candidates += after - before - 1
          }
        }
      }
    }
  }
  { hits: collector.finish(heap), evaluated_candidates, skipped_candidates }
}