/**
 * @file Trace.hh
 *
 * Tracing utility
 * 
 * Copyright (c) 2016 - 2018, Peter Helfer
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#ifndef TRACE_HH
#define TRACE_HH

#include <stdio.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_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;
            }
        }
        fprintf(stderr, "Unknown trace level '%s'\n", levelString);
        return false;
    }

    static TraceLevel getTraceLevel()
    {
        return traceLevel;
    }

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


#ifdef TRACE_ON
    #define TRACE(lvl, fmt, ...) \
        do { \
            if (lvl >= Trace::getTraceLevel()) { \
                Trace::trace(lvl, __FILE__, __LINE__, __FUNCTION__, fmt, ##__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_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, fmt, ...)
    #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_INFO_IS_ON    false
    #define TRACE_WARN_IS_ON    false
    #define TRACE_ERROR_IS_ON   false
#endif

#define TRACE_FLOW(fmt, ...)  TRACE(Trace::TRACE_Flow,    fmt, ##__VA_ARGS__)
#define TRACE_DEBUG3(fmt, ...) TRACE(Trace::TRACE_Debug3, fmt, ##__VA_ARGS__)
#define TRACE_DEBUG2(fmt, ...) TRACE(Trace::TRACE_Debug2, fmt, ##__VA_ARGS__)
#define TRACE_DEBUG1(fmt, ...) TRACE(Trace::TRACE_Debug1, fmt, ##__VA_ARGS__)
#define TRACE_DEBUG(fmt, ...) TRACE(Trace::TRACE_Debug,   fmt, ##__VA_ARGS__)
#define TRACE_INFO(fmt, ...)  TRACE(Trace::TRACE_Info,    fmt, ##__VA_ARGS__)
#define TRACE_WARN(fmt, ...)  TRACE(Trace::TRACE_Warn,    fmt, ##__VA_ARGS__)
#define TRACE_ERROR(fmt, ...) TRACE(Trace::TRACE_Error,   fmt, ##__VA_ARGS__)
#define TRACE_FATAL(fmt, ...)                                           \
    do {                                                                \
        Trace::trace(Trace::TRACE_Fatal, __FILE__, __LINE__, __FUNCTION__, fmt, ##__VA_ARGS__); \
        abort(); \
    } while (0)
// TRACE_FATAL will always abort the process, even when compiled without TRACE_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 printf-style format string for error message
  * @param ... Optional parameters values for error message
  */
#define ABORT_IF(cond, fmt, ...) \
    if (cond) {                  \
        TRACE_DEBUG("Aborting because: %s", #cond);     \
        TRACE_FATAL(fmt, ##__VA_ARGS__);    \
    }

#define ABORT_UNLESS(cond, fmt, ...) \
    ABORT_IF(!(cond), fmt, ##__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)
     
    static void trace(TraceLevel lvl,
                      const char *file,
                      int line,
                      const char *func,
                      const char *fmt, ...)
    {
        va_list ap;
        va_start(ap, fmt);
        FILE *dest = lvl >= TRACE_Warn ? stderr : stdout;
        fprintf(dest, "%s%s %s[%d] %s(): ",
                indentStr(), traceLevelString(lvl), file, line, func);
        vfprintf(dest, fmt, ap);
        fprintf(dest, "\n");
        va_end(ap);
    }
    
    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_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 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