/* Copyright (c) 2002-2018 Pigeonhole authors, see the included 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;
}

