/* 
 * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com)
 * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org>
 *
 * 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; either version 2 of the License, or
 * (at your option) any later version.
 *
 * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA 
 */
#include <pj/types.h>
#include <pj/log.h>
#include <pj/string.h>
#include <pj/os.h>
#include <pj/compat/stdarg.h>

#if PJ_LOG_MAX_LEVEL >= 1

#if 0
PJ_DEF_DATA(int) pj_log_max_level = PJ_LOG_MAX_LEVEL;
#else
static int pj_log_max_level = PJ_LOG_MAX_LEVEL;
#endif

static void *g_last_thread;

#if PJ_HAS_THREADS
static long thread_suspended_tls_id = -1;
#  if PJ_LOG_ENABLE_INDENT
static long thread_indent_tls_id = -1;
#  endif
#endif

#if !PJ_LOG_ENABLE_INDENT || !PJ_HAS_THREADS
static int log_indent;
#endif

static pj_log_func *log_writer = &pj_log_write;
static unsigned log_decor = PJ_LOG_HAS_TIME | PJ_LOG_HAS_MICRO_SEC |
                            PJ_LOG_HAS_SENDER | PJ_LOG_HAS_NEWLINE |
                            PJ_LOG_HAS_SPACE | PJ_LOG_HAS_THREAD_SWC |
                            PJ_LOG_HAS_INDENT
#if (defined(PJ_WIN32) && PJ_WIN32!=0) || \
    (defined(PJ_WIN64) && PJ_WIN64!=0)
                            | PJ_LOG_HAS_COLOR
#endif
                            ;

static pj_color_t PJ_LOG_COLOR_0 = PJ_TERM_COLOR_BRIGHT | PJ_TERM_COLOR_R;
static pj_color_t PJ_LOG_COLOR_1 = PJ_TERM_COLOR_BRIGHT | PJ_TERM_COLOR_R;
static pj_color_t PJ_LOG_COLOR_2 = PJ_TERM_COLOR_BRIGHT | 
                                   PJ_TERM_COLOR_R | 
                                   PJ_TERM_COLOR_G;
static pj_color_t PJ_LOG_COLOR_3 = PJ_TERM_COLOR_BRIGHT | 
                                   PJ_TERM_COLOR_R | 
                                   PJ_TERM_COLOR_G | 
                                   PJ_TERM_COLOR_B;
static pj_color_t PJ_LOG_COLOR_4 = PJ_TERM_COLOR_R | 
                                   PJ_TERM_COLOR_G | 
                                   PJ_TERM_COLOR_B;
static pj_color_t PJ_LOG_COLOR_5 = PJ_TERM_COLOR_R | 
                                   PJ_TERM_COLOR_G | 
                                   PJ_TERM_COLOR_B;
static pj_color_t PJ_LOG_COLOR_6 = PJ_TERM_COLOR_R | 
                                   PJ_TERM_COLOR_G | 
                                   PJ_TERM_COLOR_B;
/* Default terminal color */
static pj_color_t PJ_LOG_COLOR_77 = PJ_TERM_COLOR_R | 
                                    PJ_TERM_COLOR_G | 
                                    PJ_TERM_COLOR_B;

#if PJ_LOG_USE_STACK_BUFFER==0
static char log_buffer[PJ_LOG_MAX_SIZE];
#endif

#define LOG_MAX_INDENT          80

#if PJ_HAS_THREADS
static void logging_shutdown(void)
{
    if (thread_suspended_tls_id != -1) {
        pj_thread_local_free(thread_suspended_tls_id);
        thread_suspended_tls_id = -1;
    }
#  if PJ_LOG_ENABLE_INDENT
    if (thread_indent_tls_id != -1) {
        pj_thread_local_free(thread_indent_tls_id);
        thread_indent_tls_id = -1;
    }
#  endif
}
#endif  /* PJ_HAS_THREADS */

#if PJ_LOG_ENABLE_INDENT && PJ_HAS_THREADS
PJ_DEF(void) pj_log_set_indent(int indent)
{
    if (indent < 0) indent = 0;
    pj_thread_local_set(thread_indent_tls_id, (void*)(pj_ssize_t)indent);
}

static int log_get_raw_indent(void)
{
    return (long)(pj_ssize_t)pj_thread_local_get(thread_indent_tls_id);
}

#else
PJ_DEF(void) pj_log_set_indent(int indent)
{
    log_indent = indent;
    if (log_indent < 0) log_indent = 0;
}

static int log_get_raw_indent(void)
{
    return log_indent;
}
#endif  /* PJ_LOG_ENABLE_INDENT && PJ_HAS_THREADS */

PJ_DEF(int) pj_log_get_indent(void)
{
    int indent = log_get_raw_indent();
    return indent > LOG_MAX_INDENT ? LOG_MAX_INDENT : indent;
}

PJ_DEF(void) pj_log_add_indent(int indent)
{
    pj_log_set_indent(log_get_raw_indent() + indent);
}

PJ_DEF(void) pj_log_push_indent(void)
{
    pj_log_add_indent(PJ_LOG_INDENT_SIZE);
}

PJ_DEF(void) pj_log_pop_indent(void)
{
    pj_log_add_indent(-PJ_LOG_INDENT_SIZE);
}

pj_status_t pj_log_init(void)
{
#if PJ_HAS_THREADS
    if (thread_suspended_tls_id == -1) {
        pj_status_t status;
        status = pj_thread_local_alloc(&thread_suspended_tls_id);
        if (status != PJ_SUCCESS) {
            thread_suspended_tls_id = -1;
            return status;
        }

#  if PJ_LOG_ENABLE_INDENT
        status = pj_thread_local_alloc(&thread_indent_tls_id);
        if (status != PJ_SUCCESS) {
            pj_thread_local_free(thread_suspended_tls_id);
            thread_suspended_tls_id = -1;
            thread_indent_tls_id = -1;
            return status;
        }
#  endif
        pj_atexit(&logging_shutdown);
    }
#endif
    g_last_thread = NULL;

    /* Normalize log decor, e.g: unset thread flags when threading is
     * disabled.
     */
    pj_log_set_decor(pj_log_get_decor());

    return PJ_SUCCESS;
}

PJ_DEF(void) pj_log_set_decor(unsigned decor)
{
    log_decor = decor;

#if !PJ_HAS_THREADS
    /* Unset thread related flags */
    if (log_decor & PJ_LOG_HAS_THREAD_ID) {
       log_decor &= ~(PJ_LOG_HAS_THREAD_ID);
    }
    if (log_decor & PJ_LOG_HAS_THREAD_SWC) {
       log_decor &= ~(PJ_LOG_HAS_THREAD_SWC);
    }
#endif
}

PJ_DEF(unsigned) pj_log_get_decor(void)
{
    return log_decor;
}

PJ_DEF(void) pj_log_set_color(int level, pj_color_t color)
{
    switch (level) 
    {
        case 0: PJ_LOG_COLOR_0 = color; 
            break;
        case 1: PJ_LOG_COLOR_1 = color; 
            break;
        case 2: PJ_LOG_COLOR_2 = color; 
            break;
        case 3: PJ_LOG_COLOR_3 = color; 
            break;
        case 4: PJ_LOG_COLOR_4 = color; 
            break;
        case 5: PJ_LOG_COLOR_5 = color; 
            break;
        case 6: PJ_LOG_COLOR_6 = color; 
            break;
        /* Default terminal color */
        case 77: PJ_LOG_COLOR_77 = color; 
            break;
        default:
            /* Do nothing */
            break;
    }
}

PJ_DEF(pj_color_t) pj_log_get_color(int level)
{
    switch (level) {
        case 0:
            return PJ_LOG_COLOR_0;
        case 1:
            return PJ_LOG_COLOR_1;
        case 2:
            return PJ_LOG_COLOR_2;
        case 3:
            return PJ_LOG_COLOR_3;
        case 4:
            return PJ_LOG_COLOR_4;
        case 5:
            return PJ_LOG_COLOR_5;
        case 6:
            return PJ_LOG_COLOR_6;
        default:
            /* Return default terminal color */
            return PJ_LOG_COLOR_77;
    }
}

PJ_DEF(void) pj_log_set_level(int level)
{
    pj_log_max_level = level;
}

#if 1
PJ_DEF(int) pj_log_get_level(void)
{
    return pj_log_max_level;
}
#endif

PJ_DEF(void) pj_log_set_log_func( pj_log_func *func )
{
    log_writer = func;
}

PJ_DEF(pj_log_func*) pj_log_get_log_func(void)
{
    return log_writer;
}

/* Temporarily suspend logging facility for this thread.
 * If thread local storage/variable is not used or not initialized, then
 * we can only suspend the logging globally across all threads. This may
 * happen e.g. when log function is called before PJLIB is fully initialized
 * or after PJLIB is shutdown.
 */
static void suspend_logging(int *saved_level)
{
    /* Save the level regardless, just in case PJLIB is shutdown
     * between suspend and resume.
     */
    *saved_level = pj_log_max_level;

#if PJ_HAS_THREADS
    if (thread_suspended_tls_id != -1) 
    {
        static pj_ssize_t suspend_log = PJ_TRUE;
        pj_thread_local_set(thread_suspended_tls_id, 
                            (void*)&suspend_log);
    } 
    else
#endif
    {
        pj_log_max_level = 0;
    }
}

/* Resume logging facility for this thread */
static void resume_logging(int *saved_level)
{
#if PJ_HAS_THREADS
    if (thread_suspended_tls_id != -1) 
    {
        static pj_ssize_t suspend_log = PJ_FALSE;
        pj_thread_local_set(thread_suspended_tls_id, 
                            (void*)&suspend_log);
    }
    else
#endif
    {
        /* Only revert the level if application doesn't change the
         * logging level between suspend and resume.
         */
        if (pj_log_max_level==0 && *saved_level)
            pj_log_max_level = *saved_level;
    }
}

/* Is logging facility suspended for this thread? */
static pj_bool_t is_logging_suspended(void)
{
#if PJ_HAS_THREADS
    if (thread_suspended_tls_id != -1) 
    {
        pj_ssize_t *ils;
        ils = (pj_ssize_t *)pj_thread_local_get(thread_suspended_tls_id);
        return (ils? (*ils == PJ_TRUE): PJ_FALSE);
    }
    else
#endif
    {
        return pj_log_max_level == 0;
    }
}

PJ_DEF(void) pj_log( const char *sender, int level, 
                    PJ_PRINT_PARAM_DECOR const char *format, va_list marker)
{
    pj_time_val now;
    pj_parsed_time ptime;
    char *pre;
#if PJ_LOG_USE_STACK_BUFFER
    char log_buffer[PJ_LOG_MAX_SIZE];
#endif
    int saved_level, len, print_len;

    PJ_CHECK_STACK();

    if (level > pj_log_max_level)
        return;

    if (is_logging_suspended())
        return;

    /* Temporarily disable logging for this thread. Some of PJLIB APIs that
     * this function calls below will recursively call the logging function 
     * back, hence it will cause infinite recursive calls if we allow that.
     */
    suspend_logging(&saved_level);

    /* Get current date/time. */
    pj_gettimeofday(&now);
    pj_time_decode(&now, &ptime);

    pre = log_buffer;
    if (log_decor & PJ_LOG_HAS_LEVEL_TEXT) {
        static const char *ltexts[] = { "FATAL:", "ERROR:", " WARN:", 
                              " INFO:", "DEBUG:", "TRACE:", "DETRC:"};
        pj_ansi_strxcpy(pre, ltexts[level], PJ_LOG_MAX_SIZE);
        pre += 6;
    }
    if (log_decor & PJ_LOG_HAS_DAY_NAME) {
        static const char *wdays[] = { "Sun", "Mon", "Tue", "Wed",
                                       "Thu", "Fri", "Sat"};
        pj_ansi_strxcpy(pre, wdays[ptime.wday], PJ_LOG_MAX_SIZE-6);
        pre += 3;
    }
    if (log_decor & PJ_LOG_HAS_YEAR) {
        if (pre!=log_buffer) *pre++ = ' ';
        pre += pj_utoa(ptime.year, pre);
    }
    if (log_decor & PJ_LOG_HAS_MONTH) {
        *pre++ = '-';
        pre += pj_utoa_pad(ptime.mon+1, pre, 2, '0');
    }
    if (log_decor & PJ_LOG_HAS_DAY_OF_MON) {
        *pre++ = '-';
        pre += pj_utoa_pad(ptime.day, pre, 2, '0');
    }
    if (log_decor & PJ_LOG_HAS_TIME) {
        if (pre!=log_buffer) *pre++ = ' ';
        pre += pj_utoa_pad(ptime.hour, pre, 2, '0');
        *pre++ = ':';
        pre += pj_utoa_pad(ptime.min, pre, 2, '0');
        *pre++ = ':';
        pre += pj_utoa_pad(ptime.sec, pre, 2, '0');
    }
    if (log_decor & PJ_LOG_HAS_MICRO_SEC) {
        *pre++ = '.';
        pre += pj_utoa_pad(ptime.msec, pre, 3, '0');
    }
    if (log_decor & PJ_LOG_HAS_SENDER) {
        enum { SENDER_WIDTH = PJ_LOG_SENDER_WIDTH };
        pj_size_t sender_len = strlen(sender);
        if (pre!=log_buffer) *pre++ = ' ';
        if (sender_len <= SENDER_WIDTH) {
            while (sender_len < SENDER_WIDTH)
                *pre++ = ' ', ++sender_len;
            while (*sender)
                *pre++ = *sender++;
        } else {
            int i;
            for (i=0; i<SENDER_WIDTH; ++i)
                *pre++ = *sender++;
        }
    }
    if (log_decor & PJ_LOG_HAS_THREAD_ID) {
        enum { THREAD_WIDTH = PJ_LOG_THREAD_WIDTH };
        const char *thread_name = pj_thread_get_name(pj_thread_this());
        pj_size_t thread_len = strlen(thread_name);
        *pre++ = ' ';
        if (thread_len <= THREAD_WIDTH) {
            while (thread_len < THREAD_WIDTH)
                *pre++ = ' ', ++thread_len;
            while (*thread_name)
                *pre++ = *thread_name++;
        } else {
            int i;
            for (i=0; i<THREAD_WIDTH; ++i)
                *pre++ = *thread_name++;
        }
    }

    if (log_decor != 0 && log_decor != PJ_LOG_HAS_NEWLINE)
        *pre++ = ' ';

    if (log_decor & PJ_LOG_HAS_THREAD_SWC) {
        void *current_thread = (void*)pj_thread_this();
        if (current_thread != g_last_thread) {
            *pre++ = '!';
            g_last_thread = current_thread;
        } else {
            *pre++ = ' ';
        }
    } else if (log_decor & PJ_LOG_HAS_SPACE) {
        *pre++ = ' ';
    }

#if PJ_LOG_ENABLE_INDENT
    if (log_decor & PJ_LOG_HAS_INDENT) {
        int indent = pj_log_get_indent();
        if (indent > 0) {
            pj_memset(pre, PJ_LOG_INDENT_CHAR, indent);
            pre += indent;
        }
    }
#endif

    len = (int)(pre - log_buffer);

    /* Print the whole message to the string log_buffer. */
    print_len = pj_ansi_vsnprintf(pre, sizeof(log_buffer)-len, format, 
                                  marker);
    if (print_len < 0) {
        level = 1;
        print_len = pj_ansi_snprintf(pre, sizeof(log_buffer)-len, 
                                     "<logging error: msg too long>");
    }
    if (print_len < 0 || print_len >= (int)(sizeof(log_buffer)-len)) {
        print_len = sizeof(log_buffer) - len - 1;
    }
    len = len + print_len;
    if (len >= 0 && len < (int)sizeof(log_buffer)-2) {
        if (log_decor & PJ_LOG_HAS_CR) {
            log_buffer[len++] = '\r';
        }
        if (log_decor & PJ_LOG_HAS_NEWLINE) {
            log_buffer[len++] = '\n';
        }
        log_buffer[len] = '\0';
    } else {
        len = sizeof(log_buffer)-1;
        if (log_decor & PJ_LOG_HAS_CR) {
            log_buffer[sizeof(log_buffer)-3] = '\r';
        }
        if (log_decor & PJ_LOG_HAS_NEWLINE) {
            log_buffer[sizeof(log_buffer)-2] = '\n';
        }
        log_buffer[sizeof(log_buffer)-1] = '\0';
    }

    /* It should be safe to resume logging at this point. Application can
     * recursively call the logging function inside the callback.
     */
    resume_logging(&saved_level);

    if (log_writer)
        (*log_writer)(level, log_buffer, len);
}

/*
PJ_DEF(void) pj_log_0(const char *obj, const char *format, ...)
{
    va_list arg;
    va_start(arg, format);
    pj_log(obj, 0, format, arg);
    va_end(arg);
}
*/

PJ_DEF(void) pj_log_1(const char *obj, 
                      PJ_PRINT_PARAM_DECOR const char *format, ...)
{
    va_list arg;
    va_start(arg, format);
    pj_log(obj, 1, format, arg);
    va_end(arg);
}
#endif  /* PJ_LOG_MAX_LEVEL >= 1 */

#if PJ_LOG_MAX_LEVEL >= 2
PJ_DEF(void) pj_log_2(const char *obj, 
                      PJ_PRINT_PARAM_DECOR const char *format, ...)
{
    va_list arg;
    va_start(arg, format);
    pj_log(obj, 2, format, arg);
    va_end(arg);
}
#endif

#if PJ_LOG_MAX_LEVEL >= 3
PJ_DEF(void) pj_log_3(const char *obj, 
                      PJ_PRINT_PARAM_DECOR const char *format, ...)
{
    va_list arg;
    va_start(arg, format);
    pj_log(obj, 3, format, arg);
    va_end(arg);
}
#endif

#if PJ_LOG_MAX_LEVEL >= 4
PJ_DEF(void) pj_log_4(const char *obj, 
                      PJ_PRINT_PARAM_DECOR const char *format, ...)
{
    va_list arg;
    va_start(arg, format);
    pj_log(obj, 4, format, arg);
    va_end(arg);
}
#endif

#if PJ_LOG_MAX_LEVEL >= 5
PJ_DEF(void) pj_log_5(const char *obj, 
                      PJ_PRINT_PARAM_DECOR const char *format, ...)
{
    va_list arg;
    va_start(arg, format);
    pj_log(obj, 5, format, arg);
    va_end(arg);
}
#endif

#if PJ_LOG_MAX_LEVEL >= 6
PJ_DEF(void) pj_log_6(const char *obj, 
                      PJ_PRINT_PARAM_DECOR const char *format, ...)
{
    va_list arg;
    va_start(arg, format);
    pj_log(obj, 6, format, arg);
    va_end(arg);
}
#endif

