///|
/// Minimal field-aware query-time analysis. Query syntax parsing remains a
/// separate concern; this type converts raw text into existing Query objects.
pub struct QueryParser {
  schema : Schema
  tokenizers : TokenizerManager
  mut conjunction_by_default : Bool
}

///|
pub fn QueryParser::new(
  schema : Schema,
  tokenizers : TokenizerManager,
) -> QueryParser {
  { schema, tokenizers: tokenizers.snapshot(), conjunction_by_default: false }
}

///|
pub fn QueryParser::set_conjunction_by_default(
  self : QueryParser,
  conjunction : Bool,
) -> Unit {
  self.conjunction_by_default = conjunction
}

///|
pub fn QueryParser::analyze(
  self : QueryParser,
  field_id : FieldId,
  text : String,
) -> Array[Token] raise AnalysisError {
  guard self.schema.is_indexed(field_id) else {
    raise AnalysisError::FieldNotIndexed(field_id)
  }
  let tokenizer_name = match self.schema.tokenizer_name(field_id) {
    Some(name) => name
    None => raise AnalysisError::FieldNotIndexed(field_id)
  }
  self.tokenizers.analyze(tokenizer_name, text)
}

///|
/// Analyzes raw text and creates one TermQuery or a BooleanQuery. Multiple
/// terms are disjunctive by default, matching Tantivy's default parser mode.
pub fn QueryParser::parse_query(
  self : QueryParser,
  field_id : FieldId,
  text : String,
) -> &Query raise AnalysisError {
  let tokens = self.analyze(field_id, text)
  guard tokens.length() > 0 else { raise AnalysisError::EmptyQuery }
  let paths = @analysis.TokenGraph::from_tokens(tokens).finite_strings()
  if self.conjunction_by_default && paths.length() > 1 {
    let alternatives : Array[BooleanClause] = []
    for path in paths {
      let path_clauses : Array[BooleanClause] = []
      for token in path.tokens() {
        path_clauses.push(
          BooleanClause::new(
            Occur::Must,
            TermQuery::new(Term::new(field_id, token.text)),
          ),
        )
      }
      let path_query : &Query = if path_clauses.length() == 1 {
        path_clauses[0].query
      } else {
        BooleanQuery::new(path_clauses) as &Query
      }
      alternatives.push(BooleanClause::new(Occur::Should, path_query))
    }
    return BooleanQuery::new(alternatives) as &Query
  }
  let terms : Array[String] = []
  for token in tokens {
    if terms.search_by(text => text == token.text) is None {
      terms.push(token.text)
    }
  }
  if terms.length() == 1 {
    return TermQuery::new(Term::new(field_id, terms[0])) as &Query
  }
  let occur = if self.conjunction_by_default {
    Occur::Must
  } else {
    Occur::Should
  }
  let clauses : Array[BooleanClause] = []
  for term_text in terms {
    clauses.push(
      BooleanClause::new(occur, TermQuery::new(Term::new(field_id, term_text))),
    )
  }
  BooleanQuery::new(clauses) as &Query
}

///|
/// Analyzes raw text and preserves analyzer positions in a PhraseQuery.
pub fn QueryParser::parse_phrase(
  self : QueryParser,
  field_id : FieldId,
  text : String,
) -> &Query raise AnalysisError {
  let tokens = self.analyze(field_id, text)
  guard tokens.length() > 0 else { raise AnalysisError::EmptyQuery }
  let paths = @analysis.TokenGraph::from_tokens(tokens).finite_strings()
  let alternatives : Array[BooleanClause] = []
  for path in paths {
    let texts : Array[String] = []
    let positions : Array[Int] = []
    for token in path.tokens() {
      texts.push(token.text)
      positions.push(token.position)
    }
    alternatives.push(
      BooleanClause::new(
        Occur::Should,
        PhraseQuery::with_positions(field_id, texts, positions),
      ),
    )
  }
  if alternatives.length() == 1 {
    alternatives[0].query
  } else {
    BooleanQuery::new(alternatives) as &Query
  }
}