/** @file
 * @brief Xapian::ESet::Internal class
 */
/* Copyright (C) 2008,2010,2011,2013,2016,2017,2018 Olly Betts
 * Copyright (C) 2011 Action Without Borders
 *
 * 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 "esetinternal.h"

#include "xapian/enquire.h"
#include "xapian/expanddecider.h"
#include "backends/databaseinternal.h"
#include "backends/multi.h"
#include "debuglog.h"
#include "api/rsetinternal.h"
#include "expandweight.h"
#include "heap.h"
#include "omassert.h"
#include "ortermlist.h"
#include "str.h"
#include "api/termlist.h"
#include "termlistmerger.h"
#include "unicode/description_append.h"

#include <functional>
#include <memory>
#include <set>
#include <string>
#include <vector>

using namespace std;

namespace Xapian {

string
Internal::ExpandTerm::get_description() const
{
    string desc("ExpandTerm(");
    desc += str(wt);
    desc += ", ";
    description_append(desc, term);
    desc += ')';
    return desc;
}

/** Build a tree of binary TermList objects like QueryOptimiser does for
 *  OrPostList objects.
 */
static TermList *
build_termlist_tree(const Xapian::Database &db, const RSet & rset)
{
    Assert(!rset.empty());

    const set<Xapian::docid> & docids = rset.internal->docs;

    vector<TermList*> termlists;
    termlists.reserve(docids.size());

    try {
	for (Xapian::docid did : docids) {
	    termlists.push_back(db.internal->open_term_list_direct(did));
	}
	Assert(!termlists.empty());
	return make_termlist_merger(termlists);
    } catch (...) {
	for_each(termlists.begin(), termlists.end(),
		 [](TermList* p) { delete p; });
	throw;
    }
}

void
ESet::Internal::expand(Xapian::termcount max_esize,
		       const Xapian::Database & db,
		       const RSet & rset,
		       const Xapian::ExpandDecider * edecider,
		       Xapian::Internal::ExpandWeight & eweight,
		       double min_wt)
{
    LOGCALL_VOID(EXPAND, "ESet::Internal::expand", max_esize | db | rset | edecider | eweight);
    // These two cases are handled by our caller.
    Assert(max_esize);
    Assert(!rset.empty());
    // This method should only be called once for a given ESet::Internal, so
    // check we're empty.
    Assert(ebound == 0);
    Assert(items.empty());

    unique_ptr<TermList> tree(build_termlist_tree(db, rset));
    Assert(tree);

    bool is_heap = false;
    while (true) {
	// See if the root needs replacing.
	TermList * new_root = tree->next();
	if (new_root == tree.get()) {
	    // No more entries.
	    break;
	}
	if (new_root) {
	    LOGLINE(EXPAND, "Replacing the root of the termlist tree");
	    tree.reset(new_root);
	}

	string term = tree->get_termname();

	// If there's an ExpandDecider, see if it accepts the term.
	if (edecider && !(*edecider)(term)) continue;

	++ebound;

	/* Set up the ExpandWeight by clearing the existing statistics and
	   collecting statistics for the new term. */
	eweight.collect_stats(tree.get(), term);

	double wt = eweight.get_weight();

	// If the weights are equal, we prefer the lexically smaller term and
	// since we process terms in ascending order we use "<=" not "<" here.
	if (wt <= min_wt) continue;

	if (items.size() < max_esize) {
	    items.emplace_back(wt, term);
	    continue;
	}

	// We have the desired number of items, so it's one-in one-out from
	// now on.
	Assert(items.size() == max_esize);
	if (rare(!is_heap)) {
	    Heap::make(items.begin(), items.end(),
		       std::less<Xapian::Internal::ExpandTerm>());
	    min_wt = items.front().wt;
	    is_heap = true;
	    if (wt <= min_wt) continue;
	}

	items.front() = Xapian::Internal::ExpandTerm(wt, term);
	Heap::replace(items.begin(), items.end(),
		      std::less<Xapian::Internal::ExpandTerm>());
	min_wt = items.front().wt;
    }

    // Now sort the contents of the new ESet.
    if (is_heap) {
	Heap::sort(items.begin(), items.end(),
		   std::less<Xapian::Internal::ExpandTerm>());
    } else {
	sort(items.begin(), items.end());
    }
}

string
ESet::Internal::get_description() const
{
    string desc("ESet::Internal(ebound=");
    desc += str(ebound);

    for (auto&& i : items) {
	desc += ", ";
	desc += i.get_description();
    }
    desc += ')';

    return desc;
}

ESet::ESet() : internal(new ESet::Internal) {}

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

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

ESet::ESet(ESet &&) = default;

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

ESet::~ESet() { }

Xapian::termcount
ESet::size() const
{
    return Xapian::termcount(internal->items.size());
}

Xapian::termcount
ESet::get_ebound() const
{
    return internal->ebound;
}

std::string
ESet::get_description() const
{
    string desc = "ESet(";
    desc += ')';
    return desc;
}


std::string
ESetIterator::operator*() const
{
    Assert(off_from_end != 0);
    AssertRel(off_from_end, <=, eset.internal->items.size());
    return (eset.internal->items.end() - off_from_end)->get_term();
}

double
ESetIterator::get_weight() const
{
    Assert(off_from_end != 0);
    AssertRel(off_from_end, <=, eset.internal->items.size());
    return (eset.internal->items.end() - off_from_end)->get_weight();
}

std::string
ESetIterator::get_description() const
{
    string desc = "ESetIterator(";
    if (off_from_end == 0) {
	desc += "end";
    } else {
	desc += str(eset.internal->items.size() - off_from_end);
    }
    desc += ')';
    return desc;
}

}
