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

		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]");
}
