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

#include "lib.h"
#include "string.h"
#include "ostream.h"
#include "hash.h"
#include "mail-storage.h"

#include "sieve.h"
#include "sieve-code.h"
#include "sieve-commands.h"
#include "sieve-extensions.h"
#include "sieve-validator.h"
#include "sieve-generator.h"
#include "sieve-binary.h"
#include "sieve-dump.h"

#include "testsuite-common.h"
#include "testsuite-objects.h"
#include "testsuite-message.h"

/*
 * Testsuite core objects
 */

enum testsuite_object_code {
	TESTSUITE_OBJECT_MESSAGE,
	TESTSUITE_OBJECT_ENVELOPE
};

const struct testsuite_object_def *testsuite_core_objects[] = {
	&message_testsuite_object,
	&envelope_testsuite_object,
};

const unsigned int testsuite_core_objects_count =
	N_ELEMENTS(testsuite_core_objects);

/*
 * Testsuite object registry
 */

static inline struct sieve_validator_object_registry *
_get_object_registry(struct sieve_validator *valdtr)
{
	struct testsuite_validator_context *ctx =
		testsuite_validator_context_get(valdtr);

	return ctx->object_registrations;
}

void testsuite_object_register(struct sieve_validator *valdtr,
			       const struct sieve_extension *ext,
			       const struct testsuite_object_def *tobj_def)
{
	struct sieve_validator_object_registry *regs =
		_get_object_registry(valdtr);

	sieve_validator_object_registry_add(regs, ext, &tobj_def->obj_def);
}

static const struct testsuite_object *
testsuite_object_create(struct sieve_validator *valdtr,
			struct sieve_command *cmd, const char *identifier)
{
	struct sieve_validator_object_registry *regs =
		_get_object_registry(valdtr);
	struct sieve_object object;
	struct testsuite_object *tobj;

	if (!sieve_validator_object_registry_find(regs, identifier, &object))
		return NULL;

	tobj = p_new(sieve_command_pool(cmd), struct testsuite_object, 1);
	tobj->object = object;
	tobj->def = (const struct testsuite_object_def *)object.def;

	return tobj;
}

void testsuite_register_core_objects(struct testsuite_validator_context *ctx)
{
	struct sieve_validator_object_registry *regs =
		ctx->object_registrations;
	unsigned int i;

	/* Register core testsuite objects */
	for (i = 0; i < testsuite_core_objects_count; i++) {
		const struct testsuite_object_def *tobj_def =
			testsuite_core_objects[i];

		sieve_validator_object_registry_add(
			regs, testsuite_ext, &tobj_def->obj_def);
	}
}

/*
 * Testsuite object code
 */

const struct sieve_operand_class sieve_testsuite_object_operand_class =
	{ "testsuite object" };

static const struct sieve_extension_objects core_testsuite_objects =
	SIEVE_EXT_DEFINE_OBJECTS(testsuite_core_objects);

const struct sieve_operand_def testsuite_object_operand = {
	.name = "testsuite-object",
	.ext_def = &testsuite_extension,
	.code = TESTSUITE_OPERAND_OBJECT,
	.class = &sieve_testsuite_object_operand_class,
	.interface = &core_testsuite_objects,
};

static void
testsuite_object_emit(struct sieve_binary_block *sblock,
		      const struct testsuite_object *tobj, int member_id)
{
	sieve_opr_object_emit(sblock, tobj->object.ext, tobj->object.def);

	if (tobj->def != NULL && tobj->def->get_member_id != NULL)
		(void)sieve_binary_emit_byte(sblock, (unsigned char)member_id);
}

bool testsuite_object_read(struct sieve_binary_block *sblock,
			   sieve_size_t *address, struct testsuite_object *tobj)
{
	struct sieve_operand oprnd;

	if (!sieve_operand_read(sblock, address, NULL, &oprnd))
		return FALSE;

	if (!sieve_opr_object_read_data(sblock, &oprnd,
					&sieve_testsuite_object_operand_class,
					address, &tobj->object))
		return FALSE;

	tobj->def = (const struct testsuite_object_def *)tobj->object.def;
	i_assert(tobj->def != NULL);
	return TRUE;
}

bool testsuite_object_read_member(struct sieve_binary_block *sblock,
				  sieve_size_t *address,
				  struct testsuite_object *tobj,
				 int *member_id_r)
{
	if (!testsuite_object_read(sblock, address, tobj))
		return FALSE;

	*member_id_r = -1;
	if (tobj->def->get_member_id != NULL) {
		if (!sieve_binary_read_code(sblock, address, member_id_r))
			return FALSE;
	}
	return TRUE;
}

const char *
testsuite_object_member_name(const struct testsuite_object *object,
			     int member_id)
{
	const struct testsuite_object_def *obj_def = object->def;
	const char *member = NULL;

	if (obj_def->get_member_id != NULL) {
		if (obj_def->get_member_name != NULL)
			member = obj_def->get_member_name(member_id);
	} else
		return obj_def->obj_def.identifier;

	if (member == NULL) {
		return t_strdup_printf("%s.%d", obj_def->obj_def.identifier,
				       member_id);
	}
	return t_strdup_printf("%s.%s", obj_def->obj_def.identifier, member);
}

bool testsuite_object_dump(const struct sieve_dumptime_env *denv,
			   sieve_size_t *address)
{
	struct testsuite_object object;
	int member_id;

	sieve_code_mark(denv);

	if (!testsuite_object_read_member(denv->sblock, address,
					  &object, &member_id))
		return FALSE;

	sieve_code_dumpf(denv, "%s: %s",
			 sieve_testsuite_object_operand_class.name,
			testsuite_object_member_name(&object, member_id));
	return TRUE;
}

/*
 * Testsuite object argument
 */

static bool
arg_testsuite_object_generate(const struct sieve_codegen_env *cgenv,
			      struct sieve_ast_argument *arg,
			      struct sieve_command *cmd);

const struct sieve_argument_def testsuite_object_argument = {
	.identifier = "testsuite-object",
	.generate = arg_testsuite_object_generate
};

struct testsuite_object_argctx {
	const struct testsuite_object *object;
	int member;
};

bool testsuite_object_argument_activate(struct sieve_validator *valdtr,
					struct sieve_ast_argument *arg,
					struct sieve_command *cmd)
{
	const char *objname = sieve_ast_argument_strc(arg);
	const struct testsuite_object *tobj;
	int member_id;
	const char *member;
	struct testsuite_object_argctx *ctx;

	/* Parse the object specifier */

	member = strchr(objname, '.');
	if (member != NULL) {
		objname = t_strdup_until(objname, member);
		member++;
	}

	/* Find the object */

	tobj = testsuite_object_create(valdtr, cmd, objname);
	if (tobj == NULL) {
		sieve_argument_validate_error(valdtr, arg,
					      "unknown testsuite object '%s'",
					      objname);
		return FALSE;
	}

	/* Find the object member */

	member_id = -1;
	if (member != NULL) {
		if (tobj->def == NULL || tobj->def->get_member_id == NULL ||
		    (member_id = tobj->def->get_member_id(member)) == -1) {
			sieve_argument_validate_error(valdtr, arg,
				"member '%s' does not exist for testsuite object '%s'",
				member, objname);
			return FALSE;
		}
	}

	/* Assign argument context */

	ctx = p_new(sieve_command_pool(cmd), struct testsuite_object_argctx, 1);
	ctx->object = tobj;
	ctx->member = member_id;

	arg->argument = sieve_argument_create(
		arg->ast, &testsuite_object_argument, testsuite_ext, 0);
	arg->argument->data = ctx;
	return TRUE;
}

static bool
arg_testsuite_object_generate(const struct sieve_codegen_env *cgenv,
			      struct sieve_ast_argument *arg,
			      struct sieve_command *cmd ATTR_UNUSED)
{
	struct testsuite_object_argctx *ctx =
		(struct testsuite_object_argctx *)arg->argument->data;

	testsuite_object_emit(cgenv->sblock, ctx->object, ctx->member);
	return TRUE;
}

/*
 * Testsuite core object implementation
 */

static bool tsto_message_set_member(const struct sieve_runtime_env *renv,
				    int id, string_t *value);

static int tsto_envelope_get_member_id(const char *identifier);
static const char *tsto_envelope_get_member_name(int id);
static bool tsto_envelope_set_member(const struct sieve_runtime_env *renv,
				     int id, string_t *value);

const struct testsuite_object_def message_testsuite_object = {
	SIEVE_OBJECT("message", &testsuite_object_operand,
		     TESTSUITE_OBJECT_MESSAGE),
	.set_member = tsto_message_set_member,
};

const struct testsuite_object_def envelope_testsuite_object = {
	SIEVE_OBJECT("envelope", &testsuite_object_operand,
		     TESTSUITE_OBJECT_ENVELOPE),
	.get_member_id = tsto_envelope_get_member_id,
	.get_member_name = tsto_envelope_get_member_name,
	.set_member = tsto_envelope_set_member,
};

enum testsuite_object_envelope_field {
	TESTSUITE_OBJECT_ENVELOPE_FROM,
	TESTSUITE_OBJECT_ENVELOPE_TO,
	TESTSUITE_OBJECT_ENVELOPE_ORIG_TO,
	TESTSUITE_OBJECT_ENVELOPE_AUTH_USER,
};

static bool
tsto_message_set_member(const struct sieve_runtime_env *renv, int id,
			string_t *value)
{
	if (id != -1)
		return FALSE;

	testsuite_message_set_string(renv, value);
	return TRUE;
}

static int tsto_envelope_get_member_id(const char *identifier)
{
	if (strcasecmp(identifier, "from") == 0)
		return TESTSUITE_OBJECT_ENVELOPE_FROM;
	if (strcasecmp(identifier, "to") == 0)
		return TESTSUITE_OBJECT_ENVELOPE_TO;
	if (strcasecmp(identifier, "orig_to") == 0)
		return TESTSUITE_OBJECT_ENVELOPE_ORIG_TO;
	if (strcasecmp(identifier, "auth") == 0)
		return TESTSUITE_OBJECT_ENVELOPE_AUTH_USER;
	return -1;
}

static const char *tsto_envelope_get_member_name(int id)
{
	switch (id) {
	case TESTSUITE_OBJECT_ENVELOPE_FROM:
		return "from";
	case TESTSUITE_OBJECT_ENVELOPE_TO:
		return "to";
	case TESTSUITE_OBJECT_ENVELOPE_ORIG_TO:
		return "orig_to";
	case TESTSUITE_OBJECT_ENVELOPE_AUTH_USER:
		return "auth";
	}
	return NULL;
}

static bool
tsto_envelope_set_member(const struct sieve_runtime_env *renv, int id,
			 string_t *value)
{
	switch (id) {
	case TESTSUITE_OBJECT_ENVELOPE_FROM:
		testsuite_envelope_set_sender(renv, str_c(value));
		return TRUE;
	case TESTSUITE_OBJECT_ENVELOPE_TO:
		testsuite_envelope_set_recipient(renv, str_c(value));
		return TRUE;
	case TESTSUITE_OBJECT_ENVELOPE_ORIG_TO:
		testsuite_envelope_set_orig_recipient(renv, str_c(value));
		return TRUE;
	case TESTSUITE_OBJECT_ENVELOPE_AUTH_USER:
		testsuite_envelope_set_auth_user(renv, str_c(value));
		return TRUE;
	}
	return FALSE;
}
