/** @file
 * @brief Honey backend database class
 */
/* Copyright 2015,2017,2018,2022,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 "honey_database.h"

#include "honey_alltermslist.h"
#include "honey_document.h"
#include "honey_metadata.h"
#include "honey_termlist.h"
#include "honey_spellingwordslist.h"
#include "honey_valuelist.h"

#include "backends/backends.h"
#include "backends/contiguousalldocspostlist.h"
#include "backends/leafpostlist.h"
#include "xapian/error.h"

#include <string_view>

using namespace std;

void
HoneyDatabase::throw_termlist_table_close_exception() const
{
    // Either the database has been closed, or else there's no termlist table.
    // Check if the postlist table is open to determine which is the case.
    if (!postlist_table.is_open())
	HoneyTable::throw_database_closed();
    throw Xapian::FeatureUnavailableError("Database has no termlist");
}

// Relied on below - opening to read should allow the termlist to be missing.
static_assert(Xapian::DB_READONLY_ & Xapian::DB_NO_TERMLIST,
	"Xapian::DB_READONLY_ should imply Xapian::DB_NO_TERMLIST");

HoneyDatabase::HoneyDatabase(std::string_view path_, int flags)
    : Xapian::Database::Internal(TRANSACTION_READONLY),
      path(path_),
      version_file(path_),
      docdata_table(path, true),
      postlist_table(path, true),
      position_table(path, true),
      spelling_table(path, true),
      synonym_table(path, true),
      // Note: (Xapian::DB_READONLY_ & Xapian::DB_NO_TERMLIST) is true, so
      // opening to read we always allow the termlist to be missing.
      termlist_table(path, true, (flags & Xapian::DB_NO_TERMLIST)),
      value_manager(postlist_table, termlist_table)
{
    version_file.read();
    auto rev = version_file.get_revision();
    docdata_table.open(flags, version_file.get_root(Honey::DOCDATA), rev);
    postlist_table.open(flags, version_file.get_root(Honey::POSTLIST), rev);
    position_table.open(flags, version_file.get_root(Honey::POSITION), rev);
    spelling_table.open(flags, version_file.get_root(Honey::SPELLING), rev);
    synonym_table.open(flags, version_file.get_root(Honey::SYNONYM), rev);
    termlist_table.open(flags, version_file.get_root(Honey::TERMLIST), rev);
}

HoneyDatabase::HoneyDatabase(int fd, int flags)
    : Xapian::Database::Internal(TRANSACTION_READONLY),
      version_file(fd),
      docdata_table(fd, version_file.get_offset(), true),
      postlist_table(fd, version_file.get_offset(), true),
      position_table(fd, version_file.get_offset(), true),
      spelling_table(fd, version_file.get_offset(), true),
      synonym_table(fd, version_file.get_offset(), true),
      // Note: (Xapian::DB_READONLY_ & Xapian::DB_NO_TERMLIST) is true, so
      // opening to read we always allow the termlist to be missing.
      termlist_table(fd, version_file.get_offset(), true,
		     (flags & Xapian::DB_NO_TERMLIST)),
      value_manager(postlist_table, termlist_table)
{
    version_file.read();
    auto rev = version_file.get_revision();
    docdata_table.open(flags, version_file.get_root(Honey::DOCDATA), rev);
    postlist_table.open(flags, version_file.get_root(Honey::POSTLIST), rev);
    position_table.open(flags, version_file.get_root(Honey::POSITION), rev);
    spelling_table.open(flags, version_file.get_root(Honey::SPELLING), rev);
    synonym_table.open(flags, version_file.get_root(Honey::SYNONYM), rev);
    termlist_table.open(flags, version_file.get_root(Honey::TERMLIST), rev);
}

HoneyDatabase::~HoneyDatabase()
{
    delete doclen_cursor;
}

void
HoneyDatabase::readahead_for_query(const Xapian::Query& query) const
{
    (void)query;
    // FIXME: Implement - pre-read the start of the postlist table?
}

Xapian::doccount
HoneyDatabase::get_doccount() const
{
    return version_file.get_doccount();
}

Xapian::docid
HoneyDatabase::get_lastdocid() const
{
    return version_file.get_last_docid();
}

Xapian::totallength
HoneyDatabase::get_total_length() const
{
    return version_file.get_total_doclen();
}

Xapian::termcount
HoneyDatabase::get_doclength(Xapian::docid did) const
{
    Assert(did != 0);
    if (usual(did <= version_file.get_last_docid())) {
	if (doclen_cursor == NULL) {
	    doclen_cursor = get_postlist_cursor();
	} else {
	    if (doclen_chunk_reader.find_doclength(did)) {
		return doclen_chunk_reader.get_doclength();
	    }
	}

	// If exact is true, the desired docid is the last in this chunk.
	bool exact =
	    doclen_cursor->find_entry_ge(Honey::make_doclenchunk_key(did));
	if (doclen_chunk_reader.update(doclen_cursor)) {
	    if (exact)
		return doclen_chunk_reader.back();
	    if (doclen_chunk_reader.find_doclength(did)) {
		return doclen_chunk_reader.get_doclength();
	    }
	}
    }

    string message = "Document ID not in use: ";
    message += str(did);
    throw Xapian::DocNotFoundError(message);
}

Xapian::termcount
HoneyDatabase::get_unique_terms(Xapian::docid did) const
{
    Assert(did != 0);
    return HoneyTermList(this, did).get_unique_terms();
}

Xapian::termcount
HoneyDatabase::get_wdfdocmax(Xapian::docid did) const
{
    Assert(did != 0);
    HoneyTermList termlist(this, did);
    Xapian::termcount max_wdf = 0;
    while (termlist.next() == NULL) {
	Xapian::termcount current_wdf = termlist.get_wdf();
	if (current_wdf > max_wdf) max_wdf = current_wdf;
    }
    return max_wdf;
}

void
HoneyDatabase::get_freqs(string_view term,
			 Xapian::doccount* termfreq_ptr,
			 Xapian::termcount* collfreq_ptr) const
{
    postlist_table.get_freqs(term, termfreq_ptr, collfreq_ptr);
}

Xapian::doccount
HoneyDatabase::get_value_freq(Xapian::valueno slot) const
{
    return value_manager.get_value_freq(slot);
}

string
HoneyDatabase::get_value_lower_bound(Xapian::valueno slot) const
{
    return value_manager.get_value_lower_bound(slot);
}

string
HoneyDatabase::get_value_upper_bound(Xapian::valueno slot) const
{
    return value_manager.get_value_upper_bound(slot);
}

Xapian::termcount
HoneyDatabase::get_doclength_lower_bound() const
{
    return version_file.get_doclength_lower_bound();
}

Xapian::termcount
HoneyDatabase::get_doclength_upper_bound() const
{
    return version_file.get_doclength_upper_bound();
}

Xapian::termcount
HoneyDatabase::get_wdf_upper_bound(string_view term) const
{
    Xapian::termcount wdf_bound = version_file.get_wdf_upper_bound();
    // It's unlikely wdf is always 0, but when it is there's no need to do any
    // further work.
    if (usual(wdf_bound != 0)) {
	// We don't store per-term wdf upper bounds currently, but
	// HoneyPostListTable can provide an upper bound based on termfreq,
	// coll_freq, and the first wdf value, which more often than not is
	// actually the exact bound (in 77% of cases in an example database of
	// wikipedia data).
	wdf_bound = min(wdf_bound, postlist_table.get_wdf_upper_bound(term));
    }
    return wdf_bound;
}

Xapian::termcount
HoneyDatabase::get_unique_terms_lower_bound() const
{
    return version_file.get_unique_terms_lower_bound();
}

Xapian::termcount
HoneyDatabase::get_unique_terms_upper_bound() const
{
    return version_file.get_unique_terms_upper_bound();
}

bool
HoneyDatabase::term_exists(string_view term) const
{
    if (term.empty())
	return HoneyDatabase::get_doccount() != 0;
    return postlist_table.term_exists(term);
}

bool
HoneyDatabase::has_positions() const
{
    return !position_table.empty();
}

PostList*
HoneyDatabase::open_post_list(string_view term) const
{
    return HoneyDatabase::open_leaf_post_list(term, false);
}

LeafPostList*
HoneyDatabase::open_leaf_post_list(string_view term, bool need_read_pos) const
{
    if (term.empty()) {
	Assert(!need_read_pos);
	Xapian::doccount doccount = get_doccount();
	if (rare(doccount == 0)) {
	    return nullptr;
	}
	if (doccount == get_lastdocid()) {
	    // The used docid range is exactly 1 to doccount inclusive.
	    return new ContiguousAllDocsPostList(doccount);
	}
	return new HoneyAllDocsPostList(this, doccount);
    }

    return postlist_table.open_post_list(this, term, need_read_pos);
}

ValueList*
HoneyDatabase::open_value_list(Xapian::valueno slot) const
{
    return new HoneyValueList(slot, this);
}

TermList*
HoneyDatabase::open_term_list(Xapian::docid did) const
{
    Assert(did != 0);
    if (!termlist_table.is_open())
	throw_termlist_table_close_exception();
    HoneyTermList* tl = new HoneyTermList(this, did);
    if (tl->size() == 0) {
	// It could be the document has no terms, but maybe it doesn't exist -
	// in the latter case we ought to throw DocNotFoundError.  FIXME: If
	// the document has no terms, but does have values, we should be able
	// to avoid this check.

	// Put the pointer in a unique_ptr so it gets released if an exception
	// is thrown.
	unique_ptr<TermList> tl_ptr(tl);

	// This will throw DocNotFoundError if did isn't in use.
	(void)HoneyDatabase::get_doclength(did);
	tl_ptr.release();
    }
    return tl;
}

TermList*
HoneyDatabase::open_term_list_direct(Xapian::docid did) const
{
    // Same as open_term_list() except for MultiDatabase.
    return HoneyDatabase::open_term_list(did);
}

TermList*
HoneyDatabase::open_allterms(string_view prefix) const
{
    return new HoneyAllTermsList(this, prefix);
}

PositionList*
HoneyDatabase::open_position_list(Xapian::docid did, string_view term) const
{
    return position_table.open_position_list(did, term);
}

Xapian::Document::Internal*
HoneyDatabase::open_document(Xapian::docid did, bool lazy) const
{
    Assert(did != 0);
    if (!lazy) {
	// This will throw DocNotFoundError if did isn't in use.
	(void)HoneyDatabase::get_doclength(did);
    }
    return new HoneyDocument(this, did, &value_manager, &docdata_table);
}

TermList*
HoneyDatabase::open_spelling_termlist(string_view word) const
{
    return spelling_table.open_termlist(word);
}

TermList*
HoneyDatabase::open_spelling_wordlist() const
{
    auto cursor = spelling_table.cursor_get();
    if (rare(cursor == NULL)) {
	// No spelling table.
	return NULL;
    }
    return new HoneySpellingWordsList(this, cursor);
}

Xapian::doccount
HoneyDatabase::get_spelling_frequency(string_view word) const
{
    return spelling_table.get_word_frequency(word);
}

void
HoneyDatabase::add_spelling(string_view word, Xapian::termcount freqinc) const
{
    (void)word;
    (void)freqinc;
    throw Xapian::UnimplementedError("Honey backend doesn't support update");
}

Xapian::termcount
HoneyDatabase::remove_spelling(string_view word,
			       Xapian::termcount freqdec) const
{
    (void)word;
    (void)freqdec;
    throw Xapian::UnimplementedError("Honey backend doesn't support update");
}

TermList*
HoneyDatabase::open_synonym_termlist(string_view term) const
{
    return synonym_table.open_termlist(term);
}

TermList*
HoneyDatabase::open_synonym_keylist(string_view prefix) const
{
    auto cursor = synonym_table.cursor_get();
    if (rare(cursor == NULL)) {
	// No synonym table.
	return NULL;
    }
    return new HoneySynonymTermList(this, cursor, prefix);
}

void
HoneyDatabase::add_synonym(string_view term, string_view synonym) const
{
    (void)term;
    (void)synonym;
    throw Xapian::UnimplementedError("Honey backend doesn't support update");
}

void
HoneyDatabase::remove_synonym(string_view term, string_view synonym) const
{
    (void)term;
    (void)synonym;
    throw Xapian::UnimplementedError("Honey backend doesn't support update");
}

void
HoneyDatabase::clear_synonyms(string_view term) const
{
    (void)term;
    throw Xapian::UnimplementedError("Honey backend doesn't support update");
}

string
HoneyDatabase::get_metadata(string_view key) const
{
    return postlist_table.get_metadata(key);
}

TermList*
HoneyDatabase::open_metadata_keylist(string_view prefix) const
{
    auto cursor = postlist_table.cursor_get();
    Assert(cursor != NULL);
    return new HoneyMetadataTermList(this, cursor, prefix);
}

void
HoneyDatabase::set_metadata(string_view key, string_view value)
{
    (void)key;
    (void)value;
    throw Xapian::UnimplementedError("Honey backend doesn't support update");
}

bool
HoneyDatabase::reopen()
{
    if (!postlist_table.is_open())
	HoneyTable::throw_database_closed();
    return false;
}

void
HoneyDatabase::close()
{
    docdata_table.close(true);
    postlist_table.close(true);
    position_table.close(true);
    spelling_table.close(true);
    synonym_table.close(true);
    termlist_table.close(true);
}

void
HoneyDatabase::request_document(Xapian::docid did) const
{
    Assert(did != 0);
    (void)did; // FIXME
}

Xapian::rev
HoneyDatabase::get_revision() const
{
    return version_file.get_revision();
}

string
HoneyDatabase::get_uuid() const
{
    return version_file.get_uuid_string();
}

int
HoneyDatabase::get_backend_info(string* path_ptr) const
{
    if (path_ptr)
	*path_ptr = path;
    return BACKEND_HONEY;
}

void
HoneyDatabase::get_used_docid_range(Xapian::docid& first,
				    Xapian::docid& last) const
{
    auto doccount = version_file.get_doccount();
    if (doccount == 0) {
	// Empty database.
	first = last = 0;
	return;
    }
    auto last_docid = version_file.get_last_docid();
    if (last_docid == doccount) {
	// Contiguous range starting at 1.
	first = 1;
	last = last_docid;
	return;
    }
    postlist_table.get_used_docid_range(doccount, first, last);
}

string
HoneyDatabase::get_description() const
{
    string desc = "Honey(";
    desc += path;
    desc += ')';
    return desc;
}
