/*
 * This file is a part of the LinkAhead Project.
 *
 * Copyright (C) 2021-2022 Timm Fitschen <t.fitschen@indiscale.com>
 * Copyright (C) 2021-2022 IndiScale GmbH <info@indiscale.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 */

#ifndef LINKAHEAD_LOGGING_H
#define LINKAHEAD_LOGGING_H

#include "linkahead/log_level.h" // for LINKAHEAD_LOG_...
#include <cstdint>               // for uint64_t
#include <iosfwd>                // for streamsize
#include <memory>                // for shared_ptr
#include <string>                // for string
#include <vector>                // for vector
#include <filesystem>            // for path

namespace linkahead::logging {

const std::string logger_name = "linkahead::logging";

class LoggerOutputStream {
public:
  LoggerOutputStream(std::string channel, int level);
  auto operator<<(std::ostream &(*f)(std::ostream &)) -> LoggerOutputStream &;
  auto operator<<(bool msg) -> LoggerOutputStream &;
  auto operator<<(int msg) -> LoggerOutputStream &;
  auto operator<<(uint64_t msg) -> LoggerOutputStream &;
  auto operator<<(int64_t msg) -> LoggerOutputStream &;
  auto operator<<(std::streambuf *msg) -> LoggerOutputStream &;
  auto operator<<(const char *msg) -> LoggerOutputStream &;
  auto operator<<(const std::string &msg) -> LoggerOutputStream &;
  auto operator<<(void *msg) -> LoggerOutputStream &;
  auto operator<<(std::filesystem::path *path) -> LoggerOutputStream &;
  auto operator<<(const std::filesystem::path &path) -> LoggerOutputStream &;

  static auto get(const std::string &channel, int level) -> LoggerOutputStream {
    return LoggerOutputStream(channel, level);
  }

private:
  std::string channel;
  int level;
};

/**
 * Helper class for logging the entering and leaving of a function or method.
 *
 * Please Use the macro
 *
 *   LINKAHEAD_LOG_TRACE_ENTER_AND_LEAVE(logger_name, function_name);
 */
class TraceEnterLeaveLogger {
public:
  inline TraceEnterLeaveLogger(const std::string &channel, const std::string &function_name)
    : channel(channel), function_name(function_name) {
    linkahead::logging::LoggerOutputStream::get(this->channel, LINKAHEAD_LOG_LEVEL_TRACE)
      << "Enter " << this->function_name;
  }
  inline ~TraceEnterLeaveLogger() {
    linkahead::logging::LoggerOutputStream::get(this->channel, LINKAHEAD_LOG_LEVEL_TRACE)
      << "Leave " << this->function_name;
  }

private:
  const std::string &channel;
  const std::string function_name;
};

/**
 * This class stores the integer log level.
 */
class LevelConfiguration {
public:
  LevelConfiguration(int level) : level(level){};
  [[nodiscard]] inline auto GetLevel() const -> int { return this->level; }

private:
  int level;
};

class SinkConfiguration;

/**
 * This class stores the logging level and log sinks.
 *
 * Sinks are represented by SinkConfiguration objects.
 */
class LoggingConfiguration : public LevelConfiguration {
public:
  virtual ~LoggingConfiguration() = default;
  LoggingConfiguration(int level);
  auto AddSink(const std::shared_ptr<SinkConfiguration> &sink) -> void;
  auto GetSinks() const -> const std::vector<std::shared_ptr<SinkConfiguration>> &;

private:
  std::vector<std::shared_ptr<SinkConfiguration>> sinks;
};

auto initialize_logging_defaults() -> int;
auto initialize_logging(const LoggingConfiguration &configuration) -> void;

/**
 * A logging sink is characterized by a name and destination.
 *
 * Typical inheriting configurations exist for console, files and syslog.
 *
 * When a SinkConfiguration is created from a configuration, the sink
 * configuration must contain a \p destination key which matches one of the
 * keywords for implemented sinks.  At the moment of writing this documentation,
 * valid destinations are:
 *
 * \li \p file
 * \li \p console
 * \li \p syslog
 *
 * A \p level keyword sets the logging level, if it exists at the sink or
 * logging level of the configuration.
 */
class SinkConfiguration : public LevelConfiguration {
public:
  virtual ~SinkConfiguration() = default;
  SinkConfiguration(std::string name, int level);
  [[nodiscard]] auto GetName() const -> const std::string &;
  [[nodiscard]] virtual auto GetDestination() const -> const std::string & = 0;

  friend auto initialize_logging_defaults() -> int;
  friend auto initialize_logging(const LoggingConfiguration &logging_configuration) -> void;

protected:
  virtual auto Configure(void *settings) const -> void;

private:
  std::string name;
};

class ConsoleSinkConfiguration : public SinkConfiguration {
public:
  virtual ~ConsoleSinkConfiguration() = default;
  ConsoleSinkConfiguration(const std::string &name, int level);
  [[nodiscard]] auto GetDestination() const -> const std::string & override;
  friend auto initialize_logging_defaults() -> int;
  friend auto initialize_logging(const LoggingConfiguration &logging_configuration) -> void;

protected:
  typedef SinkConfiguration sink_configuration;
  virtual auto Configure(void *settings) const -> void override;

private:
  const std::string destination = "Console";
};

/**
 * The file name is the destination, the directory can be set separately.
 *
 * If there is a `directory` key in the configuration, that will be used as a
 * default, otherwise it is the current directory.
 */
class FileSinkConfiguration : public SinkConfiguration {
public:
  virtual ~FileSinkConfiguration() = default;
  FileSinkConfiguration(const std::string &name, int level);
  [[nodiscard]] virtual auto GetDestination() const -> const std::string & override;
  auto SetDirectory(const std::string &directory) -> void;
  friend auto initialize_logging_defaults() -> int;
  friend auto initialize_logging(const LoggingConfiguration &logging_configuration) -> void;

protected:
  typedef SinkConfiguration sink_configuration;
  virtual auto Configure(void *settings) const -> void override;

private:
  const std::string destination = "TextFile";
  std::string directory = "./";
};

class SyslogSinkConfiguration : public SinkConfiguration {
public:
  virtual ~SyslogSinkConfiguration() = default;
  SyslogSinkConfiguration(const std::string &name, int level);
  [[nodiscard]] virtual auto GetDestination() const -> const std::string & override;

private:
  const std::string destination = "Syslog";
};

/**
 * Convenience function for the C interface.
 */
void linkahead_log_fatal(const char *channel, const char *msg);
/**
 * Convenience function for the C interface.
 */
void linkahead_log_error(const char *channel, const char *msg);
/**
 * Convenience function for the C interface.
 */
void linkahead_log_warn(const char *channel, const char *msg);
/**
 * Convenience function for the C interface.
 */
void linkahead_log_info(const char *channel, const char *msg);
/**
 * Convenience function for the C interface.
 */
void linkahead_log_debug(const char *channel, const char *msg);
/**
 * Convenience function for the C interface.
 */
void linkahead_log_trace(const char *channel, const char *msg);

} // namespace linkahead::logging

#define LINKAHEAD_LOG_FATAL(Channel)                                                               \
  linkahead::logging::LoggerOutputStream::get(Channel, LINKAHEAD_LOG_LEVEL_FATAL)
#define LINKAHEAD_LOG_ERROR(Channel)                                                               \
  linkahead::logging::LoggerOutputStream::get(Channel, LINKAHEAD_LOG_LEVEL_ERROR)
#define LINKAHEAD_LOG_WARN(Channel)                                                                \
  linkahead::logging::LoggerOutputStream::get(Channel, LINKAHEAD_LOG_LEVEL_WARN)
#define LINKAHEAD_LOG_INFO(Channel)                                                                \
  linkahead::logging::LoggerOutputStream::get(Channel, LINKAHEAD_LOG_LEVEL_INFO)
#define LINKAHEAD_LOG_DEBUG(Channel)                                                               \
  linkahead::logging::LoggerOutputStream::get(Channel, LINKAHEAD_LOG_LEVEL_DEBUG)
#define LINKAHEAD_LOG_TRACE(Channel)                                                               \
  linkahead::logging::LoggerOutputStream::get(Channel, LINKAHEAD_LOG_LEVEL_TRACE)

#define LINKAHEAD_LOG_TRACE_ENTER_AND_LEAVE(Channel, FunctionName)                                 \
  const linkahead::logging::TraceEnterLeaveLogger trace_enter_leave_logger(Channel, FunctionName);

#define LINKAHEAD_LOG_ERROR_AND_RETURN_STATUS(Channel, StatusCode, Message)                        \
  LINKAHEAD_LOG_ERROR(Channel) << "StatusCode (" << StatusCode << ") "                             \
                               << linkahead::get_status_description(StatusCode) << ": "            \
                               << Message;                                                         \
  return StatusCode;

#endif