/** @file
 * @brief HoneyCursor class
 */
/* Copyright (C) 2017,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/>.
 */

#include <config.h>

//#define DEBUGGING

#include "honey_cursor.h"

#include <cerrno>
#include <string>

#ifdef DEBUGGING
# include <iostream>
#endif

using namespace std;

bool
HoneyCursor::do_next()
{
    if (is_at_end) {
        Assert(false);
        return false;
    }

    if (val_size) {
        // Skip val data we've not looked at.
        store.skip(val_size);
        val_size = 0;
    }

    if (store.get_pos() >= root) {
        AssertEq(store.get_pos(), root);
        is_at_end = true;
        return false;
    }

    int ch = store.read();
    if (ch == EOF) {
        // The root check above should mean this can't legitimately happen.
        throw Xapian::DatabaseCorruptError("EOF reading key");
    }

    size_t reuse = ch;
    if (reuse > last_key.size()) {
        throw Xapian::DatabaseCorruptError("Reuse > previous key size");
    }
    ch = store.read();
    if (ch == EOF) {
        throw Xapian::DatabaseError("EOF/error while reading key length",
                                    errno);
    }
    size_t key_size = ch;
    char buf[256];
    store.read(buf, key_size);
    current_key.assign(last_key, 0, reuse);
    current_key.append(buf, key_size);
    last_key = current_key;

#ifdef DEBUGGING
    {
        string esc;
        description_append(esc, current_key);
        cerr << "K:" << esc << endl;
    }
#endif

    return next_from_index();
}

bool
HoneyCursor::next_from_index()
{
    char buf[8];
    int r;
    {
        // FIXME: rework to take advantage of buffering that's happening
        // anyway?
        char* p = buf;
        for (int i = 0; i < 8; ++i) {
            int ch2 = store.read();
            if (ch2 == EOF) {
                break;
            }
            *p++ = char(ch2);
            if (ch2 < 128) break;
        }
        r = p - buf;
    }
    const char* p = buf;
    const char* end = p + r;
    if (!unpack_uint(&p, end, &val_size)) {
        throw Xapian::DatabaseError("val_size unpack_uint invalid");
    }
    if (p != end) abort();
    current_compressed = val_size & 1;
    val_size >>= 1;

    // FIXME: Always resize to 0?  Not doing so avoids always having to clear
    // all the data before reading it.
    if (true && val_size == 0)
        current_tag.resize(0);

    is_at_end = false;
    return true;
}

bool
HoneyCursor::read_tag(bool keep_compressed)
{
    if (val_size) {
        if (store.was_forced_closed()) {
            HoneyTable::throw_database_closed();
        }

        current_tag.resize(val_size);
        store.read(&(current_tag[0]), val_size);
#ifdef DEBUGGING
        {
            cerr << "read " << val_size << " bytes of value data ending @"
                 << store.get_pos() << endl;
            string esc;
            description_append(esc, current_tag);
            cerr << "V:" << esc << endl;
        }
#endif
        val_size = 0;
    }
    if (!keep_compressed && current_compressed) {
        // Need to decompress.
        comp_stream.decompress_start();
        string new_tag;
        if (!comp_stream.decompress_chunk(current_tag.data(),
                                          current_tag.size(),
                                          new_tag)) {
            // Decompression didn't complete.
            abort();
        }
        swap(current_tag, new_tag);
        current_compressed = false;
#ifdef DEBUGGING
        {
            cerr << "decompressed to " << current_tag.size()
                 << "bytes of value data" << endl;
        }
#endif
    }
    return current_compressed;
}

bool
HoneyCursor::do_find(string_view key, bool greater_than)
{
    // FIXME: Actually use this!
    (void)greater_than;

#ifdef DEBUGGING
    {
        string esc;
        description_append(esc, key);
        cerr << "do_find(" << esc << ", " << greater_than << ") @"
             << store.get_pos() << endl;
    }
#endif

    if (key.empty()) {
        rewind();
        do_next();
        return false;
    }

    bool use_index = true;
    if (!is_at_end && !last_key.empty() && last_key[0] == key[0]) {
        int cmp0 = last_key.compare(key);
        if (cmp0 == 0) {
            current_key = last_key;
            return true;
        }
        if (cmp0 < 0) {
            // We're going forwards to a key with the same first character, so
            // an array index won't help us.
            use_index = false;
        }
    }

    if (store.was_forced_closed()) {
        HoneyTable::throw_database_closed();
    }

    if (use_index) {
        store.rewind(root);
        int index_type = store.read();
        switch (index_type) {
            case EOF:
                return false;
            case 0x00: {
                unsigned char first = key[0] - store.read();
                unsigned char range = store.read();
                if (first > range) {
                    is_at_end = true;
                    return false;
                }
                store.skip(first * 4); // FIXME: pointer width
                off_t jump = store.read_uint4_be();
                store.rewind(jump);
                // The jump point will be an entirely new key (because it is
                // the first key with that initial character), and we drop in
                // as if this was the first key so set last_key to be empty.
                last_key = string();
                break;
            }
            case 0x01: {
                size_t j = store.read_uint4_be();
                if (j == 0) {
                    is_at_end = true;
                    return false;
                }
                off_t base = store.get_pos();
                char kkey[SSTINDEX_BINARY_CHOP_KEY_SIZE];
                size_t kkey_len = 0;
                size_t i = 0;
                while (j - i > 1) {
                    size_t k = i + (j - i) / 2;
                    store.set_pos(base + k * SSTINDEX_BINARY_CHOP_ENTRY_SIZE);
                    store.read(kkey, SSTINDEX_BINARY_CHOP_KEY_SIZE);
                    kkey_len = 4;
                    while (kkey_len > 0 && kkey[kkey_len - 1] == '\0')
                        --kkey_len;
                    int r = key.compare(0, SSTINDEX_BINARY_CHOP_KEY_SIZE,
                                        kkey, kkey_len);
                    if (r < 0) {
                        j = k;
                    } else {
                        i = k;
                        if (r == 0) {
                            break;
                        }
                    }
                }
                store.set_pos(base + i * SSTINDEX_BINARY_CHOP_ENTRY_SIZE);
                store.read(kkey, SSTINDEX_BINARY_CHOP_KEY_SIZE);
                kkey_len = 4;
                while (kkey_len > 0 && kkey[kkey_len - 1] == '\0') --kkey_len;
                off_t jump = store.read_uint4_be();
                store.rewind(jump);
                // The jump point is to the first key with prefix kkey, so will
                // work if we set last key to kkey.  Unless we're jumping to the
                // start of the table, in which case last_key needs to be empty.
                last_key.assign(kkey, jump == 0 ? 0 : kkey_len);
                break;
            }
            case 0x02: {
                // FIXME: If "close" just seek forwards?  Or consider seeking
                // from current index pos?
                // off_t pos = store.get_pos();
                string index_key, prev_index_key;
                make_unsigned_t<off_t> ptr = 0;
                int cmp0 = 1;
#ifdef DEBUGGING
                {
                    cerr << "Using skiplist index\n";
                }
#endif
                while (true) {
                    int reuse = store.read();
                    if (reuse == EOF) break;
                    int len = store.read();
                    if (len == EOF) abort(); // FIXME
#ifdef DEBUGGING
                    {
                        cerr << "reuse = " << reuse << " len = " << len << endl;
                    }
#endif
                    index_key.resize(reuse + len);
                    store.read(&index_key[reuse], len);

#ifdef DEBUGGING
                    {
                        string desc;
                        description_append(desc, index_key);
                        cerr << "Index key: " << desc << endl;
                    }
#endif

                    cmp0 = index_key.compare(key);
                    if (cmp0 > 0) {
                        index_key = prev_index_key;
                        break;
                    }
                    char buf[8];
                    char* e = buf;
                    while (true) {
                        int b = store.read();
                        *e++ = b;
                        if ((b & 0x80) == 0) break;
                    }
                    const char* p = buf;
                    if (!unpack_uint(&p, e, &ptr) || p != e) abort(); // FIXME
#ifdef DEBUGGING
                    {
                        cerr << " -> " << ptr << endl;
                    }
#endif
                    if (cmp0 == 0)
                        break;
                    prev_index_key = index_key;
#ifdef DEBUGGING
                    {
                        string desc;
                        description_append(desc, prev_index_key);
                        cerr << "prev_index_key -> " << desc << endl;
                    }
#endif
                }
#ifdef DEBUGGING
                {
                    string desc;
                    description_append(desc, index_key);
                    cerr << " index_key = " << desc << ", cmp0 = " << cmp0
                         << ", going to " << ptr << endl;
                }
#endif
                store.set_pos(ptr);

                if (ptr != 0) {
                    last_key = current_key = index_key;
                    bool res = next_from_index();
                    (void)res;
                    Assert(res);
                    if (cmp0 == 0) {
                        Assert(ptr != 0);
                        return true;
                    }
                    store.skip(val_size);
                } else {
                    last_key = current_key = string();
                }

#ifdef DEBUGGING
                {
                    string desc;
                    description_append(desc, current_key);
                    cerr << "cmp0 was " << cmp0
                         << ", Dropped to data layer on key: " << desc << endl;
                }
#endif

                break;
            }
            default: {
                string m = "HoneyCursor: Unknown index type ";
                m += str(index_type);
                throw Xapian::DatabaseCorruptError(m);
            }
        }
        is_at_end = false;
        val_size = 0;
    }

    while (do_next()) {
        int cmp = current_key.compare(key);
        if (cmp == 0) return true;
        if (cmp > 0) break;
    }
    return false;
}

bool
HoneyCursor::prev()
{
    if (store.was_forced_closed()) {
        HoneyTable::throw_database_closed();
    }

    string key;
    if (is_at_end) {
        // To position on the last key we just do a < search for a key greater
        // than any possible key - one longer than the longest possible length
        // and consisting entirely of the highest sorting byte value.
        key.assign(HONEY_MAX_KEY_LENGTH + 1, '\xff');
    } else {
        if (current_key.empty())
            return false;
        key = current_key;
    }

    // FIXME: use index - for an array index we can look at index points for
    // first characters starting with key[0] and working down; for a binary
    // chop index we can start at the entry including the current key, or the
    // one before if this is the first key for that index entry; for a skiplist
    // index we can find the previous entry at the index level above.
    rewind();

    off_t pos;
    string k;
    size_t vs;
    bool compressed;
    do {
        pos = store.get_pos();
        k = current_key;
        vs = val_size;
        compressed = current_compressed;
    } while (do_next() && current_key < key);

    // Back up to previous entry.
    is_at_end = false;
    last_key = current_key = k;
    val_size = vs;
    current_compressed = compressed;
    store.set_pos(pos);

    return true;
}
