/*
 *  BaseLog.h
 *  PlexMediaServer
 *
 *  Created by Elan Feingold ages ago.
 *  Copyright 2010 Plex, Inc. All rights reserved.
 *
 */

#pragma once

#include <fmt/format.h>
#include <fmt/printf.h>
#include <list>   // For std::list
#include <string> // For std::string

#include "LogContext.h"
#include "LogLevel.h"

#if defined(INCLUDE_PMS_LOGGER) || (defined(INCLUDE_MINIMAL_SERVER) && defined(INSIDE_SERVER))
#include "pms_export.h"
#else
#define PMS_EXPORT
#endif

class LogItem;
typedef std::shared_ptr<LogItem> LogItemPtr;

struct BaseLogState;

///////////////////////////////////////////////////////////////////////////////////////////////////
class PMS_EXPORT BaseLog
{
public:
  virtual ~BaseLog();

  void out(LogLevel level, const char* format, fmt::ArgList args);
  FMT_VARIADIC(void, out, LogLevel, const char*)
  void vOut(LogLevel level, const char* format, va_list args);

  void fatalError(const char* format, fmt::ArgList args);
  FMT_VARIADIC(void, fatalError, const char*)

  static std::string LogLevelName(LogLevel level);
  bool logLevelEnabled(LogLevel level) const;

  /// Compute the file name by index.
  std::string getLogPath(const std::string& logBaseName, size_t index);

  void rotateLogs(const std::string& logBaseName);

  template <class T>
  static void PushContext(T&& newContext)
  {
    std::list<std::string>& contextStack = GetContextStack();
    if (std::any_of(contextStack.begin(), contextStack.end(), [&](const std::string& str) { return str == newContext; }))
    {
      // We're pushing the same context twice, probably due to two classes calling each other or the like.
      // This would be redundant, so just add an empty string instead (we need _something_ to pop later).
      //
      contextStack.emplace_back();
      return;
    }

    contextStack.emplace_back(std::forward<T>(newContext));
  }

  static void PopContext() { GetContextStack().pop_back(); }
  static void AddContextToMessage(const LogItemPtr& item);

  static std::list<std::string>& GetContextStack();

  /// Creats a version of the passed-in callable with the current context stack bound to it
  template <class Func, class... Args>
  static auto BindContext(Func&& func)
  {
    return [contextStack = GetContextStack(), func = std::forward<Func>(func)](Args... args) {
      ScopedLogContextStack activeStack(contextStack);
      return func(args...);
    };
  }

  template <size_t N>
  static void ElidePattern(std::string& message, const char (&substr)[N], size_t chars)
  {
    // N is deduced as the size of substr, including the terminating \0,
    // so we subtract 1 to get the unterminated length of the literal.
    // Three-arg variant of string::find() skips a strlen().
    //
    size_t pos = 0;
    while ((pos = message.find(substr, pos, N - 1)) != std::string::npos)
    {
      pos += N - 1;

      // Don't write past the end of the string.
      size_t len = std::min(chars, message.length() - pos);
      message.replace(pos, len, len, 'x');

      pos += len;
    }
  }

protected:
  BaseLog();

#if defined(__linux__)
  virtual std::string sysLogName() const = 0;
  virtual bool sysLogEnabled() const = 0;
#endif
#if defined(__ANDROID__)
  virtual bool androidLogEnabled() const = 0;
#endif
#ifdef _WIN32
  virtual void createLogDirectory() const = 0;
#endif
  virtual void doInitialize() {}
  virtual void notifyLogSent(const LogItemPtr& item) {}
  virtual std::string getLogDirPath() const = 0;
  virtual int getMaxFiles() const = 0;
  virtual std::string getPapertrailUsername() const = 0;
  virtual void printLogHeader() const = 0;
  virtual void elidePatterns(std::string& message) const = 0;

  void initialize(const std::string& logName);

  void setLogLevel(LogLevel level);

  void setSynchronous(bool sync);

  void forceRotateLogs();

  /// Endlessly read the queue logging individual items.
  void outputWorker();

  /// Enqueue a log item into the queue.
  void enqueue(const LogItemPtr& item);

  /// Dequeue and output a log item from the queue; blocking until one is available.
  void dequeueAndLog();

  /// Set an item's thread, time, etc. and submit it for logging.
  void submitItem(const LogItemPtr& item);

  /// Log a single item.
  void logItem(const LogItemPtr& item);

  /// Enable papertrail logging for 30 minutes.
  bool enablePapertrail(int minutes = 30);

  /// Send the log over to Papertrail.
  void sendToPapertrail(LogLevel level, const std::string& str);
#if defined(__linux__)
  bool sendToSyslog(LogLevel level, const string& str);
#endif
#if defined(__ANDROID__)
  bool sendToAndroidLog(const std::string& logName, LogLevel level, pid_t threadId, const std::string& str);
#endif

  /// Take existing log file and perfom rotation.
  virtual void rotate();

  /// Flush everything that's currently in the log to disk.  Useful if we are around to throw and potentially kill the proc
  void flush();

  /// Stop accepting incoming logs and wait for the queue to empty out
  void shutdown();

private:
  std::unique_ptr<BaseLogState> m_state;

  /// Load the verbose and debug prefs into fast local atomics
  void loadPrefs();
};

///////////////////////////////////////////////////////////////////////////////////////////////////
class PMS_EXPORT ScopedLogContext
{
public:
  template <class... Args>
  ScopedLogContext(Args&&... args)
    : m_contextCount(0)
  {
    addArgs(std::forward<Args>(args)...);
  }

  ~ScopedLogContext()
  {
    for (int i = 0; i < m_contextCount; i++)
      BaseLog::PopContext();
  }

  // Non-default-constructable, noncopyable, nonmovable
  ScopedLogContext() = delete;
  ScopedLogContext(const ScopedLogContext&) = delete;
  ScopedLogContext(ScopedLogContext&&) = delete;

private:
  template <class T>
  void addArgs(T&& contextStr)
  {
    BaseLog::PushContext(std::forward<T>(contextStr));
    m_contextCount++;
  }

  template <class T, class... Args>
  void addArgs(T&& contextStr, Args&&... args)
  {
    // Add the one arg
    addArgs(std::forward<T>(contextStr));

    // And add the rest
    addArgs(std::forward<Args>(args)...);
  }

  int m_contextCount;
};
