/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */

#include "lib.h"
#include "ioloop.h"
#include "str.h"
#include "utc-offset.h"
#include "imap-date.h"
#include "imap-util.h"
#include "imap-quote.h"
#include "mail-search.h"
#include "mail-search-mime.h"

/*
 *
 */

static struct mail_search_mime_arg *
mail_search_mime_arg_dup_one(pool_t pool,
			     const struct mail_search_mime_arg *arg)
{
	struct mail_search_mime_arg *new_arg;

	new_arg = p_new(pool, struct mail_search_mime_arg, 1);
	new_arg->type = arg->type;
	new_arg->match_not = arg->match_not;
	new_arg->match_always = arg->match_always;
	new_arg->nonmatch_always = arg->nonmatch_always;

	switch (arg->type) {
	case SEARCH_MIME_OR:
	case SEARCH_MIME_SUB:
		new_arg->value.subargs =
			mail_search_mime_arg_dup(pool, arg->value.subargs);
		break;
	case SEARCH_MIME_SIZE_EQUAL:
	case SEARCH_MIME_SIZE_LARGER:
	case SEARCH_MIME_SIZE_SMALLER:
		new_arg->value.size = arg->value.size;
		break;
	case SEARCH_MIME_HEADER:
		new_arg->field_name = p_strdup(pool, arg->field_name);
		/* fall through */
	case SEARCH_MIME_DESCRIPTION:
	case SEARCH_MIME_DISPOSITION_TYPE:
	case SEARCH_MIME_DISPOSITION_PARAM:
	case SEARCH_MIME_ENCODING:
	case SEARCH_MIME_ID:
	case SEARCH_MIME_LANGUAGE:
	case SEARCH_MIME_LOCATION:
	case SEARCH_MIME_MD5:
	case SEARCH_MIME_TYPE:
	case SEARCH_MIME_SUBTYPE:
	case SEARCH_MIME_PARAM:
	case SEARCH_MIME_BODY:
	case SEARCH_MIME_TEXT:
	case SEARCH_MIME_CC:
	case SEARCH_MIME_BCC:
	case SEARCH_MIME_FROM:
	case SEARCH_MIME_IN_REPLY_TO:
	case SEARCH_MIME_MESSAGE_ID:
	case SEARCH_MIME_REPLY_TO:
	case SEARCH_MIME_SENDER:
	case SEARCH_MIME_SUBJECT:
	case SEARCH_MIME_TO:
	case SEARCH_MIME_FILENAME_IS:
	case SEARCH_MIME_FILENAME_CONTAINS:
	case SEARCH_MIME_FILENAME_BEGINS:
	case SEARCH_MIME_FILENAME_ENDS:
		new_arg->value.str = p_strdup(pool, arg->value.str);
		break;
	case SEARCH_MIME_SENTBEFORE:
	case SEARCH_MIME_SENTON:
	case SEARCH_MIME_SENTSINCE:
		new_arg->value.time = arg->value.time;
		break;
	case SEARCH_MIME_PARENT:
	case SEARCH_MIME_CHILD:
		if (new_arg->value.subargs != NULL) {
			new_arg->value.subargs =
				mail_search_mime_arg_dup(pool, arg->value.subargs);
		}
		break;
	case SEARCH_MIME_DEPTH_EQUAL:
	case SEARCH_MIME_DEPTH_MIN:
	case SEARCH_MIME_DEPTH_MAX:
	case SEARCH_MIME_INDEX:
		new_arg->value.number = arg->value.number;
		break;
	}
	return new_arg;
}

struct mail_search_mime_arg *
mail_search_mime_arg_dup(pool_t pool, const struct mail_search_mime_arg *arg)
{
	struct mail_search_mime_arg *new_arg = NULL, **dest = &new_arg;

	for (; arg != NULL; arg = arg->next) {
		*dest = mail_search_mime_arg_dup_one(pool, arg);
		dest = &(*dest)->next;
	}
	return new_arg;
}

struct mail_search_mime_part *
mail_search_mime_part_dup(pool_t pool,
			  const struct mail_search_mime_part *mpart)
{
	struct mail_search_mime_part *new_mpart;

	new_mpart = p_new(pool, struct mail_search_mime_part, 1);
	new_mpart->simplified = mpart->simplified;
	new_mpart->args = mail_search_mime_arg_dup(pool, mpart->args);
	return new_mpart;
}

/*
 *
 */

void mail_search_mime_args_reset(struct mail_search_mime_arg *args,
				 bool full_reset)
{
	while (args != NULL) {
		if (args->type == SEARCH_MIME_OR ||
		    args->type == SEARCH_MIME_SUB)
			mail_search_mime_args_reset(args->value.subargs, full_reset);

		if (args->match_always) {
			if (!full_reset)
				args->result = 1;
			else {
				args->match_always = FALSE;
				args->result = -1;
			}
		} else if (args->nonmatch_always) {
			if (!full_reset)
				args->result = 0;
			else {
				args->nonmatch_always = FALSE;
				args->result = -1;
			}
		} else {
			args->result = -1;
		}

		args = args->next;
	}
}

static void
search_mime_arg_foreach(struct mail_search_mime_arg *arg,
			mail_search_mime_foreach_callback_t *callback,
			void *context)
{
	struct mail_search_mime_arg *subarg;

	if (arg->result != -1)
		return;

	if (arg->type == SEARCH_MIME_SUB) {
		/* sublist of conditions */
		i_assert(arg->value.subargs != NULL);

		arg->result = 1;
		subarg = arg->value.subargs;
		while (subarg != NULL) {
			if (subarg->result == -1)
				search_mime_arg_foreach(subarg, callback, context);

			if (subarg->result == -1)
				arg->result = -1;
			else if (subarg->result == 0) {
				/* didn't match */
				arg->result = 0;
				break;
			}

			subarg = subarg->next;
		}
		if (arg->match_not && arg->result != -1)
			arg->result = arg->result > 0 ? 0 : 1;
	} else if (arg->type == SEARCH_MIME_OR) {
		/* OR-list of conditions */
		i_assert(arg->value.subargs != NULL);

		subarg = arg->value.subargs;
		arg->result = 0;
		while (subarg != NULL) {
			if (subarg->result == -1)
				search_mime_arg_foreach(subarg, callback, context);

			if (subarg->result == -1)
				arg->result = -1;
			else if (subarg->result > 0) {
				/* matched */
				arg->result = 1;
				break;
			}

			subarg = subarg->next;
		}
		if (arg->match_not && arg->result != -1)
			arg->result = arg->result > 0 ? 0 : 1;
	} else {
		/* just a single condition */
		callback(arg, context);
	}
}

#undef mail_search_mime_args_foreach
int mail_search_mime_args_foreach(struct mail_search_mime_arg *args,
				  mail_search_mime_foreach_callback_t *callback,
				  void *context)
{
	int result;

	result = 1;
	for (; args != NULL; args = args->next) {
		search_mime_arg_foreach(args, callback, context);

		if (args->result == 0) {
			/* didn't match */
			return 0;
		}

		if (args->result == -1)
			result = -1;
	}

	return result;
}

/*
 *
 */

bool mail_search_mime_arg_one_equals(const struct mail_search_mime_arg *arg1,
				     const struct mail_search_mime_arg *arg2)
{
	if (arg1->type != arg2->type || arg1->match_not != arg2->match_not)
		return FALSE;

	switch (arg1->type) {
	case SEARCH_MIME_OR:
	case SEARCH_MIME_SUB:
		return mail_search_mime_arg_equals(arg1->value.subargs,
						   arg2->value.subargs);

	case SEARCH_MIME_SIZE_EQUAL:
	case SEARCH_MIME_SIZE_LARGER:
	case SEARCH_MIME_SIZE_SMALLER:
		return arg1->value.size == arg2->value.size;

	case SEARCH_MIME_HEADER:
	case SEARCH_MIME_DISPOSITION_PARAM:
	case SEARCH_MIME_PARAM:
		if (strcasecmp(arg1->field_name, arg2->field_name) != 0)
			return FALSE;
		/* fall through */
	case SEARCH_MIME_DESCRIPTION:
	case SEARCH_MIME_DISPOSITION_TYPE:
	case SEARCH_MIME_ENCODING:
	case SEARCH_MIME_ID:
	case SEARCH_MIME_LANGUAGE:
	case SEARCH_MIME_LOCATION:
	case SEARCH_MIME_MD5:
	case SEARCH_MIME_TYPE:
	case SEARCH_MIME_SUBTYPE:
	case SEARCH_MIME_BODY:
	case SEARCH_MIME_TEXT:
	case SEARCH_MIME_CC:
	case SEARCH_MIME_BCC:
	case SEARCH_MIME_FROM:
	case SEARCH_MIME_IN_REPLY_TO:
	case SEARCH_MIME_MESSAGE_ID:
	case SEARCH_MIME_REPLY_TO:
	case SEARCH_MIME_SENDER:
	case SEARCH_MIME_SUBJECT:
	case SEARCH_MIME_TO:
	case SEARCH_MIME_FILENAME_IS:
	case SEARCH_MIME_FILENAME_CONTAINS:
	case SEARCH_MIME_FILENAME_BEGINS:
	case SEARCH_MIME_FILENAME_ENDS:
		/* don't bother doing case-insensitive comparison. we should support
		   full i18n case-insensitivity (or the active comparator
		   in future). */
		return strcmp(arg1->value.str, arg2->value.str) == 0;

	case SEARCH_MIME_SENTBEFORE:
	case SEARCH_MIME_SENTON:
	case SEARCH_MIME_SENTSINCE:
		return arg1->value.time == arg2->value.time;

	case SEARCH_MIME_PARENT:
	case SEARCH_MIME_CHILD:
		if (arg1->value.subargs == NULL)
			return arg2->value.subargs == NULL;
		if (arg2->value.subargs == NULL)
			return FALSE;
		return mail_search_mime_arg_equals(arg1->value.subargs,
						   arg2->value.subargs);

	case SEARCH_MIME_DEPTH_EQUAL:
	case SEARCH_MIME_DEPTH_MIN:
	case SEARCH_MIME_DEPTH_MAX:
	case SEARCH_MIME_INDEX:
		return arg1->value.number == arg2->value.number;
	}
	i_unreached();
}

bool mail_search_mime_arg_equals(const struct mail_search_mime_arg *arg1,
				 const struct mail_search_mime_arg *arg2)
{
	while (arg1 != NULL && arg2 != NULL) {
		if (!mail_search_mime_arg_one_equals(arg1, arg2))
			return FALSE;
		arg1 = arg1->next;
		arg2 = arg2->next;
	}
	return arg1 == NULL && arg2 == NULL;
}

bool mail_search_mime_parts_equal(const struct mail_search_mime_part *mpart1,
				  const struct mail_search_mime_part *mpart2)
{
	i_assert(mpart1->simplified == mpart2->simplified);

	return mail_search_mime_arg_equals(mpart1->args, mpart2->args);
}

/*
 *
 */

void mail_search_mime_simplify(struct mail_search_mime_part *mpart)
{
	mpart->simplified = TRUE;

	// FIXME: implement and use
}

/*
 *
 */

static bool
mail_search_mime_subargs_to_imap(string_t *dest,
				 const struct mail_search_mime_arg *args,
				 const char *prefix,
				 enum imap_quote_flags qflags,
				 const char **error_r)
{
	bool utf8 = HAS_ALL_BITS(qflags, IMAP_QUOTE_FLAG_UTF8);
	const struct mail_search_mime_arg *arg;

	if (prefix[0] == '\0')
		str_append_c(dest, '(');
	for (arg = args; arg != NULL; arg = arg->next) {
		if (arg->next != NULL)
			str_append(dest, prefix);
		if (!mail_search_mime_arg_to_imap(dest, arg, utf8, error_r))
			return FALSE;
		if (arg->next != NULL)
			str_append_c(dest, ' ');
	}
	if (prefix[0] == '\0')
		str_append_c(dest, ')');
	return TRUE;
}

static bool
mail_search_mime_arg_to_imap_date(string_t *dest,
				  const struct mail_search_mime_arg *arg)
{
	time_t timestamp = arg->value.time;
	const char *str;
	struct tm *tm;
	int tz_offset;

	tm = localtime(&timestamp);
	tz_offset = utc_offset(tm, timestamp);
	timestamp -= tz_offset * 60;

	if (!imap_to_date(timestamp, &str))
		return FALSE;
	str_printfa(dest, " \"%s\"", str);
	return TRUE;
}

bool mail_search_mime_arg_to_imap(string_t *dest,
				  const struct mail_search_mime_arg *arg,
				  bool utf8, const char **error_r)
{
	enum imap_quote_flags qflags = (utf8 ? IMAP_QUOTE_FLAG_UTF8 : 0);

	if (arg->match_not)
		str_append(dest, "NOT ");
	switch (arg->type) {
	case SEARCH_MIME_OR:
		if (!mail_search_mime_subargs_to_imap(
			dest, arg->value.subargs, "OR ", qflags, error_r))
			return FALSE;
		break;
	case SEARCH_MIME_SUB:
		if (!mail_search_mime_subargs_to_imap(
			dest, arg->value.subargs, "", qflags, error_r))
			return FALSE;
		break;
	case SEARCH_MIME_SIZE_EQUAL:
		str_printfa(dest, "SIZE %"PRIuUOFF_T, arg->value.size);
		break;
	case SEARCH_MIME_SIZE_LARGER:
		str_printfa(dest, "SIZE LARGER %"PRIuUOFF_T, arg->value.size);
		break;
	case SEARCH_MIME_SIZE_SMALLER:
		str_printfa(dest, "SIZE SMALLER %"PRIuUOFF_T, arg->value.size);
		break;
	case SEARCH_MIME_DESCRIPTION:
		str_append(dest, "DESCRIPTION ");
		imap_append_astring(dest, arg->value.str, qflags);
		break;
	case SEARCH_MIME_DISPOSITION_TYPE:
		str_append(dest, "DISPOSITION TYPE ");
		imap_append_astring(dest, arg->value.str, 0);
		break;
	case SEARCH_MIME_DISPOSITION_PARAM:
		str_append(dest, "DISPOSITION PARAM ");
		imap_append_astring(dest, arg->field_name, 0);
		str_append_c(dest, ' ');
		imap_append_astring(dest, arg->value.str, qflags);
		break;
	case SEARCH_MIME_ENCODING:
		str_append(dest, "ENCODING ");
		imap_append_astring(dest, arg->value.str, 0);
		break;
	case SEARCH_MIME_ID:
		str_append(dest, "ID ");
		imap_append_astring(dest, arg->value.str, 0);
		break;
	case SEARCH_MIME_LANGUAGE:
		str_append(dest, "LANGUAGE ");
		imap_append_astring(dest, arg->value.str, 0);
		break;
	case SEARCH_MIME_LOCATION:
		str_append(dest, "LOCATION ");
		imap_append_astring(dest, arg->value.str, 0);
		break;
	case SEARCH_MIME_MD5:
		str_append(dest, "MD5 ");
		imap_append_astring(dest, arg->value.str, 0);
		break;
	case SEARCH_MIME_TYPE:
		str_append(dest, "TYPE ");
		imap_append_astring(dest, arg->value.str, 0);
		break;
	case SEARCH_MIME_SUBTYPE:
		str_append(dest, "SUBTYPE ");
		imap_append_astring(dest, arg->value.str, 0);
		break;
	case SEARCH_MIME_PARAM:
		str_append(dest, "PARAM ");
		imap_append_astring(dest, arg->field_name, 0);
		str_append_c(dest, ' ');
		imap_append_astring(dest, arg->value.str, qflags);
		break;
	case SEARCH_MIME_HEADER:
		str_append(dest, "HEADER ");
		imap_append_astring(dest, arg->field_name, 0);
		str_append_c(dest, ' ');
		imap_append_astring(dest, arg->value.str, qflags);
		break;
	case SEARCH_MIME_BODY:
		str_append(dest, "BODY ");
		imap_append_astring(dest, arg->value.str, qflags);
		break;
	case SEARCH_MIME_TEXT:
		str_append(dest, "TEXT ");
		imap_append_astring(dest, arg->value.str, qflags);
		break;
	case SEARCH_MIME_CC:
		str_append(dest, "CC ");
		imap_append_astring(dest, arg->value.str, qflags);
		break;
	case SEARCH_MIME_BCC:
		str_append(dest, "BCC ");
		imap_append_astring(dest, arg->value.str, qflags);
		break;
	case SEARCH_MIME_FROM:
		str_append(dest, "FROM ");
		imap_append_astring(dest, arg->value.str, qflags);
		break;
	case SEARCH_MIME_IN_REPLY_TO:
		str_append(dest, "IN-REPLY-TO ");
		imap_append_astring(dest, arg->value.str, qflags);
		break;
	case SEARCH_MIME_MESSAGE_ID:
		str_append(dest, "MESSAGE-ID ");
		imap_append_astring(dest, arg->value.str, qflags);
		break;
	case SEARCH_MIME_REPLY_TO:
		str_append(dest, "REPLY-TO ");
		imap_append_astring(dest, arg->value.str, qflags);
		break;
	case SEARCH_MIME_SENDER:
		str_append(dest, "SENDER ");
		imap_append_astring(dest, arg->value.str, qflags);
		break;
	case SEARCH_MIME_SENTBEFORE:
		str_append(dest, "SENTBEFORE");
		if (!mail_search_mime_arg_to_imap_date(dest, arg)) {
			*error_r = t_strdup_printf(
				"SENTBEFORE can't be written as IMAP MIMEPART key "
				"for timestamp %"PRIdTIME_T, arg->value.time);
			return FALSE;
		}
		break;
	case SEARCH_MIME_SENTON:
		str_append(dest, "SENTON");
		if (!mail_search_mime_arg_to_imap_date(dest, arg)) {
			*error_r = t_strdup_printf(
				"SENTON can't be written as IMAP MIMEPART key "
				"for timestamp %"PRIdTIME_T, arg->value.time);
			return FALSE;
		}
		break;
	case SEARCH_MIME_SENTSINCE:
		str_append(dest, "SENTSINCE");
		if (!mail_search_mime_arg_to_imap_date(dest, arg)) {
			*error_r = t_strdup_printf(
				"SENTSINCE can't be written as IMAP MIMEPART key "
				"for timestamp %"PRIdTIME_T, arg->value.time);
			return FALSE;
		}
		break;
	case SEARCH_MIME_SUBJECT:
		str_append(dest, "SUBJECT ");
		imap_append_astring(dest, arg->value.str, qflags);
		break;
	case SEARCH_MIME_TO:
		str_append(dest, "TO ");
		imap_append_astring(dest, arg->value.str, qflags);
		break;
	case SEARCH_MIME_DEPTH_EQUAL:
		str_printfa(dest, "DEPTH %u", arg->value.number);
		break;
	case SEARCH_MIME_DEPTH_MIN:
		str_printfa(dest, "DEPTH MIN %u", arg->value.number);
		break;
	case SEARCH_MIME_DEPTH_MAX:
		str_printfa(dest, "DEPTH MAX %u", arg->value.number);
		break;
	case SEARCH_MIME_INDEX:
		str_printfa(dest, "INDEX %u", arg->value.number);
		break;
	case SEARCH_MIME_PARENT:
		str_append(dest, "PARENT ");
		if (arg->value.subargs == NULL)
			str_append(dest, "EXISTS");
		else if (!mail_search_mime_subargs_to_imap(
				dest, arg->value.subargs, "", qflags, error_r))
			return FALSE;
		break;
	case SEARCH_MIME_CHILD:
		str_append(dest, "CHILD ");
		if (arg->value.subargs == NULL)
			str_append(dest, "EXISTS");
		else if (!mail_search_mime_subargs_to_imap(
				dest, arg->value.subargs, "", qflags, error_r))
			return FALSE;
		break;
	case SEARCH_MIME_FILENAME_IS:
		str_append(dest, "FILENAME IS ");
		imap_append_astring(dest, arg->value.str, qflags);
		break;
	case SEARCH_MIME_FILENAME_CONTAINS:
		str_append(dest, "FILENAME CONTAINS ");
		imap_append_astring(dest, arg->value.str, qflags);
		break;
	case SEARCH_MIME_FILENAME_BEGINS:
		str_append(dest, "FILENAME BEGINS ");
		imap_append_astring(dest, arg->value.str, qflags);
		break;
	case SEARCH_MIME_FILENAME_ENDS:
		str_append(dest, "FILENAME ENDS ");
		imap_append_astring(dest, arg->value.str, qflags);
		break;
	}
	return TRUE;
}

bool mail_search_mime_part_to_imap(string_t *dest,
				   const struct mail_search_mime_part *mpart,
				   bool utf8, const char **error_r)
{
	const struct mail_search_mime_arg *arg;

	i_assert(mpart->args != NULL);
	if (mpart->args->next == NULL) {
		if (!mail_search_mime_arg_to_imap(dest, mpart->args, utf8,
						  error_r))
			return FALSE;
	} else {
		str_append_c(dest, '(');
		for (arg = mpart->args; arg != NULL; arg = arg->next) {
			if (!mail_search_mime_arg_to_imap(dest, arg, utf8,
							  error_r))
				return FALSE;
			if (arg->next != NULL)
				str_append_c(dest, ' ');
		}
		str_append_c(dest, ')');
	}
	return TRUE;
}
