/** @file
 * @brief HoneyTable 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_table.h"

#include "honey_cursor.h"
#include "stringutils.h"

#include "unicode/description_append.h"

#include <cerrno>

#ifdef DEBUGGING
# include <iostream>
#endif

using Honey::RootInfo;

using namespace std;

void
HoneyTable::create_and_open(int flags_, const RootInfo& root_info)
{
    Assert(!single_file());
    flags = flags_;
    compress_min = root_info.get_compress_min();
    if (read_only) {
	num_entries = root_info.get_num_entries();
	root = root_info.get_root();
	// FIXME: levels
    }
    if (!store.open(path, read_only))
	throw Xapian::DatabaseOpeningError("Failed to open HoneyTable", errno);
}

void
HoneyTable::open(int flags_, const RootInfo& root_info, honey_revision_number_t)
{
    flags = flags_;
    compress_min = root_info.get_compress_min();
    num_entries = root_info.get_num_entries();
    offset = root_info.get_offset();
    root = root_info.get_root();
    if (!single_file() && !store.open(path, read_only)) {
	if (!lazy)
	    throw Xapian::DatabaseOpeningError("Failed to open HoneyTable",
					       errno);
    }
    store.set_pos(offset);
}

void
HoneyTable::add(std::string_view key,
		const char* val,
		size_t val_size,
		bool compressed)
{
    if (rare(val_size == 0))
	throw Xapian::DatabaseError("HoneyTable::add() passed empty value");
    if (store.was_forced_closed())
	throw_database_closed();
    if (!compressed && compress_min > 0 && val_size > compress_min) {
	size_t compressed_size = val_size;
	CompressionStream comp_stream; // FIXME: reuse
	const char* p = comp_stream.compress(val, &compressed_size);
	if (p) {
	    add(key, p, compressed_size, true);
	    return;
	}
    }

    if (read_only)
	throw Xapian::InvalidOperationError("add() on read-only HoneyTable");
    if (key.size() == 0 || key.size() > HONEY_MAX_KEY_LENGTH)
	throw Xapian::InvalidArgumentError("Invalid key size: " +
					   str(key.size()));
    if (key <= last_key)
	throw Xapian::InvalidOperationError("New key <= previous key");
    size_t reuse = common_prefix_length(last_key, key);

#ifdef SSTINDEX_ARRAY
    if (reuse == 0) {
	index.maybe_add_entry(key, store.get_pos());
    }
#elif defined SSTINDEX_BINARY_CHOP
    // For a binary chop index, the index point is before the key info - the
    // index key must have the same N first bytes as the previous key, where
    // N >= the keep length.
    index.maybe_add_entry(key, store.get_pos());
#elif defined SSTINDEX_SKIPLIST
    // Handled below.
#else
# error SSTINDEX type not specified
#endif

    store.write(static_cast<unsigned char>(reuse));
    store.write(static_cast<unsigned char>(key.size() - reuse));
    store.write(key.data() + reuse, key.size() - reuse);
    ++num_entries;

#ifdef SSTINDEX_SKIPLIST
    // For a skiplist index, the index provides the full key, so the index
    // point is after the key at the level below.
    index.maybe_add_entry(key, store.get_pos());
#endif

    // Encode "compressed?" flag in bottom bit.
    // FIXME: Don't do this if a table is uncompressed?  That saves a byte
    // for each item where the extra bit pushes the length up by a byte.
    size_t val_size_enc = (val_size << 1) | size_t{compressed};
    std::string val_len;
    pack_uint(val_len, val_size_enc);
    // FIXME: pass together so we can potentially writev() both?
    store.write(val_len.data(), val_len.size());
    store.write(val, val_size);
    last_key = key;
}

void
HoneyTable::commit(honey_revision_number_t, RootInfo* root_info)
{
    if (root < 0)
	throw Xapian::InvalidOperationError("root not set");

    root_info->set_num_entries(num_entries);
    // offset should already be set.
    root_info->set_root(root);
    // Not really meaningful.
    // root_info->set_free_list(std::string());

    read_only = true;
    store.rewind(offset);
    last_key = string();
}

bool
HoneyTable::read_key(std::string& key,
		     size_t& val_size,
		     bool& compressed) const
{
#ifdef DEBUGGING
    {
	string desc;
	description_append(desc, key);
	cerr << "HoneyTable::read_key(" << desc << ", ...) for path=" << path
	     << endl;
    }
#endif
    if (!read_only) {
	return false;
    }

    AssertRel(store.get_pos(), >=, offset);
    if (store.get_pos() >= root) {
	AssertEq(store.get_pos(), root);
	return false;
    }
    int ch = store.read();
    if (ch == EOF) return false;

    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);
    key.assign(last_key, 0, reuse);
    key.append(buf, key_size);
    last_key = key;

#ifdef DEBUGGING
    if (false) {
	std::string esc;
	description_append(esc, key);
	std::cout << "K:" << esc << std::endl;
    }
#endif

    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");
    }
    compressed = val_size & 1;
    val_size >>= 1;
    Assert(p == end);
    return true;
}

void
HoneyTable::read_val(std::string& val, size_t val_size) const
{
    AssertRel(store.get_pos() + val_size, <=, size_t(root));
    val.resize(val_size);
    store.read(&(val[0]), val_size);
#ifdef DEBUGGING
    if (false) {
	std::string esc;
	description_append(esc, val);
	std::cout << "V:" << esc << std::endl;
    }
#endif
}

bool
HoneyTable::get_exact_entry(std::string_view key, std::string* tag) const
{
    if (!read_only) std::abort();
    if (rare(!store.is_open())) {
	if (store.was_forced_closed())
	    throw_database_closed();
	return false;
    }
    store.rewind(root);
    if (rare(key.empty()))
	return false;
    bool exact_match = false;
    bool compressed = false;
    size_t val_size = 0;
    int index_type = store.read();
    switch (index_type) {
	case EOF:
	    return false;
	case 0x00: {
	    unsigned char first =
		static_cast<unsigned char>(key[0] - store.read());
	    unsigned char range = store.read();
	    if (first > range)
		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)
		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;
	    while (true) {
		int reuse = store.read();
		if (reuse == EOF) break;
		int len = store.read();
		if (len == EOF) abort(); // FIXME
		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
	    {
		cerr << " cmp0 = " << cmp0 << ", going to " << ptr << endl;
	    }
#endif
	    store.set_pos(ptr);

	    if (ptr != 0) {
		last_key = index_key;
		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++ = 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");
		}
		compressed = val_size & 1;
		val_size >>= 1;
		Assert(p == end);
	    } else {
		last_key = string();
	    }

	    if (cmp0 == 0) {
		exact_match = true;
		break;
	    }

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

	    break;
	}
	default: {
	    string m = "HoneyTable: Unknown index type ";
	    m += str(index_type);
	    throw Xapian::DatabaseCorruptError(m);
	}
    }

    std::string k;
    int cmp;
    if (!exact_match) {
	do {
	    if (val_size) {
		// Skip val data we've not looked at.
		store.skip(val_size);
		val_size = 0;
	    }
	    if (!read_key(k, val_size, compressed)) return false;
	    cmp = k.compare(key);
	} while (cmp < 0);
	if (cmp > 0) return false;
    }
    if (tag != NULL) {
	if (compressed) {
	    std::string v;
	    read_val(v, val_size);
	    CompressionStream comp_stream;
	    comp_stream.decompress_start();
	    tag->resize(0);
	    if (!comp_stream.decompress_chunk(v.data(), v.size(), *tag)) {
		// Decompression didn't complete.
		abort();
	    }
	} else {
	    read_val(*tag, val_size);
	}
    }
    return true;
}

HoneyCursor*
HoneyTable::cursor_get() const
{
    if (rare(!store.is_open())) {
	if (store.was_forced_closed())
	    throw_database_closed();
	return NULL;
    }
    return new HoneyCursor(this);
}
