/*  eurephia_log.c  --  eurephia logging
 *
 *  GPLv2 only - Copyright (C) 2008 - 2012
 *               David Sommerseth <dazo@users.sourceforge.net>
 *
 *  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; version 2
 *  of the License.
 *
 *  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.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 */

/**
 * @file   eurephia_log.c
 * @author David Sommerseth <dazo@users.sourceforge.net>
 * @date   2008-08-06
 *
 * @brief  Function for unified logging
 *
 */


#include <stdio.h>
#include <stdarg.h>
#include <syslog.h>
#include <string.h>
#include <time.h>
#include <pthread.h>
#include <assert.h>

#include <eurephia_context.h>
#include <eurephia_log.h>
#include <eurephia_nullsafe.h>

/**
 * Maps eurephia log types (priorities) to string values
 *
 * @param prio eurephia logtype, such as LOG_INFO, LOG_DEBUG, LOG_FATAL, etc
 *
 * @return String containing the log type/prio
 */
static inline const char *logprio_str(int prio) {
        switch( prio ) {
        case LOG_INFO:
                return "-- INFO --      \0";
        case LOG_DEBUG:
                return "-- DEBUG --     \0";
        case LOG_WARNING:
                return "** WARNING **   \0";
        case LOG_ERROR:
                return "** ERROR **     \0";
        case LOG_CRITICAL:
                return "** CRITICAL **  \0";
        case LOG_FATAL:
                return "** - FATAL - ** \0";
        case LOG_PANIC:
                return "** * PANIC * ** \0";
        default:
                return "[[ UNKNOWN ]]\0";
        }
}


/**
 *  Mapping table for eurephia log types to syslog log types
 */
static const int syslog_priority[] = {
        -1,
        LOG_INFO,       /**< LOG_INFO     */
        LOG_DEBUG,      /**< LOG_DEBUG    */
        LOG_WARNING,    /**< LOG_WARNING  */
        LOG_ERR,        /**< LOG_ERROR    */
        LOG_CRIT,       /**< LOG_CRITICAL */
        LOG_ALERT,      /**< LOG_FATAL    */
        LOG_EMERG       /**< LOG_PANIC    */
};


/**
 * Converts eurephiaLOGTYPE value to a string
 *
 * @param lt eurephiaLOGTYPE, must be either logFILE or logSYSLOG
 *
 * @return Returns a static string with log type
 */
static inline const char *logtype_str(eurephiaLOGTYPE lt) {
        switch( lt ) {
        case logFILE:
                return "file\0";
        case logSYSLOG:
                return "syslog\0";
        }
        return NULL;
}


/**
 *  Converts a string of log destination/facilities to syslog values
 *
 * @param dest String containing the log destination
 *
 * @return Returns a syslog compatible value, such as LOG_AUTHPRIV, LOG_LOCAL{0-7}, LOG_USER, etc.
 *         Unknown types will be set to syslog default, LOG_USER.
 *
 * @remark The following list will list up facilities which will be ignored and LOG_USER will be used
 *         instead: LOG_CRON, LOG_FTP, LOG_KERN, LOG_LPR, LOG_MAIL, LOG_NEWS, LOG_SYSLOG, LOG_UUCP.
 *
 */
static const int syslog_logdest(const char *dest) {
        if( dest == NULL ) {
                return LOG_USER;
        }

        if( strcasecmp(dest, "auth") == 0 ) {
                return LOG_AUTHPRIV;  // LOG_AUTH is deprecated, and LOG_AUTHPRIV should be used instead
        } else if( strcasecmp(dest, "authpriv") == 0 ) {
                return LOG_AUTHPRIV;
        } else if( strcasecmp(dest, "daemon") == 0 ) {
                return LOG_DAEMON;
        } else if( strcasecmp(dest, "local0") == 0 ) {
                return LOG_LOCAL0;
        } else if( strcasecmp(dest, "local1") == 0 ) {
                return LOG_LOCAL1;
        } else if( strcasecmp(dest, "local2") == 0 ) {
                return LOG_LOCAL2;
        } else if( strcasecmp(dest, "local3") == 0 ) {
                return LOG_LOCAL3;
        } else if( strcasecmp(dest, "local4") == 0 ) {
                return LOG_LOCAL4;
        } else if( strcasecmp(dest, "local5") == 0 ) {
                return LOG_LOCAL5;
        } else if( strcasecmp(dest, "local6") == 0 ) {
                return LOG_LOCAL6;
        } else if( strcasecmp(dest, "local7") == 0 ) {
                return LOG_LOCAL7;
        } else {
                return LOG_USER;
        }
}


/**
 * POSIX Mutex to avoid simultaneously logging activity from
 * several threads at the same time
 */
pthread_mutex_t log_mutex = PTHREAD_MUTEX_INITIALIZER;


/**
 * Simple function for logging entries to a file with timestamp
 *
 * @param log     FILE pointer to a log file
 * @param logdst  Log destiation/priority
 * @param loglvl  Log level of the message
 * @param file String containing file name of the place this function was called.  Usually the __FILE__ macro.
 * @param line Line number of the source file this function was called.  Usually the __LINE__ macro.
 * @param fmt     stdarg, format string
 * @param ap      stdarg va_list, prepared by va_start()
 */
static void file_log(FILE *log, int logdst, int loglvl, const char *file, const int line,
                     const char *fmt, va_list ap)
{
        char tstmp_str[200];
        time_t tstmp;
        struct tm *loctstmp;

        if( log == NULL ) {
                return;
        }

        // Get time stamp
        memset(&tstmp_str, 0, 200);
        tstmp = time(NULL);
        loctstmp = localtime(&tstmp);
        if( loctstmp != NULL ) {
                if( strftime(tstmp_str, 198, "%Y-%m-%d %H:%M:%S %Z", loctstmp) == 0 ) {
                        snprintf(tstmp_str, 198, "(error getting time stamp string)");
                }
        } else {
                snprintf(tstmp_str, 198, "(error getting timestamp)");
        }

        // Do the logging
        pthread_mutex_lock(&log_mutex); // Block other threads from writing when we write
#ifdef ENABLE_DEBUG
        fprintf(log, "[%s] %s [%i] {%s:%i} ", tstmp_str, logprio_str(logdst), loglvl, file, line);
#else
        fprintf(log, "[%s] %s [%i] ", tstmp_str, logprio_str(logdst), loglvl);
#endif
        vfprintf(log, fmt, ap);
        fprintf(log, "\n");
        fflush(log);
        pthread_mutex_unlock(&log_mutex); // Unblock other threads
}


/**
 * Internal function.  This function should normally be called via the veurephia_log() function.
 *
 * @param ctx    eurephiaCTX
 * @param logdst Log destination, can be LOG_INFO, LOG_DEBUG, LOG_WARNING, LOG_ERROR, 
 *        LOG_CRITICAL, LOG_FATAL or LOG_PANIC
 * @param loglvl Log level of the message.  If the eurephiaCTX has a lower log level setup
 *        than what this parameter is set to, the message will not be logged.
 * @param file String containing file name of the place this function was called.  Usually the __FILE__ macro.
 * @param line Line number of the source file this function was called.  Usually the __LINE__ macro.
 * @param ap   stdarg's va_list data
 * @param fmt  stdarg's char *fmt pointer
 */
void _veurephia_log_func(eurephiaCTX *ctx, int logdst, int loglvl, const char *file, const int line,
                         va_list ap, const char *fmt)
{
        // Only log if we have an open log file and which has high enough log level
        if( (ctx != NULL) && (ctx->log != NULL) && (ctx->log->opened == 1)
            && (ctx->log->loglevel >= loglvl) ) {
                switch( ctx->log->logtype ) {
                case logFILE:
                        file_log(ctx->log->logfile, logdst, loglvl, file, line, fmt, ap);
                        break;
                case logSYSLOG:
                        vsyslog(syslog_priority[logdst], fmt, ap);
                        break;
                }
        }
}


/**
 * Closes an eurephia log context
 *
 * @param ctx eurephiaCTX containing the log context to be closed
 *
 */
void eurephia_log_close(eurephiaCTX *ctx) {
        if( (ctx == NULL) || (ctx->log == NULL) ) {
                return;
        }

        eurephia_log(ctx, LOG_INFO, 2, "Closing %s logging (%s).",
                     logtype_str(ctx->log->logtype), ctx->log->destination);
        if( ctx->log->opened == 1 ) {
                switch( ctx->log->logtype ) {
                case logFILE:
                        if( ctx->log->logfile != NULL ) {
                                fflush(ctx->log->logfile);
                                fclose(ctx->log->logfile);
                        }
                        ctx->log->logfile = NULL;
                        break;

                case logSYSLOG:
                        closelog();
                        break;
                }
                ctx->log->opened = 0;
        }
        free_nullsafe(ctx, ctx->log->destination);
        free_nullsafe(ctx, ctx->log);
}

/**
 * Initialises and preapres the log device
 *
 * @param ctx      eurephiaCTX where the log context will be associated
 * @param ident    Log ident.  Used when logging to syslog primarily, to identify log entries in syslog logs
 * @param dest     Log destination.  For can be a file name, "syslog:", "stdout:", "stderr:" or "none:".
 *                 If it is "syslog:" it must continue with a string describing log facility such as
 *                 "syslog:authpriv", "syslog:local0", "syslog:local1", "syslog:user", etc.  If the
 *                 facility is unknown, it will default to "user"
 * @param loglevel Sets the verbosity level for the log file.  The higher number, the more information
 *                 will be logged.
 *
 * @return Returns 1 on success, otherwise 0;
 */
int eurephia_log_init(eurephiaCTX *ctx, const char *ident, const char *dest, int loglevel) {

        assert( (ctx != NULL) && (dest != NULL) );

        // Create log context
        ctx->log = (eurephiaLOG *) malloc_nullsafe(ctx, sizeof(eurephiaLOG)+2);
        if( ctx->log == NULL ) {
                return 0;
        }

        if( strncmp(dest, "syslog:", 7) == 0 ) {
                ctx->log->logtype = logSYSLOG;
                ctx->log->destination = strdup(dest+7);
        } else {
                ctx->log->logtype = logFILE;
                ctx->log->destination = strdup(dest);
        }
        if( ctx->log->destination == NULL) {
                free_nullsafe(ctx, ctx->log);
                return 0;
        }
        ctx->log->loglevel = loglevel;

        switch( ctx->log->logtype ) {
        case logFILE: // Open log file
                if( strcmp(dest, "stdout:") == 0 ) {
                        ctx->log->logfile = stdout;
                } else if( strcmp(dest, "stderr:") == 0 ) {
                        ctx->log->logfile = stderr;
                } else if( strcmp(dest, "none:") == 0 ) {
                        ctx->log->logfile = NULL;
                        ctx->log->loglevel = 0;
                        ctx->log->opened = 0;
                        return 1;
                } else if( (ctx->log->logfile = fopen(dest, "aw")) == NULL ) {
                        fprintf(stderr, "ERROR: Could not open log file: %s\n", ctx->log->destination);
                        free_nullsafe(ctx, ctx->log->destination);
                        free_nullsafe(ctx, ctx->log);
                        return 0;
                }
                break;

        case logSYSLOG: // Open syslog
                openlog(ident, LOG_PID, syslog_logdest(ctx->log->destination));
                break;
        }
        ctx->log->opened = 1;
        eurephia_log(ctx, LOG_INFO, 1, "Logging to %s (%s) started",
                     logtype_str(ctx->log->logtype), ctx->log->destination);
        return 1;
}
