/*
{{copyright}} 
*/

/*
{{version}} 
*/

/*
{{license}} 
*/

#pragma once

#include "../console/ansi.h"

#ifdef _WIN32
#  ifndef WIN32_LEAN_AND_MEAN
#    define WIN32_LEAN_AND_MEAN     // Disable windows mess
#  endif
#  ifndef NOMINMAX
#    define NOMINMAX
#  endif
#  include <Windows.h>
#  undef GetMessage     // To avoid conflict with other libraries
#endif

#include <chrono>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <map>
#include <mutex>
#include <source_location>
#include <sstream>
#include <string>

/*
 * Usage :
 *
 *  {
 *    int value = 42;
 *    std::string user = "Alice";
 *
 *    // Usage example:
 *    LogMessage() << "System ready" << std::endl;
 *    LogDebug()   << "Checking configuration file..." << std::endl;
 *    LogInfo()    << "User " << user << " logged in. (Value: " << value << ")" << std::endl;
 *    LogWarning() << "Device connection lost..." << std::endl;
 *    LogError()   << "Failed to connect to server on port " << 8080 << "!" << std::endl;
 *    LogComm()    << "<< 0123456789" << std::endl;
 *    LogDev()     << "<< 0123456789" << std::endl;
 *  }
 */

namespace sdi_toolBox::logs
{
//--------------------------------------------------------------
enum class LogLevel : uint8_t
{
  None,
  Dev,
  Comm,
  Debug,
  Info,
  Warning,
  Error,
  Message,
};
//--------------------------------------------------------------

//--------------------------------------------------------------
class ReLog
{
  public:
  struct Log
  {
    LogLevel                              level;
    std::chrono::system_clock::time_point timestamp;
    std::string                           message;
  };

  using ColorLogMap   = std::map<LogLevel, std::string>;
  using VisibleLogMap = std::map<LogLevel, bool>;
  static VisibleLogMap getVisibleLogMap();                                // Return the map of visible log messages
  static void          setVisibleLogMap(const VisibleLogMap &logMap);     // Define the map of visible log messages

  static ReLog &systemInit(const std::filesystem::path &filepath = "");     // Initialize log system
  static ReLog &getInstance();                                              // Gets singleton instance

  public:
  virtual ~ReLog()                       = default;     // Default destructor
  ReLog(const ReLog &obj)                = delete;      // Copy constructor
  ReLog(ReLog &&obj) noexcept            = delete;      // Move constructor
  ReLog &operator=(const ReLog &obj)     = delete;      // Copy assignment operator
  ReLog &operator=(ReLog &&obj) noexcept = delete;      // Move assignment operator

  void openFile(const std::filesystem::path &filepath);     // Open or create log file
  void closeFile();                                         // Close log file

  void log(const Log &log);     // Core log method

  void printColorTest() const;     // Print the different configurations and colors on the terminal

  protected:
  ReLog();     // Default constructor

  bool m_vtModeEnabled = false;

  std::mutex    m_mtx;
  std::ofstream m_file;

  static inline ColorLogMap   m_colorLogMap;
  static inline VisibleLogMap m_visibleLogMap;

  private:
  void enableVTMode();

  static void init();     // Settings initialization

  static std::string createLogMessage(const Log &log, const bool isFile = false);     // Print log messages
  void               printDate();

  void saveToFile(const std::string &message);     // Save log messages in file
};
//--------------------------------------------------------------

/* --- */

//--------------------------------------------------------------
/* Return the map of visible log messages */
inline ReLog::VisibleLogMap ReLog::getVisibleLogMap()
{
  return m_visibleLogMap;
}
//--------------------------------------------------------------
/* Define the map of visible log messages */
inline void ReLog::setVisibleLogMap(const VisibleLogMap &logMap)
{
  m_visibleLogMap = logMap;
}
//--------------------------------------------------------------
/* Initialize log system */
inline ReLog &ReLog::systemInit(const std::filesystem::path &filepath)
{
  auto &logSystem = getInstance();

  // Post initialization
  if (!filepath.empty())
    logSystem.openFile(filepath);
  logSystem.printDate();
  return logSystem;
}
//--------------------------------------------------------------
/* Gets singleton instance */
inline ReLog &ReLog::getInstance()
{
  static ReLog instance;
  return instance;
}
//--------------------------------------------------------------
/* Constructor */
inline ReLog::ReLog()
{
  // Initialization
  init();
  enableVTMode();

  // Test on debug mode
#ifdef DEBUG
  // printColorTest();
#endif
}
//--------------------------------------------------------------
/* Open or create log file */
inline void ReLog::openFile(const std::filesystem::path &filepath)
{
  std::lock_guard lock(m_mtx);

  m_file = std::ofstream(filepath, std::ofstream::out | std::ofstream::app | std::ofstream::binary);
  if (!m_file)
    throw std::runtime_error("unable to open log file");
  m_file << "\n-----------------------------------------------------------------" << std::endl;
}
//--------------------------------------------------------------
/* Close log file */
inline void ReLog::closeFile()
{
  std::lock_guard lock(m_mtx);

  if (m_file.is_open())
    m_file.close();
}
//--------------------------------------------------------------
/* Core log method */
inline void ReLog::log(const Log &log)
{
  std::lock_guard lock(m_mtx);

  const auto termMessage = createLogMessage(log);
  if (!termMessage.empty())
  {
    if (log.level == LogLevel::Error)
      std::cerr << termMessage << std::endl;
    else
      std::cout << termMessage << std::endl;
  }

  const auto fileMessage = createLogMessage(log, true);
  if (!fileMessage.empty())
    saveToFile(fileMessage);
}
//--------------------------------------------------------------
/* Print the different configurations and colors on the terminal */
inline void ReLog::printColorTest() const
{
  const auto coutTest = [&](const auto &color, const std::string &label)
  {
    std::cout << sdi_toolBox::console::ANSI::EscapeCommand::get(color) << label << sdi_toolBox::console::ANSI::EscapeCommand::clear();
    std::cout << sdi_toolBox::console::ANSI::EscapeCommand::get(color, sdi_toolBox::console::ANSI::EscapeCommand::UNDERLINE) << label << sdi_toolBox::console::ANSI::EscapeCommand::clear();
    std::cout << sdi_toolBox::console::ANSI::EscapeCommand::get(color, sdi_toolBox::console::ANSI::EscapeCommand::DIM | sdi_toolBox::console::ANSI::EscapeCommand::UNDERLINE) << label << sdi_toolBox::console::ANSI::EscapeCommand::clear();
    std::cout << sdi_toolBox::console::ANSI::EscapeCommand::get(color, sdi_toolBox::console::ANSI::EscapeCommand::LIGHT | sdi_toolBox::console::ANSI::EscapeCommand::UNDERLINE) << label << sdi_toolBox::console::ANSI::EscapeCommand::clear();
    std::cout << sdi_toolBox::console::ANSI::EscapeCommand::get(color, sdi_toolBox::console::ANSI::EscapeCommand::LIGHT) << label << sdi_toolBox::console::ANSI::EscapeCommand::clear();
    std::cout << sdi_toolBox::console::ANSI::EscapeCommand::get(color, sdi_toolBox::console::ANSI::EscapeCommand::BLINK) << label << sdi_toolBox::console::ANSI::EscapeCommand::clear();
    std::cout << sdi_toolBox::console::ANSI::EscapeCommand::get(color, sdi_toolBox::console::ANSI::EscapeCommand::BRIGHT) << label << sdi_toolBox::console::ANSI::EscapeCommand::clear();
    std::cout << sdi_toolBox::console::ANSI::EscapeCommand::get(color, sdi_toolBox::console::ANSI::EscapeCommand::DIM) << label << sdi_toolBox::console::ANSI::EscapeCommand::clear();
    std::cout << sdi_toolBox::console::ANSI::EscapeCommand::get(color, sdi_toolBox::console::ANSI::EscapeCommand::REVERSE) << label << sdi_toolBox::console::ANSI::EscapeCommand::clear();
    std::cout << " | ";
    std::cout << sdi_toolBox::console::ANSI::EscapeCommand::get(color, 0, sdi_toolBox::console::ANSI::EscapeCommand::Color::Black) << label << sdi_toolBox::console::ANSI::EscapeCommand::clear();
    std::cout << sdi_toolBox::console::ANSI::EscapeCommand::get(color, 0, sdi_toolBox::console::ANSI::EscapeCommand::Color::Red) << label << sdi_toolBox::console::ANSI::EscapeCommand::clear();
    std::cout << sdi_toolBox::console::ANSI::EscapeCommand::get(color, 0, sdi_toolBox::console::ANSI::EscapeCommand::Color::Green) << label << sdi_toolBox::console::ANSI::EscapeCommand::clear();
    std::cout << sdi_toolBox::console::ANSI::EscapeCommand::get(color, 0, sdi_toolBox::console::ANSI::EscapeCommand::Color::Yellow) << label << sdi_toolBox::console::ANSI::EscapeCommand::clear();
    std::cout << sdi_toolBox::console::ANSI::EscapeCommand::get(color, 0, sdi_toolBox::console::ANSI::EscapeCommand::Color::Blue) << label << sdi_toolBox::console::ANSI::EscapeCommand::clear();
    std::cout << sdi_toolBox::console::ANSI::EscapeCommand::get(color, 0, sdi_toolBox::console::ANSI::EscapeCommand::Color::Magenta) << label << sdi_toolBox::console::ANSI::EscapeCommand::clear();
    std::cout << sdi_toolBox::console::ANSI::EscapeCommand::get(color, 0, sdi_toolBox::console::ANSI::EscapeCommand::Color::Cyan) << label << sdi_toolBox::console::ANSI::EscapeCommand::clear();
    std::cout << sdi_toolBox::console::ANSI::EscapeCommand::get(color, 0, sdi_toolBox::console::ANSI::EscapeCommand::Color::White) << label << sdi_toolBox::console::ANSI::EscapeCommand::clear();
    std::cout << " | ";
    std::cout << sdi_toolBox::console::ANSI::EscapeCommand::get(color, 0, sdi_toolBox::console::ANSI::EscapeCommand::Color::Black, sdi_toolBox::console::ANSI::EscapeCommand::LIGHT) << label << sdi_toolBox::console::ANSI::EscapeCommand::clear();
    std::cout << sdi_toolBox::console::ANSI::EscapeCommand::get(color, 0, sdi_toolBox::console::ANSI::EscapeCommand::Color::Red, sdi_toolBox::console::ANSI::EscapeCommand::LIGHT) << label << sdi_toolBox::console::ANSI::EscapeCommand::clear();
    std::cout << sdi_toolBox::console::ANSI::EscapeCommand::get(color, 0, sdi_toolBox::console::ANSI::EscapeCommand::Color::Green, sdi_toolBox::console::ANSI::EscapeCommand::LIGHT) << label << sdi_toolBox::console::ANSI::EscapeCommand::clear();
    std::cout << sdi_toolBox::console::ANSI::EscapeCommand::get(color, 0, sdi_toolBox::console::ANSI::EscapeCommand::Color::Yellow, sdi_toolBox::console::ANSI::EscapeCommand::LIGHT) << label << sdi_toolBox::console::ANSI::EscapeCommand::clear();
    std::cout << sdi_toolBox::console::ANSI::EscapeCommand::get(color, 0, sdi_toolBox::console::ANSI::EscapeCommand::Color::Blue, sdi_toolBox::console::ANSI::EscapeCommand::LIGHT) << label << sdi_toolBox::console::ANSI::EscapeCommand::clear();
    std::cout << sdi_toolBox::console::ANSI::EscapeCommand::get(color, 0, sdi_toolBox::console::ANSI::EscapeCommand::Color::Magenta, sdi_toolBox::console::ANSI::EscapeCommand::LIGHT) << label << sdi_toolBox::console::ANSI::EscapeCommand::clear();
    std::cout << sdi_toolBox::console::ANSI::EscapeCommand::get(color, 0, sdi_toolBox::console::ANSI::EscapeCommand::Color::Cyan, sdi_toolBox::console::ANSI::EscapeCommand::LIGHT) << label << sdi_toolBox::console::ANSI::EscapeCommand::clear();
    std::cout << sdi_toolBox::console::ANSI::EscapeCommand::get(color, 0, sdi_toolBox::console::ANSI::EscapeCommand::Color::White, sdi_toolBox::console::ANSI::EscapeCommand::LIGHT) << label << sdi_toolBox::console::ANSI::EscapeCommand::clear();
    std::cout << std::endl;
  };
  coutTest(sdi_toolBox::console::ANSI::EscapeCommand::Color::Black, " B ");
  coutTest(sdi_toolBox::console::ANSI::EscapeCommand::Color::Red, " R ");
  coutTest(sdi_toolBox::console::ANSI::EscapeCommand::Color::Green, " G ");
  coutTest(sdi_toolBox::console::ANSI::EscapeCommand::Color::Yellow, " Y ");
  coutTest(sdi_toolBox::console::ANSI::EscapeCommand::Color::Blue, " B ");
  coutTest(sdi_toolBox::console::ANSI::EscapeCommand::Color::Magenta, " M ");
  coutTest(sdi_toolBox::console::ANSI::EscapeCommand::Color::Cyan, " C ");
  coutTest(sdi_toolBox::console::ANSI::EscapeCommand::Color::White, " W ");
  std::cout << std::endl;
}
//--------------------------------------------------------------
inline void ReLog::enableVTMode()
{
  if (m_vtModeEnabled)
    return;

  m_vtModeEnabled = false;

  // 1. Get the handle to the standard output
  const HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
  if (hOut == INVALID_HANDLE_VALUE)
    return;

  // 2. Get the current console mode
  DWORD dwMode = 0;
  if (!GetConsoleMode(hOut, &dwMode))
    return;

  // 3. Set the ENABLE_VIRTUAL_TERMINAL_PROCESSING flag (using bitwise OR)
  dwMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;

  // 4. Apply the new console mode
  if (!SetConsoleMode(hOut, dwMode))     // This is often the point of failure on older Windows versions (pre-Win10 v1511)
    return;

  m_vtModeEnabled = true;
}
//--------------------------------------------------------------
/* Settings initialization */
inline void ReLog::init()
{
  m_colorLogMap[LogLevel::None]    = sdi_toolBox::console::ANSI::EscapeCommand::clear();
  m_colorLogMap[LogLevel::Dev]     = sdi_toolBox::console::ANSI::EscapeCommand::get(sdi_toolBox::console::ANSI::EscapeCommand::Color::Green, sdi_toolBox::console::ANSI::EscapeCommand::DIM);
  m_colorLogMap[LogLevel::Comm]    = sdi_toolBox::console::ANSI::EscapeCommand::get(sdi_toolBox::console::ANSI::EscapeCommand::Color::Magenta);
  m_colorLogMap[LogLevel::Debug]   = sdi_toolBox::console::ANSI::EscapeCommand::get(sdi_toolBox::console::ANSI::EscapeCommand::Color::Yellow, sdi_toolBox::console::ANSI::EscapeCommand::DIM);
  m_colorLogMap[LogLevel::Info]    = sdi_toolBox::console::ANSI::EscapeCommand::get(sdi_toolBox::console::ANSI::EscapeCommand::Color::White);
  m_colorLogMap[LogLevel::Warning] = sdi_toolBox::console::ANSI::EscapeCommand::get(sdi_toolBox::console::ANSI::EscapeCommand::Color::Yellow);
  m_colorLogMap[LogLevel::Error]   = sdi_toolBox::console::ANSI::EscapeCommand::get(sdi_toolBox::console::ANSI::EscapeCommand::Color::Red, sdi_toolBox::console::ANSI::EscapeCommand::LIGHT | sdi_toolBox::console::ANSI::EscapeCommand::BRIGHT);
  m_colorLogMap[LogLevel::Message] = sdi_toolBox::console::ANSI::EscapeCommand::get(sdi_toolBox::console::ANSI::EscapeCommand::Color::White, sdi_toolBox::console::ANSI::EscapeCommand::LIGHT);

  m_visibleLogMap[LogLevel::Dev]     = true;
  m_visibleLogMap[LogLevel::Comm]    = true;
  m_visibleLogMap[LogLevel::Debug]   = true;
  m_visibleLogMap[LogLevel::Info]    = true;
  m_visibleLogMap[LogLevel::Warning] = true;
  m_visibleLogMap[LogLevel::Error]   = true;
  m_visibleLogMap[LogLevel::Message] = true;
}
//--------------------------------------------------------------
/* Print log messages */
inline std::string ReLog::createLogMessage(const Log &log, const bool isFile)
{
  // Check if the message should be visible or not
  if (m_visibleLogMap.contains(log.level) && m_visibleLogMap[log.level] == false)
    return "";

  std::ostringstream stream;

  // Prepare message color
  const auto levelStyle   = (m_colorLogMap.contains(log.level)) ? m_colorLogMap[log.level] : sdi_toolBox::console::ANSI::EscapeCommand::clear();
  const auto messageStyle = (!isFile) ? levelStyle : "";

  if (!log.message.empty())
  {
    switch (log.level)
    {
      case LogLevel::Error:
        stream << messageStyle << "[E] ";
        break;
      case LogLevel::Warning:
        stream << messageStyle << "[W] ";
        break;
      case LogLevel::Info:
        stream << messageStyle << "[I] ";
        break;
      case LogLevel::Debug:
        stream << messageStyle << "[D] ";
        break;
      case LogLevel::Comm:
        stream << messageStyle << "[C] ";
        break;
      case LogLevel::Dev:
        stream << messageStyle << "[-] ";
        break;
      case LogLevel::Message:
      default:;
        stream << messageStyle;
        break;
    }

    if (log.level != LogLevel::Message)
    {
      const auto local_tz   = std::chrono::current_zone();
      const auto local_time = std::chrono::zoned_time(local_tz, log.timestamp);
      stream << std::format("{0:%T} - ", local_time);
    }
  }

  // Print message
  stream << log.message;

  // Reset style
  if (!messageStyle.empty())
    stream << sdi_toolBox::console::ANSI::EscapeCommand::clear();

  return stream.str();
}
//--------------------------------------------------------------
inline void ReLog::printDate()
{
  const auto now        = std::chrono::system_clock::now();
  const auto local_tz   = std::chrono::current_zone();
  const auto local_time = std::chrono::zoned_time(local_tz, now);

  const Log logMessage{ .level     = LogLevel::Info,
                        .timestamp = now,
                        .message   = std::format("current date: {0:%F}", local_time) };
  log(logMessage);
}
//--------------------------------------------------------------
/* Save log messages in file */
inline void ReLog::saveToFile(const std::string &message)
{
  // Remove escape sequences
  std::string messageResult;
  bool        in_escape_sequence = false;

  constexpr char ANSI_ESCAPE_CHAR = '\x1b';
  for (const char c : message)
  {
    if (in_escape_sequence)
    {
      if (c == 'm')
        in_escape_sequence = false;     // End of the escape sequence
    }
    else if (c == ANSI_ESCAPE_CHAR)
      in_escape_sequence = true;     // Begin of the escape sequence
    else
      messageResult += c;
  }

  if (m_file.is_open())
    m_file << messageResult << "\n";
}
//--------------------------------------------------------------

/* --- */

//--------------------------------------------------------------
class ReLogStreamProxy
{
  public:
  ReLogStreamProxy() = delete;                                           // Constructor
  explicit ReLogStreamProxy(const LogLevel &level);                      // Constructor
  virtual ~ReLogStreamProxy();                                           // Destructor
  ReLogStreamProxy(ReLogStreamProxy &obj)                  = delete;     // Copy constructor
  ReLogStreamProxy(ReLogStreamProxy &&obj)                 = delete;     // Move constructor
  ReLogStreamProxy &operator=(const ReLogStreamProxy &obj) = delete;     // Copy assignment operator
  ReLogStreamProxy &operator=(ReLogStreamProxy &&obj)      = delete;     // Move assignment operator

  template<typename T>
  ReLogStreamProxy &operator<<(const T &data);                              // Overload for various data types (int, string, double, etc.)
  ReLogStreamProxy &operator<<(std::ostream &(*manip)(std::ostream &));     // Overload for std::endl and other manipulators

  protected:
  LogLevel                              m_level;
  std::chrono::system_clock::time_point m_timestamp;
  std::stringstream                     m_buffer;

  private:
  void flush();     // Pass the buffered content to the central Logger
};
//--------------------------------------------------------------
/* Constructor */
inline ReLogStreamProxy::ReLogStreamProxy(const LogLevel &level)
{
  // Initialization
  m_level = level;
}
//--------------------------------------------------------------
/* Destructor */
inline ReLogStreamProxy::~ReLogStreamProxy()
{
  if (!m_buffer.str().empty())
    flush();
}
//--------------------------------------------------------------
/* Overload for various data types (int, string, double, etc.) */
template<typename T>
inline ReLogStreamProxy &ReLogStreamProxy::operator<<(const T &data)
{
  const auto now = std::chrono::system_clock::now();

  if (m_buffer.str().empty())
    m_timestamp = now;

  m_buffer << data;
  return *this;
}
//--------------------------------------------------------------
/* Overload for std::endl and other manipulators */
inline ReLogStreamProxy &ReLogStreamProxy::operator<<(std::ostream &(*manip)(std::ostream &))
{
  if (manip == static_cast<std::ostream &(*)(std::ostream &)>(std::endl))
  {
    flush();
  }
  else
  {
    // Handle other manipulators if necessary (e.g., std::hex)
    m_buffer << manip;
  }
  return *this;
}
//--------------------------------------------------------------
/* Pass the buffered content to the central Logger */
inline void ReLogStreamProxy::flush()
{
  // Pass the buffered content to the central Logger
  ReLog::getInstance().log(ReLog::Log{ .level     = m_level,
                                       .timestamp = m_timestamp,
                                       .message   = m_buffer.str() });

  // Reset stream content
  m_buffer.str("");
  m_buffer.clear();
}
//--------------------------------------------------------------
}     // namespace sdi_toolBox::logs

//--------------------------------------------------------------
/*
 * User interface
 */
inline sdi_toolBox::logs::ReLogStreamProxy LogError()
{
  return sdi_toolBox::logs::ReLogStreamProxy(sdi_toolBox::logs::LogLevel::Error);
}
//--------------------------------------------------------------
inline sdi_toolBox::logs::ReLogStreamProxy LogWarning()
{
  return sdi_toolBox::logs::ReLogStreamProxy(sdi_toolBox::logs::LogLevel::Warning);
}
//--------------------------------------------------------------
inline sdi_toolBox::logs::ReLogStreamProxy LogInfo()
{
  return sdi_toolBox::logs::ReLogStreamProxy(sdi_toolBox::logs::LogLevel::Info);
}
//--------------------------------------------------------------
inline sdi_toolBox::logs::ReLogStreamProxy LogDebug()
{
  return sdi_toolBox::logs::ReLogStreamProxy(sdi_toolBox::logs::LogLevel::Debug);
}
//--------------------------------------------------------------
inline sdi_toolBox::logs::ReLogStreamProxy LogComm()
{
  return sdi_toolBox::logs::ReLogStreamProxy(sdi_toolBox::logs::LogLevel::Comm);
}
//--------------------------------------------------------------
inline sdi_toolBox::logs::ReLogStreamProxy LogMessage()
{
  return sdi_toolBox::logs::ReLogStreamProxy(sdi_toolBox::logs::LogLevel::Message);
}
//--------------------------------------------------------------
inline sdi_toolBox::logs::ReLogStreamProxy LogDev()
{
  return sdi_toolBox::logs::ReLogStreamProxy(sdi_toolBox::logs::LogLevel::Dev);
}
//--------------------------------------------------------------
inline void LogCode(const std::string &msg = "", const std::source_location &location = std::source_location::current())
{
  LogDev() << msg << ((!msg.empty()) ? " - " : "") << "(" << location.file_name() << " l." << location.line() << ")" << std::endl;
}
//--------------------------------------------------------------
