/** @file
 *  @brief Calculated bounds on and estimate of number of matches
 */
/* Copyright (C) 2022,2026 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 "estimateop.h"

#include "omassert.h"
#include "overflow.h"

#include <algorithm>
#include <memory>

using namespace std;

Estimates
EstimateOp::resolve(Xapian::doccount db_size,
		    Xapian::docid db_first,
		    Xapian::docid db_last)
{
    // Clamp first and last to those from the DB - we lazily set first=1 and/or
    // last=Xapian::docid(-1) when we don't have better bounds.
    estimates.first = max(estimates.first, db_first);
    estimates.last = min(estimates.last, db_last);

    // If there are gaps in the docid numbering then max_range could be greater
    // than db_size.  It might seem tempting to clamping max_range to db_size
    // but the gaps will tend to inflate all the range lengths proportionally
    // so the effect should tend to cancel out if we leave max_range alone.
    Xapian::doccount max_range = estimates.last - estimates.first + 1;

    Estimates result;
    // LocalSubMatch::resolve() checks db_size == 0 so we should be able to
    // assume it is non-zero in all the cases below.
    Assert(db_size);
    switch (type) {
      case KNOWN:
      case POSTING_SOURCE:
	result = estimates;
	break;
      case AND: {
	result.min = max_range;
	result.max = max_range;
	double est = max_range;
	auto n_subqueries = sub_estimates.size();
	for (unsigned n = 0; n < n_subqueries; ++n) {
	    Estimates r = sub_estimates[n]->resolve(db_size, db_first, db_last);

	    // The number of matching documents is minimised when we have the
	    // minimum number of matching documents from each sub-postlist, and
	    // these are maximally disjoint.
	    auto range_i = r.last - r.first + 1;
	    // min_overlap is the minimum number of documents from this subquery
	    // which have to be in the range where all the subqueries overlap.
	    Xapian::doccount min_overlap;
	    if (!sub_overflows(r.min, range_i - max_range, min_overlap)) {
		if (!add_overflows(result.min, min_overlap, result.min) &&
		    result.min <= max_range) {
		    // It's possible there's no overlap.
		    result.min = 0;
		} else {
		    // The pair-wise overlap is (a + b - max_range) - this works
		    // even if (a + b) overflows (a and b are each <= max_range
		    // so subtracting max_range must un-overflow us).
		    result.min -= max_range;
		}
	    } else {
		// This child could have nothing in the overlap.
		result.min = 0;
	    }

	    result.max = std::min(result.max, r.max);

	    // We calculate the estimate assuming independence - it's the
	    // product of (est/range) for the children, multiplied by the
	    // range for the AND.
	    if (usual(range_i > 0)) {
		est = est * r.est / range_i;
	    }
	}

	result.est = static_cast<Xapian::doccount>(est + 0.5);
	result.est = std::clamp(result.est, result.min, result.max);
	break;
      }
      case AND_NOT: {
	AssertEq(sub_estimates.size(), 2);
	result = sub_estimates[0]->resolve(db_size, db_first, db_last);
	Estimates r = sub_estimates[1]->resolve(db_size, db_first, db_last);

	// An AND_NOT with no overlap gets optimised away before now, so we
	// know the ranges overlap.
	Xapian::doccount overlap =
	    min(result.last, r.last) - max(result.first, r.first) + 1;

	// The maximum number of documents that could be in common.
	auto max_actual_overlap = min(overlap, r.max);

	if (sub_overflows(result.min, max_actual_overlap, result.min)) {
	    // This AND_NOT could match no documents.
	    result.min = 0;
	}

	Xapian::doccount range_l = result.last - result.first + 1;
	Xapian::doccount range_r = r.last - r.first + 1;
	// We can't match more documents than our left-side does.
	// We also can't more documents than when the non-overlaps are
	// full and the overlap as segregated as possible with the LHS
	// achieving its maximum and the RHS its minimum.
	//
	// Note that the second expression can't overflow the type.
	result.max = std::min(result.max, range_l - overlap + range_r - r.min);

	// Max is when:
	// <---- LHS ------->
	// [LLLLLLL|llllbbrr|RRRRRR]
	//         <-------RHS----->
	//
	// #L = range_l - overlap
	// #R = range_r - overlap
	// #r+#b = r_min - #R
	// #l+#b = l_max - #L
	// #b = rmin - #R + l_max - #L - overlap
	// result.max = #L + #l = #L + l_max - #L -
	//           - (rmin - #R + l_max - #L - overlap)
	//            = - rmin + #R + #L + overlap)
	//            = #L + #R + overlap - rmin
	//            = range_l + range_r - overlap - rmin

	// We calculate the estimate assuming independence, but taking into
	// account the range information.
	double est = result.est * (1.0 - double(overlap) * r.est /
		     (double(range_l) * range_r));

	result.est = static_cast<Xapian::doccount>(est + 0.5);
	break;
      }
      case OR: {
	// FIXME: We don't currently make full use of the range information
	// here.  Trivial testing suggested it doesn't make much difference
	// for the estimate but we should investigate more deeply as it may
	// be useful for min and max, and perhaps for the estimate in cases
	// we didn't try.
	//
	// We also only tried applying the range version of
	// estimate_or_assuming_indep() from orpostlist.cc which works on
	// pairswise OR multiple times.  A fuller algorithm would work
	// through the range starts and ends something like:
	//
	// * Sort start and ends into order and work through (at most 2*N-1
	//   regions)
	// * For min allocate to where there's most overlap, e.g.
	//
	//     AAAAAAAAAAAAAAAAAAAAAAA
	// |   |      | aaaaaaa|aaaaa|        |       |       |
	// |   |      BBBBBBBBBBBBBBBBBBBBBBBBB       |       |
	// |   |      | bbbbbbb|bbbbb|bbbbbb  |       |       |
	// |   |      |        CCCCCCCCCCCCCCCCCCCCCCCC       |
	// |   |      |        |ccccc|ccc     |       |       |
	//
	// * For max try to allocate where there's least overlap
	// * For est assume each range splits proportionally over the regions
	//   it participates (potentially O(n*n) in number of subqueries)
	result = sub_estimates[0]->resolve(db_size, db_first, db_last);
	double scale = max_range == 0.0 ? 1.0 : 1.0 / max_range;
	double P_est = result.est * scale;
	auto n_subqueries = sub_estimates.size();
	for (unsigned i = 1; i != n_subqueries; ++i) {
	    Estimates r = sub_estimates[i]->resolve(db_size, db_first, db_last);

	    result.min = std::max(result.min, r.min);

	    // Sum(max) over subqueries but saturating at max_range.
	    if (max_range - result.max <= r.max) {
		result.max = max_range;
	    } else {
		result.max += r.max;
	    }

	    // We calculate the estimate assuming independence.  The simplest
	    // way to calculate this seems to be a series of (n_subqueries - 1)
	    // pairwise calculations, which gives the same answer regardless of
	    // the order.
	    double P_i = r.est * scale;
	    P_est += P_i - P_est * P_i;
	}

	result.est = static_cast<Xapian::doccount>(P_est * max_range + 0.5);
	break;
      }
      case XOR: {
	// FIXME: We don't currently make full use of the range information
	// here.
	double scale = max_range == 0.0 ? 1.0 : 1.0 / max_range;

	bool all_exact = true;
	bool invert = false;

	unsigned max_overflow = 0;
	Xapian::doccount max_sum = 0;

	// We calculate the estimate assuming independence.  The simplest
	// way to calculate this seems to be a series of (n_subqueries - 1)
	// pairwise calculations, which gives the same answer regardless of the
	// order.
	double P_est = 0.0;

	unsigned j = 0;

	// Need to buffer min and max values from subqueries so we can compute
	// our min as a second pass.
	auto n_subqueries = sub_estimates.size();
	unique_ptr<Estimates[]> min_and_max{new Estimates[n_subqueries]};
	for (unsigned i = 0; i < n_subqueries; ++i) {
	    min_and_max[j] =
		sub_estimates[i]->resolve(db_size, db_first, db_last);
	    const Estimates& r = min_and_max[j];

	    if (r.min == db_size) {
		// A subquery matches all documents which just inverts which
		// documents match - note that but otherwise ignore this
		// subquery.
		invert = !invert;
	    } else {
		// Maximum is if all sub-postlists are disjoint.
		Xapian::doccount max_i = r.max;
		if (add_overflows(max_sum, max_i, max_sum)) {
		    // Track how many times we overflow the type.
		    ++max_overflow;
		}
		all_exact = all_exact && (max_i == r.min);

		double P_i = r.est * scale;
		P_est += P_i - 2.0 * P_est * P_i;

		++j;
	    }
	}

	if (max_overflow || max_sum > db_size) {
	    if (all_exact) {
		// If the sub-postlist tfs are all exact, then if the sum of
		// them has a different odd/even-ness to db_size then max tf of
		// the XOR can't achieve db_size.
		result.max = db_size - ((max_sum & 1) != (db_size & 1));
	    } else {
		result.max = db_size;
	    }
	} else {
	    result.max = max_sum;
	}

	result.est = static_cast<Xapian::doccount>(P_est * max_range + 0.5);

	// Calculate min.
	Xapian::doccount min = 0;
	if (max_overflow <= 1) {
	    // If min_i(i) > sum(j!=i)(max_i(j)) then all the other subqueries
	    // can't cancel out subquery i.  If we overflowed more than once,
	    // then the true value of max_sum is greater than the maximum
	    // possible tf, so there's no point checking.
	    for (unsigned i = 0; i < j; ++i) {
		const Estimates& r = min_and_max[i];
		Xapian::doccount all_the_rest = max_sum - r.max;
		// If no overflow, or we un-overflowed again...
		if (max_overflow == 0 || all_the_rest > max_sum) {
		    if (r.min > all_the_rest) {
			min = std::max(min, r.min - all_the_rest);
		    }
		}
	    }
	}
	if (all_exact && min == 0) {
	    // If all the subqueries termfreqs are exact then the parity of
	    // result.min must match the parity of max_sum.  That's already
	    // ensured unless min was clamped to zero in which case result.min
	    // should be 1 if max_sum is odd.
	    result.min = max_sum & 1;
	} else {
	    result.min = min;
	}

	if (invert) {
	    auto old_min = result.min;
	    result.min = db_size - result.max;
	    result.est = db_size - result.est;
	    result.max = db_size - old_min;
	}
	break;
      }
      case DECIDER:
      case NEAR:
      case PHRASE:
      case EXACT_PHRASE: {
	result = sub_estimates[0]->resolve(db_size, db_first, db_last);
	if (estimates.min == 0 && estimates.max == 0) {
	    result.min = 0;
	    // We've arranged for type's value to be the factor we want to
	    // scale by in the absence of accept/reject counts.
	    result.est /= unsigned(type);
	    break;
	}
	result.min = estimates.min;
	result.max -= estimates.max;
	double scale = double(estimates.min) / (estimates.min + estimates.max);
	result.est = Xapian::doccount(result.est * scale + 0.5);
	result.est = std::clamp(result.est, result.min, result.max);
	break;
      }
#if defined __GNUC__ && !defined __clang__
      default:
	// Without this, GCC (noted with 13.3.0 and 15.2.0) incorrectly warns
	// that result.max may be used uninitialised.
	//
	// It seems unhelpful to add a default initialisation as that would
	// mean compilers wouldn't warn if we genuinely failed to initialise.
	//
	// Adding this default case unconditionally seems similarly unhelpful
	// as it would prevent compilers warning if an enum value isn't handled
	// by this switch.
	//
	// (GCC now won't warn about the latter, but at least other compilers
	// still can.)
	__builtin_unreachable();
#endif
    }
    AssertRel(result.min, <=, result.est);
    AssertRel(result.est, <=, result.max);

    result.first = estimates.first;
    result.last = estimates.last;

    // The range size is an upper bound for max and est.  This is redundant in
    // some cases (for example it isn't needed for AND) but it's very cheap and
    // simpler to always ensure this.
    if (max_range < result.max) {
	result.max = max_range;
	if (max_range < result.est) {
	    result.est = max_range;
	}
    }
    return result;
}
