/* Copyright (c) Pigeonhole authors, see top-level COPYING file */

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

#include "ext-mime-common.h"

#include <ctype.h>

/* break
 *
 * Syntax:
 *   break [":name" <name: string>]
 *
 */

static bool
cmd_break_registered(struct sieve_validator *valdtr,
		     const struct sieve_extension *ext,
		     struct sieve_command_registration *cmd_reg);
static bool
cmd_break_pre_validate(struct sieve_validator *valdtr,
		       struct sieve_command *cmd);
static bool
cmd_break_validate(struct sieve_validator *valdtr, struct sieve_command *cmd);
static bool
cmd_break_generate(const struct sieve_codegen_env *cgenv,
		   struct sieve_command *ctx);

const struct sieve_command_def cmd_break = {
	.identifier = "break",
	.type = SCT_COMMAND,
	.positional_args = 0,
	.subtests = 0,
	.block_allowed = FALSE,
	.block_required = FALSE,
	.registered = cmd_break_registered,
	.pre_validate = cmd_break_pre_validate,
	.validate = cmd_break_validate,
	.generate = cmd_break_generate,
};

/*
 * Tagged arguments
 */

/* Forward declarations */

static bool
cmd_break_validate_name_tag(struct sieve_validator *valdtr,
			    struct sieve_ast_argument **arg,
			    struct sieve_command *cmd);

/* Argument objects */

static const struct sieve_argument_def break_name_tag = {
	.identifier = "name",
	.validate = cmd_break_validate_name_tag,
};

/*
 * Break operation
 */

static bool
cmd_break_operation_dump(const struct sieve_dumptime_env *denv,
			 sieve_size_t *address);
static int
cmd_break_operation_execute(const struct sieve_runtime_env *renv,
			    sieve_size_t *address);

const struct sieve_operation_def break_operation = {
	.mnemonic = "BREAK",
	.ext_def = &foreverypart_extension,
	.code = EXT_FOREVERYPART_OPERATION_BREAK,
	.dump = cmd_break_operation_dump,
	.execute = cmd_break_operation_execute,
};

/*
 * Validation data
 */

struct cmd_break_data {
	struct sieve_ast_argument *name;
	struct sieve_command *loop_cmd;
};

/*
 * Tag validation
 */

static bool
cmd_break_validate_name_tag(struct sieve_validator *valdtr,
			    struct sieve_ast_argument **arg,
			    struct sieve_command *cmd)
{
	struct cmd_break_data *data = (struct cmd_break_data *)cmd->data;
	struct sieve_ast_argument *tag = *arg;

	/* Detach the tag itself */
	*arg = sieve_ast_arguments_detach(*arg, 1);

	/* Check syntax:
	 *   :name <string>
	 */
	if (!sieve_validate_tag_parameter(valdtr, cmd, tag, *arg, NULL, 0,
					  SAAT_STRING, TRUE))
		return FALSE;
	data->name = *arg;

	/* Skip parameter */
	*arg = sieve_ast_argument_next(*arg);
	return TRUE;
}

/*
 * Command registration
 */

static bool
cmd_break_registered(struct sieve_validator *valdtr,
		     const struct sieve_extension *ext,
		     struct sieve_command_registration *cmd_reg)
{
	sieve_validator_register_tag(valdtr, cmd_reg, ext, &break_name_tag, 0);
	return TRUE;
}

/*
 * Command validation
 */

static bool
cmd_break_pre_validate(struct sieve_validator *valdtr ATTR_UNUSED,
		       struct sieve_command *cmd)
{
	struct cmd_break_data *data;
	pool_t pool = sieve_command_pool(cmd);

	data = p_new(pool, struct cmd_break_data, 1);
	cmd->data = data;
	return TRUE;
}

static bool
cmd_break_validate(struct sieve_validator *valdtr, struct sieve_command *cmd)
{
	struct cmd_break_data *data = (struct cmd_break_data *)cmd->data;
	struct sieve_ast_node *node = cmd->ast_node;
	const char *name = (data->name == NULL ?
			    NULL : sieve_ast_argument_strc(data->name));

	i_assert(node != NULL);
	while (node != NULL && node->command != NULL) {
		if (sieve_command_is(node->command, cmd_foreverypart)) {
			struct ext_foreverypart_loop *loop =
				(struct ext_foreverypart_loop *)
					node->command->data;
			if (name == NULL ||
			    (name != NULL && loop->name != NULL &&
			     strcmp(name, loop->name) == 0)) {
				data->loop_cmd = node->command;
				break;
			}
		}
		node = sieve_ast_node_parent(node);
	}

	if (data->loop_cmd == NULL) {
		if (name == NULL) {
			sieve_command_validate_error(
				valdtr, cmd,
				"the break command is not placed inside "
				"a foreverypart loop");
		} else {
			sieve_command_validate_error(
				valdtr, cmd,
				"the break command is not placed inside "
				"a foreverypart loop named '%s'", name);
		}
		return FALSE;
	}

	sieve_command_exit_block_unconditionally(cmd);
	return TRUE;
}

/*
 * Code generation
 */

static bool
cmd_break_generate(const struct sieve_codegen_env *cgenv,
		   struct sieve_command *cmd)
{
	struct cmd_break_data *data = (struct cmd_break_data *)cmd->data;
	struct ext_foreverypart_loop *loop;

	i_assert(data->loop_cmd != NULL);
	loop = (struct ext_foreverypart_loop *)data->loop_cmd->data;

	sieve_operation_emit(cgenv->sblock, cmd->ext, &break_operation);
	sieve_jumplist_add(loop->exit_jumps,
			   sieve_binary_emit_offset(cgenv->sblock, 0));
	return TRUE;
}

/*
 * Code dump
 */

static bool
cmd_break_operation_dump(const struct sieve_dumptime_env *denv,
			 sieve_size_t *address)
{
	unsigned int pc = *address;
	sieve_offset_t offset;

	sieve_code_dumpf(denv, "BREAK");
	sieve_code_descend(denv);

 	if (!sieve_binary_read_offset(denv->sblock, address, &offset))
		return FALSE;

	sieve_code_dumpf(denv, "END: %d [%08x]", offset, pc + offset);
	return TRUE;
}

/*
 * Code execution
 */

static int
cmd_break_operation_execute(const struct sieve_runtime_env *renv,
			    sieve_size_t *address)
{
	struct sieve_interpreter_loop *loop;
	unsigned int pc = *address;
	sieve_offset_t offset;
	sieve_size_t loop_end;

	/*
	 * Read operands
	 */

	if (!sieve_binary_read_offset(renv->sblock, address, &offset))
	{
		sieve_runtime_trace_error(renv, "invalid loop end offset");
		return SIEVE_EXEC_BIN_CORRUPT;
	}

	loop_end = pc + offset;

	/*
	 * Perform operation
	 */

	sieve_runtime_trace(renv, SIEVE_TRLVL_ACTIONS, "break command");
	sieve_runtime_trace_descend(renv);

	loop = sieve_interpreter_loop_get(renv->interp, loop_end,
					  &foreverypart_extension);
	if (loop == NULL) {
		sieve_runtime_trace_error(renv, "no matching loop found");
		return SIEVE_EXEC_BIN_CORRUPT;
	}

	sieve_interpreter_loop_break(renv->interp, loop);
	return SIEVE_EXEC_OK;
}
