/* A proposed replacement for mailutils filter chains.
   Copyright (C) 2021-2025 Sergey Poznyakoff

   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, see <http://www.gnu.org/licenses/>. */

#include <mailutils/mailutils.h>

enum {
	TOK_EOF = 0,
	TOK_OPAREN = '(',
	TOK_CPAREN = ')',
	TOK_COMMA = ',',
	TOK_PIPE = '|',
	TOK_FILTER = 256,
	TOK_STRING,
	TOK_IDENT,
	TOK_BOGUS
};

struct context {
	/*
	 * Syntactic context
	 */
	char const *buffer;   /* Input buffer */
	char const *curptr;   /* Current position in buffer */

	/*
	 * Current token.
	 */
	int token;            /* Token code */
	int point;            /* Index in buffer where the token starts */
				 
	char *tokbuf;         /* Token buffer */
	size_t toksize;       /* Size of tokbuf */
	size_t toklen;        /* Length of the used portion of tokbuf
				 (not counting terminating '\0') */


        /*
	 * User-provided context.
	 */
	int (*user_filter)(mu_stream_t *, mu_stream_t, int, int,
			   size_t, char **, void *);
	void *closure;

	void (*user_error)(int ec, /* Error code */
			   char const *msg, /* Error message */
			   char const *input, /* Input string */
			   int begin,  /* Error position: start */
			   int end,    /* Error position: end */
			   void *closure);
	
	/*
	 * Filter context.
	 */
	int mode;             /* Default filter mode: MU_FILTER_ENCODE or
				 MU_FILTER_DECODE */
	int flags;            /* Filter flags (a bitmask of MU_STREAM_*
				 flags) */
	
	mu_stream_t stream;   /* On input: input stream.  On return:
				 output stream. The caller must cleanup
				 it on errors. */
};

static int
context_point(struct context *ctx)
{
	return ctx->curptr - ctx->buffer;
}

static void
context_error(struct context *ctx, int ec, char const *fmt, ...)
{
	if (ctx->user_error) {
		char *msg = NULL;
		size_t size = 0;
		va_list ap;
		int rc;
		
		va_start(ap, fmt);
		rc = mu_vasnprintf(&msg, &size, fmt, ap);
		va_end(ap);

		if (rc) {
			msg = "out of memory trying to format error message";
			ec = ENOMEM;
		}

		ctx->user_error(ec, 
				msg,         /* Error message */
				ctx->buffer, /* Input string */
				ctx->point,  /* Error position: start */
				context_point(ctx)-1, /* Error position: end */
				ctx->closure);

		free(msg);
	}
}	

static void
context_init(struct context *ctx, char const *text, int mode, int flags,
	     mu_stream_t input)
{
	memset(ctx, 0, sizeof(ctx[0]));
	ctx->buffer = ctx->curptr = text;
	ctx->mode = mode;
	ctx->flags = flags;
	ctx->stream = input;
}

static void
context_free(struct context *ctx)
{
	free(ctx->tokbuf);
}

static void
token_assert(struct context *ctx, size_t n)
{
	n += ctx->toklen;
	while (n > ctx->toksize)
		ctx->tokbuf = mu_2nrealloc(ctx->tokbuf, &ctx->toksize, 1);
}

static void
skipws(struct context *ctx)
{
	ctx->curptr = mu_str_skip_class(ctx->curptr, MU_CTYPE_SPACE);
}

static int
is_delim(int c)
{
	return strchr("(),|", c) != 0;
}

static int
nextkn(struct context *ctx)
{
	skipws(ctx);
	ctx->point = context_point(ctx);
	switch (*ctx->curptr) {
	case 0:
		token_assert(ctx, 1);
		ctx->tokbuf[0] = 0;
		ctx->toklen = 0;
		ctx->token = 0;
		break;
		
	case '(':
	case ')':
	case ',':
	case '|':
		token_assert(ctx, 2);
		ctx->tokbuf[0] = *ctx->curptr++;
		ctx->tokbuf[1] = 0;
		ctx->toklen = 1;
		ctx->token = ctx->tokbuf[0];
		break;

	case '"':
		ctx->curptr++;
		if (!*ctx->curptr) {
			token_assert(ctx, 2);
			ctx->tokbuf[0] = '"';
			ctx->tokbuf[1] = 0;
			ctx->toklen = 1;
			ctx->token = TOK_STRING;
		} else {
			ctx->toklen = 0;
			while (*ctx->curptr != '"') {
				if (*ctx->curptr == '\\') 
					++ctx->curptr;
				
				if (*ctx->curptr == 0) {
					context_error(ctx,
						      MU_ERR_PARSE,
						      "unexpected end of input in quoted string");
					ctx->token = TOK_BOGUS;
					return ctx->token;
				}

				token_assert(ctx, 1);
				ctx->tokbuf[ctx->toklen++] = *ctx->curptr++;
			}

			token_assert(ctx, 1);
			ctx->tokbuf[ctx->toklen] = 0;
			++ctx->curptr;
			ctx->token = TOK_STRING;
		}
		break;

	case '\'':
		ctx->curptr++;
		if (!*ctx->curptr) {
			token_assert(ctx, 2);
			ctx->tokbuf[0] = '\'';
			ctx->tokbuf[1] = 0;
			ctx->toklen = 1;
			ctx->token = TOK_STRING;
		} else {
			ctx->toklen = 0;
			while (*ctx->curptr != '\'') {
				if (*ctx->curptr == 0) {
					context_error(ctx,
						      MU_ERR_PARSE,
						      "unexpected end of input in quoted string");
					ctx->token = TOK_BOGUS;
					return ctx->token;
				}
				
				token_assert(ctx, 1);
				ctx->tokbuf[ctx->toklen++] = *ctx->curptr++;
			}
			
			token_assert(ctx, 1);
			ctx->tokbuf[ctx->toklen] = 0;
			++ctx->curptr;
			ctx->token = TOK_STRING;
		}
		break;

	case '\\':
		ctx->curptr++;
		token_assert(ctx, 2);
		if (!*ctx->curptr) {
			ctx->tokbuf[0] = '\\';
		} else {
			ctx->tokbuf[0] = *++ctx->curptr;
			ctx->toklen = 1;
		}
		ctx->toklen = 1;
		ctx->tokbuf[1] = 0;
		ctx->token = TOK_STRING;
		break;
		
	default:
		ctx->toklen = 0;
		while (*ctx->curptr != 0) {
			if (*ctx->curptr == '\\') {
				if (ctx->curptr[1])
					++ctx->curptr;
			} else if (mu_isspace(*ctx->curptr) ||
				   is_delim(*ctx->curptr))
				break;
			
			token_assert(ctx, 1);
			ctx->tokbuf[ctx->toklen++] = *ctx->curptr++;
		}
		ctx->tokbuf[ctx->toklen] = 0;
		ctx->token = TOK_IDENT;
		break;
	}

	return ctx->token;
}

struct argcvbuf {
	size_t argc;
	char **argv;
	size_t argn;
};

#define ARGCVBUF_INITIALIZER {0,NULL,0}

static void
argcvbuf_append(struct argcvbuf *ap, char const *p)
{
	if (ap->argc == ap->argn)
		ap->argv = mu_2nrealloc(ap->argv, &ap->argn, sizeof(ap->argv[0]));
	if (p) 
		ap->argv[ap->argc++] = mu_strdup(p);
	else
		ap->argv[ap->argc] = NULL;
}

static void
argcvbuf_free(struct argcvbuf *ap)
{
	size_t i;

	for (i = 0; i < ap->argc; i++)
		free(ap->argv[i]);
	free(ap->argv);
}

static int
add_filter(struct context *ctx, int mode, struct argcvbuf *ap)
{
	int rc = ENOSYS;
	mu_stream_t str;

	if (ctx->user_filter) {
		rc = ctx->user_filter(&str, ctx->stream,
				      mode, ctx->flags,
				      ap->argc, ap->argv,
				      ctx->closure);
	}

	if (rc == ENOSYS)
		rc = mu_filter_create_args(&str, ctx->stream, ap->argv[0],
					   ap->argc, (char const **)ap->argv,
					   mode, ctx->flags);

	if (rc == 0) {
		mu_stream_unref (ctx->stream);
		ctx->stream = str;
	}

	return rc;
}

/*
 * ARG [ ',' ARG ... ]
 */
static int
parse_args(struct context *ctx, struct argcvbuf *ap)
{
	do {
		nextkn(ctx);
		if (ctx->token != TOK_STRING && ctx->token != TOK_IDENT)
			break;
		argcvbuf_append(ap, ctx->tokbuf);
	} while (nextkn(ctx) == ',');
	return 0;
}

static int parse_transcoder(struct context *ctx, int mode);

/*
 * FILTER
 * FILTER '(' arglist ')'
 */
static int
parse_filter(struct context *ctx, int mode)
{
	struct argcvbuf args = ARGCVBUF_INITIALIZER;
	int rc = 0;
	int pt;
	
	if (ctx->token != TOK_IDENT) {
		if (ctx->token == TOK_STRING)
			context_error(ctx, MU_ERR_PARSE,
				      "expected filter name, but found string");
		else
			context_error(ctx, MU_ERR_PARSE,
				      "expected filter name, but found \"%s\"",
				      ctx->tokbuf);
		return MU_ERR_PARSE;
	}

	if (strcmp(ctx->tokbuf, "encode") == 0 ||
	    strcmp(ctx->tokbuf, "decode") == 0)
		return parse_transcoder(ctx, mode);

	pt = ctx->point;
	
	argcvbuf_append(&args, ctx->tokbuf);

	if (nextkn(ctx) == '(') {
		int pt1 = ctx->point;

		if (parse_args(ctx, &args)) {
			rc = MU_ERR_PARSE;
		} else if (ctx->token != ')') {
			ctx->point = pt1; /* Indicate error starting point */
			context_error(ctx,
				      MU_ERR_PARSE,
				      "unexpected end of input in argument list");
			rc = MU_ERR_PARSE;
		} else
			nextkn(ctx);
	}

	if (rc == 0) {
		rc = add_filter(ctx, mode, &args);
		if (rc) {
			ctx->point = pt; /* Indicate error starting point */
			context_error(ctx,
				      rc,
				      "failed to create filter %s: %s",
				      args.argv[0], mu_strerror(rc));
		}
	}
		
	argcvbuf_free(&args);

	return rc;
}

/*
 * filter ['|' filter ...]
 */
static int
parse_pipe(struct context *ctx, int mode)
{
	int rc;
	while ((rc = parse_filter(ctx, mode)) == 0 && ctx->token == '|')
		nextkn(ctx);
	return rc;
}

/*
 * '(' pipe ')'
 */
static int
parse_group(struct context *ctx, int mode)
{
	if (ctx->token == '(') {
		int rc;
		int pt = ctx->point;
		nextkn(ctx);
		if ((rc = parse_pipe(ctx, mode)) != 0)
			return rc;
		if (ctx->token != ')') {
			ctx->point = pt;
			context_error(ctx,
				      MU_ERR_PARSE,
				      "unexpected end of input in pipe group");
			return MU_ERR_PARSE;
		}
		nextkn(ctx);
		return 0;
	} else
		return parse_pipe(ctx, mode);
}

/*
 * "encode" ( pipe )
 * "decode" ( pipe )
 */
static int
parse_transcoder(struct context *ctx, int mode)
{
	if (ctx->token == TOK_IDENT) {
		if (strcmp(ctx->tokbuf, "encode") == 0) {
			mode = MU_FILTER_ENCODE;
			nextkn(ctx);
		} else if (strcmp(ctx->tokbuf, "decode") == 0) {
			mode = MU_FILTER_DECODE;
			nextkn(ctx);
		}
	}
	return parse_group(ctx, mode);
}

static int
parse_filter_command(struct context *ctx)
{
	int rc;
	
	do {
		nextkn(ctx);
		rc = parse_transcoder(ctx, ctx->mode);
	} while (rc == 0 && ctx->token == '|');

	if (rc == 0 && ctx->token != TOK_EOF) {
		rc = MU_ERR_PARSE;
		if (ctx->token == TOK_STRING)
			context_error(ctx, rc,
				      "expected pipe or end of input, but found string");
		else
			context_error(ctx, rc,
				      "expected pipe or end of input, but found \"%s\"",
				      ctx->tokbuf);
	}
	return rc;
}

int
mfl_filter_pipe_create(mu_stream_t *pret, mu_stream_t transport,
		       int mode, int flags,
		       char const *cmdline,
		       int (*fltfunc)(mu_stream_t *, mu_stream_t, int, int, size_t, char **, void *),

		       void (*errfunc)(int, char const *, char const *, int, int, void *),
		       void *closure)
{
	struct context ctx;
	int rc;
	
	mu_stream_ref(transport);
	context_init(&ctx, cmdline, mode, flags, transport);
	ctx.user_filter = fltfunc;
	ctx.user_error = errfunc;
	ctx.closure = closure;
	
	rc = parse_filter_command(&ctx);
	context_free(&ctx);
	if (rc == 0)
		*pret = ctx.stream;
	else
		mu_stream_destroy(&ctx.stream);
	return rc;
}
