#ifndef LOGGING_H
#define LOGGING_H

#include <iostream>
#include <iomanip>
#include <chrono>
#include <ctime>

// Logging levels
#define LOG_ERROR 0
#define LOG_WARN 1
#define LOG_INFO 2
#define LOG_DEBUG 3
#define LOG_TRACE 4

// Set logging arguments
// TODO: set these by compiler arguments or env vars or something
#define LOGGING LOG_DEBUG // logging level
#define LOGGING_COLOR true // enable color
#define LOGGING_TIMESTAMP true // enable timestamp
#define LOGGING_TIMESTAMP_FMT "%Y-%m-%dT%H:%M:%S%z" // timestamp format (local time)
#define LOGGING_POSITION true // display position (only works on C++20 or newer)

// Color codes
#define ANSI_RESET   "\033[0m"
#define ANSI_BLACK   "\033[30m"      /* Black */
#define ANSI_RED     "\033[31m"      /* Red */
#define ANSI_GREEN   "\033[32m"      /* Green */
#define ANSI_YELLOW  "\033[33m"      /* Yellow */
#define ANSI_BLUE    "\033[34m"      /* Blue */
#define ANSI_MAGENTA "\033[35m"      /* Magenta */
#define ANSI_CYAN    "\033[36m"      /* Cyan */
#define ANSI_WHITE   "\033[37m"      /* White */
#define ANSI_BOLD    "\033[1m"       /* Bold */

template <typename T>
void print(T t) {
  std::cout << t << std::endl;
}

template <typename T, typename... Args>
void print(T t, Args... args) {
  std::cout << t;
  print(args...);
}

inline void printTimestamp() {
  #if LOGGING_TIMESTAMP
    auto now = std::chrono::system_clock::now();
    auto time_c = std::chrono::system_clock::to_time_t(now);
    std::tm time_tm;
    localtime_r(&time_c, &time_tm);
    std::cout << std::put_time(&time_tm, LOGGING_TIMESTAMP_FMT) << ": ";
  #endif
}

// if we're on C++20 or later, then use the source_location header and add source location to logs
#if __cplusplus >= 202002L
#include <source_location>

inline void printPosition(std::source_location& location) {
  #if LOGGING_POSITION
  std::cout << location.file_name() << ":" << location.function_name << ":" << ANSI_CYAN << location.line() << ANSI_RESET <<  ": ";
  #endif
}

inline void printHeader(std::string name, std::string color, std::source_location& location) {
  #if LOGGING_COLOR
    std::cout << ANSI_BOLD << color << "[" << name << "] " << ANSI_RESET;
    printTimestamp();
    printPosition(location);
  #else
    printHeader(name);
  #endif
}

inline void printHeader(std::string name, std::source_location& location) {
  std::cout << "[" << name << "] ";
  printTimestamp();
  printPosition(location);
}

template <typename... Args, typename Sl = std::source_location>
void trace(Args... args, Sl location = std::source_location::current()) {
  #if LOGGING >= LOG_TRACE
    printHeader("TRACE", ANSI_CYAN, location);
    print(args...);
  #endif
}

template <typename... Args, typename Sl = std::source_location>
void debug(Args... args, Sl location = std::source_location::current()) {
  #if LOGGING >= LOG_DEBUG
    printHeader("DEBUG", ANSI_MAGENTA, location);
    print(args...);
  #endif
}

template <typename... Args, typename Sl = std::source_location>
void info(Args... args, Sl location = std::source_location::current()) {
  #if LOGGING >= LOG_INFO
    printHeader("INFO", ANSI_GREEN, location);
    print(args...);
  #endif
}

template <typename... Args, typename Sl = std::source_location>
void warn(Args... args, Sl location = std::source_location::current()) {
  #if LOGGING >= LOG_WARN
    printHeader("WARN", ANSI_YELLOW, location);
    print(args...);
  #endif
}

template <typename... Args, typename Sl = std::source_location>
void error(Args... args, Sl location = std::source_location::current()) {
  #if LOGGING >= LOG_ERROR
    printHeader("ERROR", ANSI_RED, location);
    print(args...);
  #endif
}
#else
inline void printHeader(std::string name, std::string color) {
  #if LOGGING_COLOR
    std::cout << ANSI_BOLD << color << "[" << name << "] " << ANSI_RESET;
    printTimestamp();
  #else
    printHeader(name);
  #endif
}

inline void printHeader(std::string name) {
  std::cout << "[" << name << "] ";
  printTimestamp();
}

template <typename... Args>
void trace(Args... args) {
  #if LOGGING >= LOG_TRACE
    printHeader("TRACE", ANSI_CYAN);
    print(args...);
  #endif
}

template <typename... Args>
void debug(Args... args) {
  #if LOGGING >= LOG_DEBUG
    printHeader("DEBUG", ANSI_MAGENTA);
    print(args...);
  #endif
}

template <typename... Args>
void info(Args... args) {
  #if LOGGING >= LOG_INFO
    printHeader("INFO", ANSI_GREEN);
    print(args...);
  #endif
}

template <typename... Args>
void warn(Args... args) {
  #if LOGGING >= LOG_WARN
    printHeader("WARN", ANSI_YELLOW);
    print(args...);
  #endif
}

template <typename... Args>
void error(Args... args) {
  #if LOGGING >= LOG_ERROR
    printHeader("ERROR", ANSI_RED);
    print(args...);
  #endif
}
#endif

#endif //LOGGING_H