//============================================================================
//  Copyright (c) Kitware, Inc.
//  All rights reserved.
//  See LICENSE.txt for details.
//
//  This software is distributed WITHOUT ANY WARRANTY; without even
//  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
//  PURPOSE.  See the above copyright notice for more information.
//============================================================================

#include <vtkm/cont/Logging.h>

#ifdef VTKM_ENABLE_LOGGING

// disable MSVC warnings in loguru.hpp
#ifdef VTKM_MSVC
#pragma warning(push)
#pragma warning(disable : 4722)
#endif // VTKM_MSVC

#define LOGURU_USE_ANONYMOUS_NAMESPACE
#define LOGURU_WITH_STREAMS 1
#define LOGURU_SCOPE_TIME_PRECISION 6

#include <vtkm/thirdparty/loguru/vtkmloguru/loguru.cpp>

#ifdef VTKM_MSVC
#pragma warning(pop)
#endif // VTKM_MSVC

#endif // VTKM_ENABLE_LOGGING

#include <cstdlib>
#include <iomanip>
#include <sstream>
#include <stdexcept>
#include <type_traits>
#include <unordered_map>

namespace
{

// This won't be needed under C++14, as strongly typed enums are automatically
// hashed then. But for now...
struct LogHasher
{
  std::size_t operator()(vtkm::cont::LogLevel level) const
  {
    return static_cast<std::size_t>(level);
  }
};

using LevelMapType = std::unordered_map<vtkm::cont::LogLevel, std::string, LogHasher>;

static bool Initialized = false;
static LevelMapType LogLevelNames;

void setLogLevelName(vtkm::cont::LogLevel level, const std::string& name) noexcept
{
  // if the log has been initialized, prevent modifications of the name map
  // to prevent race conditions.
  if (!Initialized)
  {
    LogLevelNames[level] = name;
  }
}

// Throws std::out_of_range if level not found.
const std::string& getLogLevelName(vtkm::cont::LogLevel level)
{
  const LevelMapType& names = LogLevelNames;
  return names.at(static_cast<vtkm::cont::LogLevel>(level));
}

#ifdef VTKM_ENABLE_LOGGING
const char* verbosityToNameCallback(loguru::Verbosity v)
{
  try
  {
    // Calling c_str on const string&.
    return getLogLevelName(static_cast<vtkm::cont::LogLevel>(v)).c_str();
  }
  catch (std::out_of_range&)
  {
    return nullptr;
  }
}

loguru::Verbosity nameToVerbosityCallback(const char* name)
{
  const LevelMapType& names = LogLevelNames;
  for (auto& kv : names)
  {
    if (kv.second == name)
    {
      return static_cast<loguru::Verbosity>(kv.first);
    }
  }
  return loguru::Verbosity_INVALID;
}
#endif // VTKM_ENABLE_LOGGING

} // end anon namespace

namespace vtkm
{
namespace cont
{

VTKM_CONT
void InitLogging(int& argc,
                 char* argv[],
                 const std::string& loggingFlag,
                 const std::string& loggingEnv)
{
  SetLogLevelName(vtkm::cont::LogLevel::Off, "Off");
  SetLogLevelName(vtkm::cont::LogLevel::Fatal, "FATL");
  SetLogLevelName(vtkm::cont::LogLevel::Error, "ERR");
  SetLogLevelName(vtkm::cont::LogLevel::Warn, "WARN");
  SetLogLevelName(vtkm::cont::LogLevel::Info, "Info");
  SetLogLevelName(vtkm::cont::LogLevel::DevicesEnabled, "Dev");
  SetLogLevelName(vtkm::cont::LogLevel::Perf, "Perf");
  SetLogLevelName(vtkm::cont::LogLevel::MemCont, "MemC");
  SetLogLevelName(vtkm::cont::LogLevel::MemExec, "MemE");
  SetLogLevelName(vtkm::cont::LogLevel::MemTransfer, "MemT");
  SetLogLevelName(vtkm::cont::LogLevel::KernelLaunches, "Kern");
  SetLogLevelName(vtkm::cont::LogLevel::Cast, "Cast");


#ifdef VTKM_ENABLE_LOGGING
  if (!Initialized)
  {
    loguru::set_verbosity_to_name_callback(&verbosityToNameCallback);
    loguru::set_name_to_verbosity_callback(&nameToVerbosityCallback);

    const char* envLevel = std::getenv(loggingEnv.c_str());
    if (envLevel != nullptr)
    {
      SetStderrLogLevel(envLevel);
    }
    else
    {
      // Set the default log level to warning
      SetStderrLogLevel(vtkm::cont::LogLevel::Warn);
    }
    loguru::init(argc, argv, loggingFlag.c_str());
  }
#else  // VTKM_ENABLE_LOGGING
  (void)argc;
  (void)argv;
  (void)loggingFlag;
#endif // VTKM_ENABLE_LOGGING

  // Prevent LogLevelNames from being modified (makes thread safety easier)
  Initialized = true;
}

void InitLogging()
{
  int argc = 1;
  char dummy[1] = { '\0' };
  char* argv[2] = { dummy, nullptr };
  InitLogging(argc, argv);
}

VTKM_CONT
void SetStderrLogLevel(const char* verbosity)
{
#ifdef VTKM_ENABLE_LOGGING
  loguru::g_stderr_verbosity = loguru::get_verbosity_from_name(verbosity);
#else  // VTKM_ENABLE_LOGGING
  (void)verbosity;
#endif // VTKM_ENABLE_LOGGING
}

VTKM_CONT
void SetStderrLogLevel(LogLevel level)
{
#ifdef VTKM_ENABLE_LOGGING
  loguru::g_stderr_verbosity = static_cast<loguru::Verbosity>(level);
#else  // VTKM_ENABLE_LOGGING
  (void)level;
#endif // VTKM_ENABLE_LOGGING
}

VTKM_CONT
vtkm::cont::LogLevel GetStderrLogLevel()
{
#ifdef VTKM_ENABLE_LOGGING
  return static_cast<vtkm::cont::LogLevel>(loguru::g_stderr_verbosity);
#else  // VTKM_ENABLE_LOGGING
  return vtkm::cont::LogLevel::Off;
#endif // VTKM_ENABLE_LOGGING
}

VTKM_CONT
void SetLogThreadName(const std::string& name)
{
#ifdef VTKM_ENABLE_LOGGING
  loguru::set_thread_name(name.c_str());
#else  // VTKM_ENABLE_LOGGING
  (void)name;
#endif // VTKM_ENABLE_LOGGING
}

VTKM_CONT
std::string GetLogThreadName()
{
#ifdef VTKM_ENABLE_LOGGING
  char buffer[128];
  loguru::get_thread_name(buffer, 128, false);
  return buffer;
#else  // VTKM_ENABLE_LOGGING
  return "N/A";
#endif // VTKM_ENABLE_LOGGING
}

VTKM_CONT
std::string GetLogErrorContext()
{
#ifdef VTKM_ENABLE_LOGGING
  auto ctx = loguru::get_error_context();
  return ctx.c_str();
#else  // VTKM_ENABLE_LOGGING
  return "N/A";
#endif // VTKM_ENABLE_LOGGING
}

VTKM_CONT
std::string GetStackTrace(vtkm::Int32 skip)
{
  (void)skip; // unsed when logging disabled.

  std::string result;

#ifdef VTKM_ENABLE_LOGGING
  result = loguru::stacktrace(skip + 2).c_str();
#endif // VTKM_ENABLE_LOGGING

  if (result.empty())
  {
    result = "(Stack trace unavailable)";
  }

  return result;
}


namespace
{
/// Convert a size in bytes to a human readable string (e.g. "64 bytes",
/// "1.44 MiB", "128 GiB", etc). @a prec controls the fixed point precision
/// of the stringified number.
inline VTKM_CONT std::string HumanSize(vtkm::UInt64 bytes, int prec = 2)
{
  vtkm::UInt64 current = bytes;
  vtkm::UInt64 previous = bytes;

  constexpr static const char* units[] = { "bytes", "KiB", "MiB", "GiB", "TiB", "PiB" };

  //this way reduces the number of float divisions we do
  int i = 0;
  while (current > 1024)
  {
    previous = current;
    current = current >> 10; //shift up by 1024
    ++i;
  }

  const double bytesf =
    (i == 0) ? static_cast<double>(previous) : static_cast<double>(previous) / 1024.;
  std::ostringstream out;
  out << std::fixed << std::setprecision(prec) << bytesf << " " << units[i];
  return out.str();
}
}

VTKM_CONT
std::string GetHumanReadableSize(vtkm::UInt64 bytes, int prec)
{
  return HumanSize(bytes, prec);
}

VTKM_CONT
std::string GetSizeString(vtkm::UInt64 bytes, int prec)
{
  return HumanSize(bytes, prec) + " (" + std::to_string(bytes) + " bytes)";
}

VTKM_CONT
void SetLogLevelName(LogLevel level, const std::string& name)
{
  if (Initialized)
  {
    VTKM_LOG_F(LogLevel::Error, "SetLogLevelName called after InitLogging.");
    return;
  }
  setLogLevelName(level, name);
}

VTKM_CONT
std::string GetLogLevelName(LogLevel level)
{
#ifdef VTKM_ENABLE_LOGGING
  { // Check loguru lookup first:
    const char* name = loguru::get_verbosity_name(static_cast<loguru::Verbosity>(level));
    if (name)
    {
      return name;
    }
  }
#else
  {
    try
    {
      return getLogLevelName(level);
    }
    catch (std::out_of_range&)
    { /* fallthrough */
    }
  }
#endif

  // Create a string from the numeric value otherwise:
  using T = std::underlying_type<LogLevel>::type;
  return std::to_string(static_cast<T>(level));
}

VTKM_CONT std::string TypeToString(const std::type_info& t)
{
#ifdef VTKM_ENABLE_LOGGING
  return loguru::demangle(t.name()).c_str();
#else  // VTKM_ENABLE_LOGGING
  return t.name();
#endif // VTKM_ENABLE_LOGGING
}

VTKM_CONT std::string TypeToString(const std::type_index& t)
{
#ifdef VTKM_ENABLE_LOGGING
  return loguru::demangle(t.name()).c_str();
#else  // VTKM_ENABLE_LOGGING
  return t.name();
#endif // VTKM_ENABLE_LOGGING
}

#ifdef VTKM_ENABLE_LOGGING
VTKM_CONT
int getVerbosityByLevel(LogLevel level)
{
  return static_cast<loguru::Verbosity>(level);
}

namespace detail
{

struct LogScope::InternalStruct : loguru::LogScopeRAII
{
  template <typename... Ts>
  InternalStruct(Ts&&... args)
    : loguru::LogScopeRAII(std::forward<Ts>(args)...)
  {
  }
};

VTKM_CONT
LogScope::LogScope(LogLevel level, const char* file, unsigned line, const char* format...)
{
  auto verbosity = getVerbosityByLevel(level);

  if (verbosity > loguru::current_verbosity_cutoff())
  {
    this->Internals = std::make_unique<InternalStruct>();
  }
  else
  {
    va_list args;
    va_start(args, format);
    this->Internals = std::make_unique<InternalStruct>(verbosity, file, line, format, args);
    va_end(args);
  }
}

LogScope::~LogScope() = default;

} // namespace detail

VTKM_CONT
void LogCond(LogLevel level, bool cond, const char* file, unsigned line, const char* format...)
{
  if (cond)
  {
    auto verbosity = getVerbosityByLevel(level);

    if (verbosity <= loguru::current_verbosity_cutoff())
    {
      va_list args;
      va_start(args, format);
      loguru::vlog(verbosity, file, line, format, args);
      va_end(args);
    }
  }
}

VTKM_CONT
LogCondStream::~LogCondStream() noexcept(false)
{
  LogCond(this->Level, this->Condition, this->File, this->Line, this->SStream.str().c_str());
}
#endif // VTKM_ENABLE_LOGGING

}
} // end namespace vtkm::cont
