/* clog: Extremely simple logger for C.
 *
 * Features:
 * - Implemented purely as a single header file.
 * - Create multiple loggers.
 * - Four log levels (debug, info, warn, error).
 * - Custom formats.
 * - Fast.
 *
 * Dependencies:
 * - Should conform to C89, C++98 (but requires vsnprintf, unfortunately).
 * - POSIX environment.
 *
 * USAGE:
 *
 * Include this header in any file that wishes to write to logger(s).  In
 * exactly one file (per executable), define CLOG_MAIN first (e.g. in your
 * main .c file).
 *
 *     #define CLOG_MAIN
 *     #include "clog.h"
 *
 * This will define the actual objects that all the other units will use.
 *
 * Loggers are identified by integers (0 - 15).  It's expected that you'll
 * create meaningful constants and then refer to the loggers as such.
 *
 * Example:
 *
 *  const int MY_LOGGER = 0;
 *
 *  int main() {
 *      int r;
 *      r = clog_init_path(MY_LOGGER, "my_log.txt");
 *      if (r != 0) {
 *          fprintf(stderr, "Logger initialization failed.\n");
 *          return 1;
 *      }
 *      clog_info(CLOG(MY_LOGGER), "Hello, world!");
 *      clog_free(MY_LOGGER);
 *      return 0;
 *  }
 *
 * The CLOG macro used in the call to clog_info is a helper that passes the
 * __FILE__ and __LINE__ parameters for you, so you don't have to type them
 * every time. (It could be prettier with variadic macros, but that requires
 * C99 or C++11 to be standards compliant.)
 *
 * Errors encountered by clog will be printed to stderr.  You can suppress
 * these by defining a macro called CLOG_SILENT before including clog.h.
 *
 * License: Do whatever you want. It would be nice if you contribute
 * improvements as pull requests here:
 *
 *   https://github.com/mmueller/clog
 *
 * Copyright 2013 Mike Mueller <mike@subfocal.net>.
 *
 * As is; no warranty is provided; use at your own risk.
 */

#ifndef __CLOG_H__
#define __CLOG_H__

#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <stdarg.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#ifndef _MSC_VER
#include <unistd.h>
#else
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <io.h>
#define open _open
#define close _close
#define write _write
#define snprintf _snprintf
#endif

/* Number of loggers that can be defined. */
#define CLOG_MAX_LOGGERS 16

/* Format strings cannot be longer than this. */
#define CLOG_FORMAT_LENGTH 256

/* Formatted times and dates should be less than this length. If they are not,
 * they will not appear in the log. */
#define CLOG_DATETIME_LENGTH 256

/* Default format strings. */
#define CLOG_DEFAULT_FORMAT "%d %t %f(%n): %l: %m\n"
#define CLOG_DEFAULT_DATE_FORMAT "%Y-%m-%d"
#define CLOG_DEFAULT_TIME_FORMAT "%H:%M:%S"

#ifdef __cplusplus
extern "C" {
#endif

enum clog_level {
    CLOG_DEBUG,
    CLOG_INFO,
    CLOG_WARN,
    CLOG_ERROR
};

struct clog;

/**
 * Create a new logger writing to the given file path.  The file will always
 * be opened in append mode.
 *
 * @param id
 * A constant integer between 0 and 15 that uniquely identifies this logger.
 *
 * @param path
 * Path to the file where log messages will be written.
 *
 * @return
 * Zero on success, non-zero on failure.
 */
int clog_init_path(int id, const char *const path);

/**
 * Create a new logger writing to a file descriptor.
 *
 * @param id
 * A constant integer between 0 and 15 that uniquely identifies this logger.
 *
 * @param fd
 * The file descriptor where log messages will be written.
 *
 * @return
 * Zero on success, non-zero on failure.
 */
int clog_init_fd(int id, int fd);

/**
 * Destroy (clean up) a logger.  You should do this at the end of execution,
 * or when you are done using the logger.
 *
 * @param id
 * The id of the logger to destroy.
 */
void clog_free(int id);

#define CLOG(id) __FILE__, __LINE__, id

/**
 * Log functions (one per level).  Call these to write messages to the log
 * file.  The first three arguments can be replaced with a call to the CLOG
 * macro defined above, e.g.:
 *
 *     clog_debug(CLOG(MY_LOGGER_ID), "This is a log message.");
 *
 * @param sfile
 * The name of the source file making this log call (e.g. __FILE__).
 *
 * @param sline
 * The line number of the call in the source code (e.g. __LINE__).
 *
 * @param id
 * The id of the logger to write to.
 *
 * @param fmt
 * The format string for the message (printf formatting).
 *
 * @param ...
 * Any additional format arguments.
 */
void clog_debug(const char *sfile, int sline, int id, const char *fmt, va_list ap);
void clog_info(const char *sfile, int sline, int id, const char *fmt, va_list ap);
void clog_warn(const char *sfile, int sline, int id, const char *fmt, va_list ap);
void clog_error(const char *sfile, int sline, int id, const char *fmt, va_list ap);

/**
 * Set the minimum level of messages that should be written to the log.
 * Messages below this level will not be written.  By default, loggers are
 * created with level == CLOG_DEBUG.
 *
 * @param id
 * The identifier of the logger.
 *
 * @param level
 * The new minimum log level.
 *
 * @return
 * Zero on success, non-zero on failure.
 */
int clog_set_level(int id, enum clog_level level);

/**
 * Set the format string used for times.  See strftime(3) for how this string
 * should be defined.  The default format string is CLOG_DEFAULT_TIME_FORMAT.
 *
 * @param fmt
 * The new format string, which must be less than CLOG_FORMAT_LENGTH bytes.
 *
 * @return
 * Zero on success, non-zero on failure.
 */
int clog_set_time_fmt(int id, const char *fmt);

/**
 * Set the format string used for dates.  See strftime(3) for how this string
 * should be defined.  The default format string is CLOG_DEFAULT_DATE_FORMAT.
 *
 * @param fmt
 * The new format string, which must be less than CLOG_FORMAT_LENGTH bytes.
 *
 * @return
 * Zero on success, non-zero on failure.
 */
int clog_set_date_fmt(int id, const char *fmt);

/**
 * Set the format string for log messages.  Here are the substitutions you may
 * use:
 *
 *     %f: Source file name generating the log call.
 *     %n: Source line number where the log call was made.
 *     %m: The message text sent to the logger (after printf formatting).
 *     %d: The current date, formatted using the logger's date format.
 *     %t: The current time, formatted using the logger's time format.
 *     %l: The log level (one of "DEBUG", "INFO", "WARN", or "ERROR").
 *     %%: A literal percent sign.
 *
 * The default format string is CLOG_DEFAULT_FORMAT.
 *
 * @param fmt
 * The new format string, which must be less than CLOG_FORMAT_LENGTH bytes.
 * You probably will want to end this with a newline (\n).
 *
 * @return
 * Zero on success, non-zero on failure.
 */
int clog_set_fmt(int id, const char *fmt);

/**
 * Set the callback function.
 *
 * @param cb
 * The new callback function.
 *
 * @return
 * Zero on success, non-zero on failure.
 */
int clog_set_cb(int id, void (*cb)(const char *msg, size_t len));

/**
 * Set the file descriptor.
 *
 * @param id
 * The identifier of the logger.
 *
 * @param fd
 * The new file descriptor.
 *
 * @return
 * Zero on success, non-zero on failure.
 */
int clog_set_fd(int id, int fd);


/*
 * No need to read below this point.
 */

/**
 * The C logger structure.
 */
struct clog {

    /* The current level of this logger. Messages below it will be dropped. */
    enum clog_level level;

    /* The file being written. */
    int fd;

    /* The format specifier. */
    char fmt[CLOG_FORMAT_LENGTH];

    /* Date format */
    char date_fmt[CLOG_FORMAT_LENGTH];

    /* Time format */
    char time_fmt[CLOG_FORMAT_LENGTH];

    /* Tracks whether the fd needs to be closed eventually. */
    int opened;

    /* Callback function for each log message. */
    void (*cb)(const char *msg, size_t len);
};

void _clog_err(const char *fmt, ...);

#ifdef CLOG_MAIN
struct clog *_clog_loggers[CLOG_MAX_LOGGERS] = { 0 };
#else
extern struct clog *_clog_loggers[CLOG_MAX_LOGGERS];
#endif

#ifdef CLOG_MAIN

const char *const CLOG_LEVEL_NAMES[] = {
    "Debug",
    "Info",
    "Warning",
    "Error",
};

int
clog_init_path(int id, const char *const path)
{
    int fd = open(path, O_CREAT | O_WRONLY | O_APPEND, 0666);
    if (fd == -1) {
        _clog_err("Unable to open %s: %s\n", path, strerror(errno));
        return 1;
    }
    if (clog_init_fd(id, fd)) {
        close(fd);
        return 1;
    }
    _clog_loggers[id]->opened = 1;
    return 0;
}

int
clog_init_fd(int id, int fd)
{
    struct clog *logger;

    if (_clog_loggers[id] != NULL) {
        _clog_err("Logger %d already initialized.\n", id);
        return 1;
    }

    logger = (struct clog *) malloc(sizeof(struct clog));
    if (logger == NULL) {
        _clog_err("Failed to allocate logger: %s\n", strerror(errno));
        return 1;
    }

    logger->level = CLOG_DEBUG;
    logger->fd = fd;
    logger->opened = 0;
    strcpy(logger->fmt, CLOG_DEFAULT_FORMAT);
    strcpy(logger->date_fmt, CLOG_DEFAULT_DATE_FORMAT);
    strcpy(logger->time_fmt, CLOG_DEFAULT_TIME_FORMAT);
    logger->cb = NULL;

    _clog_loggers[id] = logger;
    return 0;
}

void
clog_free(int id)
{
    if (_clog_loggers[id]) {
        if (_clog_loggers[id]->opened) {
            close(_clog_loggers[id]->fd);
        }
        free(_clog_loggers[id]);
        _clog_loggers[id]=NULL;
    }
}

int
clog_set_level(int id, enum clog_level level)
{
    if (_clog_loggers[id] == NULL) {
        return 1;
    }
    if ((unsigned) level > CLOG_ERROR) {
        return 1;
    }
    _clog_loggers[id]->level = level;
    return 0;
}

int
clog_set_fd(int id, int fd)
{
    if (_clog_loggers[id] == NULL) {
        return 1;
    }
    _clog_loggers[id]->fd = fd;
    return 0;
}

int
clog_set_time_fmt(int id, const char *fmt)
{
    struct clog *logger = _clog_loggers[id];
    if (logger == NULL) {
        _clog_err("clog_set_time_fmt: No such logger: %d\n", id);
        return 1;
    }
    if (strlen(fmt) >= CLOG_FORMAT_LENGTH) {
        _clog_err("clog_set_time_fmt: Format specifier too long.\n");
        return 1;
    }
    strcpy(logger->time_fmt, fmt);
    return 0;
}

int
clog_set_date_fmt(int id, const char *fmt)
{
    struct clog *logger = _clog_loggers[id];
    if (logger == NULL) {
        _clog_err("clog_set_date_fmt: No such logger: %d\n", id);
        return 1;
    }
    if (strlen(fmt) >= CLOG_FORMAT_LENGTH) {
        _clog_err("clog_set_date_fmt: Format specifier too long.\n");
        return 1;
    }
    strcpy(logger->date_fmt, fmt);
    return 0;
}

int
clog_set_fmt(int id, const char *fmt)
{
    struct clog *logger = _clog_loggers[id];
    if (logger == NULL) {
        _clog_err("clog_set_fmt: No such logger: %d\n", id);
        return 1;
    }
    if (strlen(fmt) >= CLOG_FORMAT_LENGTH) {
        _clog_err("clog_set_fmt: Format specifier too long.\n");
        return 1;
    }
    strcpy(logger->fmt, fmt);
    return 0;
}

int
clog_set_cb(int id, void (*cb)(const char *msg, size_t len))
{
    struct clog *logger = _clog_loggers[id];
    if (logger == NULL) {
        _clog_err("clog_set_cb: No such logger: %d\n", id);
        return 1;
    }
    logger->cb = cb;
    return 0;
}

/* Internal functions */

size_t
_clog_append_str(char **dst, char *orig_buf, const char *src, size_t cur_size)
{
    size_t new_size = cur_size;

    while (strlen(*dst) + strlen(src) >= new_size) {
        new_size *= 2;
    }
    if (new_size != cur_size) {
        if (*dst == orig_buf) {
            *dst = (char *) malloc(new_size);
            strcpy(*dst, orig_buf);
        } else {
            *dst = (char *) realloc(*dst, new_size);
        }
    }

    strcat(*dst, src);
    return new_size;
}

size_t
_clog_append_int(char **dst, char *orig_buf, long int d, size_t cur_size)
{
    char buf[40]; /* Enough for 128-bit decimal */
    if (snprintf(buf, 40, "%ld", d) >= 40) {
        return cur_size;
    }
    return _clog_append_str(dst, orig_buf, buf, cur_size);
}

size_t
_clog_append_time(char **dst, char *orig_buf, struct tm *lt,
                  const char *fmt, size_t cur_size)
{
    char buf[CLOG_DATETIME_LENGTH];
    size_t result = strftime(buf, CLOG_DATETIME_LENGTH, fmt, lt);

    if (result > 0) {
        return _clog_append_str(dst, orig_buf, buf, cur_size);
    }

    return cur_size;
}

const char *
_clog_basename(const char *path)
{
    const char *slash = strrchr(path, '/');
    if (slash) {
        path = slash + 1;
    }
#ifdef _WIN32
    slash = strrchr(path, '\\');
    if (slash) {
        path = slash + 1;
    }
#endif
    return path;
}

char *
_clog_format(const struct clog *logger, char buf[], size_t buf_size,
             const char *sfile, int sline, const char *level,
             const char *message)
{
    size_t cur_size = buf_size;
    char *result = buf;
    enum { NORMAL, SUBST } state = NORMAL;
    size_t fmtlen = strlen(logger->fmt);
    size_t i;
    time_t t = time(NULL);
    struct tm *lt = localtime(&t);

    sfile = _clog_basename(sfile);
    result[0] = 0;
    for (i = 0; i < fmtlen; ++i) {
        if (state == NORMAL) {
            if (logger->fmt[i] == '%') {
                state = SUBST;
            } else {
                char str[2] = { 0 };
                str[0] = logger->fmt[i];
                cur_size = _clog_append_str(&result, buf, str, cur_size);
            }
        } else {
            switch (logger->fmt[i]) {
                case '%':
                    cur_size = _clog_append_str(&result, buf, "%", cur_size);
                    break;
                case 't':
                    cur_size = _clog_append_time(&result, buf, lt,
                                                 logger->time_fmt, cur_size);
                    break;
                case 'd':
                    cur_size = _clog_append_time(&result, buf, lt,
                                                 logger->date_fmt, cur_size);
                    break;
                case 'l':
                    cur_size = _clog_append_str(&result, buf, level, cur_size);
                    break;
                case 'n':
                    cur_size = _clog_append_int(&result, buf, sline, cur_size);
                    break;
                case 'f':
                    cur_size = _clog_append_str(&result, buf, sfile, cur_size);
                    break;
                case 'm':
                    cur_size = _clog_append_str(&result, buf, message,
                                                cur_size);
                    break;
            }
            state = NORMAL;
        }
    }

    return result;
}

void
_clog_log(const char *sfile, int sline, enum clog_level level,
          int id, const char *fmt, va_list ap)
{
    /* For speed: Use a stack buffer until message exceeds 4096, then switch
     * to dynamically allocated.  This should greatly reduce the number of
     * memory allocations (and subsequent fragmentation). */
    char buf[4096];
    size_t buf_size = 4096;
    char *dynbuf = buf;
    char *message;
    int result;
    struct clog *logger = _clog_loggers[id];

    if (!logger) {
        _clog_err("No such logger: %d\n", id);
        return;
    }

    if (level < logger->level) {
        return;
    }

    /* Format the message text with the argument list. */
    result = vsnprintf(dynbuf, buf_size, fmt, ap);
    if ((size_t) result >= buf_size) {
        buf_size = result + 1;
        dynbuf = (char *) malloc(buf_size);
        result = vsnprintf(dynbuf, buf_size, fmt, ap);
        if ((size_t) result >= buf_size) {
            /* Formatting failed -- too large */
            _clog_err("Formatting failed (1).\n");
            free(dynbuf);
            return;
        }
    }

    /* Format according to log format and write to log */
    {
        char message_buf[4096];
        message = _clog_format(logger, message_buf, 4096, sfile, sline,
                               CLOG_LEVEL_NAMES[level], dynbuf);
        if (!message) {
            _clog_err("Formatting failed (2).\n");
            if (dynbuf != buf) {
                free(dynbuf);
            }
            return;
        }
        result = write(logger->fd, message, strlen(message));
        if (logger->cb) logger->cb(message,strlen(message));
        if (result == -1) {
            _clog_err("Unable to write to log file: %s\n", strerror(errno));
        }
        if (message != message_buf) {
            free(message);
        }
        if (dynbuf != buf) {
            free(dynbuf);
        }
#ifndef _MSC_VER
        fsync(logger->fd);
#else
        HANDLE h = (HANDLE) _get_osfhandle(logger->fd);
        if (h != INVALID_HANDLE_VALUE) {
            // This call will fail on a console fd, but that's ok.
            FlushFileBuffers(h);
        }
#endif
    }
}

void
clog_debug(const char *sfile, int sline, int id, const char *fmt, va_list ap)
{
    _clog_log(sfile, sline, CLOG_DEBUG, id, fmt, ap);
}

void
clog_info(const char *sfile, int sline, int id, const char *fmt, va_list ap)
{
    _clog_log(sfile, sline, CLOG_INFO, id, fmt, ap);
}

void
clog_warn(const char *sfile, int sline, int id, const char *fmt, va_list ap)
{
    _clog_log(sfile, sline, CLOG_WARN, id, fmt, ap);
}

void
clog_error(const char *sfile, int sline, int id, const char *fmt, va_list ap)
{
    _clog_log(sfile, sline, CLOG_ERROR, id, fmt, ap);
}

void
_clog_err(const char *fmt, ...)
{
#ifdef CLOG_SILENT
    (void) fmt;
#else
    va_list ap;

    va_start(ap, fmt);
    vfprintf(stderr, fmt, ap);
#endif
}

#endif /* CLOG_MAIN */

#ifdef __cplusplus
} /* extern "C" */
#endif

#endif /* __CLOG_H__ */
