/*
 * Copyright (C) 2009-2013 Tobias Brunner
 * Copyright (C) 2006-2008 Martin Willi
 *
 * Copyright (C) secunet Security Networks 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 "printf_hook.h"

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

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

typedef struct private_printf_hook_t private_printf_hook_t;
typedef struct printf_hook_handler_t printf_hook_handler_t;

/**
 * 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, PA_*
	 */
	int argtypes[3];
};

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

	/**
	 * Output FILE stream
	 */
	FILE *stream;
};

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

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

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

	va_start(args, fmt);
	written = vfprintf(data->stream, fmt, args);
	va_end(args);

	if (written < 0)
	{
		written = 0;
	}
	return written;
}

/**
 * Printf hook print function. This is actually of type "printf_function",
 * however glibc does it typedef to function, but uclibc to a pointer.
 * So we redefine it here.
 */
static int custom_print(FILE *stream, const struct printf_info *info,
						const void *const *args)
{
	printf_hook_spec_t spec;
	printf_hook_handler_t *handler;
	printf_hook_data_t data = {
		.stream = stream,
	};

	handler =  printf_hooks[SPEC_TO_INDEX(info->spec)];
	spec.hash = info->alt;
	spec.plus = info->showsign;
	spec.minus = info->left;
	spec.width = info->width;

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

/**
 * Printf hook arginfo function, which is actually of type
 * "printf_arginfo_[size_]function".
 */
static int custom_arginfo(const struct printf_info *info, size_t n, int *argtypes
#ifdef HAVE_PRINTF_SPECIFIER
						  , int *size
#endif
						  )
{
	int i;
	printf_hook_handler_t *handler;

	handler = printf_hooks[SPEC_TO_INDEX(info->spec)];
	if (handler->numargs <= n)
	{
		for (i = 0; i < handler->numargs; ++i)
		{
			argtypes[i] = handler->argtypes[i];
		}
	}
	/* we never set "size", as we have no user defined types */
	return handler->numargs;
}

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 >= countof(handler->argtypes))
		{
			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] = PA_INT;
				break;
			case PRINTF_HOOK_ARGTYPE_POINTER:
				handler->argtypes[i] = PA_POINTER;
				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)
	{
#	ifdef HAVE_PRINTF_SPECIFIER
		register_printf_specifier(spec, custom_print, custom_arginfo);
#	else
		register_printf_function(spec, custom_print, custom_arginfo);
#	endif
		printf_hooks[SPEC_TO_INDEX(spec)] = handler;
	}
	else
	{
		free(handler);
	}
}

METHOD(printf_hook_t, destroy, void,
	private_printf_hook_t *this)
{
	int i;

	for (i = 0; i < countof(printf_hooks); i++)
	{
		free(printf_hooks[i]);
	}
	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));

	return &this->public;
}
