#pragma once

#ifndef LOG_H_SPSC31OG
#define LOG_H_SPSC31OG

#ifdef RPCLIB_ENABLE_LOGGING

#include "format.h"
#include <chrono>
#include <cstdlib>
#include <iomanip>
#include <mutex>
#include <sstream>

#include <inttypes.h>
#include <math.h>
#include <stdio.h>
#include <time.h>

#ifdef _MSC_VER
#include <Windows.h>
#endif

namespace rpc {
namespace detail {
class logger {
public:
    static logger &instance() {
        static logger inst;
        return inst;
    }

    template <typename... Args>
    void trace(const char *channel, const char *file, const char *func,
               std::size_t line, const char *msg, Args... args) {
        (void)func;
        basic_log("TRACE", channel,
                  RPCLIB_FMT::format("{} ({}:{})",
                                     RPCLIB_FMT::format(msg, args...), file,
                                     line));
    }

    template <typename... Args>
    void debug(const char *channel, const char *file, const char *func,
               std::size_t line, const char *msg, Args... args) {
        (void)func;
        basic_log("DEBUG", channel,
                  RPCLIB_FMT::format("{} ({}:{})",
                                     RPCLIB_FMT::format(msg, args...), file,
                                     line));
    }

    template <typename... Args>
    void warn(const char *channel, const char *msg, Args... args) {
        basic_log("WARN", channel, RPCLIB_FMT::format(msg, args...));
    }

    template <typename... Args>
    void error(const char *channel, const char *msg, Args... args) {
        basic_log("ERROR", channel, RPCLIB_FMT::format(msg, args...));
    }

    template <typename... Args>
    void info(const char *channel, const char *msg, Args... args) {
        basic_log("INFO", channel, RPCLIB_FMT::format(msg, args...));
    }

private:
    logger() {}

#ifdef _MSC_VER
    static std::string now() {
        std::stringstream ss;
        SYSTEMTIME t;
        GetSystemTime(&t);
        ss << RPCLIB_FMT::format("{}-{}-{} {}:{}:{}.{:03}", t.wYear, t.wMonth,
                                 t.wDay, t.wHour, t.wMinute, t.wSecond,
                                 t.wMilliseconds);
        return ss.str();
    }
#else
    static std::string now() {
        std::stringstream ss;
        timespec now_t = {};
        clock_gettime(CLOCK_REALTIME, &now_t);
#if __GNUC__ >= 5
        ss << std::put_time(
                  std::localtime(reinterpret_cast<time_t *>(&now_t.tv_sec)),
                  "%F %T")
#else
        char mltime[128];
        strftime(mltime, sizeof(mltime), "%c %Z",
                std::localtime(reinterpret_cast<time_t *>(&now_t.tv_sec)));
        ss << mltime
#endif
            << RPCLIB_FMT::format(
                  ".{:03}", round(static_cast<double>(now_t.tv_nsec) / 1.0e6));

        return ss.str();
    }
#endif

    void basic_log(const char *severity, const char *channel,
                   std::string const &msg) {
        using RPCLIB_FMT::arg;
        std::lock_guard<std::mutex> lock(mut_print_);
        RPCLIB_FMT::print("{time:16}  {severity:6}  {channel:12}    {msg:40}\n",
                          arg("severity", severity), arg("channel", channel),
                          arg("time", now()), arg("msg", msg));
    }

private:
    std::mutex mut_print_;
};
} /* detail */
} /* rpc */

#ifdef _MSC_VER
#define RPCLIB_CREATE_LOG_CHANNEL(Name)                                        \
    static constexpr const char *rpc_channel_name = #Name;
#elif defined(__GNUC__)
#define RPCLIB_CREATE_LOG_CHANNEL(Name)                                        \
    _Pragma("GCC diagnostic push")                                            \
    _Pragma("GCC diagnostic ignored \"-Wunused-variable\"")                       \
    static constexpr const char *rpc_channel_name = #Name;                  \
    _Pragma("GCC diagnostic pop")
#elif defined(__clang__)
    _Pragma("clang diagnostic push")                                            \
    _Pragma("clang diagnostic ignored \"-Wunused-variable\"")                       \
    static constexpr const char *rpc_channel_name = #Name;                  \
    _Pragma("clang diagnostic pop")
#endif

RPCLIB_CREATE_LOG_CHANNEL(global)

#define LOG_INFO(...)                                                          \
    rpc::detail::logger::instance().info(rpc_channel_name, __VA_ARGS__)

#define LOG_WARN(...)                                                          \
    rpc::detail::logger::instance().warn(rpc_channel_name, __VA_ARGS__)

#define LOG_ERROR(...)                                                         \
    rpc::detail::logger::instance().error(rpc_channel_name, __VA_ARGS__)

#define LOG_DEBUG(...)                                                         \
    rpc::detail::logger::instance().debug(rpc_channel_name, __FILE__,    \
                                             __func__, __LINE__, __VA_ARGS__)

#define LOG_TRACE(...)                                                         \
    rpc::detail::logger::instance().trace(rpc_channel_name, __FILE__,    \
                                             __func__, __LINE__, __VA_ARGS__)

#define LOG_EXPR(Level, Expr) LOG_##Level("`" #Expr "` = {}", Expr)

#else

#define LOG_INFO(...)
#define LOG_WARN(...)
#define LOG_ERROR(...)
#define LOG_DEBUG(...)
#define LOG_TRACE(...)
#define LOG_EXPR(...)
#define RPCLIB_CREATE_LOG_CHANNEL(...)

#endif

#endif /* end of include guard: LOG_H_SPSC31OG */
