/** @file
 * @brief PostList class implementing unweighted Query::OP_OR
 */
/* Copyright 2017,2018 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/>.
 */

#ifndef XAPIAN_INCLUDED_BOOLORPOSTLIST_H
#define XAPIAN_INCLUDED_BOOLORPOSTLIST_H

#include "backends/postlist.h"

/// PostList class implementing unweighted Query::OP_OR
class BoolOrPostList : public PostList {
    /// Don't allow assignment.
    void operator=(const BoolOrPostList&) = delete;

    /// Don't allow copying.
    BoolOrPostList(const BoolOrPostList&) = delete;

    /// The current docid, or zero if we haven't started or are at_end.
    Xapian::docid did = 0;

    /// The number of sub-postlists.
    size_t n_kids;

    struct PostListAndDocID {
	PostList* pl;

	Xapian::docid did = 0;

	PostListAndDocID() : pl(nullptr) { }

	PostListAndDocID(PostList* pl_) : pl(pl_) { }

	bool operator>(const PostListAndDocID& o) const {
	    return did > o.did;
	}
    };

    /// Array of pointers to sub-postlists.
    PostListAndDocID* plist = nullptr;

    /** Helper to apply operation to all postlists matching current docid.
     *
     *  For each matching postlist this helper evaluates `func`, and
     *  accumulates the returned Xapian::termcount value.  Of the three current
     *  uses, two want to accumulate a value of this type, while the other
     *  doesn't need to accumulate anything.  This is an inlined template so
     *  the compiler's optimiser should be able to see the result of the
     *  accumulation isn't used and eliminate it.
     *
     *  This function makes use of the heap structure by walking the tree
     *  of the heap in a particular order, descending to any children which
     *  match the current docid (i.e. the docid of the tip of the heap) to
     *  visit every postlist matching the current docid (due to the heap
     *  property, for any such postlist all ancestors must also match the
     *  current docid).
     *
     *  At each step we do the first of the following which stays within the
     *  heap and matches the current docid:
     *
     *  * go down+right
     *  * go down+left
     *  * go left if we're the right descendent of our parent
     *  * go up until we can go left (or we reach the root where we stop)
     *
     *  This effectively recurses the tree, but only needs O(1) storage.  It
     *  requires O(n) in time where n is the number of postlists matching the
     *  current docid, but that's inherent as we need to call `func` n times.
     *
     *  Going down+right in preference to down+left simplifies the "go left"
     *  step a little because any right descendent must have a left sibling,
     *  but if the last entry in index order is a left descendent it doesn't
     *  have a right sibling.  It also makes it easy to implement an
     *  optimisation which can ascend multiple levels in one go if the
     *  compiler provides __builtin_ffs().
     */
    template<typename F>
    Xapian::termcount
    for_all_matches(F func) const
    {
	size_t i = 0;
	Xapian::termcount result = 0;
	AssertEq(plist[0].did, did);
	while (true) {
	    result += func(plist[i].pl);
	    // Children of i are (2 * i + 1) and (2 * i + 2).
	    size_t j = 2 * i + 2;
	    if (j < n_kids && plist[j].did == did) {
		// Down right.
		i = j;
		continue;
	    }
	    --j;
	    if (j < n_kids && plist[j].did == did) {
		// Down left.
		i = j;
		continue;
	    }
    try_left:
	    if (i < 2) break;
	    if ((i & 1) == 0 && plist[i - 1].did == did) {
		// Left.
		--i;
		continue;
	    }
	    // Up.
	    //
	    // We can ascend back up to our parent, plus any sequence of
	    // contiguous left branches up from our parent, with a neat
	    // bit-twiddling trick if we have __builtin_ffs() available.
#if HAVE_DECL___BUILTIN_FFS
	    ++i;
	    i >>= __builtin_ffs(i & ~1) - 1;
	    --i;
#else
	    // Fall-back to just ascending one level at a time, which is
	    // simple and probably similarly efficient to a portable
	    // fallback ffs() implementation which doesn't make use of
	    // specialised machine code instructions, since the distance
	    // we can ascend will be usually be short.
	    i = (i - 1) / 2;
#endif
	    goto try_left;
	}
	return result;
    }

  public:
    /** Construct from 2 random-access iterators to a container of PostList*,
     *  a pointer to the matcher, and the document collection size.
     */
    template<class RandomItor>
    BoolOrPostList(RandomItor pl_begin, RandomItor pl_end,
		   Xapian::doccount db_size)
	: n_kids(pl_end - pl_begin)
    {
	plist = new PostListAndDocID[n_kids];
	// This initialises all entries to have did 0, so all entries are
	// equal, which is a valid heap.
	std::copy(pl_begin, pl_end, plist);

	// We shortcut an empty shard and avoid creating a postlist tree for it.
	Assert(db_size);
	Assert(n_kids != 0);
	// We calculate the estimate assuming independence.  The simplest
	// way to calculate this seems to be a series of (n_kids - 1) pairwise
	// calculations, which gives the same answer regardless of the order.
	double scale = 1.0 / db_size;
	double P_est = plist[0].pl->get_termfreq() * scale;
	for (size_t i = 1; i < n_kids; ++i) {
	    double P_i = plist[i].pl->get_termfreq() * scale;
	    P_est += P_i - P_est * P_i;
	}
	termfreq = static_cast<Xapian::doccount>(P_est * db_size + 0.5);
    }

    ~BoolOrPostList();

    Xapian::docid get_docid() const;

    double get_weight(Xapian::termcount doclen,
		      Xapian::termcount unique_terms,
		      Xapian::termcount wdfdocmax) const;

    bool at_end() const;

    double recalc_maxweight();

    PostList* next(double w_min);

    PostList* skip_to(Xapian::docid did, double w_min);

    void get_docid_range(Xapian::docid& first, Xapian::docid& last) const;

    std::string get_description() const;

    Xapian::termcount get_wdf() const;

    Xapian::termcount count_matching_subqs() const;

    void gather_position_lists(OrPositionList* orposlist);
};

#endif // XAPIAN_INCLUDED_BOOLORPOSTLIST_H
