/*
 * Copyright 2019 Bloomberg Finance LP
 *
 * 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.
 */

#ifndef INCLUDED_BUILDBOXCOMMON_LOGGING
#define INCLUDED_BUILDBOXCOMMON_LOGGING

#include <buildboxcommon_exception.h>

#include <absl/log/log.h>
#include <absl/log/log_sink.h>

#include <map>
#include <string>
#include <string_view>

namespace buildboxcommon {
// abseil supports these standard levels: FATAL, ERROR, WARNING, and INFO.
// FATAL terminates the program (!)

// For `LogLevel::DEBUG` and `LogLevel::TRACE` we will rely on abseil's verbose
// logging.
// That allows assigning arbitrary levels (int values) for messages that should
// be displayed only in verbose mode. Users can then specify a limit with the
// `--v=L` option so that they see messages from levels that do not exceed
// `L`.

enum { VLOG_LEVEL_DEBUG = 1, VLOG_LEVEL_TRACE = 2 };

// For example: if `--v==2`, `TRACE` messages will be shown but not if
// `--v==1`.
// Users of `buildboxcommon_logging.h` will always refer to
// `buildboxcommon::LogLevel` values.
enum LogLevel { TRACE = 0, DEBUG = 1, INFO = 2, WARNING = 3, ERROR = 4 };

// TODO: Expand this namespace to encompass the whole file
// Will require lots of MRs in dependent projects
namespace logging {

class Logger {
    /*
     * This singleton class helps configure and initialize abseil logging.
     *
     * Example usage:
     *
     * ```
     *  int main(int argc, char *argv[]) {
     *    auto &logger = buildboxcommon::logging::Logger::getLoggerInstance();
     *
     *    if (argv contains option to write logs to files) {
     *      // Redirecting outputs *before* initializing logger:
     *      logger.setOutputDirectory(outputDirectory);
     *    }
     *
     *    logger.initialize(argv[0]); // Must be always called.
     *
     *    BUILDBOX_LOG_SET_LEVEL(buildboxcommon::LogLevel::TRACE);
     *    // (Levels can be changed after initialization)
     * }
     * ```
     */
  public:
    static Logger &getLoggerInstance();

    // Write logs to files in the given directory (ERROR messages
    // will still be printed to stderr).
    // Files will be named "<programName>.<hostname>.<user name>.log.
    // <date>.<time>.<pid>".
    // This method must be called before initializing the Logger instance, so
    // the destination cannot be changed again.
    void setOutputDirectory(const char *outputDirectory);

    // Enable Logging to both stderr and files
    void enableLoggingBothStderrAndFiles();

    // Return the configured output directory or an empty string.
    std::string getOutputDirectory();

    // Set the necessary configuration and initialize abseil logging. Must be
    // called only once, before starting to write log messages. (Program name
    // should generally be `argv[0]`.)
    void initialize(const char *programName);

    // Set the maximum log level of messages that will be shown in stderr.
    // (This function can be called after initialization.)
    void setLogLevel(LogLevel logLevel);

    // Return the configured log level.
    LogLevel getLogLevel();

    // Stop printing log messages to stderr.
    void disableStderr();

    Logger(const Logger &) = delete;
    Logger &operator=(Logger const &) = delete;
    Logger(Logger &&) noexcept = delete;
    Logger &operator=(Logger &&) noexcept = delete;

  private:
    Logger();
    ~Logger() = default;

    LogLevel d_logLevel = LogLevel::INFO;

    // Optional directory to which log files are written to.
    std::string d_logOutputDirectory;

    // If true, log messages will be printed to both stderr and files.
    bool d_logBothStderrAndFiles = false;

    // Keeps track of whether `initialize()` has been called.
    bool d_abslLogInitialized = false;

    std::unique_ptr<absl::LogSink> d_directoryLogSink;

}; // namespace logging

// These maps will be deprecated. Use `stringToLogLevelMap()` and
// `logLevelToStringMap()` instead.
const std::map<std::string, LogLevel> stringToLogLevel = {
    {"trace", LogLevel::TRACE},
    {"debug", LogLevel::DEBUG},
    {"info", LogLevel::INFO},
    {"warning", LogLevel::WARNING},
    {"error", LogLevel::ERROR}};

const std::map<std::string, LogLevel> &stringToLogLevelMap();

const std::map<LogLevel, std::string> logLevelToString = {
    {LogLevel::TRACE, "trace"},
    {LogLevel::DEBUG, "debug"},
    {LogLevel::INFO, "info"},
    {LogLevel::WARNING, "warning"},
    {LogLevel::ERROR, "error"}};

const std::map<LogLevel, std::string> &logLevelToStringMap();

std::string stringifyLogLevels();

std::string printableCommandLine(const std::vector<std::string> &commandLine);

struct LogPrefix {
    LogPrefix(std::string_view severity, std::string_view file, int lineNumber)
        : d_severity(severity), d_file(file), d_lineNumber(lineNumber)
    {
    }

    std::string_view d_severity;
    std::string_view d_file;
    int d_lineNumber;
};

// This function generates the prefix that gets attached to every log line.
std::ostream &operator<<(std::ostream &os, const LogPrefix &logPrefix);

} // namespace logging
} // namespace buildboxcommon

// This sets the minimum level what will be logged to stderr.
// By default we use `INFO`.

// NOLINTBEGIN (cppcoreguidelines-macro-usage)
#define BUILDBOX_LOG_SET_LEVEL(logLevel)                                      \
    buildboxcommon::logging::Logger::getLoggerInstance().setLogLevel(logLevel);

#define BUILDBOX_LOG_TRACE(x)                                                 \
    {                                                                         \
        VLOG(buildboxcommon::VLOG_LEVEL_TRACE)                                \
            << buildboxcommon::logging::LogPrefix("TRACE", __FILE__,          \
                                                  __LINE__)                   \
            << x;                                                             \
    }

#define BUILDBOX_LOG_DEBUG(x)                                                 \
    {                                                                         \
        VLOG(buildboxcommon::VLOG_LEVEL_DEBUG)                                \
            << buildboxcommon::logging::LogPrefix("DEBUG", __FILE__,          \
                                                  __LINE__)                   \
            << x;                                                             \
    }

#define BUILDBOX_LOG_INFO(x)                                                  \
    {                                                                         \
        LOG(INFO) << buildboxcommon::logging::LogPrefix("INFO", __FILE__,     \
                                                        __LINE__)             \
                  << x;                                                       \
    }

#define BUILDBOX_LOG_WARNING(x)                                               \
    {                                                                         \
        LOG(WARNING) << buildboxcommon::logging::LogPrefix(                   \
                            "WARNING", __FILE__, __LINE__)                    \
                     << x;                                                    \
    }

#define BUILDBOX_LOG_ERROR(x)                                                 \
    {                                                                         \
        LOG(ERROR) << buildboxcommon::logging::LogPrefix("ERROR", __FILE__,   \
                                                         __LINE__)            \
                   << x;                                                      \
    }
#endif
// NOLINTEND (cppcoreguidelines-macro-usage)
