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

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

/*
 * Commands
 */

static bool
cmd_if_validate(struct sieve_validator *valdtr, struct sieve_command *cmd);
static bool
cmd_elsif_validate(struct sieve_validator *valdtr, struct sieve_command *cmd);
static bool
cmd_if_validate_const(struct sieve_validator *valdtr, struct sieve_command *cmd,
		      int *const_current, int const_next);
static bool
cmd_if_generate(const struct sieve_codegen_env *cgenv,
		struct sieve_command *cmd);
static bool
cmd_else_generate(const struct sieve_codegen_env *cgenv,
		  struct sieve_command *cmd);

/* If command
 *
 * Syntax:
 *   if <test1: test> <block1: block>
 */

const struct sieve_command_def cmd_if = {
	.identifier = "if",
	.type = SCT_COMMAND,
	.positional_args = 0,
	.subtests = 1,
	.block_allowed = TRUE,
	.block_required = TRUE,
	.validate = cmd_if_validate,
	.validate_const = cmd_if_validate_const,
	.generate = cmd_if_generate
};

/* ElsIf command
 *
 * Santax:
 *   elsif <test2: test> <block2: block>
 */

const struct sieve_command_def cmd_elsif = {
	.identifier = "elsif",
	.type = SCT_COMMAND,
	.positional_args = 0,
	.subtests = 1,
	.block_allowed = TRUE,
	.block_required = TRUE,
	.validate = cmd_elsif_validate,
	.validate_const = cmd_if_validate_const,
	.generate = cmd_if_generate
};

/* Else command
 *
 * Syntax:
 *   else <block>
 */

const struct sieve_command_def cmd_else = {
	.identifier = "else",
	.type = SCT_COMMAND,
	.positional_args = 0,
	.subtests = 0,
	.block_allowed = TRUE,
	.block_required = TRUE,
	.validate = cmd_elsif_validate,
	.validate_const = cmd_if_validate_const,
	.generate = cmd_else_generate
};

/*
 * Context management
 */

struct cmd_if_context_data {
	struct cmd_if_context_data *previous;
	struct cmd_if_context_data *next;

	int const_condition;

	bool jump_generated;
	sieve_size_t exit_jump;
};

static void
cmd_if_initialize_context_data(struct sieve_command *cmd,
			       struct cmd_if_context_data *previous)
{
	struct cmd_if_context_data *cmd_data;

	/* Assign context */
	cmd_data = p_new(sieve_command_pool(cmd),
			 struct cmd_if_context_data, 1);
	cmd_data->exit_jump = 0;
	cmd_data->jump_generated = FALSE;

	/* Update linked list of contexts */
	cmd_data->previous = previous;
	cmd_data->next = NULL;
	if (previous != NULL)
		previous->next = cmd_data;

	/* Check const status */
	cmd_data->const_condition = -1;
	while (previous != NULL) {
		if (previous->const_condition > 0) {
			cmd_data->const_condition = 0;
			break;
		}
		previous = previous->previous;
	}

	/* Assign to command context */
	cmd->data = cmd_data;
}

/*
 * Validation
 */

static bool
cmd_if_validate(struct sieve_validator *valdtr ATTR_UNUSED,
		struct sieve_command *cmd)
{
	/* Start if-command structure */
	cmd_if_initialize_context_data(cmd, NULL);

	return TRUE;
}

static bool
cmd_elsif_validate(struct sieve_validator *valdtr, struct sieve_command *cmd)
{
	struct sieve_command *prev;

	i_assert(cmd != NULL);
	prev = sieve_command_prev(cmd);

	/* Check valid command placement */
	if (prev == NULL ||
	    (!sieve_command_is(prev, cmd_if) &&
	     !sieve_command_is(prev, cmd_elsif)))
	{
		sieve_command_validate_error(
			valdtr, cmd,
			"the %s command must follow an if or elseif command",
			sieve_command_identifier(cmd));
		return FALSE;
	}

	/* Previous command in this block is 'if' or 'elsif', so we can safely
	   refer to its context data. */
	cmd_if_initialize_context_data(cmd, prev->data);

	return TRUE;
}

static bool
cmd_if_validate_const(struct sieve_validator *valdtr ATTR_UNUSED,
		      struct sieve_command *cmd, int *const_current,
		      int const_next)
{
	struct cmd_if_context_data *cmd_data =
		(struct cmd_if_context_data *)cmd->data;

	if (cmd_data != NULL) {
		if (cmd_data->const_condition == 0) {
			*const_current = cmd_data->const_condition;
			return FALSE;
		}

		cmd_data->const_condition = const_next;
	}

	*const_current = const_next;

	return (const_next < 0);
}

/*
 * Code generation
 */

/* The if command does not generate specific IF-ELSIF-ELSE opcodes, but only
   uses JMP instructions. This is why the implementation of the if command does
   not include an opcode implementation.
 */

static void
cmd_if_resolve_exit_jumps(struct sieve_binary_block *sblock,
			  struct cmd_if_context_data *cmd_data)
{
	struct cmd_if_context_data *if_ctx = cmd_data->previous;

	/* Iterate backwards through all if-command contexts and resolve the
	   exit jumps to the current code position. */
	while (if_ctx != NULL) {
		if (if_ctx->jump_generated)
			sieve_binary_resolve_offset(sblock, if_ctx->exit_jump);
		if_ctx = if_ctx->previous;
	}
}

static bool
cmd_if_generate(const struct sieve_codegen_env *cgenv,
		struct sieve_command *cmd)
{
	struct sieve_binary_block *sblock = cgenv->sblock;
	struct cmd_if_context_data *cmd_data =
		(struct cmd_if_context_data *)cmd->data;
	struct sieve_ast_node *test;
	struct sieve_jumplist jmplist;

	/* Generate test condition */
	if (cmd_data->const_condition < 0) {
		/* Prepare jumplist */
		sieve_jumplist_init_temp(&jmplist, sblock);

		test = sieve_ast_test_first(cmd->ast_node);
		if (!sieve_generate_test(cgenv, test, &jmplist, FALSE))
			return FALSE;
	}

	/* Case true { */
	if (cmd_data->const_condition != 0) {
		if (!sieve_generate_block(cgenv, cmd->ast_node))
			return FALSE;
	}

	/* Are we the final command in this if-elsif-else structure? */
	if (cmd_data->next == NULL || cmd_data->const_condition == 1) {
		/* Yes, Resolve previous exit jumps to this point */
		cmd_if_resolve_exit_jumps(sblock, cmd_data);
	} else if (cmd_data->const_condition < 0) {
		/* No, generate jump to end of if-elsif-else structure (resolved
		   later). This of course is not necessary if the {} block
		   contains a command like stop at top level that
		   unconditionally exits the block already anyway.
		 */
		if (!sieve_command_block_exits_unconditionally(cmd)) {
			sieve_operation_emit(sblock, NULL,
					     &sieve_jmp_operation);
			cmd_data->exit_jump =
				sieve_binary_emit_offset(sblock, 0);
			cmd_data->jump_generated = TRUE;
		}
	}

	if (cmd_data->const_condition < 0) {
		/* Case false ...
		   (subsequent elsif/else commands might generate more) */
		sieve_jumplist_resolve(&jmplist);
	}

	return TRUE;
}

static bool
cmd_else_generate(const struct sieve_codegen_env *cgenv,
		  struct sieve_command *cmd)
{
	struct cmd_if_context_data *cmd_data =
		(struct cmd_if_context_data *)cmd->data;

	/* Else { */
	if (cmd_data->const_condition != 0) {
		if (!sieve_generate_block(cgenv, cmd->ast_node))
			return FALSE;

		/* } End: resolve all exit blocks */
		cmd_if_resolve_exit_jumps(cgenv->sblock, cmd_data);
	}

	return TRUE;
}
