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

#include <stdio.h>
#include <string.h>

#include "lib.h"
#include "str.h"
#include "mempool.h"
#include "ostream.h"

#include "sieve-common.h"
#include "sieve-extensions.h"
#include "sieve-commands.h"
#include "sieve-code.h"
#include "sieve-actions.h"
#include "sieve-generator.h"
#include "sieve-binary.h"
#include "sieve-result.h"
#include "sieve-comparators.h"

#include "sieve-dump.h"

/*
 * Code dumper extension
 */

struct sieve_code_dumper_extension_reg {
	const struct sieve_code_dumper_extension *cdmpext;
	const struct sieve_extension *ext;
	void *context;
};

struct sieve_code_dumper {
	pool_t pool;

	/* Dump status */
	struct sieve_operation oprtn;
	sieve_size_t mark_address;
	unsigned int mark_line;
	unsigned int mark_last_line;
	unsigned int indent;

	/* Dump environment */
	struct sieve_dumptime_env *dumpenv;

	struct sieve_binary_debug_reader *dreader;

	ARRAY(struct sieve_code_dumper_extension_reg) extensions;
};

struct sieve_code_dumper *sieve_code_dumper_create
(struct sieve_dumptime_env *denv)
{
	pool_t pool;
	struct sieve_code_dumper *cdumper;

	pool = pool_alloconly_create("sieve_code_dumper", 4096);
	cdumper = p_new(pool, struct sieve_code_dumper, 1);
	cdumper->pool = pool;
	cdumper->dumpenv = denv;

	/* Setup storage for extension contexts */
	p_array_init(&cdumper->extensions, pool,
		sieve_extensions_get_count(denv->svinst));

	return cdumper;
}

void sieve_code_dumper_free(struct sieve_code_dumper **_cdumper)
{
	struct sieve_code_dumper *cdumper = *_cdumper;
	const struct sieve_code_dumper_extension_reg *eregs;
	unsigned int count, i;

	sieve_binary_debug_reader_deinit(&cdumper->dreader);

	/* Signal registered extensions that the dumper is being destroyed */
	eregs = array_get(&cdumper->extensions, &count);
	for (i = 0; i < count; i++) {
		if (eregs[i].cdmpext != NULL && eregs[i].cdmpext->free != NULL)
			eregs[i].cdmpext->free(cdumper, eregs[i].context);
	}

	pool_unref(&cdumper->pool);
	*_cdumper = NULL;
}

pool_t sieve_code_dumper_pool(struct sieve_code_dumper *cdumper)
{
	return cdumper->pool;
}

/* EXtension support */

void sieve_dump_extension_register(
	struct sieve_code_dumper *cdumper, const struct sieve_extension *ext,
	const struct sieve_code_dumper_extension *cdmpext, void *context)
{
	struct sieve_code_dumper_extension_reg *reg;

	if (ext->id < 0)
		return;

	reg = array_idx_get_space(&cdumper->extensions, (unsigned int) ext->id);
	reg->cdmpext = cdmpext;
	reg->ext = ext;
	reg->context = context;
}

void sieve_dump_extension_set_context(struct sieve_code_dumper *cdumper,
				      const struct sieve_extension *ext,
				      void *context)
{
	struct sieve_code_dumper_extension_reg *reg;

	if (ext->id < 0)
		return;

	reg = array_idx_get_space(&cdumper->extensions, (unsigned int) ext->id);
	reg->context = context;
}

void *sieve_dump_extension_get_context(struct sieve_code_dumper *cdumper,
				       const struct sieve_extension *ext)
{
	const struct sieve_code_dumper_extension_reg *reg;

	if  (ext->id < 0 ||
	     ext->id >= (int) array_count(&cdumper->extensions))
		return NULL;

	reg = array_idx(&cdumper->extensions, (unsigned int) ext->id);
	return reg->context;
}

/* Dump functions */

void sieve_code_dumpf(const struct sieve_dumptime_env *denv,
		      const char *fmt, ...)
{
	struct sieve_code_dumper *cdumper = denv->cdumper;
	unsigned tab = cdumper->indent;

	string_t *outbuf = t_str_new(128);
	va_list args;

	va_start(args, fmt);
	str_printfa(outbuf, "%08llx: ",
		    (unsigned long long) cdumper->mark_address);

	if (cdumper->mark_line > 0 &&
	    (cdumper->indent == 0 ||
	     cdumper->mark_line != cdumper->mark_last_line)) {
		str_printfa(outbuf, "%4u: ", cdumper->mark_line);
		cdumper->mark_last_line = cdumper->mark_line;
	} else {
		str_append(outbuf, "      ");
	}

	while (tab > 0)	{
		str_append(outbuf, "  ");
		tab--;
	}

	str_vprintfa(outbuf, fmt, args);
	str_append_c(outbuf, '\n');
	va_end(args);

	o_stream_nsend(denv->stream, str_data(outbuf), str_len(outbuf));
}

static inline void
sieve_code_line_mark(const struct sieve_dumptime_env *denv,
		     sieve_size_t location)
{
	if (denv->cdumper->dreader != NULL)  {
		denv->cdumper->mark_line = sieve_binary_debug_read_line(
			denv->cdumper->dreader, location);
	}
}

void sieve_code_mark(const struct sieve_dumptime_env *denv)
{
	denv->cdumper->mark_address = denv->offset;
	sieve_code_line_mark(denv, denv->offset);
}

void sieve_code_mark_specific(const struct sieve_dumptime_env *denv,
			      sieve_size_t location)
{
	denv->cdumper->mark_address = location;
	sieve_code_line_mark(denv, location);
}

void sieve_code_descend(const struct sieve_dumptime_env *denv)
{
	denv->cdumper->indent++;
}

void sieve_code_ascend(const struct sieve_dumptime_env *denv)
{
	if (denv->cdumper->indent > 0)
		denv->cdumper->indent--;
}

/* Code Dump */

static bool
sieve_code_dumper_print_operation(struct sieve_code_dumper *cdumper)
{
	struct sieve_dumptime_env *denv = cdumper->dumpenv;
	struct sieve_operation *oprtn = &(cdumper->oprtn);
	sieve_size_t *address	= &(denv->offset);

	/* Mark start address of operation */
	cdumper->indent = 0;
	cdumper->mark_address = *address;

	sieve_code_line_mark(denv, *address);

	/* Read operation */
	if (sieve_operation_read(denv->sblock, address, oprtn)) {
		const struct sieve_operation_def *opdef = oprtn->def;

		i_assert(opdef != NULL);
		if (opdef->dump != NULL)
			return opdef->dump(denv, address);
		else if (opdef->mnemonic != NULL)
			sieve_code_dumpf(denv, "%s", opdef->mnemonic);
		else
			return FALSE;

		return TRUE;
	}

	sieve_code_dumpf(denv, "Failed to read opcode.");
	return FALSE;
}

static bool
sieve_code_dumper_print_extension(struct sieve_code_dumper *cdumper)
{
	struct sieve_dumptime_env *denv = cdumper->dumpenv;
	sieve_size_t *address = &(denv->offset);
	struct sieve_binary_block *sblock = denv->sblock;
	unsigned int code = 0, deferred;
	const struct sieve_extension *ext;

	sieve_code_mark(denv);

	if (!sieve_binary_read_extension(sblock, address, &code, &ext) ||
	    !sieve_binary_read_byte(sblock, address, &deferred)) {
		return FALSE;
	}

	if (ext->def == NULL) {
		sieve_code_dumpf(denv, "[undefined]");
	} else {
		sieve_code_dumpf(denv, "%s%s", sieve_extension_name(ext),
				 (deferred > 0 ? " (deferred)" : ""));

		if (ext->def->code_dump != NULL) {
			sieve_code_descend(denv);
			if (!ext->def->code_dump(ext, denv, address))
				return FALSE;
			sieve_code_ascend(denv);
		}
	}
	return TRUE;
}

void sieve_code_dumper_run(struct sieve_code_dumper *cdumper)
{
	struct sieve_dumptime_env *denv = cdumper->dumpenv;
	struct sieve_binary *sbin = denv->sbin;
	struct sieve_binary_block *sblock = denv->sblock;
	unsigned int debug_block_id, ext_count;
	bool success;
	sieve_size_t *address;

	denv->offset = 0;
	denv->oprtn = &(cdumper->oprtn);
	address = &(denv->offset);

	/* Heading */
	o_stream_nsend_str(denv->stream, "Address   Line  Code\n");

	/* Load debug block */
	sieve_code_mark(denv);

	if (sieve_binary_read_unsigned(sblock, address, &debug_block_id)) {
		struct sieve_binary_block *debug_block =
			sieve_binary_block_get(sbin, debug_block_id);

		if (debug_block == NULL) {
			sieve_code_dumpf(denv, "Invalid id %d for debug block.",
					 debug_block_id);
			return;
		} else {
			/* Initialize debug reader */
			cdumper->dreader =
				sieve_binary_debug_reader_init(debug_block);

			/* Dump block id */
			sieve_code_dumpf(denv, "DEBUG BLOCK: %d",
					 debug_block_id);
		}
	} else {
		sieve_code_dumpf(denv, "Binary code header is corrupt.");
		return;
	}

	/* Load and dump extensions listed in code */
	sieve_code_mark(denv);

	success = TRUE;
	if (sieve_binary_read_unsigned(sblock, address, &ext_count)) {
		unsigned int i;

		sieve_code_dumpf(denv, "EXTENSIONS [%d]:", ext_count);
		sieve_code_descend(denv);

		for (i = 0; success && (i < ext_count); i++) T_BEGIN {
			success = success &&
				sieve_code_dumper_print_extension(cdumper);
		} T_END;

		sieve_code_ascend(denv);
	} else {
		success = FALSE;
	}

	if (!success) {
		sieve_code_dumpf(denv, "Binary code header is corrupt.");
		return;
	}

	while (*address < sieve_binary_block_get_size(sblock)) {

		T_BEGIN {
			success = sieve_code_dumper_print_operation(cdumper);
		} T_END;

		if (!success) {
			sieve_code_dumpf(denv, "Binary is corrupt.");
			return;
		}
	}

	/* Mark end of the binary */
	cdumper->indent = 0;
	cdumper->mark_address = sieve_binary_block_get_size(sblock);
	sieve_code_dumpf(denv, "[End of code]");
}
