/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

#include <Python.h>

#include "log_private.h"
#include "entity.h"
#include "aprintf.h"
#include <qpid/dispatch/ctools.h>
#include <qpid/dispatch/dispatch.h>
#include "alloc.h"
#include <qpid/dispatch/threading.h>
#include <qpid/dispatch/log.h>
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <syslog.h>

#define TEXT_MAX QD_LOG_TEXT_MAX
#define LOG_MAX (QD_LOG_TEXT_MAX+128)
#define LIST_MAX 1000

static qd_log_source_t      *default_log_source=0;
static qd_log_source_t      *logging_log_source=0;

int qd_log_max_len() { return TEXT_MAX; }

typedef struct qd_log_entry_t qd_log_entry_t;

struct qd_log_entry_t {
    DEQ_LINKS(qd_log_entry_t);
    const char     *module;
    int             level;
    char           *file;
    int             line;
    time_t          time;
    char            text[TEXT_MAX];
};

ALLOC_DECLARE(qd_log_entry_t);
ALLOC_DEFINE(qd_log_entry_t);

DEQ_DECLARE(qd_log_entry_t, qd_log_list_t);
static qd_log_list_t         entries = {0};

static void qd_log_entry_free_lh(qd_log_entry_t* entry) {
    DEQ_REMOVE(entries, entry);
    free(entry->file);
    free_qd_log_entry_t(entry);
}

// Ref-counted log sink, may be shared by several sources.
typedef struct log_sink_t {
    int refcount;
    char *name;
    bool syslog;
    FILE *file;
    DEQ_LINKS(struct log_sink_t);
} log_sink_t;

DEQ_DECLARE(log_sink_t, log_sink_list_t);

static log_sink_list_t sink_list = {0};

static const char* SINK_STDERR = "stderr";
static const char* SINK_SYSLOG = "syslog";
static const char* SOURCE_DEFAULT = "DEFAULT";
static const char* SOURCE_LOGGING = "LOGGING";

static log_sink_t* find_log_sink_lh(const char* name) {
    log_sink_t* sink = DEQ_HEAD(sink_list);
    DEQ_FIND(sink, strcmp(sink->name, name) == 0);
    return sink;
}

static log_sink_t* log_sink_lh(const char* name) {
    log_sink_t* sink = find_log_sink_lh(name);
    if (sink)
        sink->refcount++;
    else {
        sink = NEW(log_sink_t);
        *sink = (log_sink_t){ 1, strdup(name), };
        if (strcmp(name, SINK_STDERR) == 0) {
            sink->file = stderr;
        }
        else if (strcmp(name, SINK_SYSLOG) == 0) {
            openlog(0, 0, LOG_DAEMON);
            sink->syslog = true;
        }
        else {
            sink->file = fopen(name, "w");
            if (!sink->file) {
                char msg[TEXT_MAX];
                snprintf(msg, sizeof(msg), "Failed to open log file '%s'", name);
                perror(msg);
                exit(1);
            }
        }
        DEQ_INSERT_TAIL(sink_list, sink);
    }
    return sink;
}

// Must hold the log_source_lock
static void log_sink_free_lh(log_sink_t* sink) {
    if (!sink) return;
    assert(sink->refcount);
    if (--sink->refcount == 0) {
        DEQ_REMOVE(sink_list, sink);
        free(sink->name);
        if (sink->file && sink->file != stderr)
            fclose(sink->file);
        if (sink->syslog) closelog();
        free(sink);
    }
}

struct qd_log_source_t {
    DEQ_LINKS(qd_log_source_t);
    char *module;
    int mask;
    int timestamp;              /* boolean or -1 means not set */
    int source;                 /* boolean or -1 means not set */
    bool syslog;
    log_sink_t *sink;
};

DEQ_DECLARE(qd_log_source_t, qd_log_source_list_t);

static sys_mutex_t          *log_lock = 0;
static sys_mutex_t          *log_source_lock = 0;
static qd_log_source_list_t  source_list = {0};

typedef enum {DEFAULT, NONE, TRACE, DEBUG, INFO, NOTICE, WARNING, ERROR, CRITICAL, N_LEVELS} level_index_t;

typedef struct level_t {
    const char* name;
    int bit;     // QD_LOG bit
    int mask;    // Bit or higher
    const int syslog;
} level_t;

#define ALL_BITS (QD_LOG_CRITICAL | (QD_LOG_CRITICAL-1))

#define LEVEL(name, QD_LOG, SYSLOG) { name, QD_LOG,  ALL_BITS & ~(QD_LOG-1), SYSLOG }

static level_t levels[] = {
    {"default", -1, -1, 0},
    {"none", 0, 0, 0},
    LEVEL("trace",    QD_LOG_TRACE, LOG_DEBUG), /* syslog has no trace level */
    LEVEL("debug",    QD_LOG_DEBUG, LOG_DEBUG),
    LEVEL("info",     QD_LOG_INFO, LOG_INFO),
    LEVEL("notice",   QD_LOG_NOTICE, LOG_NOTICE),
    LEVEL("warning",  QD_LOG_WARNING, LOG_WARNING),
    LEVEL("error",    QD_LOG_ERROR, LOG_ERR),
    LEVEL("critical", QD_LOG_CRITICAL, LOG_CRIT)
};

static const char level_names[TEXT_MAX]; /* Set up in qd_log_initialize */

/// Return NULL and set qd_error if not a valid bit.
static const level_t* level_for_bit(int bit) {
    level_index_t i = 0;
    while (i < N_LEVELS && levels[i].bit != bit) ++i;
    if (i == N_LEVELS) {
        qd_error(QD_ERROR_CONFIG, "'%d' is not a valid log level bit.", bit);
        return NULL;
    }
    return &levels[i];
}

/// Return NULL and set qd_error if not a valid level.
static const level_t* level_for_name(const char *name, int len) {
    level_index_t i = 0;
    while (i < N_LEVELS && strncasecmp(levels[i].name, name, len) != 0) ++i;
    if (i == N_LEVELS) {
        qd_error(QD_ERROR_CONFIG, "'%s' is not a valid log level. Should be one of {%s}.",
                 name, level_names);
        return NULL;
    }
    return &levels[i];
}

/// Return the name of log level or 0 if not found.
static const char* level_name(int level) {
    return (0 <= level && level < N_LEVELS) ? levels[level].name : NULL;
}

static const char *SEPARATORS=", ;:";

/// Calculate the bit mask for a log enable string. Return -1 and set qd_error on error.
static int enable_mask(const char *enable_) {
    char enable[strlen(enable_)+1]; /* Non-const copy for strtok */
    strcpy(enable, enable_);
    char *saveptr = 0;
    int mask = 0;
    for (const char *token = strtok_r(enable, SEPARATORS, &saveptr);
         token;
         token = strtok_r(NULL, SEPARATORS, &saveptr))
    {
        int len = strlen(token);
        int plus = (len > 0 && token[len-1] == '+') ? 1 : 0;
        const level_t* level = level_for_name(token, len-plus);
        if (!level) return -1;  /* qd_error already set */
        mask |= (plus ? level->mask : level->bit);
    }
    return mask;
}

/// Caller must hold log_source_lock
static qd_log_source_t* lookup_log_source_lh(const char *module)
{
    if (strcasecmp(module, SOURCE_DEFAULT) == 0)
        return default_log_source;
    qd_log_source_t *src = DEQ_HEAD(source_list);
    DEQ_FIND(src, strcasecmp(module, src->module) == 0);
    return src;
}

static bool default_bool(int value, int default_value) {
    return value == -1 ? default_value : value;
}

static void write_log(qd_log_source_t *log_source, qd_log_entry_t *entry)
{
    log_sink_t* sink = log_source->sink ? log_source->sink : default_log_source->sink;
    if (!sink) return;

    char log_str[LOG_MAX];
    char *begin = log_str;
    char *end = log_str + LOG_MAX;

    const level_t *level = level_for_bit(entry->level);
    if (!level) {
        level = &levels[INFO];
        qd_error_clear();
    }

    if (default_bool(log_source->timestamp, default_log_source->timestamp)) {
        char buf[100];
        buf[0] = '\0';
        ctime_r(&entry->time, buf);
        buf[strlen(buf)-1] = '\0'; /* Get rid of trailng \n */
        aprintf(&begin, end, "%s ", buf);
    }
    aprintf(&begin, end, "%s (%s) %s", entry->module, level->name, entry->text);
    if (default_bool(log_source->source, default_log_source->source) && entry->file)
        aprintf(&begin, end, " (%s:%d)", entry->file, entry->line);
    aprintf(&begin, end, "\n");

    if (sink->file) {
        if (fputs(log_str, sink->file) == EOF) {
            char msg[TEXT_MAX];
            snprintf(msg, sizeof(msg), "Cannot write log output to '%s'", sink->name);
            perror(msg);
            exit(1);
        };
        fflush(sink->file);
    }
    if (sink->syslog) {
        int syslog_level = level->syslog;
        if (syslog_level != -1)
            syslog(syslog_level, "%s", log_str);
    }
}

/// Reset the log source to the default state
static void qd_log_source_defaults(qd_log_source_t *log_source) {
    log_source->mask = -1;
    log_source->timestamp = -1;
    log_source->source = -1;
    log_source->sink = 0;
}

/// Caller must hold the log_source_lock
static qd_log_source_t *qd_log_source_lh(const char *module)
{
    qd_log_source_t *log_source = lookup_log_source_lh(module);
    if (!log_source)
    {
        log_source = NEW(qd_log_source_t);
        memset(log_source, 0, sizeof(qd_log_source_t));
        DEQ_ITEM_INIT(log_source);
        log_source->module = (char*) malloc(strlen(module) + 1);
        strcpy(log_source->module, module);
        qd_log_source_defaults(log_source);
        DEQ_INSERT_TAIL(source_list, log_source);
    }
    return log_source;
}

qd_log_source_t *qd_log_source(const char *module)
{
    sys_mutex_lock(log_source_lock);
    qd_log_source_t* src = qd_log_source_lh(module);
    sys_mutex_unlock(log_source_lock);
    return src;
}

qd_log_source_t *qd_log_source_reset(const char *module)
{
    sys_mutex_lock(log_source_lock);
    qd_log_source_t* src = qd_log_source_lh(module);
    qd_log_source_defaults(src);
    sys_mutex_unlock(log_source_lock);
    return src;
}

static void qd_log_source_free_lh(qd_log_source_t* src) {
    DEQ_REMOVE_HEAD(source_list);
    log_sink_free_lh(src->sink);
    free(src->module);
    free(src);
}

bool qd_log_enabled(qd_log_source_t *source, qd_log_level_t level) {
    if (!source) return false;
    int mask = source->mask == -1 ? default_log_source->mask : source->mask;
    return level & mask;
}

void qd_log_impl(qd_log_source_t *source, qd_log_level_t level, const char *file, int line, const char *fmt, ...)
{
    if (!qd_log_enabled(source, level)) return;

    qd_log_entry_t *entry = new_qd_log_entry_t();
    DEQ_ITEM_INIT(entry);
    entry->module = source->module;
    entry->level  = level;
    entry->file   = file ? strdup(file) : 0;
    entry->line   = line;
    time(&entry->time);
    va_list ap;
    va_start(ap, fmt);
    vsnprintf(entry->text, TEXT_MAX, fmt, ap);
    va_end(ap);

    write_log(source, entry);

    // Bounded buffer of log entries, keep most recent.
    sys_mutex_lock(log_lock);
    DEQ_INSERT_TAIL(entries, entry);
    if (DEQ_SIZE(entries) > LIST_MAX)
        qd_log_entry_free_lh(DEQ_HEAD(entries));
    sys_mutex_unlock(log_lock);
}

static PyObject *inc_none() { Py_INCREF(Py_None); return Py_None; }

/// Return the log buffer up to limit as a python list. Called by management agent.
PyObject *qd_log_recent_py(long limit) {
    if (PyErr_Occurred()) return NULL;
    PyObject *list = PyList_New(0);
    PyObject *py_entry = NULL;
    if (!list) goto error;
    qd_log_entry_t *entry = DEQ_TAIL(entries);
    while (entry && limit) {
        const int ENTRY_SIZE=6;
        py_entry = PyList_New(ENTRY_SIZE);
        if (!py_entry) goto error;
        int i = 0;
        // NOTE: PyList_SetItem steals a reference so no leak here.
        PyList_SetItem(py_entry, i++, PyString_FromString(entry->module));
        const char* level = level_name(entry->level);
        PyList_SetItem(py_entry, i++, level ? PyString_FromString(level) : inc_none());
        PyList_SetItem(py_entry, i++, PyString_FromString(entry->text));
        PyList_SetItem(py_entry, i++, entry->file ? PyString_FromString(entry->file) : inc_none());
        PyList_SetItem(py_entry, i++, entry->file ? PyLong_FromLong(entry->line) : inc_none());
        PyList_SetItem(py_entry, i++, PyLong_FromLongLong((PY_LONG_LONG)entry->time));
        assert(i == ENTRY_SIZE);
        if (PyErr_Occurred()) goto error;
        PyList_Insert(list, 0, py_entry);
        Py_DECREF(py_entry);
        if (limit > 0) --limit;
        entry = DEQ_PREV(entry);
    }
    return list;
 error:
    Py_XDECREF(list);
    Py_XDECREF(py_entry);
    return NULL;
}

void qd_log_initialize(void)
{
    DEQ_INIT(entries);
    DEQ_INIT(source_list);
    DEQ_INIT(sink_list);

    // Set up level_names for use in error messages.
    ZERO((char*)level_names);
    char *begin = (char*)level_names, *end = (char*)level_names+sizeof(level_names);
    aprintf(&begin, end, "%s", levels[NONE].name);
    for (level_index_t i = NONE + 1; i < N_LEVELS; ++i)
        aprintf(&begin, end, ", %s", levels[i].name);

    log_lock = sys_mutex();
    log_source_lock = sys_mutex();

    default_log_source = qd_log_source(SOURCE_DEFAULT);
    default_log_source->mask = levels[INFO].mask;
    default_log_source->timestamp = true;
    default_log_source->source = 0;
    default_log_source->sink = log_sink_lh(SINK_STDERR);
    logging_log_source = qd_log_source(SOURCE_LOGGING);
}


void qd_log_finalize(void) {
    while (DEQ_HEAD(source_list))
        qd_log_source_free_lh(DEQ_HEAD(source_list));
    while (DEQ_HEAD(entries))
        qd_log_entry_free_lh(DEQ_HEAD(entries));
    while (DEQ_HEAD(sink_list))
        log_sink_free_lh(DEQ_HEAD(sink_list));
}

qd_error_t qd_log_entity(qd_entity_t *entity) {

    qd_error_clear();
    char* module = qd_entity_get_string(entity, "module"); QD_ERROR_RET();
    sys_mutex_lock(log_source_lock);
    qd_log_source_t *src = qd_log_source_lh(module); /* The original log source */
    free(module);
    qd_log_source_t copy = *src; /* A copy to modify outside the lock. */
    sys_mutex_unlock(log_source_lock);

    if (qd_entity_has(entity, "enable")) {
        char *enable = qd_entity_get_string(entity, "enable");
        copy.mask = enable_mask(enable);
        free(enable);
    }
    QD_ERROR_RET();

    if (qd_entity_has(entity, "timestamp"))
        copy.timestamp = qd_entity_get_bool(entity, "timestamp");
    QD_ERROR_RET();

    if (qd_entity_has(entity, "source"))
        copy.source = qd_entity_get_bool(entity, "source");
    QD_ERROR_RET();

    if (qd_entity_has(entity, "output")) {
         log_sink_free_lh(copy.sink); /* DEFAULT source may already have a sink */
        char* output = qd_entity_get_string(entity, "output"); QD_ERROR_RET();
        copy.sink = log_sink_lh(output);
        free(output);
        if (copy.sink->syslog) /* Timestamp off for syslog. */
            copy.timestamp = 0;
    }

    sys_mutex_lock(log_source_lock);
    *src = copy;
    sys_mutex_unlock(log_source_lock);
    return qd_error_code();
}
