/** @file
 * @brief Handle encoding and decoding prefix-compressed lists of strings
 */
/* Copyright (C) 2004,2005,2006,2007,2008,2009,2010,2018,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/>.
 */

#ifndef XAPIAN_INCLUDED_PREFIX_COMPRESSED_STRINGS_H
#define XAPIAN_INCLUDED_PREFIX_COMPRESSED_STRINGS_H

#include <xapian/error.h>

#include <algorithm>
#include <string>

#include "honey/honey_spelling.h"
#include "stringutils.h"

// We XOR the length values with this so that they are more likely to coincide
// with lower case ASCII letters, which are likely to be common.  This means
// that zlib should do a better job of compressing tag values - in tests, this
// gave 5% better compression.
#define MAGIC_XOR_VALUE 96

class PrefixCompressedStringItor {
    const unsigned char * p;
    size_t left;
    std::string current;

    /** Number of constant characters on the end of the value.
     *
     *  Valid values once iterating are 0, 1, 2.  Before iteration, can be
     *  0 (no head or tail), 2 (two tails), -1 (one head, one tail -> 1 once
     *  iterating) or -2 (two heads, no tail -> 0 once iterating).
     */
    int tail = 0;

    PrefixCompressedStringItor(PrefixCompressedStringItor& o)
        : p(o.p), left(o.left), current(o.current), tail(o.tail) {}

    PrefixCompressedStringItor(PrefixCompressedStringItor&& o)
        : p(o.p), left(o.left), current(std::move(o.current)), tail(o.tail) {}

  public:
    /** Construct for glass.
     *
     *  @param s  the encoded data.
     */
    explicit PrefixCompressedStringItor(const std::string& s)
        : p(reinterpret_cast<const unsigned char *>(s.data())),
          left(s.size()) {
        if (left) {
            operator++();
        } else {
            p = NULL;
        }
    }

    /** Construct for honey.
     *
     *  @param s    the encoded data.
     *  @param key  the key
     */
    PrefixCompressedStringItor(const std::string& s,
                               const std::string& key)
        : p(reinterpret_cast<const unsigned char *>(s.data())),
          left(s.size()) {
        Assert(!key.empty());
        unsigned char first_ch = key[0];
        AssertRel(first_ch, <, Honey::KEY_PREFIX_WORD);
        switch (first_ch) {
            case Honey::KEY_PREFIX_BOOKEND:
                tail = -1;
                break;
            case Honey::KEY_PREFIX_HEAD:
                tail = -2;
                break;
            case Honey::KEY_PREFIX_TAIL:
                tail = 2;
                break;
        }
        if (tail != 0)
            current.assign(key, 1, 2);
        if (left) {
            operator++();
        } else {
            p = NULL;
        }
    }

    const std::string & operator*() const {
        return current;
    }

    PrefixCompressedStringItor operator++(int) {
        PrefixCompressedStringItor old(*this);
        operator++();
        return old;
    }

    PrefixCompressedStringItor & operator++() {
        if (left == 0) {
            p = NULL;
        } else {
            size_t keep = 0;
            if (rare(tail < 0)) {
                tail += 2;
                keep = current.size() - tail;
            } else if (usual(!current.empty())) {
                keep = *p++ ^ MAGIC_XOR_VALUE;
                --left;
            }
            size_t add;
            if (left == 0 || (add = *p ^ MAGIC_XOR_VALUE) >= left)
                throw Xapian::DatabaseCorruptError("Bad spelling data (too little left)");
            current.replace(keep, current.size() - tail - keep,
                            reinterpret_cast<const char *>(p + 1), add);
            p += add + 1;
            left -= add + 1;
        }
        return *this;
    }

    bool at_end() const {
        return p == NULL;
    }
};

class PrefixCompressedStringWriter {
    std::string current;
    std::string & out;

    int tail = 0;

  public:
    /** Construct for glass.
     *
     *  @param out_  where to write data to.
     */
    explicit PrefixCompressedStringWriter(std::string& out_) : out(out_) { }

    /** Construct for honey.
     *
     *  @param out_  where to write data to.
     *  @param key   the key.
     */
    PrefixCompressedStringWriter(std::string& out_,
                                 const std::string& key)
        : out(out_) {
        Assert(!key.empty());
        unsigned char first_ch = key[0];
        AssertRel(first_ch, <, Honey::KEY_PREFIX_WORD);
        switch (first_ch) {
            case Honey::KEY_PREFIX_BOOKEND:
                tail = -1;
                break;
            case Honey::KEY_PREFIX_HEAD:
                tail = -2;
                break;
            case Honey::KEY_PREFIX_TAIL:
                tail = 2;
                break;
        }
        if (tail != 0)
            current.assign(key, 1, 2);
    }

    void append(const std::string & word) {
        // If this isn't the first entry, see how much of the previous one
        // we can reuse.
        if (rare(tail < 0)) {
            // First entry for BOOKEND or HEAD (tail is -1 or -2).
            AssertRel(tail, >=, -2);
            AssertEq(current[0], word[0]);
            if (tail == -2) {
                AssertEq(current[1], word[1]);
            } else {
                AssertEq(current.back(), word.back());
            }
            out += char((word.size() - 2) ^ MAGIC_XOR_VALUE);
            out.append(word, -tail, word.size() - 2);
            tail += 2;
        } else if (usual(!current.empty())) {
            // Incremental change.
            if (tail)
                AssertEq(current[current.size() - 1], word[word.size() - 1]);
            if (tail > 1)
                AssertEq(current[current.size() - 2], word[word.size() - 2]);
            size_t i = common_prefix_length(current, word);
            // Don't allow the reused prefix to overlap with tail
            i = std::min(i, word.size() - tail);
            out += char(i ^ MAGIC_XOR_VALUE);
            size_t add = word.size() - i - tail;
            out += char(add ^ MAGIC_XOR_VALUE);
            out.append(word.data() + i, add);
        } else {
            // First entry for MIDDLE or TAIL (tail is 0 or 2).
            if (tail) {
                AssertEq(current[current.size() - 1], word[word.size() - 1]);
                AssertEq(current[current.size() - 2], word[word.size() - 2]);
            }
            out += char((word.size() - tail) ^ MAGIC_XOR_VALUE);
            out.append(word, 0, word.size() - tail);
        }
        current = word;
    }
};

struct PrefixCompressedStringItorGt {
    /// Return true if and only if a's string is strictly greater than b's.
    bool operator()(const PrefixCompressedStringItor *a,
                    const PrefixCompressedStringItor *b) const {
        return (**a > **b);
    }
};

#endif // XAPIAN_INCLUDED_PREFIX_COMPRESSED_STRINGS_H
