#ifndef SOPT_LOGGING_H
#define SOPT_LOGGING_H

#include "sopt/config.h"
#include "sopt/exception.h"

#include <map>
#include <memory>
#include <cstring>

namespace sopt::logging {

/// @brief Logging system for controlled & formatted writing to stdout
class Log {
public:

  /// Log priority levels.
  enum Level {
    trace = 0,
    debug = 10,
    info = 20,
    warn = 30, warning = 30,
    error = 40,
    critical = 50, always = 50
  };
  static const int end_color{-10}; ///< Special "level-like" code to end coloring


  /// Typedef for a collection of named logs.
  using LogMap = std::map<std::string, Log>;

  /// Typedef for a collection of named log levels.
  using LevelMap = std::map<std::string, int>;

  /// @brief Typedef for a collection of shell color codes, accessed by log level.
  using ColorCodes = std::map<int, std::string>;


private:

  /// A static map of existing logs: we don't make more loggers than necessary.
  thread_local static LogMap existingLogs;

  /// A static map of default log levels.
  thread_local static LevelMap defaultLevels;

  /// Show timestamp?
  static bool showTimestamp;

  /// Show log level?
  static bool showLogLevel;

  /// Show logger name?
  static bool showLoggerName;

  /// Use shell colour escape codes?
  static bool useShellColors;


public:

  /// Set the log levels
  static void setLevel(const std::string& name, int level);
  static void setLevels(const LevelMap& logLevels);

protected:

  /// @name Hidden constructors etc.
  /// @{

  /// Constructor 1
  Log(const std::string& name);

  /// Constructor 2
  Log(const std::string& name, int level);

  /// @}

  /// @brief Get the TTY code string for coloured messages
  static std::string getColorCode(int level);


public:

  /// Get a logger with the given name. The level will be taken from the
  /// "requestedLevels" static map or will be INFO by default.
  static Log& getLog(const std::string& name);

  /// Get the priority level of this logger.
  int getLevel() const {
    return _level;
  }

  /// Set the priority level of this logger.
  Log& setLevel(int level) {
    _level = level;
    return *this;
  }

  /// Get a log level enum from a string.
  static Level getLevelFromName(const std::string& level);

  /// Get the std::string representation of a log level.
  static std::string getLevelName(int level);

  /// Get the name of this logger.
  std::string getName() const {
    return _name;
  }

  /// Set the name of this logger.
  Log& setName(const std::string& name) {
    _name = name;
    return *this;
  }

  /// Will this log level produce output on this logger at the moment?
  bool isActive(int level) const {
    return (level >= _level);
  }


private:

  /// This logger's name
  std::string _name;

  /// Threshold level for this logger.
  int _level;

protected:

  /// Write a message at a particular level.
  void log(int level, const std::string& message);

  /// Turn a message string into the current log format.
  std::string formatMessage(int level, const std::string& message);

public:

  /// The streaming operator can use Log's internals.
  friend std::ostream& operator<<(Log& log, int level);

};


/// Streaming output to a logger must have a Log::Level/int as its first argument.
std::ostream& operator<<(Log& log, int level);

/// Access method to default Log object
inline Log& getLog() { return Log::getLog("sopt::"); }

/// Method to set the logging level of the default Log object
inline void set_level(const std::string &level) {
  getLog().setLevel(Log::getLevelFromName(level));
}


/// Helper method to ireplace a set of curly braces with
/// the template argument @a arg in a string stream
template <typename Arg>
void applyFormat(std::stringstream& ss, char*& pos, Arg&& arg) {
  char* delim = strstr(pos, "{}");
  if (delim != NULL) {
    ss << std::string(pos, delim-pos) << std::forward<Arg>(arg);
    pos = delim + 2;
  }
  else {
    SOPT_THROW("Insufficient placeholders for number of arguments!");
  }
}

/// Helper method to construct formatted string
template <typename... Args>
inline std::string mkFormattedString(const char* txt, Args&& ... args) {
  std::string mys = txt;
  std::stringstream rtn;
  char* pos = (char*)txt;
  ((void)applyFormat(rtn, pos, std::forward<Args>(args)), ...);
  rtn << std::string(pos);
  return rtn.str();
}

inline const std::string& mkFormattedString(const std::string& txt) { return txt; }

/// @defgroup logmacros Logging macros
/// @{

/// @def SOPT_MSG_LVL
/// @brief Neat CPU-conserving logging macros. Use by preference!
/// @note Only usable in classes where a getLog() method is provided
#define SOPT_MSG_LVL(lvl, ...) \
  do { \
    if (sopt::logging::getLog().isActive(lvl)) { \
      sopt::logging::getLog() << lvl << sopt::logging::mkFormattedString(__VA_ARGS__) << '\n'; \
    } \
  } while (0)

//! \macro For internal use only
#define SOPT_LOG_(TYPE, ...) \
  SOPT_MSG_LVL(sopt::logging::Log::TYPE, __VA_ARGS__)


/// @}

}

//! \macro Normal but significant condition or critical error
#define SOPT_NOTICE(...) SOPT_LOG_(critical, __VA_ARGS__)
//! \macro Something is definitely wrong, algorithm exits
#define SOPT_ERROR(...) SOPT_LOG_(error, __VA_ARGS__)
//! \macro Something might be going wrong
#define SOPT_WARN(...) SOPT_LOG_(warn, __VA_ARGS__)
//! \macro Verbose informational message about normal condition
#define SOPT_INFO(...) SOPT_LOG_(info, __VA_ARGS__)
//! \macro Output some debugging
#define SOPT_DEBUG(...) SOPT_LOG_(debug, __VA_ARGS__)
//! \macro Output internal values of no interest to anyone
//! \details Except maybe when debugging.
#define SOPT_TRACE(...) SOPT_LOG_(trace, __VA_ARGS__)

//! High priority message
#define SOPT_HIGH_LOG(...) SOPT_LOG_(critical, __VA_ARGS__)
//! Medium priority message
#define SOPT_MEDIUM_LOG(...) SOPT_LOG_(info, __VA_ARGS__)
//! Low priority message
#define SOPT_LOW_LOG(...) SOPT_LOG_(debug, __VA_ARGS__)

#endif
