/** @file
 * @brief Xapian::Enquire class
 */
/* Copyright (C) 2009,2017,2024 Olly Betts
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see
 * <https://www.gnu.org/licenses/>.
 */

#include <config.h>

#include "xapian/enquire.h"
#include "enquireinternal.h"

#include "expand/esetinternal.h"
#include "expand/expandweight.h"
#include "matcher/matcher.h"
#include "msetinternal.h"
#include "omassert.h"
#include "vectortermlist.h"
#include "weight/weightinternal.h"
#include "xapian/database.h"
#include "xapian/error.h"
#include "xapian/expanddecider.h"
#include "xapian/intrusive_ptr.h"
#include "xapian/keymaker.h"
#include "xapian/matchspy.h"
#include "xapian/query.h"
#include "xapian/rset.h"
#include "xapian/weight.h"

#include <memory>
#include <string>
#include <vector>

using namespace std;

[[noreturn]]
static void
throw_invalid_arg(const char* msg) {
    throw Xapian::InvalidArgumentError(msg);
}

namespace Xapian {

Enquire::Enquire(const Enquire&) = default;

Enquire&
Enquire::operator=(const Enquire&) = default;

Enquire::Enquire(Enquire&&) = default;

Enquire&
Enquire::operator=(Enquire&&) = default;

Enquire::Enquire(const Database& db) : internal(new Enquire::Internal(db)) {}

Enquire::~Enquire() {}

void
Enquire::set_query(const Query& query, termcount query_length)
{
    internal->query = query;
    internal->query_length = query_length;
}

const Query&
Enquire::get_query() const
{
    return internal->query;
}

void
Enquire::set_weighting_scheme(const Weight& weight)
{
    internal->weight.reset(weight.clone());
}

void
Enquire::set_docid_order(docid_order order)
{
    internal->order = order;
}

void
Enquire::set_sort_by_relevance()
{
    internal->sort_by = Internal::REL;
}

void
Enquire::set_sort_by_value(valueno sort_key, bool reverse)
{
    internal->sort_by = Internal::VAL;
    internal->sort_functor = NULL;
    internal->sort_key = sort_key;
    internal->sort_val_reverse = reverse;
}

void
Enquire::set_sort_by_key(KeyMaker* sorter, bool reverse)
{
    if (sorter == NULL) {
	throw_invalid_arg("Enquire::set_sort_by_key(): sorter cannot be NULL");
    }
    internal->sort_by = Internal::VAL;
    internal->sort_functor = sorter;
    internal->sort_val_reverse = reverse;
}

void
Enquire::set_sort_by_value_then_relevance(valueno sort_key, bool reverse)
{
    internal->sort_by = Internal::VAL_REL;
    internal->sort_functor = NULL;
    internal->sort_key = sort_key;
    internal->sort_val_reverse = reverse;
}

void
Enquire::set_sort_by_key_then_relevance(KeyMaker* sorter, bool reverse)
{
    if (sorter == NULL) {
	throw_invalid_arg("Enquire::set_sort_by_key_then_relevance(): "
			  "sorter cannot be NULL");
    }
    internal->sort_by = Internal::VAL_REL;
    internal->sort_functor = sorter;
    internal->sort_val_reverse = reverse;
}

void
Enquire::set_sort_by_relevance_then_value(valueno sort_key, bool reverse)
{
    internal->sort_by = Internal::REL_VAL;
    internal->sort_functor = NULL;
    internal->sort_key = sort_key;
    internal->sort_val_reverse = reverse;
}

void
Enquire::set_sort_by_relevance_then_key(KeyMaker* sorter, bool reverse)
{
    if (sorter == NULL) {
	throw_invalid_arg("Enquire::set_sort_by_relevance_then_key(): "
			  "sorter cannot be NULL");
    }
    internal->sort_by = Internal::REL_VAL;
    internal->sort_functor = sorter;
    internal->sort_val_reverse = reverse;
}

void
Enquire::set_collapse_key(valueno collapse_key, doccount collapse_max)
{
    internal->collapse_key = collapse_key;
    internal->collapse_max = collapse_max;
}

void
Enquire::set_cutoff(int percent_threshold, double weight_threshold)
{
    internal->percent_threshold = percent_threshold;
    internal->weight_threshold = weight_threshold;
}

void
Enquire::add_matchspy(MatchSpy* spy)
{
    using Xapian::Internal::opt_intrusive_ptr;
    if (spy == NULL)
	throw_invalid_arg("Enquire::add_matchspy(): spy cannot be NULL");
    internal->matchspies.push_back(opt_intrusive_ptr<MatchSpy>(spy));
}

void
Enquire::clear_matchspies()
{
    internal->matchspies.clear();
}

void
Enquire::set_time_limit(double time_limit)
{
    internal->time_limit = time_limit;
}

MSet
Enquire::get_mset(doccount first,
		  doccount maxitems,
		  doccount checkatleast,
		  const RSet* rset,
		  const MatchDecider* mdecider) const
{
    return internal->get_mset(first, maxitems, checkatleast, rset, mdecider);
}

TermIterator
Enquire::get_matching_terms_begin(docid did) const
{
    return internal->get_matching_terms_begin(did);
}

void
Enquire::set_expansion_scheme(std::string_view eweightname,
			      double expand_k) const
{
    if (eweightname == "bo1") {
	internal->eweight = Enquire::Internal::EXPAND_BO1;
    } else if (eweightname == "prob" || eweightname == "trad") {
	internal->eweight = Enquire::Internal::EXPAND_PROB;
    } else {
	throw_invalid_arg("Enquire::set_expansion_scheme(): eweightname must "
			  "be 'bo1', 'prob' or 'trad'");
    }
    internal->expand_k = expand_k;
}

ESet
Enquire::get_eset(termcount maxitems,
		  const RSet& rset,
		  int flags,
		  const ExpandDecider* edecider,
		  double min_weight) const
{
    return internal->get_eset(maxitems, rset, flags, edecider, min_weight);
}

std::string
Enquire::get_description() const
{
    string desc = "Enquire(db=";
    desc += internal->db.get_description();
    if (!internal->query.empty()) {
	desc += ", query=";
	desc += internal->query.get_description();
    }
    desc += ')';
    return desc;
}

Enquire::Internal::Internal(const Database& db_)
    : db(db_) {}

MSet
Enquire::Internal::get_mset(doccount first,
			    doccount maxitems,
			    doccount checkatleast,
			    const RSet* rset,
			    const MatchDecider* mdecider) const
{
    if (query.empty()) {
	MSet mset;
	mset.internal->set_first(first);
	return mset;
    }

    if (percent_threshold && (sort_by == VAL || sort_by == VAL_REL)) {
	throw Xapian::UnimplementedError("Use of a percentage cutoff while "
					 "sorting primary by value isn't "
					 "currently supported");
    }

    // Lazily initialise weight to its default if necessary.
    if (!weight)
	weight.reset(new BM25Weight);

    // Lazily initialise query_length if it wasn't explicitly specified.
    if (query_length == 0) {
	query_length = query.get_length();
    }

    Xapian::doccount first_orig = first;
    {
	Xapian::doccount docs = db.get_doccount();
	first = min(first, docs);
	maxitems = min(maxitems, docs - first);
	checkatleast = min(checkatleast, docs);
	checkatleast = max(checkatleast, first + maxitems);
    }

    unique_ptr<Xapian::Weight::Internal> stats(new Xapian::Weight::Internal);
    ::Matcher match(db,
		    query,
		    query_length,
		    rset,
		    *stats,
		    *weight,
		    (mdecider != NULL),
		    collapse_key,
		    collapse_max,
		    percent_threshold,
		    weight_threshold,
		    order,
		    sort_key,
		    sort_by,
		    sort_val_reverse,
		    time_limit,
		    matchspies);

    MSet mset = match.get_mset(first,
			       maxitems,
			       checkatleast,
			       *stats,
			       *weight,
			       mdecider,
			       sort_functor.get(),
			       collapse_key,
			       collapse_max,
			       percent_threshold,
			       weight_threshold,
			       order,
			       sort_key,
			       sort_by,
			       sort_val_reverse,
			       time_limit,
			       matchspies);

    if (first_orig != first) {
	mset.internal->set_first(first_orig);
    }

    mset.internal->set_enquire(this);

    if (!mset.internal->get_stats()) {
	mset.internal->set_stats(stats.release());
    }

    return mset;
}

TermIterator
Enquire::Internal::get_matching_terms_begin(docid did) const
{
    if (query.empty())
	return TermIterator();

    struct term_and_pos {
	string term;
	Xapian::termpos pos;

	term_and_pos(const string& term_, Xapian::termpos pos_)
	    : term(term_), pos(pos_) {}
    };

    vector<term_and_pos> query_terms;
    Xapian::termpos pos = 1;
    for (auto t = query.get_terms_begin(); t != query.get_terms_end(); ++t) {
	query_terms.emplace_back(*t, pos++);
    }

    if (query_terms.empty())
	return TermIterator();

    // Reorder by term, secondary sort by position.
    sort(query_terms.begin(), query_terms.end(),
	 [](const term_and_pos& a, const term_and_pos& b) {
	     int cmp = a.term.compare(b.term);
	     return cmp ? cmp < 0 : a.pos < b.pos;
	 });

    // Loop through the query terms, skipping the document terms for each to
    // see which match, and shuffling down the matching ones.  Also discard
    // repeats, keeping the smallest position.
    size_t i = 0, j = 0;
    auto t = db.termlist_begin(did);
    do {
	const string& term = query_terms[i].term;
	if (j == 0 || term != query_terms[j - 1].term) {
	    t.skip_to(term);
	    if (t == db.termlist_end(did)) {
		break;
	    }

	    if (*t == term) {
		// Matched, so move down if necessary.
		if (i != j)
		    query_terms[j] = std::move(query_terms[i]);
		++j;
	    }
	}
    } while (++i != query_terms.size());

    // Truncate to leave just the matching terms.
    query_terms.erase(query_terms.begin() + j, query_terms.end());

    // Reorder by ascending query position.
    sort(query_terms.begin(), query_terms.end(),
	 [](const term_and_pos& a, const term_and_pos& b) {
	     return a.pos < b.pos;
	 });

    // Iterator adaptor to present query_terms as a container of just strings.
    struct Itor {
	vector<term_and_pos>::const_iterator it;

	explicit
	Itor(vector<term_and_pos>::const_iterator it_) : it(it_) {}

	const std::string& operator*() const {
	    return it->term;
	}

	Itor& operator++() {
	    ++it;
	    return *this;
	}

	Itor operator++(int) {
	    Itor retval = *this;
	    ++it;
	    return retval;
	}

	bool operator!=(const Itor& o) { return it != o.it; }
    };

    return TermIterator(new VectorTermList(Itor(query_terms.cbegin()),
					   Itor(query_terms.cend())));
}

ESet
Enquire::Internal::get_eset(termcount maxitems,
			    const RSet& rset,
			    int flags,
			    const ExpandDecider* edecider_,
			    double min_weight) const
{
    using Xapian::Internal::opt_intrusive_ptr;
    opt_intrusive_ptr<const ExpandDecider> edecider(edecider_);

    Xapian::ESet eset;

    if (maxitems == 0 || rset.empty()) {
	// Either we were asked for no results, or wouldn't produce any
	// because no documents were marked as relevant.
	return eset;
    }

    // Excluding query terms is a no-op without a query.
    if ((flags & Enquire::INCLUDE_QUERY_TERMS) == 0 && !query.empty()) {
	auto edft = new ExpandDeciderFilterTerms(query.get_terms_begin(),
						 query.get_terms_end());
	if (!edecider) {
	    edecider = edft->release();
	} else {
	    // Make sure ExpandDeciderFilterTerms doesn't leak if new throws.
	    opt_intrusive_ptr<const ExpandDecider> ptr(edft->release());
	    edecider = (new ExpandDeciderAnd(ptr.get(),
					     edecider.get()))->release();
	}
    }

    bool use_exact_termfreq = flags & Enquire::USE_EXACT_TERMFREQ;
    if (eweight == Enquire::Internal::EXPAND_BO1) {
	using Xapian::Internal::Bo1EWeight;
	Bo1EWeight bo1eweight(db, rset.size(), use_exact_termfreq);
	eset.internal->expand(maxitems, db, rset, edecider.get(), bo1eweight,
			      min_weight);
    } else {
	AssertEq(eweight, Enquire::Internal::EXPAND_PROB);
	using Xapian::Internal::ProbEWeight;
	ProbEWeight probeweight(db, rset.size(), use_exact_termfreq, expand_k);
	eset.internal->expand(maxitems, db, rset, edecider.get(), probeweight,
			      min_weight);
    }

    return eset;
}

}
