/**
 * @file Trace.hh
 *
 * Author: Peter Helfer
 * Date: 2011-05-23
 */

#ifndef TRACE_HH
#define TRACE_HH

#include <string>
#include <unordered_set>
using std::string;
using std::unordered_set;

#include "fmt/format.h"
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>

/**
 * Trace class
 */
class Trace
{
public:
    enum TraceLevel {
        TRACE_Flow,
        TRACE_Debug3,
        TRACE_Debug2,
        TRACE_Debug1,
        TRACE_Debug,
        TRACE_Info1,
        TRACE_Info,
        TRACE_Warn,
        TRACE_Error,
        TRACE_Fatal,

        TRACE_Maxval
    };

    static void setTraceLevel(TraceLevel level)
    {
        traceLevel = level;
    }

    static bool setTraceLevel(const char *levelString)
    {
        for (uint i = 0; i < TRACE_Maxval; i++) {
            if (strcasecmp(levelString, traceLevelString((TraceLevel) i)) == 0) {
                setTraceLevel((TraceLevel) i);
                return true;
            }
        }
        fmt::print(stderr, "Unknown trace level '{}'\n", levelString);
        return false;
    }

    static TraceLevel getTraceLevel()
    {
        return traceLevel;
    }

    static const char *getTraceLevelString()
    {
        return traceLevelString(traceLevel);
    }

    static void setTraceTag(const string &tag)
    {
        traceTags.insert(tag);
    }

    static bool isSet(const string &tag)
    {
        return traceTags.find(tag) != traceTags.end();
    }

#ifdef TRACE_ON
    #define TRACE(lvl, ...) \
        do { \
            if (lvl >= Trace::getTraceLevel()) { \
                Trace::trace(lvl, __FILE__, __LINE__, __FUNCTION__, __VA_ARGS__); \
            } \
        } while(0);

#define TTRACE(tag, lvl, ...)               \
        do { \
            if (Trace::isSet(tag) && lvl >= Trace::getTraceLevel()) {   \
                Trace::trace(lvl, __FILE__, __LINE__, __FUNCTION__, __VA_ARGS__); \
            } \
        } while(0);

    #define TRACE_FLOW_IS_ON    (Trace::TRACE_Flow   >= Trace::getTraceLevel())
    #define TRACE_DEBUG3_IS_ON  (Trace::TRACE_Debug3 >= Trace::getTraceLevel())
    #define TRACE_DEBUG2_IS_ON  (Trace::TRACE_Debug2 >= Trace::getTraceLevel())
    #define TRACE_DEBUG1_IS_ON  (Trace::TRACE_Debug1 >= Trace::getTraceLevel())
    #define TRACE_DEBUG_IS_ON   (Trace::TRACE_Debug  >= Trace::getTraceLevel())
    #define TRACE_INFO1_IS_ON   (Trace::TRACE_Info1  >= Trace::getTraceLevel())
    #define TRACE_INFO_IS_ON    (Trace::TRACE_Info   >= Trace::getTraceLevel())
    #define TRACE_WARN_IS_ON    (Trace::TRACE_Warn   >= Trace::getTraceLevel())
    #define TRACE_ERROR_IS_ON   (Trace::TRACE_Error  >= Trace::getTraceLevel())
#else
    #define TRACE(lvl, ...)
    #define TTRACE(tag, lvl, ...)
    #define TRACE_FLOW_IS_ON    false
    #define TRACE_DEBUG3_IS_ON  false
    #define TRACE_DEBUG2_IS_ON  false
    #define TRACE_DEBUG1_IS_ON  false
    #define TRACE_DEBUG_IS_ON   false
    #define TRACE_INFO1_IS_ON   false
    #define TRACE_INFO_IS_ON    false
    #define TRACE_WARN_IS_ON    false
    #define TRACE_ERROR_IS_ON   false
#endif

#define TRACE_FLOW(...)  TRACE(Trace::TRACE_Flow,    __VA_ARGS__)
#define TRACE_DEBUG3(...) TRACE(Trace::TRACE_Debug3, __VA_ARGS__)
#define TRACE_DEBUG2(...) TRACE(Trace::TRACE_Debug2, __VA_ARGS__)
#define TRACE_DEBUG1(...) TRACE(Trace::TRACE_Debug1, __VA_ARGS__)
#define TRACE_DEBUG(...) TRACE(Trace::TRACE_Debug,   __VA_ARGS__)
#define TRACE_INFO1(...) TRACE(Trace::TRACE_Info1,   __VA_ARGS__)
#define TRACE_INFO(...)  TRACE(Trace::TRACE_Info,    __VA_ARGS__)
#define TRACE_WARN(...)  TRACE(Trace::TRACE_Warn,    __VA_ARGS__)
#define TRACE_ERROR(...) TRACE(Trace::TRACE_Error,   __VA_ARGS__)

#define TTRACE_FLOW(tag, ...)   TTRACE(tag, Trace::TRACE_Flow,   __VA_ARGS__)
#define TTRACE_DEBUG3(tag, ...) TTRACE(tag, Trace::TRACE_Debug3, __VA_ARGS__)
#define TTRACE_DEBUG2(tag, ...) TTRACE(tag, Trace::TRACE_Debug2, __VA_ARGS__)
#define TTRACE_DEBUG1(tag, ...) TTRACE(tag, Trace::TRACE_Debug1, __VA_ARGS__)
#define TTRACE_DEBUG(tag, ...)  TTRACE(tag, Trace::TRACE_Debug,  __VA_ARGS__)
#define TTRACE_INFO1(tag, ...)  TTRACE(tag, Trace::TRACE_Info1,  __VA_ARGS__)
#define TTRACE_INFO(tag, ...)   TTRACE(tag, Trace::TRACE_Info,   __VA_ARGS__)
#define TTRACE_WARN(tag, ...)   TTRACE(tag, Trace::TRACE_Warn,   __VA_ARGS__)
#define TTRACE_ERROR(tag, ...)  TTRACE(tag, Trace::TRACE_Error,  __VA_ARGS__)

#define TRACE_FATAL(...)                                           \
    do {                                                                \
        Trace::trace(Trace::TRACE_Fatal, __FILE__, __LINE__, __FUNCTION__, __VA_ARGS__); \
        abort(); \
    } while (0)
// TRACE_FATAL will always abort the process, even when compiled without TRACE_ON

#define TTRACE_FLOW_IS_ON(tag)   (Trace::isSet(tag) && TRACE_FLOW_IS_ON)
#define TTRACE_DEBUG3_IS_ON(tag) (Trace::isSet(tag) && TRACE_DEBUG3_IS_ON)
#define TTRACE_DEBUG2_IS_ON(tag) (Trace::isSet(tag) && TRACE_DEBUG2_IS_ON)
#define TTRACE_DEBUG1_IS_ON(tag) (Trace::isSet(tag) && TRACE_DEBUG1_IS_ON)
#define TTRACE_DEBUG_IS_ON(tag)  (Trace::isSet(tag) && TRACE_DEBUG_IS_ON)
#define TTRACE_INFO1_IS_ON(tag)  (Trace::isSet(tag) && TRACE_INFO1_IS_ON)
#define TTRACE_INFO_IS_ON(tag)   (Trace::isSet(tag) && TRACE_INFO_IS_ON)
#define TTRACE_WARN_IS_ON(tag)   (Trace::isSet(tag) && TRACE_WARN_IS_ON)
#define TTRACE_ERROR_IS_ON(tag)  (Trace::isSet(tag) && TRACE_ERROR_IS_ON)


/**
  * Abort if a condition is true. Unlike assert(3), ABORT_IF cannot be
  * disabled by a compile option like -DNDEBUG.
  * @param cond The condition
  * @param fmt fmtlib format string for error message
  * @param ... Optional parameters values for error message
  */
#define ABORT_IF(cond, ...) \
    if (cond) {                  \
        TRACE_DEBUG("Aborting because: {}", #cond);     \
        TRACE_FATAL( __VA_ARGS__);    \
    }

#define ABORT_UNLESS(cond, ...) \
    ABORT_IF(!(cond), __VA_ARGS__)

#define TRACE_ENTER()   do { TRACE_FLOW("-->"); Trace::incrIndent(); } while (0);
#define TRACE_EXIT()    do { Trace::decrIndent(); TRACE_FLOW("<--"); } while (0);
#define TRACE_RETURN(x) do { TRACE_EXIT(); return x; } while (0)
     
    template <typename... Args>
    static void trace(TraceLevel lvl,
                      const char *file,
                      int line,
                      const char *func,
                      const char *fmt,
                      Args  ... args)
    {
        FILE *dest = lvl >= TRACE_Warn ? stderr : stdout;
        fmt::print(dest, "{}{} {}[{}] {}(): ",
                indentStr(), traceLevelString(lvl), file, line, func);
        fmt::vprint(dest, fmt, fmt::make_format_args(args...));
        fmt::print(dest, "\n");
    }
    
    static void incrIndent()
    {
        if (indentLevel < MAXINDENT) indentLevel++;
    }

        static void decrIndent()
    {
        if (indentLevel > 0) indentLevel--;
    }
    
    static const char *traceLevelString(TraceLevel lvl)
    {
        switch (lvl) {
            case TRACE_Flow:   return "FLOW";
            case TRACE_Debug3: return "DEBUG3";
            case TRACE_Debug2: return "DEBUG2";
            case TRACE_Debug1: return "DEBUG1";
            case TRACE_Debug:  return "DEBUG";
            case TRACE_Info1:  return "INFO1";
            case TRACE_Info:   return "INFO";
            case TRACE_Warn:   return "WARN";
            case TRACE_Error:  return "ERROR";
            case TRACE_Fatal:  return "FATAL";
            default:           return "UNKNOWN TRACE LEVEL";
        }
    }

private:
    static TraceLevel traceLevel;
    static unordered_set<string> traceTags;
    static uint indentLevel;
    
    enum { MAXINDENT = 128 };
    
    static const char *indentStr()
    {
        if (traceLevel <= TRACE_Flow) {
            static char buf[MAXINDENT + 1];
            static bool firstTime = true;
            if (firstTime) {
                for (uint i = 0; i < MAXINDENT; i++) {
                    buf[i] = ' ';
                }
                buf[MAXINDENT] = 0;
                firstTime = false;
            }
            return buf + MAXINDENT - indentLevel;
        } else {
            return "";
        }
    }
};

#endif // TRACE_HH