/*
 * Copyright (C) 2015 Tobias Brunner
 * Hochschule fuer Technik Rapperswil
 *
 * Copyright (C) 2014 Martin Willi
 * Copyright (C) 2014 revosec AG
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation; either version 2 of the License, or (at your
 * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * for more details.
 */

#include "vici_message.h"
#include "vici_builder.h"

#include <bio/bio_reader.h>
#include <bio/bio_writer.h>

#include <errno.h>

typedef struct private_vici_message_t private_vici_message_t;

/**
 * Private data of an vici_message_t object.
 */
struct private_vici_message_t {

	/**
	 * Public vici_message_t interface.
	 */
	vici_message_t public;

	/**
	 * Message encoding
	 */
	chunk_t encoding;

	/**
	 * Free encoding during destruction?
	 */
	bool cleanup;

	/**
	 * Allocated strings we maintain for get_str()
	 */
	linked_list_t *strings;
};

ENUM(vici_type_names, VICI_START, VICI_END,
	"start",
	"section-start",
	"section-end",
	"key-value",
	"list-start",
	"list-item",
	"list-end",
	"end"
);

/**
 * See header.
 */
bool vici_stringify(chunk_t chunk, char *buf, size_t size)
{
	if (!chunk_printable(chunk, NULL, 0))
	{
		return FALSE;
	}
	snprintf(buf, size, "%.*s", (int)chunk.len, chunk.ptr);
	return TRUE;
}

/**
 * See header.
 */
bool vici_verify_type(vici_type_t type, u_int section, bool list)
{
	if (list)
	{
		if (type != VICI_LIST_END && type != VICI_LIST_ITEM)
		{
			DBG1(DBG_ENC, "'%N' within list", vici_type_names, type);
			return FALSE;
		}
	}
	else
	{
		if (type == VICI_LIST_ITEM || type == VICI_LIST_END)
		{
			DBG1(DBG_ENC, "'%N' outside list", vici_type_names, type);
			return FALSE;
		}
	}
	if (type == VICI_SECTION_END && section == 0)
	{
		DBG1(DBG_ENC, "'%N' outside of section", vici_type_names, type);
		return FALSE;
	}
	if (type == VICI_END)
	{
		if (section)
		{
			DBG1(DBG_ENC, "'%N' within section", vici_type_names, type);
			return FALSE;
		}
		if (list)
		{
			DBG1(DBG_ENC, "'%N' within list", vici_type_names, type);
			return FALSE;
		}
	}
	return TRUE;
}

/**
 * Enumerator parsing message
 */
typedef struct {
	/* implements enumerator */
	enumerator_t public;
	/** reader to parse from */
	bio_reader_t *reader;
	/** section nesting level */
	int section;
	/** currently parsing list? */
	bool list;
	/** string currently enumerating */
	char name[257];
} parse_enumerator_t;

METHOD(enumerator_t, parse_enumerate, bool,
	parse_enumerator_t *this, vici_type_t *out, char **name, chunk_t *value)
{
	u_int8_t type;
	chunk_t data;

	if (!this->reader->remaining(this->reader) ||
		!this->reader->read_uint8(this->reader, &type))
	{
		*out = VICI_END;
		return TRUE;
	}
	if (!vici_verify_type(type, this->section, this->list))
	{
		return FALSE;
	}

	switch (type)
	{
		case VICI_SECTION_START:
			if (!this->reader->read_data8(this->reader, &data) ||
				!vici_stringify(data, this->name, sizeof(this->name)))
			{
				DBG1(DBG_ENC, "invalid '%N' encoding", vici_type_names, type);
				return FALSE;
			}
			*name = this->name;
			this->section++;
			break;
		case VICI_SECTION_END:
			this->section--;
			break;
		case VICI_KEY_VALUE:
			if (!this->reader->read_data8(this->reader, &data) ||
				!vici_stringify(data, this->name, sizeof(this->name)) ||
				!this->reader->read_data16(this->reader, value))
			{
				DBG1(DBG_ENC, "invalid '%N' encoding", vici_type_names, type);
				return FALSE;
			}
			*name = this->name;
			break;
		case VICI_LIST_START:
			if (!this->reader->read_data8(this->reader, &data) ||
				!vici_stringify(data, this->name, sizeof(this->name)))
			{
				DBG1(DBG_ENC, "invalid '%N' encoding", vici_type_names, type);
				return FALSE;
			}
			*name = this->name;
			this->list = TRUE;
			break;
		case VICI_LIST_ITEM:
			this->reader->read_data16(this->reader, value);
			break;
		case VICI_LIST_END:
			this->list = FALSE;
			break;
		case VICI_END:
			return TRUE;
		default:
			DBG1(DBG_ENC, "unknown encoding type: %u", type);
			return FALSE;
	}

	*out = type;

	return TRUE;
}

METHOD(enumerator_t, parse_destroy, void,
	parse_enumerator_t *this)
{
	this->reader->destroy(this->reader);
	free(this);
}

METHOD(vici_message_t, create_enumerator, enumerator_t*,
	private_vici_message_t *this)
{
	parse_enumerator_t *enumerator;

	INIT(enumerator,
		.public = {
			.enumerate = (void*)_parse_enumerate,
			.destroy = _parse_destroy,
		},
		.reader = bio_reader_create(this->encoding),
	);

	return &enumerator->public;
}

/**
 * Find a value for given vararg key
 */
static bool find_value(private_vici_message_t *this, chunk_t *value,
					   char *fmt, va_list args)
{
	enumerator_t *enumerator;
	char buf[128], *name, *key, *dot, *next;
	int section = 0, keysection = 0;
	bool found = FALSE;
	chunk_t current;
	vici_type_t type;

	vsnprintf(buf, sizeof(buf), fmt, args);
	next = buf;

	enumerator = create_enumerator(this);

	/* descent into section */
	while (TRUE)
	{
		dot = strchr(next, '.');
		if (!dot)
		{
			key = next;
			break;
		}
		*dot = '\0';
		key = next;
		next = dot + 1;
		keysection++;

		while (enumerator->enumerate(enumerator, &type, &name, &current))
		{
			switch (type)
			{
				case VICI_SECTION_START:
					section++;
					if (section == keysection && streq(name, key))
					{
						break;
					}
					continue;
				case VICI_SECTION_END:
					section--;
					continue;
				case VICI_END:
					break;
				default:
					continue;
			}
			break;
		}
	}

	/* find key/value in current section */
	while (enumerator->enumerate(enumerator, &type, &name, &current))
	{
		switch (type)
		{
			case VICI_KEY_VALUE:
				if (section == keysection && streq(key, name))
				{
					*value = current;
					found = TRUE;
					break;
				}
				continue;
			case VICI_SECTION_START:
				section++;
				continue;
			case VICI_SECTION_END:
				section--;
				continue;
			case VICI_END:
				break;
			default:
				continue;
		}
		break;
	}

	enumerator->destroy(enumerator);

	return found;
}

METHOD(vici_message_t, vget_str, char*,
	private_vici_message_t *this, char *def, char *fmt, va_list args)
{
	chunk_t value;
	bool found;
	char *str;

	found = find_value(this, &value, fmt, args);
	if (found)
	{
		if (chunk_printable(value, NULL, 0))
		{
			str = strndup(value.ptr, value.len);
			/* keep a reference to string, so caller doesn't have to care */
			this->strings->insert_last(this->strings, str);
			return str;
		}
	}
	return def;
}

METHOD(vici_message_t, get_str, char*,
	private_vici_message_t *this, char *def, char *fmt, ...)
{
	va_list args;
	char *str;

	va_start(args, fmt);
	str = vget_str(this, def, fmt, args);
	va_end(args);
	return str;
}

METHOD(vici_message_t, vget_int, int,
	private_vici_message_t *this, int def, char *fmt, va_list args)
{
	chunk_t value;
	bool found;
	char buf[32], *pos;
	int ret;

	found = find_value(this, &value, fmt, args);
	if (found)
	{
		if (value.len == 0)
		{
			return def;
		}
		if (chunk_printable(value, NULL, 0))
		{
			snprintf(buf, sizeof(buf), "%.*s", (int)value.len, value.ptr);
			errno = 0;
			ret = strtol(buf, &pos, 0);
			if (errno == 0 && pos == buf + strlen(buf))
			{
				return ret;
			}
		}
	}
	return def;
}

METHOD(vici_message_t, get_int, int,
	private_vici_message_t *this, int def, char *fmt, ...)
{
	va_list args;
	int val;

	va_start(args, fmt);
	val = vget_int(this, def, fmt, args);
	va_end(args);
	return val;
}

METHOD(vici_message_t, vget_bool, bool,
	private_vici_message_t *this, bool def, char *fmt, va_list args)
{
	chunk_t value;
	bool found;
	char buf[16];

	found = find_value(this, &value, fmt, args);
	if (found)
	{
		if (value.len == 0)
		{
			return def;
		}
		if (chunk_printable(value, NULL, 0))
		{
			snprintf(buf, sizeof(buf), "%.*s", (int)value.len, value.ptr);
			return settings_value_as_bool(buf, def);
		}
	}
	return def;
}

METHOD(vici_message_t, get_bool, bool,
	private_vici_message_t *this, bool def, char *fmt, ...)
{
	va_list args;
	bool val;

	va_start(args, fmt);
	val = vget_bool(this, def, fmt, args);
	va_end(args);
	return val;
}

METHOD(vici_message_t, vget_value, chunk_t,
	private_vici_message_t *this, chunk_t def, char *fmt, va_list args)
{
	chunk_t value;
	bool found;

	found = find_value(this, &value, fmt, args);
	if (found)
	{
		return value;
	}
	return def;
}

METHOD(vici_message_t, get_value, chunk_t,
	private_vici_message_t *this, chunk_t def, char *fmt, ...)
{
	va_list args;
	chunk_t value;

	va_start(args, fmt);
	value = vget_value(this, def, fmt, args);
	va_end(args);
	return value;
}

METHOD(vici_message_t, get_encoding, chunk_t,
	private_vici_message_t *this)
{
	return this->encoding;
}

/**
 * Private parse context data
 */
struct vici_parse_context_t {
	/** current section nesting level */
	int level;
	/** parse enumerator */
	enumerator_t *e;
};

METHOD(vici_message_t, parse, bool,
	private_vici_message_t *this, vici_parse_context_t *ctx,
	vici_section_cb_t section, vici_value_cb_t kv, vici_value_cb_t li,
	void *user)
{
	vici_parse_context_t root = {};
	char *name, *list = NULL;
	vici_type_t type;
	chunk_t value;
	int base;
	bool ok = TRUE;

	if (!ctx)
	{
		ctx = &root;
		root.e = create_enumerator(this);
	}

	base = ctx->level;

	while (ok)
	{
		ok = ctx->e->enumerate(ctx->e, &type, &name, &value);
		if (ok)
		{
			switch (type)
			{
				case VICI_START:
					/* should never occur */
					continue;
				case VICI_KEY_VALUE:
					if (ctx->level == base && kv)
					{
						name = strdup(name);
						this->strings->insert_last(this->strings, name);
						ok = kv(user, &this->public, name, value);
					}
					continue;
				case VICI_LIST_START:
					if (ctx->level == base)
					{
						list = strdup(name);
						this->strings->insert_last(this->strings, list);
					}
					continue;
				case VICI_LIST_ITEM:
					if (list && li)
					{
						name = strdup(name);
						this->strings->insert_last(this->strings, name);
						ok = li(user, &this->public, list, value);
					}
					continue;
				case VICI_LIST_END:
					if (ctx->level == base)
					{
						list = NULL;
					}
					continue;
				case VICI_SECTION_START:
					if (ctx->level++ == base && section)
					{
						name = strdup(name);
						this->strings->insert_last(this->strings, name);
						ok = section(user, &this->public, ctx, name);
					}
					continue;
				case VICI_SECTION_END:
					if (ctx->level-- == base)
					{
						break;
					}
					continue;
				case VICI_END:
					break;
			}
		}
		break;
	}

	if (ctx == &root)
	{
		root.e->destroy(root.e);
	}
	return ok;
}

METHOD(vici_message_t, dump, bool,
	private_vici_message_t *this, char *label, bool pretty, FILE *out)
{
	enumerator_t *enumerator;
	int ident = 0, delta;
	vici_type_t type, last_type = VICI_START;
	char *name, *term, *sep, *separ, *assign;
	chunk_t value;

	/* pretty print uses indentation on multiple lines */
	if (pretty)
	{
		delta  = 2;
		term   = "\n";
		separ  = "";
		assign = " = ";
	}
	else
	{
		delta  = 0;
		term   = "";
		separ  = " ";
		assign = "=";
	}

	fprintf(out, "%s {%s", label, term);
	ident += delta;

	enumerator = create_enumerator(this);
	while (enumerator->enumerate(enumerator, &type, &name, &value))
	{
		switch (type)
		{
			case VICI_START:
				/* should never occur */
				break;
			case VICI_SECTION_START:
				sep = (last_type != VICI_SECTION_START &&
					   last_type != VICI_START) ? separ : "";
				fprintf(out, "%*s%s%s {%s", ident, "", sep, name, term);
				ident += delta;
				break;
			case VICI_SECTION_END:
				ident -= delta;
				fprintf(out, "%*s}%s", ident, "", term);
				break;
			case VICI_KEY_VALUE:
				sep = (last_type != VICI_SECTION_START &&
					   last_type != VICI_START) ? separ : "";
				if (chunk_printable(value, NULL, ' '))
				{
					fprintf(out, "%*s%s%s%s%.*s%s", ident, "", sep, name,
							assign, (int)value.len, value.ptr, term);
				}
				else
				{
					fprintf(out, "%*s%s%s%s0x%+#B%s", ident, "", sep, name,
							assign, &value, term);
				}
				break;
			case VICI_LIST_START:
				sep = (last_type != VICI_SECTION_START &&
					   last_type != VICI_START) ? separ : "";
				fprintf(out, "%*s%s%s%s[%s", ident, "", sep, name, assign, term);
				ident += delta;
				break;
			case VICI_LIST_END:
				ident -= delta;
				fprintf(out, "%*s]%s", ident, "", term);
				break;
			case VICI_LIST_ITEM:
				sep = (last_type != VICI_LIST_START) ? separ : "";
				if (chunk_printable(value, NULL, ' '))
				{
					fprintf(out, "%*s%s%.*s%s", ident, "", sep,
							(int)value.len, value.ptr, term);
				}
				else
				{
					fprintf(out, "%*s%s0x%+#B%s", ident, "", sep,
							&value, term);
				}
				break;
			case VICI_END:
				fprintf(out, "}\n");
				enumerator->destroy(enumerator);
				return TRUE;
		}
		last_type = type;
	}
	enumerator->destroy(enumerator);
	return FALSE;
}

METHOD(vici_message_t, destroy, void,
	private_vici_message_t *this)
{
	if (this->cleanup)
	{
		chunk_clear(&this->encoding);
	}
	this->strings->destroy_function(this->strings, free);
	free(this);
}

/**
 * See header
 */
vici_message_t *vici_message_create_from_data(chunk_t data, bool cleanup)
{
	private_vici_message_t *this;

	INIT(this,
		.public = {
			.create_enumerator = _create_enumerator,
			.get_str = _get_str,
			.vget_str = _vget_str,
			.get_int = _get_int,
			.vget_int = _vget_int,
			.get_bool = _get_bool,
			.vget_bool = _vget_bool,
			.get_value = _get_value,
			.vget_value = _vget_value,
			.get_encoding = _get_encoding,
			.parse = _parse,
			.dump = _dump,
			.destroy = _destroy,
		},
		.strings = linked_list_create(),
		.encoding = data,
		.cleanup = cleanup,
	);

	return &this->public;
}

/**
 * See header
 */
vici_message_t *vici_message_create_from_enumerator(enumerator_t *enumerator)
{
	vici_builder_t *builder;
	vici_type_t type;
	char *name;
	chunk_t value;

	builder = vici_builder_create();
	while (enumerator->enumerate(enumerator, &type, &name, &value))
	{
		switch (type)
		{
			case VICI_SECTION_START:
			case VICI_LIST_START:
				builder->add(builder, type, name);
				continue;
			case VICI_KEY_VALUE:
				builder->add(builder, type, name, value);
				continue;
			case VICI_LIST_ITEM:
				builder->add(builder, type, value);
				continue;
			case VICI_SECTION_END:
			case VICI_LIST_END:
			default:
				builder->add(builder, type);
				continue;
			case VICI_END:
				break;
		}
		break;
	}
	enumerator->destroy(enumerator);

	return builder->finalize(builder);
}

/**
 * See header
 */
vici_message_t *vici_message_create_from_args(vici_type_t type, ...)
{
	vici_builder_t *builder;
	va_list args;
	char *name;
	chunk_t value;

	builder = vici_builder_create();
	va_start(args, type);
	while (type != VICI_END)
	{
		switch (type)
		{
			case VICI_LIST_START:
			case VICI_SECTION_START:
				name = va_arg(args, char*);
				builder->add(builder, type, name);
				break;
			case VICI_KEY_VALUE:
				name = va_arg(args, char*);
				value = va_arg(args, chunk_t);
				builder->add(builder, type, name, value);
				break;
			case VICI_LIST_ITEM:
				value = va_arg(args, chunk_t);
				builder->add(builder, type, value);
				break;
			case VICI_SECTION_END:
			case VICI_LIST_END:
			default:
				builder->add(builder, type);
				break;
		}
		type = va_arg(args, vici_type_t);
	}
	va_end(args);
	return builder->finalize(builder);
}
