/*
 * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
 *
 * This software is dual-licensed to you under the MIT License (MIT) and
 * the Universal Permissive License (UPL). See the LICENSE file in the root
 * directory for license terms. You may choose either license, or both.
 */

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

#include "advanced/iotcs_log.h"
#include "util/util.h"
#include "iotcs_port_mutex.h"

#include "log/log.h"
#define IOTCSP_MODULE_LOG_CHANNEL LOG_CHANNEL_LOG
#include "log/log_template.h"

#ifndef IOTCS_LOG_MSG_BUFFER_SIZE
#define IOTCS_LOG_MSG_BUFFER_SIZE 180
#endif

static void default_log_method(iotcs_log_level level, iotcs_log_channel channel,
            const char *buf, size_t len);
static iotcs_log_method g_method = default_log_method;
static iotcs_log_level g_global_level = IOTCS_LOG_LEVEL_ALL;
static char g_log_msg_buf[IOTCS_LOG_MSG_BUFFER_SIZE];

#ifdef IOTCS_MESSAGING_THREAD_SAFETY
static iotcs_port_mutex g_log_mutex;
#define LOG_LOCK_INIT_RESULT() (NULL != (g_log_mutex = iotcs_port_mutex_create()) ? IOTCS_RESULT_OK : IOTCS_RESULT_FAIL)
#define LOG_LOCK_FINIT() do { iotcs_port_mutex_destroy(g_log_mutex);\
                              g_log_mutex = NULL; } while(0)
#define LOG_LOCK() do { iotcs_port_mutex_lock(g_log_mutex); } while(0)
#define LOG_UNLOCK() do { iotcs_port_mutex_unlock(g_log_mutex); } while(0)
#else
#define LOG_LOCK_INIT_RESULT() (IOTCS_RESULT_OK)
#define LOG_LOCK_FINIT() do { } while(0)
#define LOG_LOCK() do { } while(0)
#define LOG_UNLOCK() do { } while(0)
#endif

static iotcs_log_level g_levels[IOTCS_LOG_CHANNEL_NUM] = {
    IOTCS_LOG_LEVEL_ALL,
    IOTCS_LOG_LEVEL_ALL,
    IOTCS_LOG_LEVEL_ALL,
    IOTCS_LOG_LEVEL_ALL,
    IOTCS_LOG_LEVEL_ALL,
    IOTCS_LOG_LEVEL_ALL,
    IOTCS_LOG_LEVEL_ALL,
    IOTCS_LOG_LEVEL_ALL,
    IOTCS_LOG_LEVEL_ALL,
    IOTCS_LOG_LEVEL_ALL,
    IOTCS_LOG_LEVEL_ALL,
    IOTCS_LOG_LEVEL_ALL,
    IOTCS_LOG_LEVEL_ALL,
    IOTCS_LOG_LEVEL_ALL,
    IOTCS_LOG_LEVEL_ALL,
    IOTCS_LOG_LEVEL_ALL,
    IOTCS_LOG_LEVEL_ALL,
    IOTCS_LOG_LEVEL_ALL,
    IOTCS_LOG_LEVEL_ALL,
    IOTCS_LOG_LEVEL_ALL,
};

static const char *const g_level2str[IOTCS_LOG_LEVEL_ALL] = {
    "CRIT", "ERR ", "WARN", "INFO", "DBG",
};

static const char *const g_channel2str[IOTCS_LOG_CHANNEL_NUM] = {
    "DM  ", "EXT ", "CORE", "JSON", "MSG ", "PROT", "TAM ", "UTIL", "LOG", "TEST",
    "QUE ", "CRYP", "PTAM", "DIAG", "MEM ", "MUT ", "SSL ", "SYS ", "THR ", "MQTT"
};

static const char g_overflow_str[] = "CRIT LOG  : OVF\n";
/*
 * Public API 
 */

iotcs_log_method iotcs_log_get_method(void) {
    if (g_method == default_log_method) {
        return NULL;
    }
    return g_method;
}
    
void iotcs_log_set_method(iotcs_log_method method) {
    if (NULL == method) {
        g_method = default_log_method;
    } else {
        g_method = method;
    }
}

iotcs_log_level iotcs_log_get_global_level(void) {
    return g_global_level;
}

void iotcs_log_set_global_level(iotcs_log_level level) {
    if (level < IOTCS_LOG_LEVEL_NONE || level > IOTCS_LOG_LEVEL_ALL)
        return;
    g_global_level = level;
}

const iotcs_log_level* iotcs_log_get_levels(void) {
    return g_levels;
}

void iotcs_log_set_levels(const iotcs_log_level *levels) {
    if (levels) {
        memcpy(g_levels, levels, sizeof (g_levels));
    }
}

iotcs_log_level iotcs_log_get_level(iotcs_log_channel channel) {
    if (channel >= IOTCS_LOG_CHANNEL_NUM) {
        return IOTCS_LOG_LEVEL_NONE;
    }
    return g_levels[channel];
}

void iotcs_log_set_level(iotcs_log_channel channel, iotcs_log_level level) {
    if (channel >= IOTCS_LOG_CHANNEL_NUM) {
        return;
    }
    g_levels[channel] = level;
}
    
void iotcs_log_set_level_all(iotcs_log_level level) {
    int i;
    for(i = 0 ; i < IOTCS_LOG_CHANNEL_NUM; ++i) {
        g_levels[i] = level;
    }
}

/*
 * Private logging API
 */
static void default_log_method(iotcs_log_level level, iotcs_log_channel channel,
            const char *buf, size_t len) {
    FILE *fp = level >= IOTCS_LOG_LEVEL_INFO ? stdout : stderr;
    UNUSED_ARG(channel);
    fwrite(buf, len, 1, fp);
}

iotcs_result log_internal_init(void) {
    return LOG_LOCK_INIT_RESULT();
}

void log_internal_finit(void) {
    LOG_LOCK_FINIT();
}

static void handle_buffer_overflow(void) {
    if (g_global_level >= IOTCS_LOG_LEVEL_CRITICAL) {
        g_method(IOTCS_LOG_LEVEL_CRITICAL, IOTCS_LOG_CHANNEL_LOG, g_overflow_str, sizeof(g_overflow_str));
    }
}

#ifdef IOTCS_LOG_USE_FILE_LINE
static int last_index_of(const char*str, char ch) {
    int idx = 0;
    int rv = -1;
    
    while(*str) {
        if (ch == *str) {
            rv = idx;
        }
        str++, idx++;
    }
    return rv;
}

static int iotcs_log_common(iotcs_log_level level, iotcs_log_channel channel, const char *file, int lineno) {
#else
static int iotcs_log_common(iotcs_log_level level, iotcs_log_channel channel) {
#endif
    if (g_global_level < level || g_levels[channel] < level) {
        return 0;
    }

#ifdef IOTCS_LOG_USE_FILE_LINE
    {
        int idx;
        if (-1 == (idx = last_index_of(file, '/'))) {
            idx = last_index_of(file, '\\');
        }
        return util_safe_snprintf(g_log_msg_buf, IOTCS_LOG_MSG_BUFFER_SIZE, "%s %s %s:%d: ",
                g_level2str[level - 1], g_channel2str[channel], file + idx + 1, lineno);
    }
#else
    return util_safe_snprintf(g_log_msg_buf, IOTCS_LOG_MSG_BUFFER_SIZE, "%s %s: ",
            g_level2str[level - 1], g_channel2str[channel]);
#endif
}

#ifdef IOTCS_LOG_USE_FILE_LINE
void log_msg(iotcs_log_level level, iotcs_log_channel channel, const char *file, int lineno, const char *fmt, ...) {
#else
void log_msg(iotcs_log_level level, iotcs_log_channel channel, const char *fmt, ...) {
#endif
    int written;
    
    LOG_LOCK();

#ifdef IOTCS_LOG_USE_FILE_LINE
    written = iotcs_log_common(level, channel, file, lineno);
#else
    written = iotcs_log_common(level, channel);
#endif
    
    if (0 == written) {
        goto exit;
    }
    if (0 > written) {
        goto error;
    }
    
    {
        int rv;
        va_list ap;
        va_start(ap, fmt);
        rv = util_safe_vsnprintf(g_log_msg_buf + written, IOTCS_LOG_MSG_BUFFER_SIZE - written, fmt, ap);
        written += rv;
        va_end(ap);
        /* also check that there is a place for newline */
        if (0 > rv || IOTCS_LOG_MSG_BUFFER_SIZE - written == 0) {
            goto error;
        }
        g_log_msg_buf[written++] = '\n';
    }
    
    g_method(level, channel, g_log_msg_buf, written);
exit:
    LOG_UNLOCK();
    return;

error:
        
    handle_buffer_overflow();
    LOG_UNLOCK();
}

#ifdef IOTCS_LOG_USE_FILE_LINE

void log_str(iotcs_log_level level, iotcs_log_channel channel, const char *file, int lineno, const char *preformatted) {
    log_strn(level, channel, file, lineno, preformatted, strlen(preformatted));
}
#else
void log_str(iotcs_log_level level, iotcs_log_channel channel, const char *preformatted) {
    log_strn(level, channel, preformatted, strlen(preformatted));
}
#endif

#ifdef IOTCS_LOG_USE_FILE_LINE
void log_strn(iotcs_log_level level, iotcs_log_channel channel, const char *file, int lineno, const char *preformatted, size_t len) {
#else
void log_strn(iotcs_log_level level, iotcs_log_channel channel, const char *preformatted, size_t len) {
#endif
    const char newline = '\n';
    int written;
    
    if (len == 0) {
        return;
    }
    LOG_LOCK();

#ifdef IOTCS_LOG_USE_FILE_LINE
    written = iotcs_log_common(level, channel, file, lineno);
#else
    written = iotcs_log_common(level, channel);
#endif
    
    if (0 == written) {
        goto exit;
    }
    if (0 > written){
        goto error;
    }
    
    g_method(level, channel, g_log_msg_buf, written);
    g_method(level, channel, preformatted, len);
    g_method(level, channel, &newline, 1);
exit:
    LOG_UNLOCK();
    return;

error:
        
    handle_buffer_overflow();
    LOG_UNLOCK();
}

/* TODO: fixme!!! */
void log_printdump(const char* buffer, int sz) {
    int i, c;
    char buf[80];

    for (i = 0; i < sz; i++) {
        if ((i != 0) && (i % 16 == 0)) {
            buf[16] = '\0';
            fprintf(stderr, "    %s\n", buf);
        }
        if (i % 16 == 0)
            fprintf(stderr, "%8p:", &buffer[i]);
        c = buffer[i] & 0xFF;
        if ((c >= ' ') && (c <= 0x7E))
            buf[i % 16] = (unsigned char) c;
        else
            buf[i % 16] = '.';
        fprintf(stderr, " %02x", c & 0xFF);
    }
    if (i % 16 == 0)
        buf[16] = '\0';
    else {
        buf[i % 16] = '\0';
        for (i = i % 16; i < 16; i++)
            fputs("   ", stderr);
    }
    fprintf(stderr, "    %s\n", buf);
}