///|
/// Term query with explicit BM25 parameters and a field-level score boost.
pub struct ConfiguredTermQuery {
  term : Term
  config : Bm25Config
  boost : Double
}

///|
pub fn ConfiguredTermQuery::new(
  term : Term,
  config : Bm25Config,
  boost : Double,
) -> ConfiguredTermQuery {
  guard boost >= 0.0 else { abort("field boost must be non-negative") }
  { term, config, boost }
}

///|
priv struct ConfiguredTermWeight {
  term : Term
  scorer : Bm25Scorer
  boost : Double
}

///|
priv struct ConfiguredTermScorer {
  cursor : PostingCursor
  segment : Segment
  scorer : Bm25Scorer
  boost : Double
}

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

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

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

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

///|
impl Weight for ConfiguredTermWeight with fn scorer(self, segment) {
  ConfiguredTermScorer::{
    cursor: segment.posting_cursor(self.term),
    segment,
    scorer: self.scorer,
    boost: self.boost,
  }
  as &Scorer
}

///|
pub impl Query for ConfiguredTermQuery with fn weight(self, statistics) {
  ConfiguredTermWeight::{
    term: self.term,
    scorer: Bm25Scorer::from_statistics_with_config(
      statistics,
      self.term,
      self.config,
    ),
    boost: self.boost,
  }
  as &Weight
}

///|
pub(all) enum ScoreFunction {
  MultiplyFastField(FieldId, Double, Double)
  AddFastField(FieldId, Double, Double)
} derive(Eq, @debug.Debug)

///|
pub struct FunctionScoreQuery {
  query : &Query
  function : ScoreFunction
}

///|
pub fn FunctionScoreQuery::new(
  query : &Query,
  function : ScoreFunction,
) -> FunctionScoreQuery {
  { query, function }
}

///|
priv struct FunctionScoreWeight {
  child : &Weight
  function : ScoreFunction
}

///|
priv struct FunctionScoreScorer {
  child : &Scorer
  segment : Segment
  function : ScoreFunction
}

///|
fn fast_field_number(
  segment : Segment,
  field_id : FieldId,
  doc_id : DocId,
) -> Double? {
  for value in segment.fast_values(field_id, doc_id) {
    match field_value_as_double(value) {
      Some(number) => return Some(number)
      None => ()
    }
  }
  None
}

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

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

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

///|
impl Scorer for FunctionScoreScorer with fn score(self) {
  let base = self.child.score()
  match self.function {
    MultiplyFastField(field_id, factor, missing) => {
      let feature = match
        fast_field_number(self.segment, field_id, self.child.doc()) {
        Some(value) => value * factor
        None => missing
      }
      base * feature
    }
    AddFastField(field_id, factor, missing) => {
      let feature = match
        fast_field_number(self.segment, field_id, self.child.doc()) {
        Some(value) => value * factor
        None => missing
      }
      base + feature
    }
  }
}

///|
impl Weight for FunctionScoreWeight with fn scorer(self, segment) {
  FunctionScoreScorer::{
    child: self.child.scorer(segment),
    segment,
    function: self.function,
  }
  as &Scorer
}

///|
pub impl Query for FunctionScoreQuery with fn weight(self, statistics) {
  FunctionScoreWeight::{
    child: self.query.weight(statistics),
    function: self.function,
  }
  as &Weight
}

///|
/// One node in a machine-readable score explanation tree.
pub struct Explanation {
  matched : Bool
  value : Double
  description : String
  details : ReadOnlyArray[Explanation]
}

///|
pub fn Explanation::matched(self : Explanation) -> Bool {
  self.matched
}

///|
pub fn Explanation::value(self : Explanation) -> Double {
  self.value
}

///|
pub fn Explanation::description(self : Explanation) -> String {
  self.description
}

///|
pub fn Explanation::details(self : Explanation) -> ReadOnlyArray[Explanation] {
  self.details
}

///|
fn explanation_leaf(value : Double, description : String) -> Explanation {
  { matched: true, value, description, details: [] }
}

///|
/// Explains the final score produced by any query at one stable DocAddress.
pub fn Searcher::explain(
  self : Searcher,
  query : &Query,
  address : DocAddress,
) -> Explanation {
  if address.segment_ord < 0 || address.segment_ord >= self.segments.length() {
    return {
      matched: false,
      value: 0.0,
      description: "segment is out of range",
      details: [],
    }
  }
  let snapshot = self.segments[address.segment_ord]
  if address.doc_id.value < 0 ||
    address.doc_id.value >= snapshot.segment.doc_count() ||
    snapshot.is_deleted(address.doc_id) {
    return {
      matched: false,
      value: 0.0,
      description: "document is not live",
      details: [],
    }
  }
  let scorer = query.weight(self.statistics).scorer(snapshot.segment)
  if scorer.advance_to(address.doc_id) && scorer.doc() == address.doc_id {
    {
      matched: true,
      value: scorer.score(),
      description: "final query score",
      details: [],
    }
  } else {
    {
      matched: false,
      value: 0.0,
      description: "query does not match",
      details: [],
    }
  }
}

///|
/// Detailed BM25 explanation for an exact term query.
pub fn Searcher::explain_term(
  self : Searcher,
  term : Term,
  address : DocAddress,
  config? : Bm25Config = Bm25Config::default(),
) -> Explanation {
  if address.segment_ord < 0 || address.segment_ord >= self.segments.length() {
    return {
      matched: false,
      value: 0.0,
      description: "segment is out of range",
      details: [],
    }
  }
  let snapshot = self.segments[address.segment_ord]
  if snapshot.is_deleted(address.doc_id) {
    return {
      matched: false,
      value: 0.0,
      description: "document is deleted",
      details: [],
    }
  }
  let postings = snapshot.segment.postings_for(term)
  match postings.search_by(posting => posting.doc_id == address.doc_id) {
    None =>
      {
        matched: false,
        value: 0.0,
        description: "term does not match",
        details: [],
      }
    Some(index) => {
      let posting = postings[index]
      let scorer = Bm25Scorer::from_statistics_with_config(
        self.statistics,
        term,
        config,
      )
      let value = scorer.score(posting, snapshot.segment)
      let details = [
        explanation_leaf(scorer.idf(), "inverse document frequency"),
        explanation_leaf(posting.term_freq.to_double(), "term frequency"),
        explanation_leaf(
          snapshot.segment
          .field_length(address.doc_id, term.field_id)
          .to_double(),
          "document field length",
        ),
        explanation_leaf(scorer.average_field_length(), "average field length"),
        explanation_leaf(config.k1(), "BM25 k1"),
        explanation_leaf(config.b(), "BM25 b"),
      ]
      {
        matched: true,
        value,
        description: "BM25 term score",
        details: ReadOnlyArray::from_array(details),
      }
    }
  }
}