/*******************************************************************************
  Copyright (C) 2012 Evidence Srl - www.evidence.eu.com

 Adapted to INDI Library by Jasem Mutlaq & Geehalel.

 This library is free software; you can redistribute it and/or
 modify it under the terms of the GNU Library General Public
 License version 2 as published by the Free Software Foundation.

 This library 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
 Library General Public License for more details.

 You should have received a copy of the GNU Library General Public License
 along with this library; see the file COPYING.LIB.  If not, write to
 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 Boston, MA 02110-1301, USA.
*******************************************************************************/

#pragma once

#include "indiapi.h"
#include "defaultdevice.h"

#include <stdarg.h>
#include <fstream>
#include <ostream>
#include <string>
#include <sstream>
#include <sys/time.h>

/**
 * @brief Macro to configure the logger.
 * Example of configuration of the Logger:
 * 	DEBUG_CONF("outputfile", Logger::file_on|Logger::screen_on, DBG_DEBUG, DBG_ERROR);
 */
#define DEBUG_CONF(outputFile, configuration, fileVerbosityLevel, screenVerbosityLevel)                       \
    {                                                                                                         \
        Logger::getInstance().configure(outputFile, configuration, fileVerbosityLevel, screenVerbosityLevel); \
    }

/**
 * @brief Macro to print log messages.
 * Example of usage of the Logger:
 *	    DEBUG(DBG_DEBUG, "hello " << "world");
 */
/*
#define DEBUG(priority, msg) { \
	std::ostringstream __debug_stream__; \
	__debug_stream__ << msg; \
	Logger::getInstance().print(priority, __FILE__, __LINE__, \
			__debug_stream__.str()); \
	}
*/
#define DEBUG(priority, msg) INDI::Logger::getInstance().print(getDeviceName(), priority, __FILE__, __LINE__, msg)
#define DEBUGF(priority, msg, ...) \
    INDI::Logger::getInstance().print(getDeviceName(), priority, __FILE__, __LINE__, msg, __VA_ARGS__)

#define DEBUGDEVICE(device, priority, msg) INDI::Logger::getInstance().print(device, priority, __FILE__, __LINE__, msg)
#define DEBUGFDEVICE(device, priority, msg, ...) \
    INDI::Logger::getInstance().print(device, priority, __FILE__, __LINE__, msg, __VA_ARGS__)

/**
 * @brief Shorter logging macros. In order to use these macros, the function
 * (or method) "getDeviceName()" must be defined in the calling scope.
 *
 * Usage examples:
 *	    LOG_DEBUG("hello " << "world");
 *	    LOGF_WARN("hello %s", "world");
 */
#define LOG_ERROR(txt)  DEBUG(INDI::Logger::DBG_ERROR, (txt))
#define LOG_WARN(txt)   DEBUG(INDI::Logger::DBG_WARNING, (txt))
#define LOG_INFO(txt)   DEBUG(INDI::Logger::DBG_SESSION, (txt))
#define LOG_DEBUG(txt)  DEBUG(INDI::Logger::DBG_DEBUG, (txt))
#define LOG_EXTRA1(txt)  DEBUG(INDI::Logger::DBG_EXTRA_1, (txt))
#define LOG_EXTRA2(txt)  DEBUG(INDI::Logger::DBG_EXTRA_2, (txt))
#define LOG_EXTRA3(txt)  DEBUG(INDI::Logger::DBG_EXTRA_3, (txt))

#define LOGF_ERROR(fmt, ...) DEBUGF(INDI::Logger::DBG_ERROR, (fmt), __VA_ARGS__)
#define LOGF_WARN(fmt, ...)  DEBUGF(INDI::Logger::DBG_WARNING, (fmt), __VA_ARGS__)
#define LOGF_INFO(fmt, ...)  DEBUGF(INDI::Logger::DBG_SESSION, (fmt), __VA_ARGS__)
#define LOGF_DEBUG(fmt, ...) DEBUGF(INDI::Logger::DBG_DEBUG, (fmt), __VA_ARGS__)
#define LOGF_EXTRA1(fmt, ...) DEBUGF(INDI::Logger::DBG_EXTRA_1, (fmt), __VA_ARGS__)
#define LOGF_EXTRA2(fmt, ...) DEBUGF(INDI::Logger::DBG_EXTRA_2, (fmt), __VA_ARGS__)
#define LOGF_EXTRA3(fmt, ...) DEBUGF(INDI::Logger::DBG_EXTRA_3, (fmt), __VA_ARGS__)


namespace INDI
{
/**
 * @class INDI::Logger
 * @brief The Logger class is a simple logger to log messages to file and INDI clients. This is the implementation of a simple
 *  logger in C++. It is implemented as a Singleton, so it can be easily called through two DEBUG macros.
 * It is Pthread-safe. It allows to log on both file and screen, and to specify a verbosity threshold for both of them.
 *
 * - By default, the class defines 4 levels of debugging/logging levels:
 *      -# Errors: Use macro DEBUG(INDI::Logger::DBG_ERROR, "My Error Message)
 *
 *      -# Warnings: Use macro DEBUG(INDI::Logger::DBG_WARNING, "My Warning Message)
 *
 *      -# Session: Use macro DEBUG(INDI::Logger::DBG_SESSION, "My Message) Session messages are the regular status messages from the driver.
 *
 *      -# Driver Debug: Use macro DEBUG(INDI::Logger::DBG_DEBUG, "My Driver Debug Message)
 *
 * @note Use DEBUGF macro if you have a variable list message. e.g. DEBUGF(INDI::Logger::DBG_SESSION, "Hello %s!", "There")
 *
 * The default \e active debug levels are Error, Warning, and Session. Driver Debug can be enabled by the client.
 *
 * To add a new debug level, call addDebugLevel(). You can add an additional 4 custom debug/logging levels.
 *
 * Check INDI Tutorial two for an example simple implementation.
 */
class Logger
{
    /** Type used for the configuration */
    enum loggerConf_
    {
        L_nofile_   = 1 << 0,
        L_file_     = 1 << 1,
        L_noscreen_ = 1 << 2,
        L_screen_   = 1 << 3
    };

#ifdef LOGGER_MULTITHREAD
    /// Lock for mutual exclusion between different threads
    static pthread_mutex_t lock_;
#endif

    bool configured_ { false };

    /** Pointer to the unique Logger (i.e., Singleton) */
    static Logger *m_;

    /**
     * @brief Initial part of the name of the file used for Logging.
     * Date and time are automatically appended.
     */
    static std::string logFile_;

    /**
     * @brief Directory where log file is stored. it is created under ~/.indi/logs/[DATE]/[DRIVER_EXEC]
     */
    static std::string logDir_;

    /**
     * @brief Current configuration of the logger.
     * Variable to know if logging on file and on screen are enabled. Note that if the log on
     * file is enabled, it means that the logger has been already configured, therefore the
     * stream is already open.
     */
    static loggerConf_ configuration_;

    /// Stream used when logging on a file
    std::ofstream out_;
    /// Initial time (used to print relative times)
    struct timeval initialTime_;
    /// Verbosity threshold for files
    static unsigned int fileVerbosityLevel_;
    /// Verbosity threshold for screen
    static unsigned int screenVerbosityLevel_;
    static unsigned int rememberscreenlevel_;

    /**
     * @brief Constructor.
     * It is a private constructor, called only by getInstance() and only the
     * first time. It is called inside a lock, so lock inside this method
     * is not required.
     * It only initializes the initial time. All configuration is done inside the
     * configure() method.
     */
    Logger();

    /**
     * @brief Destructor.
     * It only closes the file, if open, and cleans memory.
     */
    ~Logger();

    /** Method to lock in case of multithreading */
    inline static void lock();

    /** Method to unlock in case of multithreading */
    inline static void unlock();

    static INDI::DefaultDevice *parentDevice;

  public:
    enum VerbosityLevel
    {
        DBG_IGNORE  = 0x0,
        DBG_ERROR   = 0x1,
        DBG_WARNING = 0x2,
        DBG_SESSION = 0x4,
        DBG_DEBUG   = 0x8,
        DBG_EXTRA_1 = 0x10,
        DBG_EXTRA_2 = 0X20,
        DBG_EXTRA_3 = 0x40,
        DBG_EXTRA_4 = 0x80
    };

    struct switchinit
    {
        char name[MAXINDINAME];
        char label[MAXINDILABEL];
        ISState state;
        unsigned int levelmask;
    };

    static const unsigned int defaultlevel = DBG_ERROR | DBG_WARNING | DBG_SESSION;
    static const unsigned int nlevels      = 8;
    static struct switchinit LoggingLevelSInit[nlevels];
    static ISwitch LoggingLevelS[nlevels];
    static ISwitchVectorProperty LoggingLevelSP;
    static ISwitch ConfigurationS[2];
    static ISwitchVectorProperty ConfigurationSP;
    typedef loggerConf_ loggerConf;
    static const loggerConf file_on    = L_nofile_;
    static const loggerConf file_off   = L_file_;
    static const loggerConf screen_on  = L_noscreen_;
    static const loggerConf screen_off = L_screen_;
    static unsigned int customLevel;
    static unsigned int nDevices;

    static std::string getLogFile() { return logFile_; }
    static loggerConf_ getConfiguration() { return configuration_; }

    /**
     * @brief Method to get a reference to the object (i.e., Singleton)
     * It is a static method.
     * @return Reference to the object.
     */
    static Logger &getInstance();

    static bool saveConfigItems(FILE *fp);

    /**
     * @brief Adds a new debugging level to the driver.
     *
     * @param debugLevelName The descriptive debug level defined to the client. e.g. Scope Status
     * @param LoggingLevelName the short logging level recorded in the logfile. e.g. SCOPE
     * @return bitmask of the new debugging level to be used for any subsequent calls to DEBUG and DEBUGF to
     * record events to this debug level.
     */
    int addDebugLevel(const char *debugLevelName, const char *LoggingLevelName);

    void print(const char *devicename, const unsigned int verbosityLevel, const std::string &sourceFile,
               const int codeLine,
               //const std::string& 	message,
               const char *message, ...);

    /**
     * @brief Method to configure the logger. Called by the DEBUG_CONF() macro. To make implementation
     * easier, the old stream is always closed.
     * Then, in case, it is open again in append mode.
     * @param outputFile of the file used for logging
     * @param configuration (i.e., log on file and on screen on or off)
     * @param fileVerbosityLevel threshold for file
     * @param screenVerbosityLevel threshold for screen
     */
    void configure(const std::string &outputFile, const loggerConf configuration, const int fileVerbosityLevel,
                   const int screenVerbosityLevel);

    static struct switchinit DebugLevelSInit[nlevels];
    static ISwitch DebugLevelS[nlevels];
    static ISwitchVectorProperty DebugLevelSP;
    static bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n);
    static bool initProperties(INDI::DefaultDevice *device);
    static bool updateProperties(bool enable);
    static char Tags[nlevels][MAXINDINAME];

    /**
     * @brief Method used to print message called by the DEBUG() macro.
     * @param i which debugging to query its rank. The lower the rank, the more priority it is.
     * @return rank of debugging level requested.
     */
    static unsigned int rank(unsigned int l);
};

inline Logger::loggerConf operator|(Logger::loggerConf __a, Logger::loggerConf __b)
{
    return Logger::loggerConf(static_cast<int>(__a) | static_cast<int>(__b));
}

inline Logger::loggerConf operator&(Logger::loggerConf __a, Logger::loggerConf __b)
{
    return Logger::loggerConf(static_cast<int>(__a) & static_cast<int>(__b));
}
}
