/** @file
 * @brief functions to convert Xapian objects to strings and back
 */
/* Copyright (C) 2006-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/document.h>
#include <xapian/positioniterator.h>
#include <xapian/termiterator.h>
#include <xapian/valueiterator.h>

#include "omassert.h"
#include "api/rsetinternal.h"
#include "pack.h"
#include "serialise.h"
#include "serialise-double.h"
#include "stringutils.h"
#include "weight/weightinternal.h"

#include <string>
#include <string_view>

using namespace std;

string
serialise_stats(const Xapian::Weight::Internal &stats)
{
    string result;

    pack_uint(result, stats.total_length);
    pack_uint(result, stats.collection_size);
    pack_uint(result, stats.rset_size);
    pack_uint(result, stats.db_doclength_lower_bound);
    pack_uint(result, stats.db_doclength_upper_bound -
                      stats.db_doclength_lower_bound);
    pack_uint(result, stats.db_unique_terms_lower_bound);
    pack_uint(result, stats.db_unique_terms_upper_bound -
                      stats.db_unique_terms_lower_bound);
    pack_bool(result, stats.have_max_part);

    pack_uint(result, stats.termfreqs.size());
    string_view prev_term;
    for (auto&& i : stats.termfreqs) {
        const string& term = i.first;
        // We reduce the size of the encoding by reusing any prefix which is in
        // common with the previous term.  This is much more compact if there
        // are a lot of terms, especially if they share a prefix such as an
        // OP_WILDCARD which expands to a lot of terms.
        size_t reuse = common_prefix_length(prev_term, term, 255);
        size_t append = term.size() - reuse;
        if (reuse == prev_term.size() && usual(term.size() <= 255)) {
            // Reuse the whole of the previous term.  In this case we store the
            // new length for the reuse count, which is longer than a valid
            // reuse count.  This saves a byte (or two if the new term is
            // >= 128 bytes long).
            AssertRel(term.size(), >, reuse);
            result += char(term.size());
        } else {
            result += char(reuse);
            pack_uint(result, append);
        }
        result.append(term.data() + reuse, append);
        pack_uint(result, i.second.termfreq);
        if (stats.rset_size != 0)
            pack_uint(result, i.second.reltermfreq);
        pack_uint(result, i.second.collfreq);
        if (stats.have_max_part)
            result += serialise_double(i.second.max_part);
        prev_term = term;
    }

    return result;
}

void
unserialise_stats(const char* p, const char* p_end,
                  Xapian::Weight::Internal& stat)
{
    size_t n;
    if (!unpack_uint(&p, p_end, &stat.total_length) ||
        !unpack_uint(&p, p_end, &stat.collection_size) ||
        !unpack_uint(&p, p_end, &stat.rset_size) ||
        !unpack_uint(&p, p_end, &stat.db_doclength_lower_bound) ||
        !unpack_uint(&p, p_end, &stat.db_doclength_upper_bound) ||
        !unpack_uint(&p, p_end, &stat.db_unique_terms_lower_bound) ||
        !unpack_uint(&p, p_end, &stat.db_unique_terms_upper_bound) ||
        !unpack_bool(&p, p_end, &stat.have_max_part) ||
        !unpack_uint(&p, p_end, &n)) {
        unpack_throw_serialisation_error(p);
    }
    stat.db_doclength_upper_bound += stat.db_doclength_lower_bound;
    stat.db_unique_terms_upper_bound += stat.db_unique_terms_lower_bound;

    string term;
    for ( ; n; --n) {
        Xapian::doccount termfreq;
        Xapian::doccount reltermfreq = 0;
        Xapian::termcount collfreq;
        if (p == p_end) {
            unpack_throw_serialisation_error(p);
        }
        size_t reuse = static_cast<unsigned char>(*p++);
        size_t append;
        if (reuse <= term.size()) {
            term.resize(reuse);
            if (!unpack_uint(&p, p_end, &append) ||
                size_t(p_end - p) < append) {
                unpack_throw_serialisation_error(p);
            }
        } else {
            append = reuse - term.size();
        }
        term.append(p, append);
        p += append;
        if (!unpack_uint(&p, p_end, &termfreq) ||
            (stat.rset_size != 0 && !unpack_uint(&p, p_end, &reltermfreq)) ||
            !unpack_uint(&p, p_end, &collfreq)) {
            unpack_throw_serialisation_error(p);
        }
        double max_part = 0.0;
        if (stat.have_max_part)
            max_part = unserialise_double(&p, p_end);
        stat.termfreqs.insert(make_pair(term,
                                        TermFreqs(termfreq,
                                                  reltermfreq,
                                                  collfreq,
                                                  max_part)));
    }
}

string
serialise_rset(const Xapian::RSet &rset)
{
    string result;
    if (rset.internal) {
        Xapian::docid lastdid = 0;
        for (Xapian::docid did : rset.internal->docs) {
            pack_uint(result, did - lastdid - 1);
            lastdid = did;
        }
    }
    return result;
}

Xapian::RSet
unserialise_rset(const string &s)
{
    Xapian::RSet rset;

    const char * p = s.data();
    const char * p_end = p + s.size();

    Xapian::docid did = 0;
    while (p != p_end) {
        Xapian::docid inc;
        if (!unpack_uint(&p, p_end, &inc)) {
            unpack_throw_serialisation_error(p);
        }
        did += inc + 1;
        rset.add_document(did);
    }

    return rset;
}

string
serialise_document(const Xapian::Document &doc)
{
    string result;

    size_t n = doc.values_count();
    pack_uint(result, n);
    Xapian::ValueIterator value;
    for (value = doc.values_begin(); value != doc.values_end(); ++value) {
        pack_uint(result, value.get_valueno());
        pack_string(result, *value);
        --n;
    }
    Assert(n == 0);

    n = doc.termlist_count();
    pack_uint(result, n);
    Xapian::TermIterator term;
    for (term = doc.termlist_begin(); term != doc.termlist_end(); ++term) {
        pack_string(result, *term);
        pack_uint(result, term.get_wdf());

        size_t x = term.positionlist_count();
        pack_uint(result, x);
        Xapian::PositionIterator pos;
        Xapian::termpos oldpos = 0;
        for (pos = term.positionlist_begin(); pos != term.positionlist_end(); ++pos) {
            pack_uint(result, *pos - oldpos);
            oldpos = *pos;
            --x;
        }
        Assert(x == 0);
        --n;
    }
    AssertEq(n, 0);

    result += doc.get_data();
    return result;
}

Xapian::Document
unserialise_document(string_view s)
{
    Xapian::Document doc;
    const char * p = s.data();
    const char * p_end = p + s.size();

    size_t n_values;
    if (!unpack_uint(&p, p_end, &n_values)) {
        unpack_throw_serialisation_error(p);
    }
    string value;
    for ( ; n_values; --n_values) {
        Xapian::valueno slot;
        if (!unpack_uint(&p, p_end, &slot) ||
            !unpack_string(&p, p_end, value)) {
            unpack_throw_serialisation_error(p);
        }
        doc.add_value(slot, value);
    }

    size_t n_terms;
    if (!unpack_uint(&p, p_end, &n_terms)) {
        unpack_throw_serialisation_error(p);
    }
    string term;
    for ( ; n_terms; --n_terms) {
        Xapian::termcount wdf;
        if (!unpack_string(&p, p_end, term) ||
            !unpack_uint(&p, p_end, &wdf)) {
            unpack_throw_serialisation_error(p);
        }
        // Set all the wdf using add_term, then pass wdf_inc 0 to add_posting.
        doc.add_term(term, wdf);

        size_t n_pos;
        if (!unpack_uint(&p, p_end, &n_pos)) {
            unpack_throw_serialisation_error(p);
        }
        Xapian::termpos pos = 0;
        for ( ; n_pos; --n_pos) {
            Xapian::termpos inc;
            if (!unpack_uint(&p, p_end, &inc)) {
                unpack_throw_serialisation_error(p);
            }
            pos += inc;
            doc.add_posting(term, pos, 0);
        }
    }

    doc.set_data(string_view(p, p_end - p));
    return doc;
}
