/** @file
 * @brief Virtual base class for Database internals
 */
/* Copyright 2003-2024 Olly Betts
 * Copyright 2008 Lemur Consulting Ltd
 *
 * 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 "databaseinternal.h"

#include "api/termlist.h"
#include "heap.h"
#include "omassert.h"
#include "postlist.h"
#include "slowvaluelist.h"
#include "stringutils.h"
#include "xapian/error.h"

#include <algorithm>
#include <limits>
#include <memory>
#include <string>
#include <string_view>

using namespace std;
using Xapian::Internal::intrusive_ptr;

namespace Xapian {

[[noreturn]]
static void invalid_operation(const char* msg)
{
    throw InvalidOperationError(msg);
}

Database::Internal::size_type
Database::Internal::size() const
{
    return 1;
}

void
Database::Internal::keep_alive()
{
    // No-op except for remote databases.
}

void
Database::Internal::readahead_for_query(const Xapian::Query &) const
{
}

Xapian::termcount
Database::Internal::get_unique_terms_lower_bound() const
{
    return get_doclength_upper_bound() ? 1 : 0;
}

Xapian::termcount
Database::Internal::get_unique_terms_upper_bound() const
{
    return get_doclength_upper_bound();
}

// Discard any exceptions - we're called from the destructors of derived
// classes so we can't safely throw.
void
Database::Internal::dtor_called_()
{
    try {
	if (transaction_active()) {
	    end_transaction(false);
	} else {
	    // TRANSACTION_READONLY and TRANSACTION_UNIMPLEMENTED should be
	    // handled by the inlined dtor_called() wrapper.
	    AssertEq(state, TRANSACTION_NONE);
	    commit();
	}
    } catch (...) {
	// We can't safely throw exceptions from a destructor in case an
	// exception is already active and causing us to be destroyed.
    }
}

void
Database::Internal::commit()
{
    // Writable databases should override this method, but this can get called
    // if a read-only shard gets added to a WritableDatabase.
    invalid_operation("WritableDatabase::commit() called with a read-only shard");
}

void
Database::Internal::cancel()
{
    // Writable databases should override this method, but this can get called
    // if a read-only shard gets added to a WritableDatabase.
    invalid_operation("WritableDatabase::cancel() called with a read-only shard");
}

void
Database::Internal::begin_transaction(bool flushed)
{
    if (state != TRANSACTION_NONE) {
	if (transaction_active()) {
	    invalid_operation("WritableDatabase::begin_transaction(): already "
			      "in a transaction");
	}
	if (is_read_only()) {
	    invalid_operation("WritableDatabase::begin_transaction(): called "
			      "with a read-only shard");
	}
	throw UnimplementedError("This backend doesn't implement transactions");
    }
    if (flushed) {
	// N.B. Call commit() before we set state since commit() isn't allowed
	// during a transaction.
	commit();
	state = TRANSACTION_FLUSHED;
    } else {
	state = TRANSACTION_UNFLUSHED;
    }
}

void
Database::Internal::end_transaction(bool do_commit)
{
    if (!transaction_active()) {
	if (state != TRANSACTION_NONE) {
	    if (is_read_only()) {
		invalid_operation(do_commit ?
				  "WritableDatabase::commit_transaction(): "
				  "called with a read-only shard" :
				  "WritableDatabase::cancel_transaction(): "
				  "called with a read-only shard");
	    }
	    throw UnimplementedError("This backend doesn't implement transactions");
	}
	invalid_operation(do_commit ?
			  "WritableDatabase::commit_transaction(): not in a "
			  "transaction" :
			  "WritableDatabase::cancel_transaction(): not in a "
			  "transaction");
    }

    auto old_state = state;
    state = TRANSACTION_NONE;
    if (!do_commit) {
	cancel();
    } else if (old_state == TRANSACTION_FLUSHED) {
	// N.B. Call commit() after we clear state since commit() isn't
	// allowed during a transaction.
	commit();
    }
}

Xapian::docid
Database::Internal::add_document(const Xapian::Document &)
{
    // Writable databases should override this method, but this can get called
    // if a read-only shard gets added to a WritableDatabase.
    invalid_operation("WritableDatabase::add_document() called with a "
		      "read-only shard");
}

void
Database::Internal::delete_document(Xapian::docid)
{
    // Writable databases should override this method, but this can get called
    // if a read-only shard gets added to a WritableDatabase.
    invalid_operation("WritableDatabase::delete_document() called with a "
		      "read-only shard");
}

void
Database::Internal::delete_document(string_view unique_term)
{
    // Default implementation - overridden for remote and sharded databases.

    if (is_read_only()) {
	// This can happen if a read-only shard gets added to a
	// WritableDatabase.
	invalid_operation("WritableDatabase::delete_document() called with a "
			  "read-only shard");
    }

    unique_ptr<PostList> pl(open_post_list(unique_term));
    if (!pl) {
	// unique_term doesn't index any documents.
	return;
    }

    // We want this operation to be atomic if possible, so if we aren't in a
    // transaction and the backend supports transactions, temporarily enter an
    // unflushed transaction.
    auto old_state = state;
    if (state != TRANSACTION_UNIMPLEMENTED)
	state = TRANSACTION_UNFLUSHED;
    try {
	while (pl->next(), !pl->at_end()) {
	    delete_document(pl->get_docid());
	}
    } catch (...) {
	state = old_state;
	throw;
    }
    state = old_state;
}

void
Database::Internal::replace_document(Xapian::docid, const Xapian::Document &)
{
    // Writable databases should override this method, but this can get called
    // if a read-only shard gets added to a WritableDatabase.
    invalid_operation("WritableDatabase::replace_document() called with a "
		      "read-only shard");
}

Xapian::docid
Database::Internal::replace_document(string_view unique_term,
				     const Xapian::Document& document)
{
    // Default implementation - overridden for remote and sharded databases.

    if (is_read_only()) {
	// This can happen if a read-only shard gets added to a
	// WritableDatabase.
	invalid_operation("WritableDatabase::replace_document() called with a "
			  "read-only shard");
    }

    unique_ptr<PostList> pl(open_post_list(unique_term));
    if (!pl || (pl->next(), pl->at_end())) {
	// unique_term doesn't index any documents.
	return add_document(document);
    }
    Xapian::docid did = pl->get_docid();

    // We want this operation to be atomic if possible, so if we aren't in a
    // transaction and the backend supports transactions, temporarily enter an
    // unflushed transaction.
    auto old_state = state;
    if (state != TRANSACTION_UNIMPLEMENTED)
	state = TRANSACTION_UNFLUSHED;
    try {
	replace_document(did, document);
	while (pl->next(), !pl->at_end()) {
	    delete_document(pl->get_docid());
	}
    } catch (...) {
	state = old_state;
	throw;
    }
    state = old_state;
    return did;
}

ValueList *
Database::Internal::open_value_list(Xapian::valueno slot) const
{
    return new SlowValueList(this, slot);
}

TermList *
Database::Internal::open_spelling_termlist(string_view) const
{
    // Only implemented for some database backends - others will just not
    // suggest spelling corrections (or not contribute to them in a multiple
    // database situation).
    return NULL;
}

TermList *
Database::Internal::open_spelling_wordlist() const
{
    // Only implemented for some database backends - others will just not
    // suggest spelling corrections (or not contribute to them in a multiple
    // database situation).
    return NULL;
}

Xapian::doccount
Database::Internal::get_spelling_frequency(string_view) const
{
    // Only implemented for some database backends - others will just not
    // suggest spelling corrections (or not contribute to them in a multiple
    // database situation).
    return 0;
}

void
Database::Internal::add_spelling(string_view, Xapian::termcount) const
{
    throw Xapian::UnimplementedError("This backend doesn't implement spelling correction");
}

Xapian::termcount
Database::Internal::remove_spelling(string_view, Xapian::termcount) const
{
    throw Xapian::UnimplementedError("This backend doesn't implement spelling correction");
}

TermList *
Database::Internal::open_synonym_termlist(string_view) const
{
    // Only implemented for some database backends - others will just not
    // expand synonyms (or not contribute to them in a multiple database
    // situation).
    return NULL;
}

TermList *
Database::Internal::open_synonym_keylist(string_view) const
{
    // Only implemented for some database backends - others will just not
    // expand synonyms (or not contribute to them in a multiple database
    // situation).
    return NULL;
}

void
Database::Internal::add_synonym(string_view, string_view) const
{
    throw Xapian::UnimplementedError("This backend doesn't implement synonyms");
}

void
Database::Internal::remove_synonym(string_view, string_view) const
{
    throw Xapian::UnimplementedError("This backend doesn't implement synonyms");
}

void
Database::Internal::clear_synonyms(string_view) const
{
    throw Xapian::UnimplementedError("This backend doesn't implement synonyms");
}

string
Database::Internal::get_metadata(string_view) const
{
    return string();
}

TermList*
Database::Internal::open_metadata_keylist(string_view) const
{
    // Only implemented for some database backends - others will simply report
    // there being no metadata keys.
    return NULL;
}

void
Database::Internal::set_metadata(string_view, string_view)
{
    throw Xapian::UnimplementedError("This backend doesn't implement metadata");
}

bool
Database::Internal::reopen()
{
    // Database backends which don't support simultaneous update and reading
    // probably don't need to do anything here.  And since we didn't do
    // anything we should return false to indicate that nothing has changed.
    return false;
}

void
Database::Internal::request_document(Xapian::docid) const
{
}

void
Database::Internal::write_changesets_to_fd(int, string_view, bool,
					   ReplicationInfo*)
{
    throw Xapian::UnimplementedError("This backend doesn't provide changesets");
}

Xapian::rev
Database::Internal::get_revision() const
{
    throw Xapian::UnimplementedError("This backend doesn't provide access to revision information");
}

string
Database::Internal::get_uuid() const
{
    return string();
}

void
Database::Internal::invalidate_doc_object(Xapian::Document::Internal*) const
{
    // Do nothing, by default.
}

void
Database::Internal::get_used_docid_range(Xapian::docid &,
					 Xapian::docid &) const
{
    throw Xapian::UnimplementedError("This backend doesn't implement get_used_docid_range()");
}

bool
Database::Internal::locked() const
{
    return false;
}

Database::Internal*
Database::Internal::update_lock(int flags)
{
    if (flags == Xapian::DB_READONLY_) return this;
    throw Xapian::DatabaseLockError("Not possible to lock for writing");
}

namespace {
    class Pos {
	Xapian::termpos pos;

	PositionList* p;

	string term;

      public:
	Pos(string&& term_, PositionList* p_)
	    : p(p_), term(term_) {
	    pos = p->get_position();
	}

	~Pos() { delete p; }

	Xapian::termpos get_pos() const { return pos; }

	const string& get_term() const { return term; }

	bool next() {
	    if (!p->next()) {
		return false;
	    }
	    pos = p->get_position();
	    return true;
	}
    };
}

static void
reconstruct_open_poslists(TermList* termlist,
			  Xapian::termpos start_pos,
			  Xapian::termpos end_pos,
			  string_view end,
			  vector<unique_ptr<Pos>>& heap,
			  size_t prefix_size = 0)
{
    constexpr Xapian::termpos LAST_POS = Xapian::termpos(-1);
    do {
	const string& term = termlist->get_termname();
	if (!end.empty() && term >= end) {
	    break;
	}
	PositionList* poslist = termlist->positionlist_begin();
	if (poslist &&
	    (start_pos ? poslist->skip_to(start_pos) : poslist->next()) &&
	    (end_pos == LAST_POS || poslist->get_position() <= end_pos)) {
	    heap.emplace_back(new Pos(term.substr(prefix_size), poslist));
	} else {
	    delete poslist;
	}
    } while (termlist->next() == NULL);
}

string
Database::Internal::reconstruct_text(Xapian::docid did,
				     size_t length,
				     std::string_view prefix,
				     Xapian::termpos start_pos,
				     Xapian::termpos end_pos) const
{
    if (end_pos == 0) {
	// Set to largest possible value.
	end_pos = numeric_limits<decltype(end_pos)>::max();
    }

    if (length == 0) {
	// Set to largest possible value.
	length = numeric_limits<decltype(length)>::max();
    }

    struct PosCmp {
	bool operator()(const unique_ptr<Pos>& a, const unique_ptr<Pos>& b) {
	    if (a->get_pos() != b->get_pos()) {
		return a->get_pos() > b->get_pos();
	    }
	    return a->get_term() > b->get_term();
	}
    };

    vector<unique_ptr<Pos>> heap;

    unique_ptr<TermList> termlist(open_term_list_direct(did));
    if (usual(termlist)) {
	if (prefix.empty()) {
	    if (termlist->next() == NULL) {
		reconstruct_open_poslists(termlist.get(), start_pos, end_pos,
					  "A", heap);
		if (termlist->skip_to("[") == NULL) {
		    reconstruct_open_poslists(termlist.get(),
					      start_pos, end_pos,
					      prefix, heap);
		}
	    }
	} else {
	    if (termlist->skip_to(prefix) == NULL) {
		// Calculate the first possible term without the specified
		// prefix.
		string term_ub{prefix};
		size_t i = term_ub.find_last_not_of('\xff');
		term_ub.resize(i + 1);
		if (i != string::npos) {
		    term_ub[i] = (unsigned char)term_ub[i] + 1;
		}
		reconstruct_open_poslists(termlist.get(), start_pos, end_pos,
					  term_ub, heap, prefix.size());
	    }
	}

	Heap::make(heap.begin(), heap.end(), PosCmp());
    }

    string result;

    Xapian::termpos old_pos = UNSIGNED_OVERFLOW_OK(start_pos - 1);
    while (!heap.empty()) {
	Pos* tip = heap.front().get();
	Xapian::termpos pos = tip->get_pos();
	if (pos > end_pos) break;

	Xapian::termpos delta = UNSIGNED_OVERFLOW_OK(pos - old_pos);
	// Ignore additional terms at the same position.
	if (delta) {
	    if (usual(!result.empty())) {
		// Insert newline for gap in used positions.
		result += (delta == 1 ? ' ' : '\n');
	    }
	    result += tip->get_term();
	}

	if (result.size() >= length) break;

	old_pos = pos;

	if (tip->next()) {
	    Heap::replace(heap.begin(), heap.end(), PosCmp());
	} else {
	    Heap::pop(heap.begin(), heap.end(), PosCmp());
	    heap.resize(heap.size() - 1);
	}
    }

    return result;
}

}
