/** @file
 * @brief Metadata for a term in a document
 */
/* 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/>.
 */

#include <config.h>

#include "terminfo.h"

#include "omassert.h"

#include <algorithm>
#include <limits>

using namespace std;

void
TermInfo::merge() const
{
    Assert(!is_deleted());
    inplace_merge(positions.begin(),
                  positions.begin() + split,
                  positions.end());
    split = 0;
}

bool
TermInfo::add_position(Xapian::termcount wdf_inc, Xapian::termpos termpos)
{
    if (rare(is_deleted())) {
        wdf = wdf_inc;
        split = 0;
        positions.push_back(termpos);
        return true;
    }

    wdf += wdf_inc;

    // Optimise the common case of adding positions in ascending order.
    if (positions.empty()) {
        positions.push_back(termpos);
        return false;
    }
    if (termpos > positions.back()) {
        if (split) {
            // Check for duplicate before split.
            auto i = lower_bound(positions.cbegin(),
                                 positions.cbegin() + split,
                                 termpos);
            if (i != positions.cbegin() + split && *i == termpos)
                return false;
        }
        positions.push_back(termpos);
        return false;
    }

    if (termpos == positions.back()) {
        // Duplicate of last entry.
        return false;
    }

    if (split > 0) {
        // We could merge in the new entry at the same time, but that seems to
        // make things much more complex for minor gains.
        merge();
    }

    // We keep positions sorted, so use lower_bound() which can binary chop to
    // find the entry.
    auto i = lower_bound(positions.cbegin(), positions.cend(), termpos);
    // Add unless termpos is already in the list.
    if (i == positions.cend() || *i != termpos) {
        auto new_split = positions.size();
        if constexpr (sizeof(split) < sizeof(Xapian::termpos)) {
            if (rare(new_split > numeric_limits<decltype(split)>::max())) {
                // The split point would be beyond the size of the type used to
                // hold it, which is really unlikely if that type is 32-bit.
                // Just insert the old way in this case.
                positions.insert(i, termpos);
                return false;
            }
        } else {
            // This assertion should always be true because we shouldn't have
            // duplicate entries and the split point can't be after the final
            // entry.
            AssertRel(new_split, <=, numeric_limits<decltype(split)>::max());
        }
        split = new_split;
        positions.push_back(termpos);
    }
    return false;
}

bool
TermInfo::remove_position(Xapian::termpos termpos)
{
    Assert(!is_deleted());

    if (rare(positions.empty()))
        return false;

    // Special case removing the final position, which we can handle in O(1).
    if (positions.back() == termpos) {
        positions.pop_back();
        if (split == positions.size()) {
            split = 0;
            // We removed the only entry from after the split.
        }
        return true;
    }

    if (split > 0) {
        // We could remove the requested entry at the same time, but that seems
        // fiddly to do.
        merge();
    }

    // We keep positions sorted, so use lower_bound() which can binary chop to
    // find the entry.
    auto i = lower_bound(positions.cbegin(), positions.cend(), termpos);
    if (i == positions.cend() || *i != termpos) {
        // Not there.
        return false;
    }
    positions.erase(i);
    return true;
}

Xapian::termpos
TermInfo::remove_positions(Xapian::termpos termpos_first,
                           Xapian::termpos termpos_last)
{
    Assert(!is_deleted());

    if (split > 0) {
        // We could remove the requested entries at the same time, but that
        // seems fiddly to do.
        merge();
    }

    // Find the range [i, j) that the specified termpos range maps to.  Use
    // binary chop to search, since this is a sorted list.
    auto i = lower_bound(positions.cbegin(), positions.cend(), termpos_first);
    if (i == positions.cend() || *i > termpos_last) {
        return 0;
    }
    auto j = upper_bound(i, positions.cend(), termpos_last);
    size_t size_before = positions.size();
    positions.erase(i, j);
    return Xapian::termpos(size_before - positions.size());
}
