/*-
 * Copyright (c) 2009-2010 The FreeBSD Foundation
 * All rights reserved.
 *
 * This software was developed by Pawel Jakub Dawidek under sponsorship from
 * the FreeBSD Foundation.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHORS AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 *
 * $FreeBSD$
 */

#include <sys/cdefs.h>
__FBSDID("$FreeBSD$");

#include <assert.h>
#include <errno.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>

#include "pjdlog.h"

static int pjdlog_mode = PJDLOG_MODE_STD;
static int pjdlog_debug_level = 0;
static char pjdlog_prefix[128];

/*
 * Configure where the logs should go.
 * By default they are send to stdout/stderr, but after going into background
 * (eg. by calling daemon(3)) application is responsible for changing mode to
 * PJDLOG_MODE_SYSLOG, so logs will be send to syslog.
 */
void
pjdlog_mode_set(int mode)
{

	assert(mode == PJDLOG_MODE_STD || mode == PJDLOG_MODE_SYSLOG);

	pjdlog_mode = mode;
}

/*
 * Return current mode.
 */
int
pjdlog_mode_get(void)
{

	return (pjdlog_mode);
}

/*
 * Set debug level. All the logs above the level specified here will be
 * ignored.
 */
void
pjdlog_debug_set(int level)
{

	assert(level >= 0);

	pjdlog_debug_level = level;
}

/*
 * Return current debug level.
 */
int
pjdlog_debug_get(void)
{

	return (pjdlog_debug_level);
}

/*
 * Set prefix that will be used before each log.
 * Setting prefix to NULL will remove it.
 */
void
pjdlog_prefix_set(const char *fmt, ...)
{
	va_list ap;

	va_start(ap, fmt);
	pjdlog_prefix_setv(fmt, ap);
	va_end(ap);
}

/*
 * Set prefix that will be used before each log.
 * Setting prefix to NULL will remove it.
 */
void
pjdlog_prefix_setv(const char *fmt, va_list ap)
{

	assert(fmt != NULL);

	vsnprintf(pjdlog_prefix, sizeof(pjdlog_prefix), fmt, ap);
}

/*
 * Convert log level into string.
 */
static const char *
pjdlog_level_string(int loglevel)
{

	switch (loglevel) {
	case LOG_EMERG:
		return ("EMERG");
	case LOG_ALERT:
		return ("ALERT");
	case LOG_CRIT:
		return ("CRIT");
	case LOG_ERR:
		return ("ERROR");
	case LOG_WARNING:
		return ("WARNING");
	case LOG_NOTICE:
		return ("NOTICE");
	case LOG_INFO:
		return ("INFO");
	case LOG_DEBUG:
		return ("DEBUG");
	}
	assert(!"Invalid log level.");
	abort();	/* XXX: gcc */
}

/*
 * Common log routine.
 */
void
pjdlog_common(int loglevel, int debuglevel, int error, const char *fmt, ...)
{
	va_list ap;

	va_start(ap, fmt);
	pjdlogv_common(loglevel, debuglevel, error, fmt, ap);
	va_end(ap);
}

/*
 * Common log routine, which can handle regular log level as well as debug
 * level. We decide here where to send the logs (stdout/stderr or syslog).
 */
void
pjdlogv_common(int loglevel, int debuglevel, int error, const char *fmt,
    va_list ap)
{

	assert(loglevel == LOG_EMERG || loglevel == LOG_ALERT ||
	    loglevel == LOG_CRIT || loglevel == LOG_ERR ||
	    loglevel == LOG_WARNING || loglevel == LOG_NOTICE ||
	    loglevel == LOG_INFO || loglevel == LOG_DEBUG);
	assert(loglevel != LOG_DEBUG || debuglevel > 0);
	assert(error >= -1);

	/* Ignore debug above configured level. */
	if (loglevel == LOG_DEBUG && debuglevel > pjdlog_debug_level)
		return;

	switch (pjdlog_mode) {
	case PJDLOG_MODE_STD:
	    {
		FILE *out;

		/*
		 * We send errors and warning to stderr and the rest to stdout.
		 */
		switch (loglevel) {
		case LOG_EMERG:
		case LOG_ALERT:
		case LOG_CRIT:
		case LOG_ERR:
		case LOG_WARNING:
			out = stderr;
			break;
		case LOG_NOTICE:
		case LOG_INFO:
		case LOG_DEBUG:
			out = stdout;
			break;
		default:
			assert(!"Invalid loglevel.");
			abort();	/* XXX: gcc */
		}

		fprintf(out, "[%s]", pjdlog_level_string(loglevel));
		/* Attach debuglevel if this is debug log. */
		if (loglevel == LOG_DEBUG)
			fprintf(out, "[%d]", debuglevel);
		fprintf(out, " ");
		fprintf(out, "%s", pjdlog_prefix);
		vfprintf(out, fmt, ap);
		if (error != -1)
			fprintf(out, ": %s.", strerror(error));
		fprintf(out, "\n");
		break;
	    }
	case PJDLOG_MODE_SYSLOG:
	    {
		char log[1024];
		int len;

		len = snprintf(log, sizeof(log), "%s", pjdlog_prefix);
		if ((size_t)len < sizeof(log))
			len += vsnprintf(log + len, sizeof(log) - len, fmt, ap);
		if (error != -1 && (size_t)len < sizeof(log)) {
			(void)snprintf(log + len, sizeof(log) - len, ": %s.",
			    strerror(error));
		}
		syslog(loglevel, "%s", log);
		break;
	    }
	default:
		assert(!"Invalid mode.");
	}
}

/*
 * Regular logs.
 */
void
pjdlogv(int loglevel, const char *fmt, va_list ap)
{

	/* LOG_DEBUG is invalid here, pjdlogv?_debug() should be used. */
	assert(loglevel == LOG_EMERG || loglevel == LOG_ALERT ||
	    loglevel == LOG_CRIT || loglevel == LOG_ERR ||
	    loglevel == LOG_WARNING || loglevel == LOG_NOTICE ||
	    loglevel == LOG_INFO);

	pjdlogv_common(loglevel, 0, -1, fmt, ap);
}

/*
 * Regular logs.
 */
void
pjdlog(int loglevel, const char *fmt, ...)
{
	va_list ap;

	va_start(ap, fmt);
	pjdlogv(loglevel, fmt, ap);
	va_end(ap);
}

/*
 * Debug logs.
 */
void
pjdlogv_debug(int debuglevel, const char *fmt, va_list ap)
{

	pjdlogv_common(LOG_DEBUG, debuglevel, -1, fmt, ap);
}

/*
 * Debug logs.
 */
void
pjdlog_debug(int debuglevel, const char *fmt, ...)
{
	va_list ap;

	va_start(ap, fmt);
	pjdlogv_debug(debuglevel, fmt, ap);
	va_end(ap);
}

/*
 * Error logs with errno logging.
 */
void
pjdlogv_errno(int loglevel, const char *fmt, va_list ap)
{

	pjdlogv_common(loglevel, 0, errno, fmt, ap);
}

/*
 * Error logs with errno logging.
 */
void
pjdlog_errno(int loglevel, const char *fmt, ...)
{
	va_list ap;

	va_start(ap, fmt);
	pjdlogv_errno(loglevel, fmt, ap);
	va_end(ap);
}

/*
 * Log error, errno and exit.
 */
void
pjdlogv_exit(int exitcode, const char *fmt, va_list ap)
{

	pjdlogv_errno(LOG_ERR, fmt, ap);
	exit(exitcode);
}

/*
 * Log error, errno and exit.
 */
void
pjdlog_exit(int exitcode, const char *fmt, ...)
{
	va_list ap;

	va_start(ap, fmt);
	pjdlogv_exit(exitcode, fmt, ap);
	/* NOTREACHED */
	va_end(ap);
}

/*
 * Log error and exit.
 */
void
pjdlogv_exitx(int exitcode, const char *fmt, va_list ap)
{

	pjdlogv(LOG_ERR, fmt, ap);
	exit(exitcode);
}

/*
 * Log error and exit.
 */
void
pjdlog_exitx(int exitcode, const char *fmt, ...)
{
	va_list ap;

	va_start(ap, fmt);
	pjdlogv_exitx(exitcode, fmt, ap);
	/* NOTREACHED */
	va_end(ap);
}