/** @file
 * @brief Abstract base class for a document
 */
/* Copyright 2017,2018,2019,2023,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/>.
 */

#ifndef XAPIAN_INCLUDED_DOCUMENTINTERNAL_H
#define XAPIAN_INCLUDED_DOCUMENTINTERNAL_H

#include <xapian/document.h>
#include <xapian/intrusive_ptr.h>
#include <xapian/types.h>

#include "api/terminfo.h"
#include "api/termlist.h"
#include "backends/databaseinternal.h"
#include "overflow.h"

#include <functional>
#include <limits>
#include <map>
#include <memory>
#include <string>
#include <string_view>

class DocumentTermList;
class DocumentValueList;
class GlassValueManager;
class HoneyValueManager;
class ValueStreamDocument;

namespace Xapian {

/// Abstract base class for a document.
class Document::Internal : public Xapian::Internal::intrusive_base {
    friend class ::DocumentTermList;
    friend class ::DocumentValueList;
    // For ensure_values_fetched():
    friend class ::GlassValueManager;
    friend class ::HoneyValueManager;
    friend class ::ValueStreamDocument;

    /// Don't allow assignment.
    void operator=(const Internal &) = delete;

    /// Don't allow copying.
    Internal(const Internal &) = delete;

    /** The document data.
     *
     *  If NULL, this hasn't been fetched or set yet.
     */
    std::unique_ptr<std::string> data;

    /** Terms in the document and their associated metadata.
     *
     *  If NULL, the terms haven't been fetched or set yet.
     *
     *  We use std::map<> rather than std::unordered_map<> because the latter
     *  invalidates existing iterators upon insert() if rehashing occurs,
     *  whereas existing iterators remain valid for std::map<>.
     */
    mutable
    std::unique_ptr<std::map<std::string, TermInfo, std::less<>>> terms;

    /** The number of distinct terms in @a terms.
     *
     *  Only valid when terms is non-NULL.
     *
     *  This may be less than terms.size() if any terms have been deleted.
     */
    mutable Xapian::termcount termlist_size;

    /** An index value, unused by Document itself.
     *
     *  This is used by the diversification code.
     *
     *  It is in a bit field with a bool flag so that it doesn't incur any
     *  additional space cost for cases where it isn't used.
     *
     *  The bool flag is stored in the top bit, which is likely to be very
     *  cheap to check (since it's the sign bit for a signed integer value).
     *
     *  We initialise this in the constructors to avoid valgrind warning
     *  that positions_modified_ is used uninitialised.  Valgrind is meant
     *  to track undefined-ness at the bit level, so this shouldn't be
     *  needed.  FIXME: Investigate!
     */
    Xapian::doccount index : 31;

    /** Are there any changes to term positions in @a terms?
     *
     *  If a document is read from a database, modified and then replaced at
     *  the same docid, then we can save a lot of work if we know when there
     *  are no changes to term positions, even if there are changes to terms
     *  (a common example is adding filter terms to an existing document).
     *
     *  It's OK for this to be true when there aren't any modifications (it
     *  just means that the backend can't shortcut as directly).
     */
    mutable bool positions_modified_ : 1;

    /** Ensure terms have been fetched from @a database.
     *
     *  After this call, @a terms will be non-NULL.  If @a database is NULL,
     *  @a terms will be initialised to an empty map if it was NULL.
     */
    void ensure_terms_fetched() const;

    /** Ensure values have been fetched from @a database.
     *
     *  After this call, @a values will be non-NULL.  If @a database is NULL,
     *  @a values will be initialised to an empty map if it was NULL.
     */
    void ensure_values_fetched() const;

  protected:
    /** Document value slots and their contents.
     *
     *  If NULL, the values haven't been fetched or set yet.
     *
     *  We use std::map<> rather than std::unordered_map<> because the latter
     *  invalidates existing iterators upon insert() if rehashing occurs,
     *  whereas existing iterators remain valid for std::map<>.
     */
    mutable std::unique_ptr<std::map<Xapian::valueno, std::string>> values;

    /** Database this document came from.
     *
     *  If this document didn't come from a database, this will be NULL.
     */
    Xapian::Internal::intrusive_ptr<const Xapian::Database::Internal> database;

    /** The document ID this document came from in @a database.
     *
     *  If this document didn't come from a database, this will be 0.
     *
     *  Note that this is the docid in the sub-database when multiple databases
     *  are being searched.
     */
    Xapian::docid did;

    /// Constructor used by subclasses.
    Internal(Xapian::Internal::intrusive_ptr<const Xapian::Database::Internal> database_,
	     Xapian::docid did_)
	: index(), positions_modified_(false), database(database_), did(did_) {}

    /// Constructor used by RemoteDocument subclass.
    Internal(const Xapian::Database::Internal* database_,
	     Xapian::docid did_,
	     std::string&& data_,
	     std::map<Xapian::valueno, std::string>&& values_)
	: data(new std::string(std::move(data_))),
	  index(), positions_modified_(false),
	  values(new std::map<Xapian::valueno, std::string>(std::move(values_))),
	  database(database_),
	  did(did_) {}

    /** Fetch the document data from the database.
     *
     *  The default implementation (used when there's no associated database)
     *  returns an empty string.
     */
    virtual std::string fetch_data() const;

    /** Fetch all set values from the database.
     *
     *  The default implementation (used when there's no associated database)
     *  clears @a values_.
     */
    virtual void fetch_all_values(std::map<Xapian::valueno,
				  std::string>& values_) const;

    /** Fetch a single value from the database.
     *
     *  The default implementation (used when there's no associated database)
     *  returns an empty string.
     */
    virtual std::string fetch_value(Xapian::valueno slot) const;

  public:
    /// Construct an empty document.
    Internal() : index(), positions_modified_(false), did(0) {}

    /** We have virtual methods and want to be able to delete derived classes
     *  using a pointer to the base class, so we need a virtual destructor.
     */
    virtual ~Internal();

    /** Return true if the document data might have been modified.
     *
     *  If the document is from a database, this means modifications
     *  compared to the version read, otherwise it means modifications
     *  compared to an empty database.
     */
    bool data_modified() const { return data != NULL; }

    /** Return true if the document's terms might have been modified.
     *
     *  If the document is from a database, this means modifications
     *  compared to the version read, otherwise it means modifications
     *  compared to an empty database.
     */
    bool terms_modified() const { return terms != NULL; }

    /** Return true if the document's values might have been modified.
     *
     *  If the document is from a database, this means modifications
     *  compared to the version read, otherwise it means modifications
     *  compared to an empty database.
     */
    bool values_modified() const { return values != NULL; }

    /** Return true if the document might have been modified in any way.
     *
     *  If the document is from a database, this means modifications
     *  compared to the version read, otherwise it means modifications
     *  compared to an empty database.
     */
    bool modified() const {
	return data_modified() || terms_modified() || values_modified();
    }

    /** Return true if the document's term positions might have been modified.
     *
     *  If the document is from a database, this means modifications
     *  compared to the version read, otherwise it means modifications
     *  compared to an empty database.
     */
    bool positions_modified() const { return positions_modified_; }

    /** Get the document ID this document came from.
     *
     *  If this document didn't come from a database, this will be 0.
     *
     *  Note that this is the docid in the sub-database when multiple databases
     *  are being searched.
     */
    Xapian::docid get_docid() const { return did; }

    /// Internal method used by MSet::diversify().
    Xapian::doccount get_index() const { return index; }

    /// Internal method used by MSet::diversify().
    void set_index(Xapian::doccount new_index) { index = new_index; }

    /// Get the document data.
    std::string get_data() const {
	if (data)
	    return *data;
	return fetch_data();
    }

    /// Set the document data.
    void set_data(std::string_view data_) {
	data.reset(new std::string(data_));
    }

    /// Add a term to this document.
    void add_term(std::string_view term, Xapian::termcount wdf_inc) {
	ensure_terms_fetched();

	auto i = terms->find(term);
	if (i == terms->end()) {
	    ++termlist_size;
	    terms->emplace(term, TermInfo(wdf_inc));
	} else {
	    if (i->second.increase_wdf(wdf_inc))
		++termlist_size;
	}
    }

    /// Remove a term from this document.
    bool remove_term(std::string_view term) {
	ensure_terms_fetched();

	auto i = terms->find(term);
	if (i == terms->end()) {
	    return false;
	}
	if (i->second.has_positions()) {
	    positions_modified_ = true;
	}
	if (!i->second.remove()) {
	    return false;
	}
	--termlist_size;
	return true;
    }

    /// Add a posting for a term.
    void add_posting(std::string_view term,
		     Xapian::termpos term_pos,
		     Xapian::termcount wdf_inc) {
	ensure_terms_fetched();
	positions_modified_ = true;

	auto i = terms->find(term);
	if (i == terms->end()) {
	    ++termlist_size;
	    terms->emplace(term, TermInfo(wdf_inc, term_pos));
	    return;
	}
	if (i->second.add_position(wdf_inc, term_pos))
	    ++termlist_size;
    }

    enum remove_posting_result { OK, NO_TERM, NO_POS };

    /// Remove a posting for a term.
    remove_posting_result
    remove_posting(std::string_view term,
		   Xapian::termpos term_pos,
		   Xapian::termcount wdf_dec) {
	ensure_terms_fetched();

	auto i = terms->find(term);
	if (i == terms->end() || i->second.is_deleted()) {
	    return remove_posting_result::NO_TERM;
	}
	if (!i->second.remove_position(term_pos)) {
	    return remove_posting_result::NO_POS;
	}
	if (i->second.decrease_wdf(wdf_dec))
	    --termlist_size;
	positions_modified_ = true;
	return remove_posting_result::OK;
    }

    /** Remove a range of postings for a term.
     *
     *  Can only return OK or NO_TERM.
     */
    remove_posting_result
    remove_postings(std::string_view term,
		    Xapian::termpos term_pos_first,
		    Xapian::termpos term_pos_last,
		    Xapian::termcount wdf_dec,
		    Xapian::termpos& n_removed) {
	ensure_terms_fetched();

	auto i = terms->find(term);
	if (i == terms->end() || i->second.is_deleted()) {
	    return remove_posting_result::NO_TERM;
	}
	n_removed = i->second.remove_positions(term_pos_first,
					       term_pos_last);
	if (n_removed) {
	    positions_modified_ = true;
	    Xapian::termcount wdf_delta;
	    if (mul_overflows(n_removed, wdf_dec, wdf_delta)) {
		// Decreasing by the maximum value will zero the wdf.
		wdf_delta = std::numeric_limits<Xapian::termcount>::max();
	    }
	    if (i->second.decrease_wdf(wdf_delta))
		--termlist_size;
	}
	return remove_posting_result::OK;
    }

    /// Clear all terms from the document.
    void clear_terms() {
	if (!terms) {
	    if (!database) {
		// We didn't come from a database, so there are no unfetched
		// terms to clear.
		return;
	    }
	    terms.reset(new std::map<std::string, TermInfo, std::less<>>());
	} else {
	    terms->clear();
	}
	termlist_size = 0;
	// Assume there was positional data if there's any in the database.
	positions_modified_ = database && database->has_positions();
    }

    /// Return the number of distinct terms in this document.
    Xapian::termcount termlist_count() const {
	if (terms)
	    return termlist_size;

	if (!database)
	    return 0;

	std::unique_ptr<TermList> tl(database->open_term_list(did));
	// get_approx_size() is exact for TermList from a database.
	return tl->get_approx_size();
    }

    /** Start iterating the terms in this document.
     *
     *  @return A new TermList object (caller takes ownership) or NULL if
     *		there are no terms.
     */
    TermList* open_term_list() const;

    /** Read a value slot in this document.
     *
     *  @return The value in slot @a slot, or an empty string if not set.
     */
    std::string get_value(Xapian::valueno slot) const {
	if (values) {
	    auto i = values->find(slot);
	    if (i != values->end())
		return i->second;
	    return std::string();
	}

	return fetch_value(slot);
    }

    /// Add a value to a slot in this document.
    void add_value(Xapian::valueno slot, std::string_view value) {
	ensure_values_fetched();

	if (!value.empty()) {
	    (*values)[slot] = value;
	} else {
	    // Empty values aren't stored, but replace any existing value by
	    // removing it.
	    values->erase(slot);
	}
    }

    /// Clear all value slots in this document.
    void clear_values() {
	if (!values) {
	    if (database) {
		values.reset(new std::map<Xapian::valueno, std::string>());
	    } else {
		// We didn't come from a database, so there are no unfetched
		// values to clear.
	    }
	} else {
	    values->clear();
	}
    }

    /// Count the value slots used in this document.
    Xapian::valueno values_count() const {
	ensure_values_fetched();
	return values->size();
    }

    Xapian::ValueIterator values_begin() const;

    /// Return a string describing this object.
    std::string get_description() const;
};

}

#endif // XAPIAN_INCLUDED_DOCUMENTINTERNAL_H
