/** @file

  Implementation file for DbgCtl class.

  @section license License

  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.
 */

#include <ctime>
#include <mutex>
#include <map>
#include <cstring>
#include <atomic>
#include <cstdarg>
#include <cinttypes>

#include "tsutil/SourceLocation.h"

#include "tsutil/ts_diag_levels.h"
#include "tsutil/ts_bw_format.h"
#include "tsutil/DbgCtl.h"
#include "tsutil/Assert.h"

#include "tscore/ink_config.h"

using namespace swoc::literals;

DbgCtl::DbgCtl(DbgCtl &&src)
{
  _ptr     = src._ptr;
  src._ptr = &_No_tag_dummy();
}

DbgCtl &
DbgCtl::operator=(DbgCtl &&src)
{
  new (this) DbgCtl{std::move(src)};

  return *this;
}

class DbgCtl::_RegistryAccessor
{
private:
  struct TagCmp {
    bool
    operator()(char const *a, char const *b) const
    {
      return std::strcmp(a, b) < 0;
    }
  };

public:
  using Map = std::map<char const *, bool, TagCmp>;

  class Registry
  {
  public:
    Map map;

  private:
    Registry() = default;

    // Destructor never called - this is a leaky singleton. See issue #12776.
    ~Registry() = default;

    std::mutex _mtx;

    friend class DbgCtl::_RegistryAccessor;
  };

  _RegistryAccessor()
  {
    if (!_registry_instance) {
      Registry *expected{nullptr};
      Registry *r{new Registry};
      if (!_registry_instance.compare_exchange_strong(expected, r)) {
        r->_mtx.lock();
        delete r;
      }
    }
    _registry_instance.load()->_mtx.lock();
    _mtx_is_locked = true;
  }

  ~_RegistryAccessor()
  {
    if (_mtx_is_locked) {
      _registry_instance.load()->_mtx.unlock();
    }
  }

  // This is not static so it can't be called with the registry mutex unlocked.
  // The registry is never deleted (leaky singleton pattern), so it's always safe to call after creation.
  //
  Registry &
  data()
  {
    return *_registry_instance;
  }

private:
  bool _mtx_is_locked{false};

  inline static std::atomic<Registry *> _registry_instance{nullptr};
};

DbgCtl::_TagData const *
DbgCtl::_new_reference(char const *tag)
{
  DebugInterface *p = DebugInterface::get_instance();
  debug_assert(tag != nullptr);

  _TagData *new_tag_data{nullptr};

  {
    _RegistryAccessor ra;

    auto &d{ra.data()};

    if (auto it = d.map.find(tag); it != d.map.end()) {
      return &*it;
    }

    auto sz = std::strlen(tag);

    debug_assert(sz > 0);

    char *t = new char[sz + 1]; // Never deleted (leaky singleton) - reclaimed by OS at process exit.
    std::memcpy(t, tag, sz + 1);
    _TagData new_elem{t, false};

    auto res = d.map.insert(new_elem);

    debug_assert(res.second);

    new_tag_data = &*res.first;
  }
  new_tag_data->second = p && p->debug_tag_activated(tag);

  // It is important that debug_tag_activated() is NOT called while the ra object exists, and the registry mutex is
  // locked.  There is a mutex in the C/C++ runtime that both dlopen() and _cxa_thread_atexit() lock while running.
  // Creating a _RegistryAccessor instance locks the registry mutex.  If the subsequent code in this function triggers
  // the construction of a thread_local variable (with a non-trivial destructor), with the registry mutex locked, the
  // following deadlock scenario is possible:
  // 1.  Thread 1 calls a DbgCtl constructor, which locks the registry mutex, but then is suspended.
  // 2.  Thread 2 calls dlopen() for a plugin, locking the runtime mutex.  It then executes the constructor for a
  //     statically allocated DbgCtl object, which blocks on locking the registry mutex.
  // 3.  Thread 1 resumes, and calls a function that causes the the construction of a thread_local variable with a
  //     non-trivial destructor.  This causes a call to _cxa_thread_atexit(), to set up a call of the variable's
  //     destructor at thread exit.  The call to _cxa_thread_atexit() will block on the runtime mutex (held by Thread 2).
  //     So Thread 1 holds the registry mutex and is blocked waiting for the runtime mutex.  And Thread 2 holds the
  //     runtime mutex and is blocked waiting for the registry mutex.  Deadlock.

  return new_tag_data;
}

// Deprecated: backward compatibility stub for ABI compatibility with plugins
// compiled against pre-leaky-singleton versions. Does nothing since the
// registry is now never destroyed.
void
DbgCtl::_rm_reference()
{
}

void
DbgCtl::update(const std::function<bool(const char *)> &f)
{
  _RegistryAccessor ra;
  auto             &d{ra.data()};

  for (auto &i : d.map) {
    i.second = f(i.first);
  }
}

void
DbgCtl::print(char const *tag, char const *file, char const *function, int line, char const *fmt_str, ...)
{
  DebugInterface *p = DebugInterface::get_instance();
  SourceLocation  src_loc{file, function, line};
  if (p) {
    va_list args;
    va_start(args, fmt_str);
    p->print_va(tag, DL_Diag, &src_loc, fmt_str, args);
    va_end(args);
  } else {
    swoc::LocalBufferWriter<1024> format_writer;
    DebugInterface::generate_format_string(format_writer, tag, DL_Diag, &src_loc, SHOW_LOCATION_DEBUG, fmt_str);
    va_list args;
    va_start(args, fmt_str);
    vprintf(format_writer.data(), args);
    va_end(args);
  }
}

std::atomic<int> DbgCtl::_config_mode{0};

bool
DbgCtl::_override_global_on()
{
  DebugInterface *p = DebugInterface::get_instance();
  if (p) {
    return p->get_override();
  } else {
    return false;
  }
}

namespace
{
static DebugInterface *di_inst;

bool
location(const SourceLocation *loc, DiagsShowLocation show, DiagsLevel level)
{
  if (loc && loc->valid()) {
    switch (show) {
    case SHOW_LOCATION_ALL:
      return true;
    case SHOW_LOCATION_DEBUG:
      return level <= DL_Debug;
    default:
      return false;
    }
  }

  return false;
}

//////////////////////////////////////////////////////////////////////////////
//
//      const char *Diags::level_name(DiagsLevel dl)
//
//      This routine returns a string name corresponding to the error
//      level <dl>, suitable for us as an output log entry prefix.
//
//////////////////////////////////////////////////////////////////////////////

} // namespace

DebugInterface *
DebugInterface::get_instance()
{
  return di_inst;
}

void
DebugInterface::set_instance(DebugInterface *i)
{
  di_inst = i;
  DbgCtl::update([&](const char *t) { return i->debug_tag_activated(t); });
}

const char *
DebugInterface::level_name(DiagsLevel dl)
{
  switch (dl) {
  case DL_Diag:
    return ("DIAG");
  case DL_Debug:
    return ("DEBUG");
  case DL_Status:
    return ("STATUS");
  case DL_Note:
    return ("NOTE");
  case DL_Warning:
    return ("WARNING");
  case DL_Error:
    return ("ERROR");
  case DL_Fatal:
    return ("FATAL");
  case DL_Alert:
    return ("ALERT");
  case DL_Emergency:
    return ("EMERGENCY");
  default:
    return ("DIAG");
  }
}

namespace
{

struct DiagTimestamp {
  std::chrono::time_point<std::chrono::system_clock> ts = std::chrono::system_clock::now();
};

swoc::BufferWriter &
bwformat(swoc::BufferWriter &w, [[maybe_unused]] swoc::bwf::Spec const &spec, DiagTimestamp const &ts)
{
  auto                        epoch = std::chrono::system_clock::to_time_t(ts.ts);
  swoc::LocalBufferWriter<48> lw;

  ctime_r(&epoch, lw.aux_data());
  lw.commit(19); // keep only leading text.
  lw.print(".{:03}", std::chrono::time_point_cast<std::chrono::milliseconds>(ts.ts).time_since_epoch().count() % 1000);
  w.write(lw.view().substr(4));

  return w;
}

struct DiagThreadname {
  char name[32];

  DiagThreadname()
  {
#if defined(HAVE_PTHREAD_GETNAME_NP)
    pthread_getname_np(pthread_self(), name, sizeof(name));
#elif defined(HAVE_PTHREAD_GET_NAME_NP)
    pthread_get_name_np(pthread_self(), name, sizeof(name));
#elif defined(HAVE_PRCTL) && defined(PR_GET_NAME)
    prctl(PR_GET_NAME, name, 0, 0, 0);
#else
    snprintf(name, sizeof(name), "0x%" PRIx64, (uint64_t)pthread_self());
#endif
  }
};

swoc::BufferWriter &
bwformat(swoc::BufferWriter &w, swoc::bwf::Spec const &spec, DiagThreadname const &n)
{
  bwformat(w, spec, std::string_view{n.name});
  return w;
}

} // namespace

size_t
DebugInterface::generate_format_string(swoc::LocalBufferWriter<1024> &format_writer, const char *debug_tag, DiagsLevel diags_level,
                                       const SourceLocation *loc, DiagsShowLocation show_location, const char *format_string)
{
  // Save room for optional newline and terminating NUL bytes.
  format_writer.restrict(2);

  format_writer.print("[{}] ", DiagTimestamp{});
  auto timestamp_offset = format_writer.size();

  format_writer.print("{} {}: ", DiagThreadname{}, level_name(diags_level));

  if (location(loc, show_location, diags_level)) {
    format_writer.print("<{}> ", *loc);
  }

  if (debug_tag) {
    format_writer.print("({}) ", debug_tag);
  }

  format_writer.print("{}", format_string);

  format_writer.restore(2);                  // restore the space for required termination.
  if (format_writer.view().back() != '\n') { // safe because always some chars in the buffer.
    format_writer.write('\n');
  }
  format_writer.write('\0');

  return timestamp_offset;
}
