//
// Copyright (c) 2020, NVIDIA CORPORATION.  All rights reserved.
//
// NVIDIA CORPORATION and its licensors retain all intellectual property
// and proprietary rights in and to this software, related documentation
// and any modifications thereto.  Any use, reproduction, disclosure or
// distribution of this software and related documentation without an express
// license agreement from NVIDIA CORPORATION is strictly prohibited.
//

#pragma once

#if defined(DEBUG)
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
#define SPDLOG_ACTIVE_LEVEL SPDLOG_LEVEL_TRACE
#else
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
#define SPDLOG_ACTIVE_LEVEL SPDLOG_LEVEL_INFO
#endif
#include <spdlog/spdlog.h>

#include <spdlog/sinks/basic_file_sink.h>
#include <spdlog/sinks/null_sink.h>
#include <spdlog/sinks/stdout_color_sinks.h>

#include <cstdlib>
#include <memory>
#include <string_view>

namespace nscq {

class logger {
  public:
    logger() noexcept {
        try {
            auto* sink_env = std::getenv("NSCQ_LOG_SINK");
            auto sink_str = std::string(sink_env == nullptr ? "" : sink_env);
            auto* level_env = std::getenv("NSCQ_LOG_LEVEL");
            auto level_str = std::string(level_env == nullptr ? "" : level_env);
            auto level = level_str.empty() ? default_log_level : spdlog::level::from_str(level_str);

            if (sink_str.empty()) {
                m_out = m_err = spdlog::null_logger_mt("out");
            } else if (sink_str == "console") {
                m_out = spdlog::stdout_color_mt("out");
                m_err = spdlog::stderr_color_mt("err");
            } else {
                m_out = m_err = spdlog::basic_logger_mt("out", sink_str);
            }

            spdlog::set_default_logger(m_out);

            // Pattern explanation:
            //  [%t] = thread ID
            //  [%Y-%m-%d %H:%M:%S.%e] = date and time, with milliseconds
            //  [%l] = log level
            //  [%s:%#] = source + line number
            //  %v = the text being logged
            spdlog::set_pattern("[%t][%Y-%m-%d %H:%M:%S.%e][%l][%s:%3#] %v");

            if (level == spdlog::level::off) {
                SPDLOG_WARN("Invalid logging level \"{}\"", level_str);
                level = default_log_level;
            }

            spdlog::set_level(level);

            SPDLOG_INFO("Logging to {}", sink_str);
        } catch (...) {
            // If we failed for some reason, disable logging
            m_out = m_err = spdlog::null_logger_mt("null");
        }
    }

    auto get_log() -> std::shared_ptr<spdlog::logger> { return m_out; }
    auto get_err_log() -> std::shared_ptr<spdlog::logger> { return m_err; }

    auto indent() -> void {
        m_indentation += std::string(indent_width, ' ');
        if (m_indentation.size() > max_indent_level * indent_width) {
            SPDLOG_LOGGER_WARN(get_log(), "Potentially excessive indentation: {}",
                               m_indentation.size());
        }
    }
    auto unindent() -> void {
        if (m_indentation.size() >= indent_width) {
            m_indentation.erase(m_indentation.end() - indent_width, m_indentation.end());
        } else {
            SPDLOG_LOGGER_WARN(get_log(), "unindent() without prior indent(): {}",
                               m_indentation.size());
        }
    }
    auto get_indentation() -> const std::string& { return m_indentation; }

  private:
#if defined(DEBUG)
    static const auto default_log_level = spdlog::level::level_enum::debug;
#else
    static const auto default_log_level = spdlog::level::level_enum::warn;
#endif
    static const auto indent_width = 2;
    static const auto max_indent_level = 10;
    std::string m_indentation;
    std::shared_ptr<spdlog::logger> m_out;
    std::shared_ptr<spdlog::logger> m_err;
}; // namespace nscq

inline logger log;

// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
#define NSCQ_LOG_TRACE(...) \
    SPDLOG_LOGGER_TRACE(nscq::log.get_log(), nscq::log.get_indentation() + __VA_ARGS__)
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
#define NSCQ_LOG_DEBUG(...) \
    SPDLOG_LOGGER_DEBUG(nscq::log.get_log(), nscq::log.get_indentation() + __VA_ARGS__)
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
#define NSCQ_LOG_INFO(...) \
    SPDLOG_LOGGER_INFO(nscq::log.get_log(), nscq::log.get_indentation() + __VA_ARGS__)
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
#define NSCQ_LOG_WARN(...) \
    SPDLOG_LOGGER_WARN(nscq::log.get_log(), nscq::log.get_indentation() + __VA_ARGS__)
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
#define NSCQ_LOG_ERROR(...) \
    SPDLOG_LOGGER_ERROR(nscq::log.get_err_log(), nscq::log.get_indentation() + __VA_ARGS__)
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
#define NSCQ_LOG_CRITICAL(...) \
    SPDLOG_LOGGER_CRITICAL(nscq::log.get_err_log(), nscq::log.get_indentation() + __VA_ARGS__)

// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
#define NSCQ_THROW(except_type, ...)                       \
    do {                                                   \
        auto _e = except_type(__VA_ARGS__);                \
        NSCQ_LOG_ERROR("{}: {}", #except_type, _e.what()); \
        throw std::move(_e);                               \
    } while (0)

} // namespace nscq
