/** @file
 * @brief Btree cursor implementation
 */
/* Copyright 1999,2000,2001 BrightStation PLC
 * Copyright 2002-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 "glass_cursor.h"

#include <xapian/error.h>

#include "glass_table.h"
#include "debuglog.h"
#include "omassert.h"

#include <string_view>

using namespace Glass;
using namespace std;

#ifdef XAPIAN_DEBUG_LOG
static string
hex_display_encode(string_view input)
{
    const char* table = "0123456789abcdef";
    string result;
    for (unsigned char val : input) {
	result += "\\x";
	result += table[val >> 4];
	result += table[val & 0x0f];
    }
    return result;
}
#endif

#define DIR_START        11

GlassCursor::GlassCursor(const GlassTable *B_, const Glass::Cursor * C_)
	: is_positioned(false),
	  is_after_end(false),
	  tag_status(UNREAD),
	  B(B_),
	  version(B_->cursor_version),
	  level(B_->level)
{
    B->cursor_created_since_last_modification = true;
    C = new Glass::Cursor[level + 1];
    if (!C_) C_ = B->C;
    for (int j = 0; j <= level; ++j) {
	C[j].clone(C_[j]);
    }
}

void
GlassCursor::rebuild()
{
    int new_level = B->level;
    if (new_level <= level) {
	for (int j = 0; j < new_level; ++j) {
	    C[j].clone(B->C[j]);
	}
	for (int j = new_level; j <= level; ++j) {
	    C[j].destroy();
	}
    } else {
	Cursor * old_C = C;
	C = new Cursor[new_level + 1];
	for (int i = 0; i < level; ++i) {
	    C[i].swap(old_C[i]);
	    C[i].clone(B->C[i]);
	}
	delete [] old_C;
	for (int j = level; j < new_level; ++j) {
	    C[j].init(B->block_size);
	}
    }
    level = new_level;
    C[level].clone(B->C[level]);
    version = B->cursor_version;
    B->cursor_created_since_last_modification = true;
}

GlassCursor::~GlassCursor()
{
    // We must not use B here, as it may have already been destroyed.
    delete [] C;
}

bool
GlassCursor::next()
{
    LOGCALL(DB, bool, "GlassCursor::next", NO_ARGS);
    Assert(!is_after_end);
    if (B->cursor_version != version) {
	// find_entry() will call rebuild().
	(void)find_entry(current_key);
	// If the key was found, we're now pointing to it, otherwise we're
	// pointing to the entry before.  Either way, we now want to move to
	// the next key.
    }
    if (tag_status == UNREAD || tag_status == UNREAD_ON_LAST_CHUNK) {
	while (true) {
	    if (!B->next(C, 0)) {
		is_positioned = false;
		break;
	    }
	    if (tag_status == UNREAD_ON_LAST_CHUNK ||
		LeafItem(C[0].get_p(), C[0].c).first_component()) {
		is_positioned = true;
		break;
	    }
	}
    }

    if (!is_positioned) {
	is_after_end = true;
	RETURN(false);
    }

    get_key(&current_key);
    tag_status = UNREAD;

    LOGLINE(DB, "Moved to entry: key=" << hex_display_encode(current_key));
    RETURN(true);
}

bool
GlassCursor::find_entry(const string &key)
{
    LOGCALL(DB, bool, "GlassCursor::find_entry", key);
    if (B->cursor_version != version) {
	rebuild();
    }

    is_after_end = false;

    bool found;

    is_positioned = true;
    if (key.size() > GLASS_BTREE_MAX_KEY_LEN) {
	// Can't find key - too long to possibly be present, so find the
	// truncated form but ignore "found".
	B->form_key(key.substr(0, GLASS_BTREE_MAX_KEY_LEN));
	(void)(B->find(C));
	found = false;
    } else {
	B->form_key(key);
	found = B->find(C);
    }

    if (found) {
	tag_status = UNREAD;
	current_key = key;
    } else {
	// Be lazy about stepping back to the first chunk, as we may never be
	// asked to read the tag.
	tag_status = UNREAD_ON_LAST_CHUNK;
	if (C[0].c < DIR_START) {
	    // It would be nice to be lazy about this too, but we need to
	    // be on an actual entry in order to read the key.
	    C[0].c = DIR_START;
	    if (!B->prev(C, 0)) {
		tag_status = UNREAD;
	    }
	}
	get_key(&current_key);
    }

    LOGLINE(DB, "Found entry: key=" << hex_display_encode(current_key));
    RETURN(found);
}

void
GlassCursor::find_entry_lt(const string &key)
{
    LOGCALL_VOID(DB, "GlassCursor::find_entry_lt", key);
    if (!find_entry(key)) {
	// The entry wasn't found, so find_entry() left us on the entry before
	// the one we asked for and we're done.
	return;
    }

    Assert(!is_after_end);
    Assert(is_positioned);

    if (!B->prev(C, 0)) {
	is_positioned = false;
	return;
    }
    tag_status = UNREAD_ON_LAST_CHUNK;
    get_key(&current_key);

    LOGLINE(DB, "Found entry: key=" << hex_display_encode(current_key));
}

bool
GlassCursor::find_exact(const string &key)
{
    LOGCALL(DB, bool, "GlassCursor::find_exact", key);
    is_after_end = false;
    is_positioned = false;
    if (rare(key.size() > GLASS_BTREE_MAX_KEY_LEN)) {
	// There can't be a match
	RETURN(false);
    }

    if (B->cursor_version != version) {
	rebuild();
    }

    B->form_key(key);
    if (!B->find(C)) {
	RETURN(false);
    }
    current_key = key;
    B->read_tag(C, &current_tag, false);
    tag_status = UNCOMPRESSED;
    is_positioned = true;

    RETURN(true);
}

bool
GlassCursor::find_entry_ge(string_view key)
{
    LOGCALL(DB, bool, "GlassCursor::find_entry_ge", key);
    if (B->cursor_version != version) {
	rebuild();
    }

    is_after_end = false;

    bool found;

    is_positioned = true;
    if (key.size() > GLASS_BTREE_MAX_KEY_LEN) {
	// Can't find key - too long to possibly be present, so find the
	// truncated form but ignore "found".
	B->form_key(key.substr(0, GLASS_BTREE_MAX_KEY_LEN));
	(void)(B->find(C));
	found = false;
    } else {
	B->form_key(key);
	found = B->find(C);
    }

    if (found) {
	current_key = key;
    } else {
	if (!B->next(C, 0)) {
	    is_after_end = true;
	    is_positioned = false;
	    RETURN(false);
	}
	Assert(LeafItem(C[0].get_p(), C[0].c).first_component());
	get_key(&current_key);
    }
    tag_status = UNREAD;

    LOGLINE(DB, "Found entry: key=" << hex_display_encode(current_key));
    RETURN(found);
}

void
GlassCursor::get_key(string * key) const
{
    Assert(B->level <= level);
    Assert(is_positioned);

    (void)LeafItem(C[0].get_p(), C[0].c).key().read(key);
}

bool
GlassCursor::read_tag(bool keep_compressed)
{
    LOGCALL(DB, bool, "GlassCursor::read_tag", keep_compressed);
    if (tag_status == UNREAD_ON_LAST_CHUNK) {
	// Back up to first chunk of this tag.
	while (!LeafItem(C[0].get_p(), C[0].c).first_component()) {
	    if (!B->prev(C, 0)) {
		is_positioned = false;
		throw Xapian::DatabaseCorruptError("find_entry failed to find any entry at all!");
	    }
	}
	tag_status = UNREAD;
    }
    if (tag_status == UNREAD) {
	Assert(B->level <= level);
	Assert(is_positioned);

	if (B->read_tag(C, &current_tag, keep_compressed)) {
	    tag_status = COMPRESSED;
	} else {
	    tag_status = UNCOMPRESSED;
	}

	// We need to call B->next(...) after B->read_tag(...) so that the
	// cursor ends up on the next key.
	is_positioned = B->next(C, 0);

	LOGLINE(DB, "tag=" << hex_display_encode(current_tag));
    }
    RETURN(tag_status == COMPRESSED);
}

bool
MutableGlassCursor::del()
{
    Assert(!is_after_end);

    // MutableGlassCursor is only constructable from a non-const GlassTable*
    // but we store that in the const GlassTable* "B" member of the GlassCursor
    // class to avoid duplicating storage.  So we know it is safe to cast away
    // that const again here.
    (const_cast<GlassTable*>(B))->del(current_key);

    // If we're iterating an older revision of the tree, then the deletion
    // happens in a new (uncommitted) revision and the cursor still sees
    // the deleted key.  But if we're iterating the new uncommitted revision
    // then the deleted key is no longer visible.  We need to handle both
    // cases - either find_entry_ge() finds the deleted key or not.
    if (!find_entry_ge(current_key)) return is_positioned;
    return next();
}

#ifdef DISABLE_GPL_LIBXAPIAN
# error GPL source we cannot relicense included in libxapian
#endif
