/** @file
 * @brief Encoding and decoding functions for honey postlists
 */
/* Copyright (C) 2015,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_HONEY_POSTLIST_ENCODINGS_H
#define XAPIAN_INCLUDED_HONEY_POSTLIST_ENCODINGS_H

#include "pack.h"

inline void
encode_initial_chunk_header(Xapian::doccount termfreq,
                            Xapian::termcount collfreq,
                            Xapian::docid first,
                            Xapian::docid last,
                            Xapian::docid chunk_last,
                            Xapian::termcount first_wdf,
                            Xapian::termcount wdf_max,
                            std::string& out)
{
    Assert(termfreq != 0);
    pack_uint(out, first - 1);
    if (termfreq == 1) {
        // Special case for a term which only occurs in one document.  By
        // Zipf's Law we expect these to be common in natural language
        // (typically 40-60% of words in a large corpus:
        // https://en.wikipedia.org/wiki/Hapax_legomenon ).  Another common
        // example is unique ID boolean terms.
        //
        // There's no postlist data after the chunk header for such terms
        // (since we know the first docid and its wdf from the data in
        // the chunk header) so we can just encode the collfreq if it is
        // non-zero - when decoding if there's no more data we'll know the
        // collfreq is zero.
        if (collfreq) {
            pack_uint(out, collfreq);
        }
        AssertEq(first, last);
        AssertEq(last, chunk_last);
        AssertEq(collfreq, wdf_max);
        AssertEq(collfreq, first_wdf);
    } else if (termfreq == 2) {
        // A term which only occurs in two documents.  By Zipf's Law these
        // are also fairly common (typically 10-15% of words in a large
        // corpus: https://en.wikipedia.org/wiki/Hapax_legomenon )
        //
        // We have to encode collfreq == 0 explicitly or else the decoder can't
        // distinguish between the cases:
        //
        //  tf = 1; collfreq = x
        //  tf = 2; last = first + x + 1; collfreq = 0
        //
        // We turn this to our advantage to encode tf = 2 lists with constant
        // wdf or where the second wdf is one more than the first (which are
        // common cases) more compactly, so instead of:
        //
        // <first - 1> <collfreq> <last - first - 1> <first_wdf>
        //
        // We encode these as:
        //
        // <first - 1> <collfreq> <last - first - 1>
        //
        // And then when it's omitted: first_wdf = collfreq >> 1
        //
        // The collfreq = 0 case is then a particular example of this.
        pack_uint(out, collfreq);
        AssertRel(last, >, first);
        pack_uint(out, last - first - 1);
        if (first_wdf != (collfreq / 2)) {
            pack_uint(out, first_wdf);
            AssertEq(std::max(first_wdf, collfreq - first_wdf), wdf_max);
        } else {
            AssertEq(collfreq - first_wdf, wdf_max);
        }
    } else if (collfreq == 0) {
        AssertEq(first_wdf, 0);
        AssertEq(wdf_max, 0);
        pack_uint(out, 0u);
        pack_uint(out, termfreq - 3);
        pack_uint(out, last - first - (termfreq - 1));
        pack_uint(out, chunk_last - first);
    } else {
        AssertRel(collfreq, >=, termfreq);
        pack_uint(out, collfreq - termfreq + 1);
        pack_uint(out, termfreq - 3);
        pack_uint(out, last - first - (termfreq - 1));
        pack_uint(out, chunk_last - first);
        pack_uint(out, first_wdf - 1);

        if (first_wdf >= collfreq - first_wdf - (termfreq - 2)) {
            AssertEq(wdf_max, first_wdf);
        } else {
            AssertRel(wdf_max, >=, first_wdf);
            pack_uint(out, wdf_max - first_wdf);
        }
    }
}

inline bool
decode_initial_chunk_header(const char** p, const char* end,
                            Xapian::doccount& termfreq,
                            Xapian::termcount& collfreq,
                            Xapian::docid& first,
                            Xapian::docid& last,
                            Xapian::docid& chunk_last,
                            Xapian::termcount& first_wdf,
                            Xapian::termcount& wdf_max)
{
    if (!unpack_uint(p, end, &first)) {
        return false;
    }
    ++first;
    if (*p == end) {
        collfreq = 0;
    } else if (!unpack_uint(p, end, &collfreq)) {
        return false;
    }
    if (*p == end) {
        // Single occurrence term.
        termfreq = 1;
        chunk_last = last = first;
        wdf_max = first_wdf = collfreq;
        return true;
    }

    if (!unpack_uint(p, end, &termfreq)) {
        return false;
    }
    if (*p == end) {
        // Double occurrence term with first_wdf = floor(collfreq / 2).
        chunk_last = last = first + termfreq + 1;
        termfreq = 2;
        first_wdf = collfreq / 2;
        wdf_max = std::max(first_wdf, collfreq - first_wdf);
        return true;
    }

    if (!unpack_uint(p, end, &last)) {
        return false;
    }
    if (*p == end) {
        // Double occurrence term.
        Assert(collfreq != 0);
        first_wdf = last;
        chunk_last = last = first + termfreq + 1;
        termfreq = 2;
        wdf_max = std::max(first_wdf, collfreq - first_wdf);
        return true;
    }

    if (!unpack_uint(p, end, &chunk_last)) {
        return false;
    }
    termfreq += 3;
    last += first + termfreq - 1;
    chunk_last += first;

    if (collfreq == 0) {
        wdf_max = first_wdf = 0;
    } else {
        collfreq += (termfreq - 1);
        if (!unpack_uint(p, end, &first_wdf)) {
            return false;
        }
        ++first_wdf;
        if (first_wdf >= collfreq - first_wdf - (termfreq - 2)) {
            wdf_max = first_wdf;
        } else {
            if (!unpack_uint(p, end, &wdf_max)) {
                return false;
            }
            wdf_max += first_wdf;
        }
    }

    return true;
}

inline bool
decode_initial_chunk_header_freqs(const char** p, const char* end,
                                  Xapian::doccount& termfreq,
                                  Xapian::termcount& collfreq)
{
    Xapian::docid first;
    if (!unpack_uint(p, end, &first)) {
        return false;
    }
    // Not used in this case.
    (void)first;
    if (*p == end) {
        collfreq = 0;
    } else if (!unpack_uint(p, end, &collfreq)) {
        return false;
    }
    if (*p == end) {
        // Single occurrence term.
        termfreq = 1;
        return true;
    }

    if (!unpack_uint(p, end, &termfreq)) {
        return false;
    }
    if (*p == end) {
        // Double occurrence term with first_wdf = floor(collfreq / 2).
        termfreq = 2;
        return true;
    }

    Xapian::docid last;
    if (!unpack_uint(p, end, &last)) {
        return false;
    }
    // Not used in this case.
    (void)last;
    if (*p == end) {
        // Double occurrence term.
        Assert(collfreq != 0);
        termfreq = 2;
        return true;
    }

    termfreq += 3;
    if (collfreq != 0) {
        collfreq += (termfreq - 1);
    }

    return true;
}

inline void
encode_delta_chunk_header(Xapian::docid chunk_first,
                          Xapian::docid chunk_last,
                          Xapian::termcount chunk_first_wdf,
                          std::string& out)
{
    Assert(chunk_first_wdf != 0);
    pack_uint(out, chunk_last - chunk_first);
    pack_uint(out, chunk_first_wdf - 1);
}

inline bool
decode_delta_chunk_header(const char** p, const char* end,
                          Xapian::docid chunk_last,
                          Xapian::docid& chunk_first,
                          Xapian::termcount& chunk_first_wdf)
{
    if (!unpack_uint(p, end, &chunk_first) ||
        !unpack_uint(p, end, &chunk_first_wdf)) {
        return false;
    }
    chunk_first = chunk_last - chunk_first;
    ++chunk_first_wdf;
    return true;
}

inline void
encode_delta_chunk_header_no_wdf(Xapian::docid chunk_first,
                                 Xapian::docid chunk_last,
                                 std::string& out)
{
    pack_uint(out, chunk_last - chunk_first);
}

inline bool
decode_delta_chunk_header_no_wdf(const char** p, const char* end,
                                 Xapian::docid chunk_last,
                                 Xapian::docid& chunk_first)
{
    if (!unpack_uint(p, end, &chunk_first)) {
        return false;
    }
    chunk_first = chunk_last - chunk_first;
    return true;
}

#endif // XAPIAN_INCLUDED_HONEY_POSTLIST_ENCODINGS_H
