/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

/*!
 * \file tvm/runtime/logging.h
 * \brief logging utilities
 *
 * We use the following facilities from tvm/ffi/error.h for
 * error handling and checking:
 *
 *  - TVM_FFI_THROW(ErrorKind) << "msg";
 *  - TVM_FFI_CHECK(cond, ErrorKind) << "msg";
 *  - TVM_FFI_CHECK_EQ(x, y, ErrorKind) << "msg";
 *  - TVM_FFI_ICHECK(x) << "msg";  // InternalError
 *  - TVM_FFI_ICHECK_EQ(x, y) << "msg";
 *  - TVM_FFI_DCHECK(x) << "msg";  // Debug-only InternalError
 *
 * LOG(INFO), LOG(WARNING), LOG(ERROR) are kept for logging.
 * LOG(FATAL) is kept for completeness, it throws InternalError.
 */
#ifndef TVM_RUNTIME_LOGGING_H_
#define TVM_RUNTIME_LOGGING_H_

#include <tvm/ffi/error.h>
#include <tvm/runtime/base.h>

#include <ctime>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <string>
#include <unordered_map>
#include <vector>

/*!
 * \brief Whether or not customize the logging output.
 *  If log customize is enabled, the user must implement
 *  tvm::runtime::detail::LogFatalImpl and tvm::runtime::detail::LogMessageImpl.
 */
#ifndef TVM_LOG_CUSTOMIZE
#define TVM_LOG_CUSTOMIZE 0
#endif

namespace tvm {
namespace runtime {

/*! \brief Internal implementation */
namespace detail {
// Provide support for customized logging.
#if TVM_LOG_CUSTOMIZE
/*!
 * \brief Custom implementations of LogFatal.
 *
 * \sa TVM_LOG_CUSTOMIZE
 */
[[noreturn]] TVM_RUNTIME_DLL void LogFatalImpl(const std::string& file, int lineno,
                                               const std::string& message);

/*!
 * \brief Custom implementations of LogMessage.
 *
 * \sa TVM_LOG_CUSTOMIZE
 */
TVM_RUNTIME_DLL void LogMessageImpl(const std::string& file, int lineno, int level,
                                    const std::string& message);

/*!
 * \brief Class to accumulate an error message and throw it. Do not use
 * directly, instead use LOG(FATAL).
 */
class LogFatal {
 public:
  LogFatal(const std::string& file, int lineno) : file_(file), lineno_(lineno) {}
#ifdef _MSC_VER
#pragma warning(push)
#pragma warning(disable : 4722)
#endif
  [[noreturn]] ~LogFatal() noexcept(false) { LogFatalImpl(file_, lineno_, stream_.str()); }
#ifdef _MSC_VER
#pragma warning(pop)
#endif
  std::ostringstream& stream() { return stream_; }

 private:
  std::ostringstream stream_;
  std::string file_;
  int lineno_;
};

/*!
 * \brief Class to accumulate an log message. Do not use directly, instead use
 * LOG(INFO), LOG(WARNING), LOG(ERROR).
 */
class LogMessage {
 public:
  LogMessage(const std::string& file, int lineno, int level)
      : file_(file), lineno_(lineno), level_(level) {}
  ~LogMessage() { LogMessageImpl(file_, lineno_, level_, stream_.str()); }
  std::ostringstream& stream() { return stream_; }

 private:
  std::string file_;
  int lineno_;
  int level_;
  std::ostringstream stream_;
};

#else

/*!
 * \brief Class to accumulate an error message and throw it. Do not use
 * directly, instead use LOG(FATAL).
 * \note The `LogFatal` class is designed to be an empty class to reduce stack size usage.
 * To play this trick, we use the thread-local storage to store its internal data.
 */
class LogFatal {
 public:
  TVM_FFI_NO_INLINE LogFatal(const char* file, int lineno) { GetEntry().Init(file, lineno); }
#ifdef _MSC_VER
#pragma warning(push)
#pragma warning(disable : 4722)
#endif
  [[noreturn]] ~LogFatal() noexcept(false) {
    GetEntry().Finalize();
    throw;
  }
#ifdef _MSC_VER
#pragma warning(pop)
#endif
  std::ostringstream& stream() { return GetEntry().stream_; }

 private:
  struct Entry {
    void Init(const char* file, int lineno) {
      this->stream_.str("");
      this->file_ = file;
      this->lineno_ = lineno;
    }
    [[noreturn]] TVM_FFI_NO_INLINE ffi::Error Finalize() noexcept(false) {
      ffi::Error error("InternalError", stream_.str(),
                       TVMFFIBacktrace(file_.c_str(), lineno_, "", 0));
      throw error;
    }
    std::ostringstream stream_;
    std::string file_;
    int lineno_;
  };

  TVM_FFI_NO_INLINE TVM_RUNTIME_DLL static Entry& GetEntry();
};

/*!
 * \brief Class to accumulate an log message. Do not use directly, instead use
 * LOG(INFO), LOG(WARNING), LOG(ERROR).
 */
class LogMessage {
 public:
  LogMessage(const std::string& file, int lineno, int level) {
    // Use inline constexpr to avoid ODR-issues with hidden static members
    // when libtvm_runtime.so is built with -fvisibility=hidden.
    static constexpr const char* kLevelStrings[] = {
        ": Debug: ",    // TVM_LOG_LEVEL_DEBUG
        ": ",           // TVM_LOG_LEVEL_INFO
        ": Warning: ",  // TVM_LOG_LEVEL_WARNING
        ": Error: ",    // TVM_LOG_LEVEL_ERROR
    };
    std::time_t t = std::time(nullptr);
    stream_ << "[" << std::put_time(std::localtime(&t), "%H:%M:%S") << "] " << file << ":" << lineno
            << kLevelStrings[level];
  }
  TVM_FFI_NO_INLINE ~LogMessage() { std::cerr << stream_.str() << std::endl; }
  std::ostringstream& stream() { return stream_; }

 private:
  std::ostringstream stream_;
};

#endif

// This class is used to explicitly ignore values in the conditional
// logging macros.  This avoids compiler warnings like "value computed
// is not used" and "statement has no effect".
class LogMessageVoidify {
 public:
  LogMessageVoidify() {}
  // This has to be an operator with a precedence lower than << but
  // higher than "?:". See its usage.
  void operator&(std::ostream&) {}
};

/*! \brief Captures the state of the \p TVM_LOG_DEBUG environment flag. */
class TVM_RUNTIME_DLL TvmLogDebugSettings {
 public:
  /*!
   * \brief Parses the \p TVM_LOG_DEBUG environment flag as per the specification given by
   * \p DebugLoggingEnabled and \p VerboseLoggingEnabled, and caches the result.
   */
  inline static const TvmLogDebugSettings& FromFlag() {
    // Parse and cache the verbosity level map.
    static const auto* settings =
        new TvmLogDebugSettings(TvmLogDebugSettings::ParseSpec(std::getenv("TVM_LOG_DEBUG")));
    return *settings;
  }

  /*!
   * \brief Parses \p opt_spec as per specification for \p TVM_LOG_DEBUG given by
   * \p DebugLoggingEnabled and \p VerboseLoggingEnabled. Throws if specification is ill-formed.
   */
  static TvmLogDebugSettings ParseSpec(const char* opt_spec);

  /*!
   * \brief Implements \p VerboseLoggingEnabled below w.r.t. the already parsed \p TVM_LOG_DEBUG
   * environment variable.
   */
  inline bool VerboseEnabled(const char* opt_filename, int level) const {
    if (opt_filename == nullptr || level < 0 || vlog_level_map_.empty()) {
      return false;
    }
    return VerboseEnabledImpl(opt_filename, level);
  }

  /*! \brief Returns true if \p DLOG statements should be executed. */
  bool dlog_enabled() const { return dlog_enabled_; }

 private:
  // Slow path for VerboseEnabled.
  bool VerboseEnabledImpl(const std::string& filename, int level) const;

  /*! \brief If true, DLOG statements are enabled. */
  bool dlog_enabled_ = false;
  /*!
   * \brief A map from canonicalized filenames to the maximum VLOG verbosity level for that file.
   * May also contain the 'wildcard' entry \p "DEFAULT" representing the level for all other files.
   */
  std::unordered_map<std::string, int> vlog_level_map_;
};

/*!
 * \brief Returns true if a DLOG statement is enabled by the \p TVM_LOG_DEBUG environment
 * variable. Requires:
 * \code
 *   TVM_LOG_DEBUG=1
 * \endcode
 * or a valid setting as described by \p VerboseLoggingEnabled below.
 */
inline bool DebugLoggingEnabled() {
  static int state = 0;
  if (state == 0) {
    state = TvmLogDebugSettings::FromFlag().dlog_enabled() ? 1 : -1;
  }
  return state == 1;
}

/*!
 * \brief Returns true if a VLOG statement in \p filename is enabled by the \p TVM_LOG_DEBUG
 * environment variable for logging at verbosity \p level. Levels should be non-negative.
 *
 * Filenames are canonicalized to be w.r.t. the src/ dir of the TVM tree. (VLOG's should not
 * appear under include/).
 *
 * To enable file \p ir/bar.cc for level 0 only set:
 * \code
 * TVM_LOG_DEBUG="ir/bar.cc=0"
 * \endcode
 *
 * To enable all files up to level 3 but disable \p ir/bar.cc set:
 * \code
 * TVM_LOG_DEBUG="DEFAULT=2,ir/bar.cc=-1"
 * \endcode
 *
 * Any of these settings will also enable DLOG statements.
 */
inline bool VerboseLoggingEnabled(const char* opt_filename, int level) {
  return TvmLogDebugSettings::FromFlag().VerboseEnabled(opt_filename, level);
}

/*!
 * \brief A stack of VLOG context messages.
 *
 * For use by \p VLOG_CONTEXT macro only.
 */
class VLogContext {
 public:
  void Push(std::stringstream* stream) { context_stack_.push_back(stream); }
  void Pop() {
    if (!context_stack_.empty()) {
      context_stack_.pop_back();
    }
  }

  std::string str() const;

 private:
  std::vector<std::stringstream*> context_stack_;
};

/*! \brief Get thread local \p VLogContext for tracking a stack of VLOG context messages. */
inline VLogContext* ThreadLocalVLogContext() {
  static thread_local VLogContext inst;
  return &inst;
}

/*!
 * \brief A RAII class to push/pos a VLOG context message onto the thread-local stack.
 *
 * For use by \p VLOG_CONTEXT macro only.
 */
class VLogContextEntry {
 public:
  VLogContextEntry() { ThreadLocalVLogContext()->Push(&sstream_); }
  ~VLogContextEntry() { ThreadLocalVLogContext()->Pop(); }
  std::ostream& stream() { return sstream_; }

 private:
  std::stringstream sstream_;
};

}  // namespace detail

#define TVM_LOG_LEVEL_DEBUG 0
#define TVM_LOG_LEVEL_INFO 1
#define TVM_LOG_LEVEL_WARNING 2
#define TVM_LOG_LEVEL_ERROR 3
#define TVM_LOG_LEVEL_FATAL 4
#define LOG(level) LOG_##level
#define LOG_DEBUG \
  ::tvm::runtime::detail::LogMessage(__FILE__, __LINE__, TVM_LOG_LEVEL_DEBUG).stream()
#define LOG_FATAL ::tvm::runtime::detail::LogFatal(__FILE__, __LINE__).stream()
#define LOG_INFO ::tvm::runtime::detail::LogMessage(__FILE__, __LINE__, TVM_LOG_LEVEL_INFO).stream()
#define LOG_ERROR \
  ::tvm::runtime::detail::LogMessage(__FILE__, __LINE__, TVM_LOG_LEVEL_ERROR).stream()
#define LOG_WARNING \
  ::tvm::runtime::detail::LogMessage(__FILE__, __LINE__, TVM_LOG_LEVEL_WARNING).stream()

#define LOG_IF(severity, condition) \
  !(condition) ? (void)0 : ::tvm::runtime::detail::LogMessageVoidify() & LOG(severity)

#if TVM_LOG_DEBUG

#define LOG_DFATAL LOG_FATAL
#define DFATAL FATAL
#define DLOG(severity) LOG_IF(severity, ::tvm::runtime::detail::DebugLoggingEnabled())
#define DLOG_IF(severity, condition) \
  LOG_IF(severity, ::tvm::runtime::detail::DebugLoggingEnabled() && (condition))

/*!
 * \brief If the \p TVM_LOG_DEBUG build flag is enabled, push a context message onto an internal
 * stack. All VLOG messages will include this stack in their prefix to help with debugging. E.g.:
 * \code
 *   VLOG_CONTEXT << "my context";
 *   VLOG(1) << "my log message";
 * \endcode
 * Thread safe. No-op with no execution overhead if the \p TVM_LOG_DEBUG build flag is not enabled.
 */
#define VLOG_CONTEXT                                    \
  ::tvm::runtime::detail::VLogContextEntry vlog_entry_; \
  vlog_entry_.stream()

#else

#define LOG_DFATAL LOG_ERROR
#define DFATAL ERROR
#define DLOG(severity) true ? (void)0 : ::tvm::runtime::detail::LogMessageVoidify() & LOG(severity)
#define DLOG_IF(severity, condition) \
  (true || !(condition)) ? (void)0 : ::tvm::runtime::detail::LogMessageVoidify() & LOG(severity)
#define VLOG_CONTEXT true ? (void)0 : ::tvm::runtime::detail::LogMessageVoidify() & LOG(INFO)

#endif

/*!
 * \brief If the \p TVM_LOG_DEBUG build flag is enabled, and the containing file has been enabled
 * at \p level or greater in the \p TVM_LOG_DEBUG environment variable, then log a message at
 * \p INFO severity.
 *
 * See \p VerboseLoggingEnabled for the format of the \p TVM_LOG_DEBUG environment variable.
 * Thread safe. No-op with no execution overhead if the \p TVM_LOG_DEBUG build flag is not enabled.
 * No-op with some execution overhead if the \p TVM_LOG_DEBUG build flag is enabled but the
 * containing file is not enabled.
 */
#define VLOG(level)                                                               \
  DLOG_IF(INFO, ::tvm::runtime::detail::VerboseLoggingEnabled(__FILE__, (level))) \
      << ::tvm::runtime::detail::ThreadLocalVLogContext()->str()

}  // namespace runtime
}  // namespace tvm
#endif  // TVM_RUNTIME_LOGGING_H_
