/*
 * Copyright (C) 2009-2013 Tobias Brunner
 * Copyright (C) 2006-2008 Martin Willi
 * HSR Hochschule fuer Technik Rapperswil
 *
 * 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 "printf_hook.h"

#include <utils/utils.h>
#include <utils/debug.h>
#include <threading/thread_value.h>

#include <vstr.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

typedef struct private_printf_hook_t private_printf_hook_t;
typedef struct printf_hook_handler_t printf_hook_handler_t;

#define PRINTF_BUF_LEN 8192
#define ARGS_MAX 3

/**
 * private data of printf_hook
 */
struct private_printf_hook_t {

	/**
	 * public functions
	 */
	printf_hook_t public;
};

/**
 * struct with information about a registered handler
 */
struct printf_hook_handler_t {

	/**
	 * callback function
	 */
	printf_hook_function_t hook;

	/**
	 * number of arguments
	 */
	int numargs;

	/**
	 * types of the arguments, VSTR_TYPE_FMT_*
	 */
	int argtypes[ARGS_MAX];

	/**
	 * name required for Vstr
	 */
	char *name;
};

/**
 * Data to pass to a printf hook.
 */
struct printf_hook_data_t {

	/**
	 * Base to append printf to
	 */
	Vstr_base *base;

	/**
	 * Position in base to write to
	 */
	size_t pos;
};

/* A-Z | 6 other chars | a-z */
static printf_hook_handler_t *printf_hooks[58];

#define SPEC_TO_INDEX(spec) ((int)(spec) - (int)'A')

/**
 * These are used below, whenever the public wrapper functions are called before
 * initialization or after destruction.
 */
#undef vprintf
#undef vfprintf
#undef vsnprintf

/**
 * Vstr variant of print_in_hook()
 */
size_t print_in_hook(printf_hook_data_t *data, char *fmt, ...)
{
	size_t written;
	va_list args;

	va_start(args, fmt);
	written = vstr_add_vfmt(data->base, data->pos, fmt, args);
	va_end(args);

	data->pos += written;
	return written;
}

/**
 * Vstr custom format specifier callback function.
 */
static int custom_fmt_cb(Vstr_base *base, size_t pos, Vstr_fmt_spec *fmt_spec)
{
	int i;
	const void *args[ARGS_MAX];
	printf_hook_spec_t spec;
	printf_hook_handler_t *handler;
	printf_hook_data_t data = {
		.base = base,
		.pos = pos,
	};

	handler = printf_hooks[SPEC_TO_INDEX(fmt_spec->name[0])];
	for (i = 0; i < handler->numargs; i++)
	{
		switch (handler->argtypes[i])
		{
			case VSTR_TYPE_FMT_INT:
				args[i] = VSTR_FMT_CB_ARG_PTR(fmt_spec, i);
				break;
			case VSTR_TYPE_FMT_PTR_VOID:
				args[i] = &VSTR_FMT_CB_ARG_PTR(fmt_spec, i);
				break;
		}
	}

	spec.hash = fmt_spec->fmt_hash;
	spec.plus = fmt_spec->fmt_plus;
	spec.minus = fmt_spec->fmt_minus;
	spec.width = fmt_spec->obj_field_width;

	handler->hook(&data, &spec, args);

	return 1;
}

/**
 * Add a custom format handler to the given Vstr_conf object
 */
static void vstr_fmt_add_handler(Vstr_conf *conf, printf_hook_handler_t *handler)
{
	int *at;

	at = handler->argtypes;
	switch (handler->numargs)
	{
		case 1:
			vstr_fmt_add(conf, handler->name, custom_fmt_cb, at[0],
						 VSTR_TYPE_FMT_END);
			break;
		case 2:
			vstr_fmt_add(conf, handler->name, custom_fmt_cb, at[0],
						 at[1], VSTR_TYPE_FMT_END);
			break;
		case 3:
			vstr_fmt_add(conf, handler->name, custom_fmt_cb, at[0],
						 at[1], at[2], VSTR_TYPE_FMT_END);
			break;
	}
}

/**
 * Thread specific vstr config
 */
static thread_value_t *vstr_conf = NULL;

/**
 * Create vstr config for current thread
 */
static Vstr_conf *create_vstr_conf()
{
	Vstr_conf *conf;
	int i;

	conf = vstr_make_conf();
	vstr_cntl_conf(conf, VSTR_CNTL_CONF_SET_FMT_CHAR_ESC, '%');
	vstr_cntl_conf(conf, VSTR_CNTL_CONF_SET_TYPE_GRPALLOC_CACHE,
						 VSTR_TYPE_CNTL_CONF_GRPALLOC_CSTR);
	vstr_cntl_conf(conf, VSTR_CNTL_CONF_SET_NUM_BUF_SZ, PRINTF_BUF_LEN);

	for (i = 0; i < countof(printf_hooks); i++)
	{
		if (printf_hooks[i])
		{
			vstr_fmt_add_handler(conf, printf_hooks[i]);
		}
	}
	return conf;
}

/**
 * Get vstr config of current thread
 */
static inline Vstr_conf *get_vstr_conf()
{
	Vstr_conf *conf = NULL;

	if (vstr_conf)
	{
		conf = (Vstr_conf*)vstr_conf->get(vstr_conf);
		if (!conf)
		{
			conf = create_vstr_conf();
			vstr_conf->set(vstr_conf, conf);
		}
	}
	return conf;
}

/**
 * Wrapper functions for printf and alike
 */
int vstr_wrapper_printf(const char *format, ...)
{
	int written;
	va_list args;
	va_start(args, format);
	written = vstr_wrapper_vprintf(format, args);
	va_end(args);
	return written;
}

int vstr_wrapper_fprintf(FILE *stream, const char *format, ...)
{
	int written;
	va_list args;
	va_start(args, format);
	written = vstr_wrapper_vfprintf(stream, format, args);
	va_end(args);
	return written;
}

int vstr_wrapper_sprintf(char *str, const char *format, ...)
{
	int written;
	va_list args;
	va_start(args, format);
	written = vstr_wrapper_vsprintf(str, format, args);
	va_end(args);
	return written;
}

int vstr_wrapper_snprintf(char *str, size_t size, const char *format, ...)
{
	int written;
	va_list args;
	va_start(args, format);
	written = vstr_wrapper_vsnprintf(str, size, format, args);
	va_end(args);
	return written;
}

int vstr_wrapper_asprintf(char **str, const char *format, ...)
{
	int written;
	va_list args;
	va_start(args, format);
	written = vstr_wrapper_vasprintf(str, format, args);
	va_end(args);
	return written;
}

static inline int vstr_wrapper_vprintf_internal(Vstr_conf *conf, FILE *stream,
												const char *format,
												va_list args)
{
	struct iovec *iov;
	int iovcnt, written = 0;
	Vstr_base *s;

	s = vstr_make_base(conf);
	vstr_add_vfmt(s, 0, format, args);
	if (vstr_export_iovec_ptr_all(s, &iov, &iovcnt))
	{
		while (iovcnt--)
		{
			if (iov->iov_base)
			{
				written += fwrite(iov->iov_base, 1, iov->iov_len, stream);
			}
			iov++;
		}
	}
	vstr_free_base(s);
	return written;
}

int vstr_wrapper_vprintf(const char *format, va_list args)
{
	Vstr_conf *conf;

	conf = get_vstr_conf();
	if (conf)
	{
		return vstr_wrapper_vprintf_internal(conf, stdout, format, args);
	}
	return vprintf(format, args);
}

int vstr_wrapper_vfprintf(FILE *stream, const char *format, va_list args)
{
	Vstr_conf *conf;

	conf = get_vstr_conf();
	if (conf)
	{
		return vstr_wrapper_vprintf_internal(conf, stream, format, args);
	}
	return vfprintf(stream, format, args);
}

static inline int vstr_wrapper_vsnprintf_internal(char *str, size_t size,
												  const char *format,
												  va_list args)
{
	Vstr_conf *conf;
	Vstr_base *s;
	int written;

	conf = get_vstr_conf();
	if (conf)
	{
		s = vstr_make_base(conf);
		vstr_add_vfmt(s, 0, format, args);
		written = s->len;
		vstr_export_cstr_buf(s, 1, s->len, str, (size > 0) ? size : s->len + 1);
		vstr_free_base(s);
		return written;
	}
	return vsnprintf(str, size, format, args);
}

int vstr_wrapper_vsprintf(char *str, const char *format, va_list args)
{
	return vstr_wrapper_vsnprintf_internal(str, 0, format, args);
}

int vstr_wrapper_vsnprintf(char *str, size_t size, const char *format,
						   va_list args)
{
	if (size > 0)
	{
		return vstr_wrapper_vsnprintf_internal(str, size, format, args);
	}
	return 0;
}

int vstr_wrapper_vasprintf(char **str, const char *format, va_list args)
{
	size_t len = 100;
	int written;

	*str = malloc(len);
	while (TRUE)
	{
		va_list ac;
		va_copy(ac, args);
		written = vstr_wrapper_vsnprintf_internal(*str, len, format, ac);
		va_end(ac);
		if (written < len)
		{
			break;
		}
		len = written + 1;
		*str = realloc(*str, len);
	}
	return written;
}

METHOD(printf_hook_t, add_handler, void,
	private_printf_hook_t *this, char spec, printf_hook_function_t hook, ...)
{
	int i = -1;
	bool failed = FALSE;
	printf_hook_handler_t *handler;
	printf_hook_argtype_t argtype;
	va_list args;

	if (SPEC_TO_INDEX(spec) <= -1 ||
		SPEC_TO_INDEX(spec) >= countof(printf_hooks))
	{
		DBG1(DBG_LIB, "'%c' is not a valid printf hook specifier, "
			 "not registered!", spec);
		return;
	}

	INIT(handler,
		.hook = hook,
	);

	va_start(args, hook);
	while (!failed)
	{
		argtype = va_arg(args, printf_hook_argtype_t);
		if (argtype == PRINTF_HOOK_ARGTYPE_END)
		{
			break;
		}
		if (++i >= ARGS_MAX)
		{
			DBG1(DBG_LIB, "Too many arguments for printf hook with "
				 "specifier '%c', not registered!", spec);
			failed = TRUE;
			break;
		}
		switch (argtype)
		{
			case PRINTF_HOOK_ARGTYPE_INT:
				handler->argtypes[i] = VSTR_TYPE_FMT_INT;
				break;
			case PRINTF_HOOK_ARGTYPE_POINTER:
				handler->argtypes[i] = VSTR_TYPE_FMT_PTR_VOID;
				break;
			default:
				DBG1(DBG_LIB, "Invalid printf hook arg type for '%c'", spec);
				failed = TRUE;
				break;
		}
	}
	va_end(args);

	handler->numargs = i + 1;

	if (!failed && handler->numargs > 0)
	{
		Vstr_conf *conf = get_vstr_conf();
		handler->name = malloc(2);
		handler->name[0] = spec;
		handler->name[1] = '\0';
		vstr_fmt_add_handler(conf, handler);
		printf_hooks[SPEC_TO_INDEX(spec)] = handler;
	}
	else
	{
		free(handler);
	}
}

METHOD(printf_hook_t, destroy, void,
	private_printf_hook_t *this)
{
	int i;
	Vstr_conf *conf;
	printf_hook_handler_t *handler;

	conf = get_vstr_conf();
	for (i = 0; i < countof(printf_hooks); ++i)
	{
		handler = printf_hooks[i];
		if (handler)
		{
			vstr_fmt_del(conf, handler->name);
			free(handler->name);
			free(handler);
		}
	}

	/* freeing the Vstr_conf of the main thread */
	vstr_conf->destroy(vstr_conf);
	vstr_conf = NULL;
	vstr_exit();
	free(this);
}

/*
 * see header file
 */
printf_hook_t *printf_hook_create()
{
	private_printf_hook_t *this;

	INIT(this,
		.public = {
			.add_handler = _add_handler,
			.destroy = _destroy,
		},
	);

	memset(printf_hooks, 0, sizeof(printf_hooks));

	if (!vstr_init())
	{
		DBG1(DBG_LIB, "failed to initialize Vstr library!");
		free(this);
		return NULL;
	}
	vstr_conf = thread_value_create((thread_cleanup_t)vstr_free_conf);

	return &this->public;
}
