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

#include "lib.h"
#include "str.h"

#include "sieve-common.h"
#include "sieve-error.h"
#include "sieve-script.h"
#include "sieve-storage.h"
#include "sieve-binary.h"
#include "sieve-generator.h"
#include "sieve-interpreter.h"
#include "sieve-dump.h"

#include "sieve-ext-variables.h"

#include "ext-include-common.h"
#include "ext-include-limits.h"
#include "ext-include-variables.h"
#include "ext-include-binary.h"

/*
 * Forward declarations
 */

static bool ext_include_binary_pre_save
	(const struct sieve_extension *ext, struct sieve_binary *sbin,
		void *context, enum sieve_error *error_r);
static bool ext_include_binary_open
	(const struct sieve_extension *ext, struct sieve_binary *sbin,
		void *context);
static bool ext_include_binary_up_to_date
	(const struct sieve_extension *ext, struct sieve_binary *sbin,
		void *context, enum sieve_compile_flags cpflags);
static void ext_include_binary_free
	(const struct sieve_extension *ext, struct sieve_binary *sbin,
		void *context);

/*
 * Binary include extension
 */

const struct sieve_binary_extension include_binary_ext = {
	.extension = &include_extension,
	.binary_pre_save = ext_include_binary_pre_save,
	.binary_open = ext_include_binary_open,
	.binary_free = ext_include_binary_free,
	.binary_up_to_date = ext_include_binary_up_to_date
};

/*
 * Binary context management
 */

struct ext_include_binary_context {
	struct sieve_binary *binary;
	struct sieve_binary_block *dependency_block;

	HASH_TABLE(struct sieve_script *,
		   struct ext_include_script_info *) included_scripts;
	ARRAY(struct ext_include_script_info *) include_index;

	struct sieve_variable_scope_binary *global_vars;

	bool outdated:1;
};

static struct ext_include_binary_context *ext_include_binary_create_context
(const struct sieve_extension *this_ext, struct sieve_binary *sbin)
{
	pool_t pool = sieve_binary_pool(sbin);

	struct ext_include_binary_context *ctx =
		p_new(pool, struct ext_include_binary_context, 1);

	ctx->binary = sbin;
	hash_table_create(&ctx->included_scripts, pool, 0,
		sieve_script_hash, sieve_script_cmp);
	p_array_init(&ctx->include_index, pool, 128);

	sieve_binary_extension_set(sbin, this_ext, &include_binary_ext, ctx);

	return ctx;
}

struct ext_include_binary_context *ext_include_binary_get_context
(const struct sieve_extension *this_ext, struct sieve_binary *sbin)
{
	struct ext_include_binary_context *ctx = (struct ext_include_binary_context *)
		sieve_binary_extension_get_context(sbin, this_ext);

	if ( ctx == NULL )
		ctx = ext_include_binary_create_context(this_ext, sbin);

	return ctx;
}

struct ext_include_binary_context *ext_include_binary_init
(const struct sieve_extension *this_ext, struct sieve_binary *sbin,
	struct sieve_ast *ast)
{
	struct ext_include_ast_context *ast_ctx =
		ext_include_get_ast_context(this_ext, ast);
	struct ext_include_binary_context *ctx;

	/* Get/create our context from the binary we are working on */
	ctx = ext_include_binary_get_context(this_ext, sbin);

	/* Create dependency block */
	if ( ctx->dependency_block == 0 )
		ctx->dependency_block =
			sieve_binary_extension_create_block(sbin, this_ext);

	if ( ctx->global_vars == NULL ) {
		ctx->global_vars =
			sieve_variable_scope_binary_create(ast_ctx->global_vars);
		sieve_variable_scope_binary_ref(ctx->global_vars);
	}

	return ctx;
}

/*
 * Script inclusion
 */

struct ext_include_script_info *ext_include_binary_script_include
(struct ext_include_binary_context *binctx, 
	enum ext_include_script_location location, enum ext_include_flags flags,
	struct sieve_script *script,	struct sieve_binary_block *inc_block)
{
	pool_t pool = sieve_binary_pool(binctx->binary);
	struct ext_include_script_info *incscript;

	incscript = p_new(pool, struct ext_include_script_info, 1);
	incscript->id = array_count(&binctx->include_index)+1;
	incscript->location = location;
	incscript->flags = flags;
	incscript->script = script;
	incscript->block = inc_block;

	/* Unreferenced on binary_free */
	sieve_script_ref(script);

	hash_table_insert(binctx->included_scripts, script, incscript);
	array_append(&binctx->include_index, &incscript, 1);

	return incscript;
}

struct ext_include_script_info *ext_include_binary_script_get_include_info
(struct ext_include_binary_context *binctx, struct sieve_script *script)
{
	struct ext_include_script_info *incscript =
		hash_table_lookup(binctx->included_scripts, script);

	return incscript;
}

const struct ext_include_script_info *ext_include_binary_script_get_included
(struct ext_include_binary_context *binctx, unsigned int include_id)
{
	if ( include_id > 0 &&
		(include_id - 1) < array_count(&binctx->include_index) ) {
		struct ext_include_script_info *const *sinfo =
			array_idx(&binctx->include_index, include_id - 1);

		return *sinfo;
	}

	return NULL;
}

const struct ext_include_script_info *ext_include_binary_script_get
(struct ext_include_binary_context *binctx, struct sieve_script *script)
{
	return hash_table_lookup(binctx->included_scripts, script);
}

unsigned int ext_include_binary_script_get_count
(struct ext_include_binary_context *binctx)
{
	return array_count(&binctx->include_index);
}

/*
 * Variables
 */

struct sieve_variable_scope_binary *ext_include_binary_get_global_scope
(const struct sieve_extension *this_ext, struct sieve_binary *sbin)
{
	struct ext_include_binary_context *binctx =
		ext_include_binary_get_context(this_ext, sbin);

	return binctx->global_vars;
}

/*
 * Binary extension
 */

static bool ext_include_binary_pre_save
(const struct sieve_extension *ext ATTR_UNUSED,
	struct sieve_binary *sbin ATTR_UNUSED, void *context,
	enum sieve_error *error_r)
{
	struct ext_include_binary_context *binctx =
		(struct ext_include_binary_context *) context;
	struct ext_include_script_info *const *scripts;
	struct sieve_binary_block *sblock = binctx->dependency_block;
	unsigned int script_count, i;
	bool result = TRUE;

	sieve_binary_block_clear(sblock);

	scripts = array_get(&binctx->include_index, &script_count);

	sieve_binary_emit_unsigned(sblock, script_count);

	for ( i = 0; i < script_count; i++ ) {
		struct ext_include_script_info *incscript = scripts[i];

		if ( incscript->block != NULL ) {
			sieve_binary_emit_unsigned
				(sblock, sieve_binary_block_get_id(incscript->block));
		} else {
			sieve_binary_emit_unsigned(sblock, 0);
		}
		sieve_binary_emit_byte(sblock, incscript->location);
		sieve_binary_emit_cstring(sblock, sieve_script_name(incscript->script));
		sieve_binary_emit_byte(sblock, incscript->flags);
		sieve_script_binary_write_metadata(incscript->script, sblock);
	}

	result = ext_include_variables_save(sblock, binctx->global_vars, error_r);

	return result;
}

static bool ext_include_binary_open
(const struct sieve_extension *ext, struct sieve_binary *sbin, void *context)
{
	struct sieve_instance *svinst = ext->svinst;
	struct ext_include_context *ext_ctx =
		(struct ext_include_context *)ext->context;
	struct ext_include_binary_context *binctx =
		(struct ext_include_binary_context *) context;
	struct sieve_binary_block *sblock;
	unsigned int depcount, i, block_id;
	sieve_size_t offset;

	sblock = sieve_binary_extension_get_block(sbin, ext);
	block_id = sieve_binary_block_get_id(sblock);

	offset = 0;

	if ( !sieve_binary_read_unsigned(sblock, &offset, &depcount) ) {
		e_error(svinst->event,
			"include: failed to read include count "
			"for dependency block %d of binary %s", block_id,
			sieve_binary_path(sbin));
		return FALSE;
	}

	/* Check include limit */
	if ( depcount > ext_ctx->max_includes ) {
		e_error(svinst->event,
			"include: binary %s includes too many scripts (%u > %u)",
			sieve_binary_path(sbin), depcount, ext_ctx->max_includes);
		return FALSE;
	}

	/* Read dependencies */
	for ( i = 0; i < depcount; i++ ) {
		unsigned int inc_block_id;
		struct sieve_binary_block *inc_block = NULL;
		unsigned int location, flags;
		string_t *script_name;
		struct sieve_storage *storage;
		struct sieve_script *script;
		enum sieve_error error;
		int ret;

		if (
			!sieve_binary_read_unsigned(sblock, &offset, &inc_block_id) ||
			!sieve_binary_read_byte(sblock, &offset, &location) ||
			!sieve_binary_read_string(sblock, &offset, &script_name) ||
			!sieve_binary_read_byte(sblock, &offset, &flags) ) {
			/* Binary is corrupt, recompile */
			e_error(svinst->event,
				"include: failed to read included script "
				"from dependency block %d of binary %s",
				block_id, sieve_binary_path(sbin));
			return FALSE;
		}

		if ( inc_block_id != 0 &&
			(inc_block=sieve_binary_block_get(sbin, inc_block_id)) == NULL ) {
			e_error(svinst->event,
				"include: failed to find block %d for included script "
				"from dependency block %d of binary %s",
				inc_block_id, block_id,
				sieve_binary_path(sbin));
			return FALSE;
		}

		if ( location >= EXT_INCLUDE_LOCATION_INVALID ) {
			/* Binary is corrupt, recompile */
			e_error(svinst->event,
				"include: dependency block %d of binary %s "
				"uses invalid script location (id %d)",
				block_id, sieve_binary_path(sbin), location);
			return FALSE;
		}

		/* Can we find the script dependency ? */
		storage = ext_include_get_script_storage
			(ext, location, str_c(script_name), &error);
		if ( storage == NULL ) {
			/* No, recompile */
			// FIXME: handle ':optional' in this case
			return FALSE;
		}

		/* Can we open the script dependency ? */
		script = sieve_storage_get_script
			(storage, str_c(script_name), &error);
		if ( script == NULL ) {
			/* No, recompile */
			return FALSE;
		}
		if ( sieve_script_open(script, &error) < 0 ) {
			if ( error != SIEVE_ERROR_NOT_FOUND ) {
				/* No, recompile */
				return FALSE;
			}

			if ( (flags & EXT_INCLUDE_FLAG_OPTIONAL) == 0 ) {
				/* Not supposed to be missing, recompile */
				if ( svinst->debug ) {
					e_debug(svinst->event, "include: "
						"script '%s' included in binary %s is missing, "
						"so recompile",
						str_c(script_name),
						sieve_binary_path(sbin));
				}
				return FALSE;
			}

		} else if (inc_block == NULL) {
			/* Script exists, but it is missing from the binary, recompile no matter
			 * what.
			 */
			if ( svinst->debug ) {
				e_debug(svinst->event, "include: "
					"script '%s' is missing in binary %s, but is now available, "
					"so recompile", str_c(script_name), sieve_binary_path(sbin));
			}
			sieve_script_unref(&script);
			return FALSE;
		}

		/* Can we read script metadata ? */
		if ( (ret=sieve_script_binary_read_metadata
			(script, sblock, &offset))	< 0 ) {
			/* Binary is corrupt, recompile */
			e_error(svinst->event, "include: "
				"dependency block %d of binary %s "
				"contains invalid script metadata for script %s",
				block_id, sieve_binary_path(sbin),
				sieve_script_location(script));
			sieve_script_unref(&script);
			return FALSE;
		}

		if ( ret == 0 )
			binctx->outdated = TRUE;

		(void)ext_include_binary_script_include
			(binctx, location, flags, script, inc_block);

		sieve_script_unref(&script);
	}

	if ( !ext_include_variables_load
		(ext, sblock, &offset, &binctx->global_vars) )
		return FALSE;

	return TRUE;
}

static bool ext_include_binary_up_to_date
(const struct sieve_extension *ext ATTR_UNUSED,
	struct sieve_binary *sbin ATTR_UNUSED, void *context,
	enum sieve_compile_flags cpflags ATTR_UNUSED)
{
	struct ext_include_binary_context *binctx =
		(struct ext_include_binary_context *) context;

	return !binctx->outdated;
}

static void ext_include_binary_free
(const struct sieve_extension *ext ATTR_UNUSED,
	struct sieve_binary *sbin ATTR_UNUSED, void *context)
{
	struct ext_include_binary_context *binctx =
		(struct ext_include_binary_context *) context;
	struct hash_iterate_context *hctx;
	struct sieve_script *script;
	struct ext_include_script_info *incscript;

	/* Release references to all included script objects */
	hctx = hash_table_iterate_init(binctx->included_scripts);
	while ( hash_table_iterate
		(hctx, binctx->included_scripts, &script, &incscript) )
		sieve_script_unref(&incscript->script);
	hash_table_iterate_deinit(&hctx);

	hash_table_destroy(&binctx->included_scripts);

	if ( binctx->global_vars != NULL )
		sieve_variable_scope_binary_unref(&binctx->global_vars);
}

/*
 * Dumping the binary
 */

bool ext_include_binary_dump
(const struct sieve_extension *ext, struct sieve_dumptime_env *denv)
{
	struct sieve_binary *sbin = denv->sbin;
	struct ext_include_binary_context *binctx =
		ext_include_binary_get_context(ext, sbin);
	struct hash_iterate_context *hctx;
	struct sieve_script *script;
	struct ext_include_script_info *incscript;

	if ( !ext_include_variables_dump(denv, binctx->global_vars) )
		return FALSE;

	hctx = hash_table_iterate_init(binctx->included_scripts);
	while ( hash_table_iterate
		(hctx, binctx->included_scripts, &script, &incscript) ) {

		if ( incscript->block == NULL ) {
			sieve_binary_dump_sectionf(denv, "Included %s script '%s' (MISSING)",
				ext_include_script_location_name(incscript->location),
				sieve_script_name(incscript->script));

		} else {
			unsigned int block_id = sieve_binary_block_get_id(incscript->block);

			sieve_binary_dump_sectionf(denv, "Included %s script '%s' (block: %d)",
				ext_include_script_location_name(incscript->location),
				sieve_script_name(incscript->script), block_id);

			denv->sblock = incscript->block;
			denv->cdumper = sieve_code_dumper_create(denv);

			if ( denv->cdumper == NULL )
				return FALSE;

			sieve_code_dumper_run(denv->cdumper);
			sieve_code_dumper_free(&(denv->cdumper));
		}
	}
	hash_table_iterate_deinit(&hctx);

	return TRUE;
}

bool ext_include_code_dump
(const struct sieve_extension *ext, const struct sieve_dumptime_env *denv,
	sieve_size_t *address ATTR_UNUSED)
{
	struct sieve_binary *sbin = denv->sbin;
	struct ext_include_binary_context *binctx =
		ext_include_binary_get_context(ext, sbin);
	struct ext_include_context *ectx = ext_include_get_context(ext);

	sieve_ext_variables_dump_set_scope
		(ectx->var_ext, denv, ext,
			sieve_variable_scope_binary_get(binctx->global_vars));

	return TRUE;
}


