/*
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed 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.
 */

#pragma once

#include <cstdlib>

#include <fmt/core.h>
#include <folly/CPortability.h>
#include <folly/Conv.h>
#include <folly/ExceptionString.h>
#include <folly/Portability.h>
#include <folly/lang/Exception.h>
#include <folly/logging/LogCategory.h>
#include <folly/logging/LogMessage.h>
#include <folly/logging/LogStream.h>
#include <folly/logging/ObjectToString.h>

namespace folly {

template <bool IsInHeaderFile>
class XlogCategoryInfo;
class XlogFileScopeInfo;

/**
 * LogStreamProcessor receives a LogStream and logs it.
 *
 * This class is primarily intended to be used through the FB_LOG*() macros.
 * Its API is designed to support these macros, and is not designed for other
 * use.
 *
 * The operator&() method is used to trigger the logging.
 * This operator is used because it has lower precedence than <<, but higher
 * precedence than the ? ternary operator, allowing it to bind with the correct
 * precedence in the log macro implementations.
 */
class LogStreamProcessor {
 public:
  enum AppendType { APPEND };
  enum FormatType { FORMAT };

  /**
   * LogStreamProcessor constructor for use with a LOG() macro with no extra
   * arguments.
   *
   * Note that the filename argument is not copied.  The caller should ensure
   * that it points to storage that will remain valid for the lifetime of the
   * LogStreamProcessor.  (This is always the case for the __FILE__
   * preprocessor macro.)
   */
  LogStreamProcessor(
      const LogCategory* category,
      LogLevel level,
      folly::StringPiece filename,
      unsigned int lineNumber,
      folly::StringPiece functionName,
      AppendType) noexcept;

  /**
   * LogStreamProcessor constructor for use with a LOG() macro with arguments
   * to be concatenated with folly::to<std::string>()
   *
   * Note that the filename argument is not copied.  The caller should ensure
   * that it points to storage that will remain valid for the lifetime of the
   * LogStreamProcessor.  (This is always the case for the __FILE__
   * preprocessor macro.)
   */
  template <typename... Args>
  LogStreamProcessor(
      const LogCategory* category,
      LogLevel level,
      folly::StringPiece filename,
      unsigned int lineNumber,
      folly::StringPiece functionName,
      AppendType,
      Args&&... args) noexcept
      : LogStreamProcessor(
            category,
            level,
            filename,
            lineNumber,
            functionName,
            INTERNAL,
            createLogString(std::forward<Args>(args)...)) {}

  /**
   * LogStreamProcessor constructor for use with a LOG() macro with arguments
   * to be concatenated with folly::to<std::string>()
   *
   * Note that the filename argument is not copied.  The caller should ensure
   * that it points to storage that will remain valid for the lifetime of the
   * LogStreamProcessor.  (This is always the case for the __FILE__
   * preprocessor macro.)
   */
  template <typename... Args>
  LogStreamProcessor(
      const LogCategory* category,
      LogLevel level,
      folly::StringPiece filename,
      unsigned int lineNumber,
      folly::StringPiece functionName,
      FormatType,
      folly::StringPiece fmt,
      Args&&... args) noexcept
      : LogStreamProcessor(
            category,
            level,
            filename,
            lineNumber,
            functionName,
            INTERNAL,
            formatLogString(fmt, std::forward<Args>(args)...)) {}

  /*
   * Versions of the above constructors for use in XLOG() statements.
   *
   * These are defined separately from the above constructor so that the work
   * of initializing the XLOG LogCategory data is done in a separate function
   * body defined in LogStreamProcessor.cpp.  We intentionally want to avoid
   * inlining this work at every XLOG() statement, to reduce the emitted code
   * size.
   */
  LogStreamProcessor(
      XlogCategoryInfo<true>* categoryInfo,
      LogLevel level,
      folly::StringPiece categoryName,
      bool isCategoryNameOverridden,
      folly::StringPiece filename,
      unsigned int lineNumber,
      folly::StringPiece functionName,
      AppendType) noexcept;
  template <typename... Args>
  LogStreamProcessor(
      XlogCategoryInfo<true>* categoryInfo,
      LogLevel level,
      folly::StringPiece categoryName,
      bool isCategoryNameOverridden,
      folly::StringPiece filename,
      unsigned int lineNumber,
      folly::StringPiece functionName,
      AppendType,
      Args&&... args) noexcept
      : LogStreamProcessor(
            categoryInfo,
            level,
            categoryName,
            isCategoryNameOverridden,
            filename,
            lineNumber,
            functionName,
            INTERNAL,
            createLogString(std::forward<Args>(args)...)) {}
  template <typename... Args>
  LogStreamProcessor(
      XlogCategoryInfo<true>* categoryInfo,
      LogLevel level,
      folly::StringPiece categoryName,
      bool isCategoryNameOverridden,
      folly::StringPiece filename,
      unsigned int lineNumber,
      folly::StringPiece functionName,
      FormatType,
      folly::StringPiece fmt,
      Args&&... args) noexcept
      : LogStreamProcessor(
            categoryInfo,
            level,
            categoryName,
            isCategoryNameOverridden,
            filename,
            lineNumber,
            functionName,
            INTERNAL,
            formatLogString(fmt, std::forward<Args>(args)...)) {}

#ifdef __INCLUDE_LEVEL__
  /*
   * Versions of the above constructors to use in XLOG() macros that appear in
   * .cpp files.  These are only used if the compiler supports the
   * __INCLUDE_LEVEL__ macro, which we need to determine that the XLOG()
   * statement is not in a header file.
   *
   * These behave identically to the XlogCategoryInfo<true> versions of the
   * APIs, but slightly more optimized, and allow the XLOG() code to avoid
   * storing category information at each XLOG() call site.
   */
  LogStreamProcessor(
      XlogFileScopeInfo* fileScopeInfo,
      LogLevel level,
      folly::StringPiece filename,
      unsigned int lineNumber,
      folly::StringPiece functionName,
      AppendType) noexcept;
  LogStreamProcessor(
      XlogFileScopeInfo* fileScopeInfo,
      LogLevel level,
      folly::StringPiece /* categoryName */,
      bool /* isCategoryNameOverridden */,
      folly::StringPiece filename,
      unsigned int lineNumber,
      folly::StringPiece functionName,
      AppendType) noexcept
      : LogStreamProcessor(
            fileScopeInfo,
            level,
            filename,
            lineNumber,
            functionName,
            APPEND) {}
  template <typename... Args>
  LogStreamProcessor(
      XlogFileScopeInfo* fileScopeInfo,
      LogLevel level,
      folly::StringPiece /* categoryName */,
      bool /* isCategoryNameOverridden */,
      folly::StringPiece filename,
      unsigned int lineNumber,
      folly::StringPiece functionName,
      AppendType,
      Args&&... args) noexcept
      : LogStreamProcessor(
            fileScopeInfo,
            level,
            filename,
            lineNumber,
            functionName,
            INTERNAL,
            createLogString(std::forward<Args>(args)...)) {}
  template <typename... Args>
  LogStreamProcessor(
      XlogFileScopeInfo* fileScopeInfo,
      LogLevel level,
      folly::StringPiece /* categoryName */,
      bool /* isCategoryNameOverridden */,
      folly::StringPiece filename,
      unsigned int lineNumber,
      folly::StringPiece functionName,
      FormatType,
      folly::StringPiece fmt,
      Args&&... args) noexcept
      : LogStreamProcessor(
            fileScopeInfo,
            level,
            filename,
            lineNumber,
            functionName,
            INTERNAL,
            formatLogString(fmt, std::forward<Args>(args)...)) {}
#endif

  ~LogStreamProcessor() noexcept;

  /**
   * This version of operator&() is typically used when the user specifies
   * log arguments using the << stream operator.  The operator<<() generally
   * returns a std::ostream&
   */
  void operator&(std::ostream& stream) noexcept;

  /**
   * This version of operator&() is used when no extra arguments are specified
   * with the << operator.  In this case the & operator is applied directly to
   * the temporary LogStream object.
   */
  void operator&(LogStream&& stream) noexcept;

  std::ostream& stream() noexcept { return stream_; }

  void logNow() noexcept;

 private:
  enum InternalType { INTERNAL };
  LogStreamProcessor(
      const LogCategory* category,
      LogLevel level,
      folly::StringPiece filename,
      unsigned int lineNumber,
      folly::StringPiece functionName,
      InternalType,
      std::string&& msg) noexcept;
  LogStreamProcessor(
      XlogCategoryInfo<true>* categoryInfo,
      LogLevel level,
      folly::StringPiece categoryName,
      bool isCategoryNameOverridden,
      folly::StringPiece filename,
      unsigned int lineNumber,
      folly::StringPiece functionName,
      InternalType,
      std::string&& msg) noexcept;
  LogStreamProcessor(
      XlogFileScopeInfo* fileScopeInfo,
      LogLevel level,
      folly::StringPiece filename,
      unsigned int lineNumber,
      folly::StringPiece functionName,
      InternalType,
      std::string&& msg) noexcept;

  std::string extractMessageString(LogStream& stream) noexcept;

  /**
   * Construct a log message string using folly::to<std::string>()
   *
   * This function attempts to avoid throwing exceptions.  If an error occurs
   * during formatting, a message including the error details is returned
   * instead.  This is done to help ensure that log statements do not generate
   * exceptions, but instead just log an error string when something goes wrong.
   */
  template <typename... Args>
  FOLLY_NOINLINE std::string createLogString(Args&&... args) noexcept {
    return folly::catch_exception<const std::exception&>(
        [&] { return folly::to<std::string>(std::forward<Args>(args)...); },
        [&](const std::exception& ex) {
          // This most likely means there was some error converting the
          // arguments to strings.  Handle the exception here, rather than
          // letting it propagate up, since callers generally do not expect log
          // statements to throw.
          //
          // Just log an error message letting indicating that something went
          // wrong formatting the log message.
          return folly::to<std::string>(
              "error constructing log message: ", exceptionStr(ex));
        });
  }

  FOLLY_NOINLINE std::string vformatLogString(
      folly::StringPiece fmt,
      fmt::format_args args,
      bool& failed) noexcept {
    return folly::catch_exception<const std::exception&>(
        [&] {
          return fmt::vformat(fmt::string_view(fmt.data(), fmt.size()), args);
        },
        [&](const std::exception& ex) {
          // This most likely means that the caller had a bug in their format
          // string/arguments.  Handle the exception here, rather than letting
          // it propagate up, since callers generally do not expect log
          // statements to throw.
          //
          // Log the format string and as much of the arguments as we can
          // convert, to aid debugging.
          failed = true;
          std::string result;
          result.append("error formatting log message: ");
          result.append(exceptionStr(ex).c_str());
          result.append("; format string: \"");
          result.append(fmt.data(), fmt.size());
          result.append("\", arguments: ");
          return result;
        });
  }

  /**
   * Construct a log message string using fmt::format()
   *
   * This function attempts to avoid throwing exceptions.  If an error occurs
   * during formatting, a message including the error details is returned
   * instead.  This is done to help ensure that log statements do not generate
   * exceptions, but instead just log an error string when something goes wrong.
   */
  template <typename... Args>
  FOLLY_NOINLINE std::string formatLogString(
      folly::StringPiece fmt,
      const Args&... args) noexcept {
    bool failed = false;
    std::string result =
        vformatLogString(fmt, fmt::make_format_args(args...), failed);
    if (failed) {
      folly::logging::appendToString(result, args...);
    }
    return result;
  }

  const LogCategory* const category_;
  LogLevel const level_;
  folly::StringPiece filename_;
  unsigned int lineNumber_;
  folly::StringPiece functionName_;
  std::string message_;
  LogStream stream_;
};

/**
 * LogStreamVoidify() is a helper class used in the FB_LOG() and XLOG() macros.
 *
 * It's only purpose is to provide an & operator overload that returns void.
 * This allows the log macros to expand roughly to:
 *
 *   (logEnabled) ? (void)0
 *                : LogStreamVoidify{} & LogStreamProcessor{}.stream() << "msg";
 *
 * This enables the right hand (':') side of the ternary ? expression to have a
 * void type, and allows various streaming operator expressions to be placed on
 * the right hand side of the expression.
 *
 * Operator & is used since it has higher precedence than ?:, but lower
 * precedence than <<.
 *
 * This class is templated on whether the log message is fatal so that the
 * operator& can be declared [[noreturn]] for fatal log messages.  This
 * prevents the compiler from complaining about functions that do not return a
 * value after a fatal log statement.
 */
template <bool Fatal>
class LogStreamVoidify {
 public:
  /**
   * In the default (non-fatal) case, the & operator implementation is a no-op.
   *
   * We perform the actual logging in the LogStreamProcessor destructor.  It
   * feels slightly hacky to perform logging in the LogStreamProcessor
   * destructor instead of here, since the LogStreamProcessor destructor is not
   * evaluated until the very end of the statement.  In practice log
   * statements really shouldn't be in the middle of larger statements with
   * other side effects, so this ordering distinction shouldn't make much
   * difference.
   *
   * However, by keeping this function a no-op we reduce the amount of code
   * generated for log statements.  This function call can be completely
   * eliminated by the compiler, leaving only the LogStreamProcessor destructor
   * invocation, which cannot be eliminated.
   */
  void operator&(std::ostream&) noexcept {}
};

template <>
class LogStreamVoidify<true> {
 public:
  /**
   * A specialized noreturn version of operator&() for fatal log statements.
   */
  [[noreturn]] void operator&(std::ostream&);
};

/**
 * logDisabledHelper() is invoked in FB_LOG() and XLOG() statements if the log
 * admittance check fails.
 *
 * This function exists solely to ensure that both sides of the log check are
 * marked [[noreturn]] for fatal log messages.  This allows the compiler to
 * recognize that the full statement is noreturn, preventing warnings about
 * missing return statements after fatal log messages.
 *
 * Unfortunately it does not appear possible to get the compiler to recognize
 * that the disabled side of the log statement should never be reached for
 * fatal messages.  Even if we make the check something like
 * `(isLogLevelFatal(level) || realCheck)`, where isLogLevelFatal() is
 * constexpr, this is not sufficient for gcc or clang to recognize that the
 * full expression is noreturn.
 *
 * Ideally this would just be a template function specialized on a boolean
 * IsFatal parameter.  Unfortunately this triggers a bug in clang, which does
 * not like differing noreturn behavior for different template instantiations.
 * Therefore we overload on integral_constant instead.
 *
 * clang-format also doesn't do a good job understanding this code and figuring
 * out how to format it.
 */
// clang-format off
inline void logDisabledHelper(std::false_type) noexcept {}
[[noreturn]] void logDisabledHelper(std::true_type) noexcept;
// clang-format on
} // namespace folly
