/*
 * This file is part of the EasyLogger Library.
 *
 * Copyright (c) 2015-2018, Armink, <armink.ztl@gmail.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * 'Software'), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 * Function: Initialize function and other general function.
 * Created on: 2015-04-28
 */

#define LOG_TAG      "elog"

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

#if !defined(ELOG_OUTPUT_LVL)
    #error "Please configure static output log level (in elog_cfg.h)"
#endif

#if !defined(ELOG_LINE_NUM_MAX_LEN)
    #error "Please configure output line number max length (in elog_cfg.h)"
#endif

#if !defined(ELOG_LINE_BUF_SIZE)
    #error "Please configure buffer size for every line's log (in elog_cfg.h)"
#endif

#if !defined(ELOG_FILTER_TAG_MAX_LEN)
    #error "Please configure output filter's tag max length (in elog_cfg.h)"
#endif

#if !defined(ELOG_FILTER_KW_MAX_LEN)
    #error "Please configure output filter's keyword max length (in elog_cfg.h)"
#endif

#if !defined(ELOG_NEWLINE_SIGN)
    #error "Please configure output newline sign (in elog_cfg.h)"
#endif

/* output filter's tag level max num */
#ifndef ELOG_FILTER_TAG_LVL_MAX_NUM
#define ELOG_FILTER_TAG_LVL_MAX_NUM          4
#endif

#ifdef ELOG_COLOR_ENABLE
/**
 * CSI(Control Sequence Introducer/Initiator) sign
 * more information on https://en.wikipedia.org/wiki/ANSI_escape_code
 */
#define CSI_START                      "\033["
#define CSI_END                        "\033[0m"
/* output log front color */
#define F_BLACK                        "30;"
#define F_RED                          "31;"
#define F_GREEN                        "32;"
#define F_YELLOW                       "33;"
#define F_BLUE                         "34;"
#define F_MAGENTA                      "35;"
#define F_CYAN                         "36;"
#define F_WHITE                        "37;"
/* output log background color */
#define B_NULL
#define B_BLACK                        "40;"
#define B_RED                          "41;"
#define B_GREEN                        "42;"
#define B_YELLOW                       "43;"
#define B_BLUE                         "44;"
#define B_MAGENTA                      "45;"
#define B_CYAN                         "46;"
#define B_WHITE                        "47;"
/* output log fonts style */
#define S_BOLD                         "1m"
#define S_UNDERLINE                    "4m"
#define S_BLINK                        "5m"
#define S_NORMAL                       "22m"
/* output log default color definition: [front color] + [background color] + [show style] */

/* color output info */
static const char *color_output_info[] = {
        [ELOG_LVL_ASSERT]  = (F_MAGENTA B_NULL S_NORMAL),
        [ELOG_LVL_ERROR]   = (F_RED B_NULL S_NORMAL),
        [ELOG_LVL_WARN]    = (F_YELLOW B_NULL S_NORMAL),
        [ELOG_LVL_INFO]    = (F_CYAN B_NULL S_NORMAL),
        [ELOG_LVL_DEBUG]   = (F_GREEN B_NULL S_NORMAL),
        [ELOG_LVL_VERBOSE] = (F_BLUE B_NULL S_NORMAL),
};
#endif /* ELOG_COLOR_ENABLE */

/* EasyLogger object */
static EasyLogger elog;
/* every line log's buffer */
static char log_buf[ELOG_LINE_BUF_SIZE] = { 0 };
/* level output info */
static const char *level_output_info[] = {
        [ELOG_LVL_ASSERT]  = "A/",
        [ELOG_LVL_ERROR]   = "E/",
        [ELOG_LVL_WARN]    = "W/",
        [ELOG_LVL_INFO]    = "I/",
        [ELOG_LVL_DEBUG]   = "D/",
        [ELOG_LVL_VERBOSE] = "V/",
};


static bool get_fmt_enabled(uint8_t level, size_t set);
static bool get_fmt_used_and_enabled_u32(uint8_t level, size_t set, uint32_t arg);
static bool get_fmt_used_and_enabled_ptr(uint8_t level, size_t set, const char* arg);
static void elog_set_filter_tag_lvl_default(void);
extern void elog_port_output(const char *log, size_t size);
extern void elog_port_output_lock(void);
extern void elog_port_output_unlock(void);

/**
 * EasyLogger initialize.
 *
 * @return result
 */
int elog_init(void) {
    extern int elog_port_init(void);
    extern int elog_async_init(void);

    int result = 0;

    if (elog.init_ok == true) {
        return result;
    }

    /* port initialize */
    result = elog_port_init();
    if (result != 0) {
        return result;
    }

#ifdef ELOG_ASYNC_OUTPUT_ENABLE
    result = elog_async_init();
    if (result != 0) {
        return result;
    }
#endif

    /* enable the output lock */
    elog_output_lock_enabled(true);
    /* output locked status initialize */
    elog.output_is_locked_before_enable = false;
    elog.output_is_locked_before_disable = false;

#ifdef ELOG_COLOR_ENABLE
    /* enable text color by default */
    elog_set_text_color_enabled(true);
#endif

    /* set level is ELOG_LVL_VERBOSE */
    elog_set_filter_lvl(ELOG_LVL_VERBOSE);

    /* set tag_level to default val */
    elog_set_filter_tag_lvl_default();

    elog.init_ok = true;

    return result;
}

/**
 * EasyLogger deinitialize.
 *
 */
void elog_deinit(void) {
    extern int elog_port_deinit(void);
    extern int elog_async_deinit(void);

    if (!elog.init_ok) {
        return ;
    }
    
#ifdef ELOG_ASYNC_OUTPUT_ENABLE
    elog_async_deinit();
#endif

    /* port deinitialize */
    elog_port_deinit();

    elog.init_ok = false;
}


/**
 * EasyLogger start after initialize.
 */
void elog_start(void) {
    if (!elog.init_ok) {
        return ;
    }
    
    /* enable output */
    elog_set_output_enabled(true);

#if defined(ELOG_ASYNC_OUTPUT_ENABLE)
    elog_async_enabled(true);
#elif defined(ELOG_BUF_OUTPUT_ENABLE)
    elog_buf_enabled(true);
#endif

    /* show version */
    log_i("EasyLogger V%s is initialize success.", ELOG_SW_VERSION);
}

/**
 * EasyLogger stop after initialize.
 */
void elog_stop(void) {
    if (!elog.init_ok) {
        return ;
    }

    /* disable output */
    elog_set_output_enabled(false);

#if defined(ELOG_ASYNC_OUTPUT_ENABLE)
    elog_async_enabled(false);
#elif defined(ELOG_BUF_OUTPUT_ENABLE)
    elog_buf_enabled(false);
#endif

    /* show version */
    log_i("EasyLogger V%s is deinitialize success.", ELOG_SW_VERSION);
}


/**
 * set output enable or disable
 *
 * @param enabled TRUE: enable FALSE: disable
 */
void elog_set_output_enabled(bool enabled) {
    assert((enabled == false) || (enabled == true));

    elog.output_enabled = enabled;
}

#ifdef ELOG_COLOR_ENABLE
/**
 * set log text color enable or disable
 * 
 * @param enabled TRUE: enable FALSE:disable
 */
void elog_set_text_color_enabled(bool enabled) {
    assert((enabled == false) || (enabled == true));

    elog.text_color_enabled = enabled;
}

/**
 * get log text color enable status
 *
 * @return enable or disable
 */
bool elog_get_text_color_enabled(void) {
    return elog.text_color_enabled;
}
#endif /* ELOG_COLOR_ENABLE */

/**
 * get output is enable or disable
 *
 * @return enable or disable
 */
bool elog_get_output_enabled(void) {
    return elog.output_enabled;
}

/**
 * set log output format. only enable or disable
 *
 * @param level level
 * @param set format set
 */
void elog_set_fmt(uint8_t level, size_t set) {
    assert(level <= ELOG_LVL_VERBOSE);

    elog.enabled_fmt_set[level] = set;
}

/**
 * set log filter all parameter
 *
 * @param level level
 * @param tag tag
 * @param keyword keyword
 */
void elog_set_filter(uint8_t level, const char *tag, const char *keyword) {
    assert(level <= ELOG_LVL_VERBOSE);

    elog_set_filter_lvl(level);
    elog_set_filter_tag(tag);
    elog_set_filter_kw(keyword);
}

/**
 * set log filter's level
 *
 * @param level level
 */
void elog_set_filter_lvl(uint8_t level) {
    assert(level <= ELOG_LVL_VERBOSE);

    elog.filter.level = level;
}

/**
 * set log filter's tag
 *
 * @param tag tag
 */
void elog_set_filter_tag(const char *tag) {
    strncpy(elog.filter.tag, tag, ELOG_FILTER_TAG_MAX_LEN);
}

/**
 * set log filter's keyword
 *
 * @param keyword keyword
 */
void elog_set_filter_kw(const char *keyword) {
    strncpy(elog.filter.keyword, keyword, ELOG_FILTER_KW_MAX_LEN);
}

/**
 * lock output 
 */
void elog_output_lock(void) {
    if (elog.output_lock_enabled) {
        elog_port_output_lock();
        elog.output_is_locked_before_disable = true;
    } else {
        elog.output_is_locked_before_enable = true;
    }
}

/**
 * unlock output
 */
void elog_output_unlock(void) {
    if (elog.output_lock_enabled) {
        elog_port_output_unlock();
        elog.output_is_locked_before_disable = false;
    } else {
        elog.output_is_locked_before_enable = false;
    }
}

/**
 * set log filter's tag level val to default
 */
static void elog_set_filter_tag_lvl_default(void)
{
    uint8_t i = 0;

    for (i =0; i< ELOG_FILTER_TAG_LVL_MAX_NUM; i++){
        memset(elog.filter.tag_lvl[i].tag, '\0', ELOG_FILTER_TAG_MAX_LEN + 1);
        elog.filter.tag_lvl[i].level = ELOG_FILTER_LVL_SILENT;
        elog.filter.tag_lvl[i].tag_use_flag = false;
    }
}

/**
 * Set the filter's level by different tag.
 * The log on this tag which level is less than it will stop output.
 *
 * example:
 *     // the example tag log enter silent mode
 *     elog_set_filter_tag_lvl("example", ELOG_FILTER_LVL_SILENT);
 *     // the example tag log which level is less than INFO level will stop output
 *     elog_set_filter_tag_lvl("example", ELOG_LVL_INFO);
 *     // remove example tag's level filter, all level log will resume output
 *     elog_set_filter_tag_lvl("example", ELOG_FILTER_LVL_ALL);
 *
 * @param tag log tag
 * @param level The filter level. When the level is ELOG_FILTER_LVL_SILENT, the log enter silent mode.
 *        When the level is ELOG_FILTER_LVL_ALL, it will remove this tag's level filer.
 *        Then all level log will resume output.
 *
 */
void elog_set_filter_tag_lvl(const char *tag, uint8_t level)
{
    assert(level <= ELOG_LVL_VERBOSE);
    assert(tag != ((void *)0));
    uint8_t i = 0;

    if (!elog.init_ok) {
        return;
    }

    elog_output_lock();
    /* find the tag in arr */
    for (i =0; i< ELOG_FILTER_TAG_LVL_MAX_NUM; i++){
        if (elog.filter.tag_lvl[i].tag_use_flag == true &&
            !strncmp(tag, elog.filter.tag_lvl[i].tag,ELOG_FILTER_TAG_MAX_LEN)){
            break;
        }
    }

    if (i < ELOG_FILTER_TAG_LVL_MAX_NUM){
        /* find OK */
        if (level == ELOG_FILTER_LVL_ALL){
            /* remove current tag's level filter when input level is the lowest level */
             elog.filter.tag_lvl[i].tag_use_flag = false;
             memset(elog.filter.tag_lvl[i].tag, '\0', ELOG_FILTER_TAG_MAX_LEN + 1);
             elog.filter.tag_lvl[i].level = ELOG_FILTER_LVL_SILENT;
        } else{
            elog.filter.tag_lvl[i].level = level;
        }
    } else{
        /* only add the new tag's level filer when level is not ELOG_FILTER_LVL_ALL */
        if (level != ELOG_FILTER_LVL_ALL){
            for (i =0; i< ELOG_FILTER_TAG_LVL_MAX_NUM; i++){
                if (elog.filter.tag_lvl[i].tag_use_flag == false){
                    strncpy(elog.filter.tag_lvl[i].tag, tag, ELOG_FILTER_TAG_MAX_LEN);
                    elog.filter.tag_lvl[i].level = level;
                    elog.filter.tag_lvl[i].tag_use_flag = true;
                    break;
                }
            }
        }
    }
    elog_output_unlock();
}

/**
 * get the level on tag's level filer
 *
 * @param tag tag
 *
 * @return It will return the lowest level when tag was not found.
 *         Other level will return when tag was found.
 */
uint8_t elog_get_filter_tag_lvl(const char *tag)
{
    assert(tag != ((void *)0));
    uint8_t i = 0;
    uint8_t level = ELOG_FILTER_LVL_ALL;

    if (!elog.init_ok) {
        return level;
    }

    elog_output_lock();
    /* find the tag in arr */
    for (i =0; i< ELOG_FILTER_TAG_LVL_MAX_NUM; i++){
        if (elog.filter.tag_lvl[i].tag_use_flag == true &&
            !strncmp(tag, elog.filter.tag_lvl[i].tag,ELOG_FILTER_TAG_MAX_LEN)){
            level = elog.filter.tag_lvl[i].level;
            break;
        }
    }
    elog_output_unlock();

    return level;
}

/**
 * output RAW format log
 *
 * @param format output format
 * @param ... args
 */
void elog_raw_output(const char *format, ...) {
    va_list args;
    size_t log_len = 0;
    int fmt_result;

    /* check output enabled */
    if (!elog.output_enabled) {
        return;
    }

    /* args point to the first variable parameter */
    va_start(args, format);

    /* lock output */
    elog_output_lock();

    /* package log data to buffer */
    fmt_result = vsnprintf(log_buf, ELOG_LINE_BUF_SIZE, format, args);

    /* output converted log */
    if ((fmt_result > -1) && (fmt_result <= ELOG_LINE_BUF_SIZE)) {
        log_len = fmt_result;
    } else {
        log_len = ELOG_LINE_BUF_SIZE;
    }
    /* output log */
#if defined(ELOG_ASYNC_OUTPUT_ENABLE)
    extern void elog_async_output(uint8_t level, const char *log, size_t size);
    /* raw log will using assert level */
    elog_async_output(ELOG_LVL_ASSERT, log_buf, log_len);
#elif defined(ELOG_BUF_OUTPUT_ENABLE)
    extern void elog_buf_output(const char *log, size_t size);
    elog_buf_output(log_buf, log_len);
#else
    elog_port_output(log_buf, log_len);
#endif
    /* unlock output */
    elog_output_unlock();

    va_end(args);
}

/**
 * output the log
 *
 * @param level level
 * @param tag tag
 * @param file file name
 * @param func function name
 * @param line line number
 * @param format output format
 * @param ... args
 *
 */
void elog_output(uint8_t level, const char *tag, const char *dirfile, const char *funcname, const long line, const char *format, ...)
{
    extern const char *elog_port_get_time(void);
    extern const char *elog_port_get_p_info(void);
    extern const char *elog_port_get_t_info(void);
    char filename[128];
    char function[128];
    sprintf(filename, "%16s", basename((char*)dirfile));
    sprintf(function, "%-32s", funcname);
    const char *file = filename;
    const char *func = function;

    size_t tag_len = strlen(tag), log_len = 0, newline_len = strlen(ELOG_NEWLINE_SIGN);
    char line_num[ELOG_LINE_NUM_MAX_LEN + 1] = { 0 };
    char tag_sapce[ELOG_FILTER_TAG_MAX_LEN / 2 + 1] = { 0 };
    va_list args;
    int fmt_result;

    assert(level <= ELOG_LVL_VERBOSE);

    /* check output enabled */
    if (!elog.output_enabled) {
        return;
    }
    /* level filter */
    if (level > elog.filter.level || level > elog_get_filter_tag_lvl(tag)) {
        return;
    } else if (!strstr(tag, elog.filter.tag)) { /* tag filter */
        return;
    }
    /* args point to the first variable parameter */
    va_start(args, format);
    /* lock output */
    elog_output_lock();

#ifdef ELOG_COLOR_ENABLE
    /* add CSI start sign and color info */
    if (elog.text_color_enabled) {
        log_len += elog_strcpy(log_len, log_buf + log_len, CSI_START);
        log_len += elog_strcpy(log_len, log_buf + log_len, color_output_info[level]);
    }
#endif

    /* package level info */
    if (get_fmt_enabled(level, ELOG_FMT_LVL)) {
        log_len += elog_strcpy(log_len, log_buf + log_len, level_output_info[level]);
    }
    /* package tag info */
    if (get_fmt_enabled(level, ELOG_FMT_TAG)) {
        log_len += elog_strcpy(log_len, log_buf + log_len, tag);
        /* if the tag length is less than 50% ELOG_FILTER_TAG_MAX_LEN, then fill space */
        if (tag_len <= ELOG_FILTER_TAG_MAX_LEN / 2) {
            memset(tag_sapce, ' ', ELOG_FILTER_TAG_MAX_LEN / 2 - tag_len);
            log_len += elog_strcpy(log_len, log_buf + log_len, tag_sapce);
        }
        log_len += elog_strcpy(log_len, log_buf + log_len, " ");
    }
    /* package time, process and thread info */
    if (get_fmt_enabled(level, ELOG_FMT_TIME | ELOG_FMT_P_INFO | ELOG_FMT_T_INFO)) {
        log_len += elog_strcpy(log_len, log_buf + log_len, " ");
        /* package time info */
        if (get_fmt_enabled(level, ELOG_FMT_TIME)) {
            log_len += elog_strcpy(log_len, log_buf + log_len, elog_port_get_time());
            if (get_fmt_enabled(level, ELOG_FMT_P_INFO | ELOG_FMT_T_INFO)) {
                log_len += elog_strcpy(log_len, log_buf + log_len, " ");
            }
        }
        /* package process info */
        if (get_fmt_enabled(level, ELOG_FMT_P_INFO)) {
            log_len += elog_strcpy(log_len, log_buf + log_len, elog_port_get_p_info());
            if (get_fmt_enabled(level, ELOG_FMT_T_INFO)) {
                log_len += elog_strcpy(log_len, log_buf + log_len, " ");
            }
        }
        /* package thread info */
        if (get_fmt_enabled(level, ELOG_FMT_T_INFO)) {
            log_len += elog_strcpy(log_len, log_buf + log_len, elog_port_get_t_info());
        }
        log_len += elog_strcpy(log_len, log_buf + log_len, " ");
    }
    /* package file directory and name, function name and line number info */
    if (get_fmt_used_and_enabled_ptr(level, ELOG_FMT_DIR, file) ||
            get_fmt_used_and_enabled_ptr(level, ELOG_FMT_FUNC, func) ||
            get_fmt_used_and_enabled_u32(level, ELOG_FMT_LINE, line)) {
        /* package func info */
        if (get_fmt_used_and_enabled_ptr(level, ELOG_FMT_FUNC, func)) {
            log_len += elog_strcpy(log_len, log_buf + log_len, func);
            
        }

        // log_len += elog_strcpy(log_len, log_buf + log_len, "[");


        /* package file info */
        if (get_fmt_used_and_enabled_ptr(level, ELOG_FMT_DIR, file)) {
            log_len += elog_strcpy(log_len, log_buf + log_len, file);
            if (get_fmt_used_and_enabled_ptr(level, ELOG_FMT_FUNC, func)) {
                log_len += elog_strcpy(log_len, log_buf + log_len, ":");
            } else if (get_fmt_used_and_enabled_u32(level, ELOG_FMT_LINE, line)) {
                log_len += elog_strcpy(log_len, log_buf + log_len, " ");
            }
        }
        /* package line info */
        if (get_fmt_used_and_enabled_u32(level, ELOG_FMT_LINE, line)) {
            snprintf(line_num, ELOG_LINE_NUM_MAX_LEN, "%3ld", line);
            log_len += elog_strcpy(log_len, log_buf + log_len, line_num);
            if (get_fmt_used_and_enabled_ptr(level, ELOG_FMT_FUNC, func)) {
                log_len += elog_strcpy(log_len, log_buf + log_len, " ");
            }
        }
        
        log_len += elog_strcpy(log_len, log_buf + log_len, "| ");
    }
    /* package other log data to buffer. '\0' must be added in the end by vsnprintf. */
    fmt_result = vsnprintf(log_buf + log_len, ELOG_LINE_BUF_SIZE - log_len, format, args);

    va_end(args);
    /* calculate log length */
    if ((log_len + fmt_result <= ELOG_LINE_BUF_SIZE) && (fmt_result > -1)) {
        log_len += fmt_result;
    } else {
        /* using max length */
        log_len = ELOG_LINE_BUF_SIZE;
    }
    /* overflow check and reserve some space for CSI end sign and newline sign */
#ifdef ELOG_COLOR_ENABLE
    if (log_len + (sizeof(CSI_END) - 1) + newline_len > ELOG_LINE_BUF_SIZE) {
        /* using max length */
        log_len = ELOG_LINE_BUF_SIZE;
        /* reserve some space for CSI end sign */
        log_len -= (sizeof(CSI_END) - 1);
#else
    if (log_len + newline_len > ELOG_LINE_BUF_SIZE) {
        /* using max length */
        log_len = ELOG_LINE_BUF_SIZE;
#endif /* ELOG_COLOR_ENABLE */
        /* reserve some space for newline sign */
        log_len -= newline_len;
    }
    /* keyword filter */
    if (elog.filter.keyword[0] != '\0') {
        /* add string end sign */
        log_buf[log_len] = '\0';
        /* find the keyword */
        if (!strstr(log_buf, elog.filter.keyword)) {
            /* unlock output */
            elog_output_unlock();
            return;
        }
    }

#ifdef ELOG_COLOR_ENABLE
    /* add CSI end sign */
    if (elog.text_color_enabled) {
        log_len += elog_strcpy(log_len, log_buf + log_len, CSI_END);
    }
#endif

    /* package newline sign */
    log_len += elog_strcpy(log_len, log_buf + log_len, ELOG_NEWLINE_SIGN);
    /* output log */
#if defined(ELOG_ASYNC_OUTPUT_ENABLE)
    extern void elog_async_output(uint8_t level, const char *log, size_t size);
    elog_async_output(level, log_buf, log_len);
#elif defined(ELOG_BUF_OUTPUT_ENABLE)
    extern void elog_buf_output(const char *log, size_t size);
    elog_buf_output(log_buf, log_len);
#else
    elog_port_output(log_buf, log_len);
#endif
    /* unlock output */
    elog_output_unlock();
}

/**
 * get format enabled
 *
 * @param level level
 * @param set format set
 *
 * @return enable or disable
 */
static bool get_fmt_enabled(uint8_t level, size_t set) {
    assert(level <= ELOG_LVL_VERBOSE);

    if (elog.enabled_fmt_set[level] & set) {
        return true;
    } else {
        return false;
    }
}

static bool get_fmt_used_and_enabled_u32(uint8_t level, size_t set, uint32_t arg) {
    return arg && get_fmt_enabled(level, set);
}
static bool get_fmt_used_and_enabled_ptr(uint8_t level, size_t set, const char* arg) {
    return arg && get_fmt_enabled(level, set);
}

/**
 * enable or disable logger output lock
 * @note disable this lock is not recommended except you want output system exception log
 *
 * @param enabled true: enable  false: disable
 */
void elog_output_lock_enabled(bool enabled) {
    elog.output_lock_enabled = enabled;
    /* it will re-lock or re-unlock before output lock enable */
    if (elog.output_lock_enabled) {
        if (!elog.output_is_locked_before_disable && elog.output_is_locked_before_enable) {
            /* the output lock is unlocked before disable, and the lock will unlocking after enable */
            elog_port_output_lock();
        } else if (elog.output_is_locked_before_disable && !elog.output_is_locked_before_enable) {
            /* the output lock is locked before disable, and the lock will locking after enable */
            elog_port_output_unlock();
        }
    }
}

/**
 * find the log level
 * @note make sure the log level is output on each format
 *
 * @param log log buffer
 *
 * @return log level, found failed will return -1
 */
int8_t elog_find_lvl(const char *log) {
    assert(log);
    /* make sure the log level is output on each format */
    assert(elog.enabled_fmt_set[ELOG_LVL_ASSERT] & ELOG_FMT_LVL);
    assert(elog.enabled_fmt_set[ELOG_LVL_ERROR] & ELOG_FMT_LVL);
    assert(elog.enabled_fmt_set[ELOG_LVL_WARN] & ELOG_FMT_LVL);
    assert(elog.enabled_fmt_set[ELOG_LVL_INFO] & ELOG_FMT_LVL);
    assert(elog.enabled_fmt_set[ELOG_LVL_DEBUG] & ELOG_FMT_LVL);
    assert(elog.enabled_fmt_set[ELOG_LVL_VERBOSE] & ELOG_FMT_LVL);

#ifdef ELOG_COLOR_ENABLE
    uint8_t i;
    size_t csi_start_len = strlen(CSI_START);
    for(i = 0; i < ELOG_LVL_TOTAL_NUM; i ++) {
        if (!strncmp(color_output_info[i], log + csi_start_len, strlen(color_output_info[i]))) {
            return i;
        }
    }
    /* found failed */
    return -1;
#else
    switch (log[0]) {
    case 'A': return ELOG_LVL_ASSERT;
    case 'E': return ELOG_LVL_ERROR;
    case 'W': return ELOG_LVL_WARN;
    case 'I': return ELOG_LVL_INFO;
    case 'D': return ELOG_LVL_DEBUG;
    case 'V': return ELOG_LVL_VERBOSE;
    default: return -1;
    }
#endif
}

/**
 * find the log tag
 * @note make sure the log tag is output on each format
 * @note the tag don't have space in it
 *
 * @param log log buffer
 * @param lvl log level, you can get it by @see elog_find_lvl
 * @param tag_len found tag length
 *
 * @return log tag, found failed will return NULL
 */
const char *elog_find_tag(const char *log, uint8_t lvl, size_t *tag_len) {
    const char *tag = NULL, *tag_end = NULL;

    assert(log);
    assert(tag_len);
    assert(lvl < ELOG_LVL_TOTAL_NUM);
    /* make sure the log tag is output on each format */
    assert(elog.enabled_fmt_set[lvl] & ELOG_FMT_TAG);

#ifdef ELOG_COLOR_ENABLE
    tag = log + strlen(CSI_START) + strlen(color_output_info[lvl]) + strlen(level_output_info[lvl]);
#else
    tag = log + strlen(level_output_info[lvl]);
#endif
    /* find the first space after tag */
    if ((tag_end = memchr(tag, ' ', ELOG_FILTER_TAG_MAX_LEN)) != NULL) {
        *tag_len = tag_end - tag;
    } else {
        tag = NULL;
    }

    return tag;
}

/**
 * dump the hex format data to log
 *
 * @param name name for hex object, it will show on log header
 * @param width hex number for every line, such as: 16, 32
 * @param buf hex buffer
 * @param size buffer size
 */
void elog_hexdump(const char *name, uint8_t width, const void *buf, uint16_t size)
{
#define __is_print(ch)       ((unsigned int)((ch) - ' ') < 127u - ' ')

    uint16_t i, j;
    uint16_t log_len = 0;
    const uint8_t *buf_p = buf;
    char dump_string[8] = {0};
    int fmt_result;

    if (!elog.output_enabled) {
        return;
    }

    /* level filter */
    if (ELOG_LVL_DEBUG > elog.filter.level) {
        return;
    } else if (!strstr(name, elog.filter.tag)) { /* tag filter */
        return;
    }

    /* lock output */
    elog_output_lock();

    for (i = 0; i < size; i += width) {
        /* package header */
        fmt_result = snprintf(log_buf, ELOG_LINE_BUF_SIZE, "D/HEX %s: %04X-%04X: ", name, i, i + width - 1);
        /* calculate log length */
        if ((fmt_result > -1) && (fmt_result <= ELOG_LINE_BUF_SIZE)) {
            log_len = fmt_result;
        } else {
            log_len = ELOG_LINE_BUF_SIZE;
        }
        /* dump hex */
        for (j = 0; j < width; j++) {
            if (i + j < size) {
                snprintf(dump_string, sizeof(dump_string), "%02X ", buf_p[i + j]);
            } else {
                strncpy(dump_string, "   ", sizeof(dump_string));
            }
            log_len += elog_strcpy(log_len, log_buf + log_len, dump_string);
            if ((j + 1) % 8 == 0) {
                log_len += elog_strcpy(log_len, log_buf + log_len, " ");
            }
        }
        log_len += elog_strcpy(log_len, log_buf + log_len, "  ");
        /* dump char for hex */
        for (j = 0; j < width; j++) {
            if (i + j < size) {
                snprintf(dump_string, sizeof(dump_string), "%c", __is_print(buf_p[i + j]) ? buf_p[i + j] : '.');
                log_len += elog_strcpy(log_len, log_buf + log_len, dump_string);
            }
        }
        /* overflow check and reserve some space for newline sign */
        if (log_len + strlen(ELOG_NEWLINE_SIGN) > ELOG_LINE_BUF_SIZE) {
            log_len = ELOG_LINE_BUF_SIZE - strlen(ELOG_NEWLINE_SIGN);
        }
        /* package newline sign */
        log_len += elog_strcpy(log_len, log_buf + log_len, ELOG_NEWLINE_SIGN);
        /* do log output */
#if defined(ELOG_ASYNC_OUTPUT_ENABLE)
        extern void elog_async_output(uint8_t level, const char *log, size_t size);
        elog_async_output(ELOG_LVL_DEBUG, log_buf, log_len);
#elif defined(ELOG_BUF_OUTPUT_ENABLE)
        extern void elog_buf_output(const char *log, size_t size);
        elog_buf_output(log_buf, log_len);
#else
        elog_port_output(log_buf, log_len);
#endif
    }
    /* unlock output */
    elog_output_unlock();
}
