/*
** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
**
** 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 3, 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, write to the Free Software Foundation,
** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
**
*/

#ifndef MU_FIELDS_HH__
#define MU_FIELDS_HH__

#include <cstdint>
#include <string_view>
#include <algorithm>
#include <array>
#include <xapian.h>
#include <utils/mu-utils.hh>
#include <utils/mu-option.hh>

namespace Mu {

// Xapian does not like terms much longer than this
constexpr auto MaxTermLength = 240;
// http://article.gmane.org/gmane.comp.search.xapian.general/3656 */

struct Field {
	/**
	 * Field Ids.
	 *
	 * Note, the Ids are also used as indices in the Fields array,
	 * so their numerical values must be 0...Count.
	 *
	 */
	enum struct Id {
		Bcc = 0,	/**< Blind Carbon-Copy */
		BodyText,	/**< Text body */
		Cc,		/**< Carbon-Copy */
		Changed,        /**< Last change time (think 'ctime') */
		Date,		/**< Message date */
		EmbeddedText,	/**< Embedded text in message */
		File,		/**< Filename */
		Flags,		/**< Message flags */
		From,		/**< Message sender */
		Maildir,	/**< Maildir path */
		MailingList,	/**< Mailing list */
		MessageId,	/**< Message Id */
		MimeType,	/**< MIME-Type */
		Path,		/**< File-system Path */
		Priority,	/**< Message priority */
		References,	/**< All references (incl. Reply-To:) */
		Size,		/**< Message size (in bytes) */
		Subject,	/**< Message subject */
		Tags,		/**< Message Tags */
		ThreadId,	/**< Thread Id */
		To,		/**< To: recipient */
		/*
		 * <private>
		 */
		XBodyHtml,	/**< HTML Body */

		_count_ /**< Number of FieldIds */
	};

	/**
	 * Get the number of Id values.
	 *
	 * @return the number.
	 */
	static constexpr size_t id_size()
	{
		return static_cast<size_t>(Id::_count_);
	}

	constexpr Xapian::valueno value_no() const {
		return static_cast<Xapian::valueno>(id);
	}

	/**
	 * Field types
	 *
	 */
	enum struct Type {
		String,		/**< String */
		StringList,	/**< List of strings */
		ContactList,	/**< List of contacts */
		ByteSize,	/**< Size in bytes */
		TimeT,		/**< A time_t value */
		Integer,	/**< An integer */
	};

	constexpr bool is_string() const { return type == Type::String; }
	constexpr bool is_string_list() const { return type == Type::StringList; }
	constexpr bool is_byte_size() const { return type == Type::ByteSize; }
	constexpr bool is_time_t() const { return type == Type::TimeT; }
	constexpr bool is_integer() const { return type == Type::Integer; }
	constexpr bool is_numerical() const { return is_byte_size() || is_time_t() || is_integer(); }

	/**
	 * Field flags
	 * note: the differences for our purposes between a xapian field and a
	 * term: - there is only a single value for some item in per document
	 * (msg), ie.  one value containing the list of To: addresses - there
	 * can be multiple terms, each containing e.g. one of the To:
	 * addresses - searching uses terms, but to display some field, it
	 * must be in the value (at least when using MuMsgIter)
	 *
	 * Rules (build-time enforced):
	 * - A field has at most one of Indexable, HasTerms, IsXapianBoolean and IsContact.
	 */

	enum struct Flag {
		/*
		 * Different kind of terms; at most one is true,
		 * and cannot be combined with IsContact. Compile-time enforced.
		 */
		NormalTerm    = 1 << 0,
		/**< Field is a searchable term */
		BooleanTerm   = 1 << 1,
		/**< Field is a boolean search-term (i.e. at most one per message);
		 * wildcards do not work */
		IndexableTerm = 1 << 2,
		/**< Field has indexable text as term */
		/*
		 * Contact flag cannot be combined with any of the term flags.
		 * This is compile-time enforced.
		 */
		Contact = 1 << 10,
		/**< field contains one or more e-mail-addresses */
		Value   = 1 << 11,
		/**< Field value is stored (so the literal value can be retrieved) */

		Range	   = 1 << 21,

		IncludeInSexp = 1 << 24,
		/**< whether to include this field in the cached sexp. */

		/**< whether this is a range field (e.g., date, size)*/
		Internal      = 1 << 26
	};

	constexpr bool any_of(Flag some_flag) const{
		return (static_cast<int>(some_flag) & static_cast<int>(flags)) != 0;
	}

	constexpr bool is_indexable_term()	const { return any_of(Flag::IndexableTerm); }
	constexpr bool is_boolean_term()	const { return any_of(Flag::BooleanTerm); }
	constexpr bool is_normal_term()		const { return any_of(Flag::NormalTerm); }
	constexpr bool is_searchable()          const { return is_indexable_term() ||
							       is_boolean_term() ||
							       is_normal_term(); }

	constexpr bool is_value()		const { return any_of(Flag::Value); }
	constexpr bool is_internal()		const { return any_of(Flag::Internal); }

	constexpr bool is_contact()		const { return any_of(Flag::Contact); }
	constexpr bool is_range()		const { return any_of(Flag::Range); }

	constexpr bool include_in_sexp()        const { return any_of(Flag::IncludeInSexp);}

	/**
	 * Field members
	 *
	 */
	Id               id;            /**< Id of the message field */
	Type             type;          /**< Type of the message field */
	std::string_view name;          /**< Name of the message field */
	std::string_view alias;         /**< Alternative name for the message field */
	std::string_view description;   /**< Decription of the message field */
	std::string_view example_query; /**< Example query */
	char             shortcut;      /**< Shortcut for the message field; a..z */
	Flag             flags;         /**< Flags */

	/**
	 * Convenience / helpers
	 *
	 */

	constexpr char xapian_prefix() const
	{ /* xapian uses uppercase shortcuts; toupper is not constexpr */
		return shortcut == 0 ? 0 : shortcut - ('a' - 'A');
	}

	/**
	 * Get the xapian term; truncated to MaxTermLength and
	 * utf8-flattened.
	 *
	 * @param s
	 *
	 * @return the xapian term
	 */
	std::string xapian_term(const std::string& s="") const;
	std::string xapian_term(std::string_view sv) const {
		return xapian_term(std::string{sv});
	}
	std::string xapian_term(char c) const {
		return xapian_term(std::string(1, c));
	}
};

MU_ENABLE_BITOPS(Field::Flag);

/**
 * Sequence of _all_ message fields
 */
static constexpr std::array<Field, Field::id_size()>
    Fields = {
	{
	    {
		Field::Id::Bcc,
		Field::Type::ContactList,
		"bcc", {},
		"Blind carbon-copy recipient",
		"bcc:foo@example.com",
		'h',
		Field::Flag::Contact |
		Field::Flag::Value |
		Field::Flag::IncludeInSexp |
		Field::Flag::IndexableTerm,

	    },
	    {
		Field::Id::BodyText,
		Field::Type::String,
		"body", {},
		"Message plain-text body",
		"body:capybara",
		'b',
		Field::Flag::IndexableTerm,
	    },
	    {
		Field::Id::Cc,
		Field::Type::ContactList,
		"cc", {},
		"Carbon-copy recipient",
		"cc:quinn@example.com",
		'c',
		Field::Flag::Contact |
		Field::Flag::Value |
		Field::Flag::IncludeInSexp |
		Field::Flag::IndexableTerm,
	    },

	    {
		Field::Id::Changed,
		Field::Type::TimeT,
		"changed", {},
		"Last change time",
		"changed:30M..",
		'k',
		Field::Flag::Value |
		Field::Flag::Range |
		Field::Flag::IncludeInSexp
	    },
	    {
		Field::Id::Date,
		Field::Type::TimeT,
		"date", {},
		"Message date",
		"date:20220101..20220505",
		'd',
		Field::Flag::Value |
		Field::Flag::Range |
		Field::Flag::IncludeInSexp
	    },
	    {
		Field::Id::EmbeddedText,
		Field::Type::String,
		"embed", {},
		"Embedded text",
		"embed:war OR embed:peace",
		'e',
		Field::Flag::IndexableTerm
	    },
	    {
		Field::Id::File,
		Field::Type::String,
		"file", {},
		"Attachment file name",
		"file:/image\\.*.jpg/",
		'j',
		Field::Flag::BooleanTerm
	    },
	    {
		Field::Id::Flags,
		Field::Type::Integer,
		"flags", "flag",
		"Message properties",
		"flag:unread AND flag:personal",
		'g',
		Field::Flag::BooleanTerm |
		Field::Flag::Value |
		Field::Flag::IncludeInSexp
	    },
	    {
		Field::Id::From,
		Field::Type::ContactList,
		"from", {},
		"Message sender",
		"from:jimbo",
		'f',
		Field::Flag::Contact |
		Field::Flag::Value |
		Field::Flag::IncludeInSexp |
		Field::Flag::IndexableTerm,
	    },
	    {
		Field::Id::Maildir,
		Field::Type::String,
		"maildir", {},
		"Maildir path for message",
		"maildir:/private/archive",
		'm',
		Field::Flag::BooleanTerm |
		Field::Flag::Value |
		Field::Flag::IncludeInSexp
	    },
	    {
		Field::Id::MailingList,
		Field::Type::String,
		"list", {},
		"Mailing list (List-Id:)",
		"list:mu-discuss.example.com",
		'v',
		Field::Flag::BooleanTerm |
		Field::Flag::Value |
		Field::Flag::IncludeInSexp
	    },
	    {
		Field::Id::MessageId,
		Field::Type::String,
		"message-id", "msgid",
		"Message-Id",
		"msgid:abc@123",
		'i',
		Field::Flag::BooleanTerm |
		Field::Flag::Value |
		Field::Flag::IncludeInSexp
	    },
	    {
		Field::Id::MimeType,
		Field::Type::String,
		"mime", "mime-type",
		"Attachment MIME-type",
		"mime:image/jpeg",
		'y',
		Field::Flag::BooleanTerm
	    },
	    {
		Field::Id::Path,
		Field::Type::String,
		"path", {},
		"File system path to message",
		"path:/a/b/Maildir/cur/msg:2,S",
		'l',
		Field::Flag::BooleanTerm |
		Field::Flag::Value |
		Field::Flag::IncludeInSexp
	    },
	    {
		Field::Id::Priority,
		Field::Type::Integer,
		"priority", "prio",
		"Priority",
		"prio:high",
		'p',
		Field::Flag::BooleanTerm |
		Field::Flag::Value |
		Field::Flag::IncludeInSexp
	    },
	    {
		Field::Id::References,
		Field::Type::StringList,
		"references", {},
		"References to related messages",
		{},
		'r',
		Field::Flag::Value |
		Field::Flag::IncludeInSexp
	    },
	    {
		Field::Id::Size,
		Field::Type::ByteSize,
		"size", {},
		"Message size in bytes",
		"size:1M..5M",
		'z',
		Field::Flag::Value |
		Field::Flag::Range |
		Field::Flag::IncludeInSexp
	    },
	    {
		Field::Id::Subject,
		Field::Type::String,
		"subject", {},
		"Message subject",
		"subject:wombat",
		's',
		Field::Flag::Value |
		Field::Flag::IndexableTerm |
		Field::Flag::IncludeInSexp
	    },
	    {
		Field::Id::Tags,
		Field::Type::StringList,
		"tags", "tag",
		"Message tags",
		"tag:projectx",
		'x',
		Field::Flag::BooleanTerm |
		Field::Flag::Value |
		Field::Flag::IncludeInSexp
	    },
	    {
		Field::Id::ThreadId,
		Field::Type::String,
		"thread", {},
		"Thread a message belongs to",
		{},
		'w',
		Field::Flag::BooleanTerm |
		Field::Flag::Value
	    },
	    {
		Field::Id::To,
		Field::Type::ContactList,
		"to", {},
		"Message recipient",
		"to:flimflam@example.com",
		't',
		Field::Flag::Contact |
		Field::Flag::Value |
		Field::Flag::IncludeInSexp |
		Field::Flag::IndexableTerm,
	    },

	    /* internal */
	    {
		Field::Id::XBodyHtml,
		Field::Type::String,
		"htmlbody", {},
		"Message html body",
		{},
		{},
		Field::Flag::Internal
	    },
	}};

/*
 * Convenience
 */

/**
 * Get the message field for the given Id.
 *
 * @param id of the message field
 *
 * @return ref of the message field.
 */
constexpr const Field&
field_from_id(Field::Id id)
{
	return Fields.at(static_cast<size_t>(id));
}

/**
 * Invoke func for each message-field
 *
 * @param func some callable
 */
template <typename Func>
constexpr void field_for_each(Func&& func) {
	for (const auto& field: Fields)
		func(field);
}

/**
 * Find a message field that satisfies some predicate
 *
 * @param pred the predicate (a callable)
 *
 * @return a message-field id, or nullopt if not found.
 */
template <typename Pred>
constexpr Option<Field> field_find_if(Pred&& pred) {
	for (auto&& field: Fields)
		if (pred(field))
			return field;
	return Nothing;
}

/**
 * Get the the message-field id for the given name or shortcut
 *
 * @param name_or_shortcut
 *
 * @return the message-field-id or nullopt.
 */
static inline
Option<Field> field_from_shortcut(char shortcut) {
	return field_find_if([&](auto&& field){
		return field.shortcut == shortcut;
	});
}
static inline
Option<Field> field_from_name(const std::string& name) {
	switch(name.length()) {
	case 0:
		return Nothing;
	case 1:
		return field_from_shortcut(name[0]);
	default:
		return field_find_if([&](auto&& field){
			return name == field.name || name == field.alias;
		});
	}
}

/**
 * Get the Field::Id for some number, or nullopt if it does not match
 *
 * @param id an id number
 *
 * @return Field::Id  or nullopt
 */
static inline
Option<Field> field_from_number(size_t id)
{
	if (id >= static_cast<size_t>(Field::Id::_count_))
		return Nothing;
	else
		return field_from_id(static_cast<Field::Id>(id));
}

} // namespace Mu
#endif /* MU_FIELDS_HH__ */
