/* Copyright (c) 2002-2018 Pigeonhole authors, see the included COPYING file
 */

/* Match-type ':matches'
 */

#include "lib.h"
#include "str.h"

#include "sieve-match-types.h"
#include "sieve-comparators.h"
#include "sieve-match.h"

#include <string.h>
#include <stdio.h>

/*
 * Forward declarations
 */

static int mcht_matches_match_key
	(struct sieve_match_context *mctx, const char *val, size_t val_size,
		const char *key, size_t key_size);

/*
 * Match-type object
 */

const struct sieve_match_type_def matches_match_type = {
	SIEVE_OBJECT("matches",
		&match_type_operand, SIEVE_MATCH_TYPE_MATCHES),
	.validate_context = sieve_match_substring_validate_context,
	.match_key = mcht_matches_match_key
};

/*
 * Match-type implementation
 */

/* Quick 'n dirty debug */
//#define MATCH_DEBUG
#ifdef MATCH_DEBUG
#define debug_printf(...) printf ("match debug: " __VA_ARGS__)
#else
#define debug_printf(...)
#endif

/* FIXME: Naive implementation, substitute this with dovecot src/lib/str-find.c
 */
static inline bool _string_find(const struct sieve_comparator *cmp,
	const char **valp, const char *vend, const char **keyp, const char *kend)
{
	while ( (*valp < vend) && (*keyp < kend) ) {
		if ( !cmp->def->char_match(cmp, valp, vend, keyp, kend) )
			(*valp)++;
	}

	return (*keyp == kend);
}

static char _scan_key_section
	(string_t *section, const char **wcardp, const char *key_end)
{
	/* Find next wildcard and resolve escape sequences */
	str_truncate(section, 0);
	while ( *wcardp < key_end && **wcardp != '*' && **wcardp != '?') {
		if ( **wcardp == '\\' ) {
			(*wcardp)++;
		}
		str_append_c(section, **wcardp);
		(*wcardp)++;
	}

	/* Record wildcard character or \0 */
	if ( *wcardp < key_end ) {
		return **wcardp;
	}

	i_assert( *wcardp == key_end );
	return '\0';
}

static int mcht_matches_match_key
(struct sieve_match_context *mctx, const char *val, size_t val_size,
	const char *key, size_t key_size)
{
	const struct sieve_comparator *cmp = mctx->comparator;
	struct sieve_match_values *mvalues;
	string_t *mvalue = NULL, *mchars = NULL;
	string_t *section, *subsection;
	const char *vend, *kend, *vp, *kp, *wp, *pvp;
	bool backtrack = FALSE; /* TRUE: match of '?'-connected sections failed */
	char wcard = '\0';      /* Current wildcard */
	char next_wcard = '\0'; /* Next  widlcard */
	unsigned int key_offset = 0;

	if ( cmp->def == NULL || cmp->def->char_match == NULL )
		return 0;

	/* Key sections */
	section = t_str_new(32);    /* Section (after beginning or *) */
	subsection = t_str_new(32); /* Sub-section (after ?) */

	/* Mark end of value and key */
	vend = (const char *) val + val_size;
	kend = (const char *) key + key_size;

	/* Initialize pointers */
	vp = val;                   /* Value pointer */
	kp = key;                   /* Key pointer */
	wp = key;                   /* Wildcard (key) pointer */

	/* Start match values list if requested */
	if ( (mvalues = sieve_match_values_start(mctx->runenv)) != NULL ) {
		/* Skip ${0} for now; added when match succeeds */
		sieve_match_values_add(mvalues, NULL);

		mvalue = t_str_new(32);     /* Match value (*) */
		mchars = t_str_new(32);     /* Match characters (.?..?.??) */
	}

	/* Match the pattern:
	 *   <pattern> = <section>*<section>*<section>...
	 *   <section> = <sub-section>?<sub-section>?<sub-section>...
	 *
	 * Escape sequences \? and \* need special attention.
	 */

	debug_printf("=== Start ===\n");
	debug_printf("  key:   %s\n", t_strdup_until(key, kend));
	debug_printf("  value: %s\n", t_strdup_until(val, vend));

	/* Loop until either key or value ends */
	while (kp < kend && vp < vend ) {
		const char *needle, *nend;

		if ( !backtrack ) {
			/* Search the next '*' wildcard in the key string */

			wcard = next_wcard;

			/* Find the needle to look for in the string */
			key_offset = 0;
			for (;;) {
				next_wcard = _scan_key_section(section, &wp, kend);

				if ( wcard == '\0' || str_len(section) > 0 )
					break;

				if ( next_wcard == '*' ) {
					break;
				}

				if ( wp < kend )
					wp++;
				else
					break;
				key_offset++;
			}

			debug_printf("found wildcard '%c' at pos [%d]\n",
				next_wcard, (int) (wp-key));

			if ( mvalues != NULL )
				str_truncate(mvalue, 0);
		} else {
			/* Backtracked; '*' wildcard is retained */
			debug_printf("backtracked");
			backtrack = FALSE;
		}

		/* Determine what we are looking for */
		needle = str_c(section);
		nend = PTR_OFFSET(needle, str_len(section));

		debug_printf("  section needle:  '%s'\n", t_strdup_until(needle, nend));
		debug_printf("  section key:     '%s'\n", t_strdup_until(kp, kend));
		debug_printf("  section remnant: '%s'\n", t_strdup_until(wp, kend));
		debug_printf("  value remnant:   '%s'\n", t_strdup_until(vp, vend));
		debug_printf("  key offset:      %d\n", key_offset);

		pvp = vp;
		if ( next_wcard == '\0' ) {
			if ( wcard == '\0' ) {
				/* No current wildcard; match needs to happen right at the beginning */
				debug_printf("next_wcard = NULL && wcard = NUL; needle should be equal to value.\n");

				if ( (vend - vp) != (nend - needle) ||
					!cmp->def->char_match(cmp, &vp, vend, &needle, nend) ) {
					debug_printf("  key not equal to value\n");
					break;
				}

			} else {
				const char *qp, *qend;
				size_t slen;

				/* No more wildcards; find the needle substring at the end of string */
				debug_printf("next_wcard = NUL; must find needle at end\n");

				/* Check if the value is still large enough */
				slen = str_len(section);
				if ( (vp + slen) > vend ) {
					debug_printf("  wont match: value is too short\n");
					break;
				}

				/* Move value pointer to where the needle should be */
				vp = vend - slen;

				/* Record match values */
				qend = vp;
				qp = vp - key_offset;

				if ( mvalues != NULL )
					str_append_data(mvalue, pvp, qp-pvp);

				/* Compare needle to end of value string */
				if ( !cmp->def->char_match(cmp, &vp, vend, &needle, nend) ) {
					debug_printf("  match at end failed\n");
					break;
				}

				/* Add match values */
				if ( mvalues != NULL ) {
					/* Append '*' match value */
					sieve_match_values_add(mvalues, mvalue);

					/* Append any initial '?' match values */
					for ( ; qp < qend; qp++ )
						sieve_match_values_add_char(mvalues, *qp);
				}
			}

			/* Finish match */
			kp = kend;
			vp = vend;

			debug_printf("  matched end of value\n");
			break;
		} else {
			/* Next wildcard found; match needle before next wildcard */

			const char *prv = NULL; /* Stored value pointer for backtrack */
			const char *prk = NULL; /* Stored key pointer for backtrack */
			const char *prw = NULL; /* Stored wildcard pointer for backtrack */
			const char *chars;

			/* Reset '?' match values */
			if ( mvalues != NULL )
				str_truncate(mchars, 0);

			if ( wcard == '\0' ) {
				/* No current wildcard; match needs to happen right at the beginning */
				debug_printf("wcard = NUL; needle should be found at the beginning.\n");
				debug_printf("  begin needle: '%s'\n", t_strdup_until(needle, nend));
				debug_printf("  begin value:  '%s'\n", t_strdup_until(vp, vend));

				if ( !cmp->def->char_match(cmp, &vp, vend, &needle, nend) ) {
					debug_printf("  failed to find needle at beginning\n");
					break;
				}

			} else {
				/* Current wildcard present; match needle between current and next wildcard */
				debug_printf("wcard != NUL; must find needle at an offset (>= %d).\n",
					key_offset);

				/* Match may happen at any offset (>= key offset): find substring */
				vp += key_offset;
				if ( (vp >= vend) || !_string_find(cmp, &vp, vend, &needle, nend) ) {
					debug_printf("  failed to find needle at an offset\n");
					break;
				}

				prv = vp - str_len(section);
				prk = kp;
				prw = wp;

				/* Append match values */
				if ( mvalues != NULL ) {
					const char *qend = vp - str_len(section);
					const char *qp = qend - key_offset;

					/* Append '*' match value */
					str_append_data(mvalue, pvp, qp-pvp);

					/* Append any initial '?' match values (those that caused the key
					 * offset.
					 */
					for ( ; qp < qend; qp++ )
						str_append_c(mchars, *qp);
				}
			}

			/* Update wildcard and key pointers for next wildcard scan */
			if ( wp < kend ) wp++;
			kp = wp;

			/* Scan successive '?' wildcards */
			while ( next_wcard == '?' ) {
				debug_printf("next_wcard = '?'; need to match arbitrary character\n");

				/* Add match value */
				if ( mvalues != NULL )
					str_append_c(mchars, *vp);

				vp++;

				/* Scan for next '?' wildcard */
				next_wcard = _scan_key_section(subsection, &wp, kend);
				debug_printf("found next wildcard '%c' at pos [%d] (fixed match)\n",
					next_wcard, (int) (wp-key));

				/* Determine what we are looking for */
				needle = str_c(subsection);
				nend = needle + str_len(subsection);

				debug_printf("  sub key:       '%s'\n", t_strdup_until(needle, nend));
				debug_printf("  value remnant: '%s'\n", vp <= vend ? t_strdup_until(vp, vend) : "");

				/* Try matching the needle at fixed position */
				if ( (needle == nend && next_wcard == '\0' && vp < vend ) ||
					!cmp->def->char_match(cmp, &vp, vend, &needle, nend) ) {

					/* Match failed: now we have a problem. We need to backtrack to the previous
					 * '*' wildcard occurrence and start scanning for the next possible match.
					 */

					debug_printf("  failed fixed match\n");

					/* Start backtrack */
					if ( prv != NULL && prv + 1 < vend ) {
						/* Restore pointers */
						vp = prv;
						kp = prk;
						wp = prw;

						/* Skip forward one value character to scan the next possible match */
						if ( mvalues != NULL )
							str_append_c(mvalue, *vp);
						vp++;

						/* Set wildcard state appropriately */
						wcard = '*';
						next_wcard = '?';

						/* Backtrack */
						backtrack = TRUE;

						debug_printf("  BACKTRACK\n");
					}

					/* Break '?' wildcard scanning loop */
					break;
				}

				/* Update wildcard and key pointers for next wildcard scan */
				if ( wp < kend ) wp++;
				kp = wp;
			}

			if ( !backtrack ) {
				unsigned int i;

				if ( next_wcard == '?' ) {
					debug_printf("failed to match '?'\n");
					break;
				}

				if ( mvalues != NULL ) {
					if ( prv != NULL )
						sieve_match_values_add(mvalues, mvalue);

					chars = (const char *) str_data(mchars);

					for ( i = 0; i < str_len(mchars); i++ ) {
						sieve_match_values_add_char(mvalues, chars[i]);
					}
				}

				if ( next_wcard != '*' ) {
					debug_printf("failed to match at end of string\n");
					break;
				}
			}
		}

		/* Check whether string ends in a wildcard
		 * (avoid scanning the rest of the string)
		 */
		if ( kp == kend && next_wcard == '*' ) {
			/* Add the rest of the string as match value */
			if ( mvalues != NULL ) {
				str_truncate(mvalue, 0);
				str_append_data(mvalue, vp, vend-vp);
				sieve_match_values_add(mvalues, mvalue);
			}

			/* Finish match */
			kp = kend;
			vp = vend;

			debug_printf("key ends with '*'\n");
			break;
		}

		debug_printf("== Loop ==\n");
	}

	/* Eat away a trailing series of *s */
	if ( vp == vend ) {
		while ( kp < kend && *kp == '*' ) kp++;
	}

	/* By definition, the match is only successful if both value and key pattern
	 * are exhausted.
	 */

	debug_printf("=== Finish ===\n");
	debug_printf("  result: %s\n", (kp == kend && vp == vend) ? "true" : "false");

	if (kp == kend && vp == vend) {
		/* Activate new match values after successful match */
		if ( mvalues != NULL ) {
			/* Set ${0} */
			string_t *matched = str_new_const(pool_datastack_create(), val, val_size);
			sieve_match_values_set(mvalues, 0, matched);

			/* Commit new match values */
			sieve_match_values_commit(mctx->runenv, &mvalues);
		}
		return 1;
	}

	/* No match; drop collected match values */
	sieve_match_values_abort(&mvalues);
	return 0;
}

