// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- 
// vim: ts=8 sw=2 smarttab

#include "include/types.h"
#include "ClassHandler.h"
#include "common/errno.h"
#include "common/ceph_context.h"
#include "include/dlfcn_compat.h"

#include <map>

#if defined(__FreeBSD__)
#include <sys/param.h>
#endif

#include "common/config.h"
#include "common/debug.h"

#define dout_subsys ceph_subsys_osd
#undef dout_prefix
#define dout_prefix *_dout


#define CLS_PREFIX "libcls_"
#define CLS_SUFFIX SHARED_LIB_SUFFIX

using std::map;
using std::set;
using std::string;

using ceph::bufferlist;


int ClassHandler::open_class(const string& cname, ClassData **pcls)
{
  std::lock_guard lock(mutex);
  ClassData *cls = _get_class(cname, true);
  if (!cls)
    return -EPERM;
  if (cls->status != ClassData::CLASS_OPEN) {
    int r = _load_class(cls);
    if (r)
      return r;
  }
  *pcls = cls;
  return 0;
}

int ClassHandler::open_all_classes()
{
  ldout(cct, 10) << __func__ << dendl;
  DIR *dir = ::opendir(cct->_conf->osd_class_dir.c_str());
  if (!dir)
    return -errno;

  struct dirent *pde = nullptr;
  int r = 0;
  while ((pde = ::readdir(dir))) {
    if (pde->d_name[0] == '.')
      continue;
    if (strlen(pde->d_name) > sizeof(CLS_PREFIX) - 1 + sizeof(CLS_SUFFIX) - 1 &&
	strncmp(pde->d_name, CLS_PREFIX, sizeof(CLS_PREFIX) - 1) == 0 &&
	strcmp(pde->d_name + strlen(pde->d_name) - (sizeof(CLS_SUFFIX) - 1), CLS_SUFFIX) == 0) {
      char cname[PATH_MAX + 1];
      strncpy(cname, pde->d_name + sizeof(CLS_PREFIX) - 1, sizeof(cname) -1);
      cname[strlen(cname) - (sizeof(CLS_SUFFIX) - 1)] = '\0';
      ldout(cct, 10) << __func__ << " found " << cname << dendl;
      ClassData *cls;
      // skip classes that aren't in 'osd class load list'
      r = open_class(cname, &cls);
      if (r < 0 && r != -EPERM)
	goto out;
    }
  }
 out:
  closedir(dir);
  return r;
}

void ClassHandler::shutdown()
{
  for (auto& cls : classes) {
    if (cls.second.handle) {
      dlclose(cls.second.handle);
    }
  }
  classes.clear();
}

/*
 * Check if @cname is in the whitespace delimited list @list, or the @list
 * contains the wildcard "*".
 *
 * This is expensive but doesn't consume memory for an index, and is performed
 * only once when a class is loaded.
 */
bool ClassHandler::in_class_list(const std::string& cname,
    const std::string& list)
{
  std::istringstream ss(list);
  std::istream_iterator<std::string> begin{ss};
  std::istream_iterator<std::string> end{};

  const std::vector<std::string> targets{cname, "*"};

  auto it = std::find_first_of(begin, end,
      targets.begin(), targets.end());

  return it != end;
}

ClassHandler::ClassData *ClassHandler::_get_class(const string& cname,
    bool check_allowed)
{
  ClassData *cls;
  map<string, ClassData>::iterator iter = classes.find(cname);

  if (iter != classes.end()) {
    cls = &iter->second;
  } else {
    if (check_allowed && !in_class_list(cname, cct->_conf->osd_class_load_list)) {
      ldout(cct, 0) << "_get_class not permitted to load " << cname << dendl;
      return NULL;
    }
    cls = &classes[cname];
    ldout(cct, 10) << "_get_class adding new class name " << cname << " " << cls << dendl;
    cls->name = cname;
    cls->handler = this;
    cls->allowed = in_class_list(cname, cct->_conf->osd_class_default_list);
  }
  return cls;
}

int ClassHandler::_load_class(ClassData *cls)
{
  // already open
  if (cls->status == ClassData::CLASS_OPEN)
    return 0;

  if (cls->status == ClassData::CLASS_UNKNOWN ||
      cls->status == ClassData::CLASS_MISSING) {
    char fname[PATH_MAX];
    snprintf(fname, sizeof(fname), "%s/" CLS_PREFIX "%s" CLS_SUFFIX,
	     cct->_conf->osd_class_dir.c_str(),
	     cls->name.c_str());
    ldout(cct, 10) << "_load_class " << cls->name << " from " << fname << dendl;

    cls->handle = dlopen(fname, RTLD_NOW);
    if (!cls->handle) {
      struct stat st;
      int r = ::stat(fname, &st);
      if (r < 0) {
        r = -errno;
	ldout(cct, 0) << __func__ << " could not stat class " << fname
		      << ": " << cpp_strerror(r) << dendl;
      } else {
	ldout(cct, 0) << "_load_class could not open class " << fname
		      << " (dlopen failed): " << dlerror() << dendl;
	r = -EIO;
      }
      cls->status = ClassData::CLASS_MISSING;
      return r;
    }

    cls_deps_t *(*cls_deps)();
    cls_deps = (cls_deps_t *(*)())dlsym(cls->handle, "class_deps");
    if (cls_deps) {
      cls_deps_t *deps = cls_deps();
      while (deps) {
	if (!deps->name)
	  break;
	ClassData *cls_dep = _get_class(deps->name, false);
	cls->dependencies.insert(cls_dep);
	if (cls_dep->status != ClassData::CLASS_OPEN)
	  cls->missing_dependencies.insert(cls_dep);
	deps++;
      }
    }
  }

  // resolve dependencies
  set<ClassData*>::iterator p = cls->missing_dependencies.begin();
  while (p != cls->missing_dependencies.end()) {
    ClassData *dc = *p;
    int r = _load_class(dc);
    if (r < 0) {
      cls->status = ClassData::CLASS_MISSING_DEPS;
      return r;
    }

    ldout(cct, 10) << "_load_class " << cls->name << " satisfied dependency " << dc->name << dendl;
    cls->missing_dependencies.erase(p++);
  }

  // initialize
  void (*cls_init)() = (void (*)())dlsym(cls->handle, "__cls_init");
  if (cls_init) {
    cls->status = ClassData::CLASS_INITIALIZING;
    cls_init();
  }

  ldout(cct, 10) << "_load_class " << cls->name << " success" << dendl;
  cls->status = ClassData::CLASS_OPEN;
  return 0;
}



ClassHandler::ClassData *ClassHandler::register_class(const char *cname)
{
  ceph_assert(ceph_mutex_is_locked(mutex));

  ClassData *cls = _get_class(cname, false);
  ldout(cct, 10) << "register_class " << cname << " status " << cls->status << dendl;

  if (cls->status != ClassData::CLASS_INITIALIZING) {
    ldout(cct, 0) << "class " << cname << " isn't loaded; is the class registering under the wrong name?" << dendl;
    return NULL;
  }
  return cls;
}

void ClassHandler::unregister_class(ClassHandler::ClassData *cls)
{
  /* FIXME: do we really need this one? */
}

ClassHandler::ClassMethod *ClassHandler::ClassData::register_method(const char *mname,
                                                                    int flags,
								    cls_method_call_t func)
{
  /* no need for locking, called under the class_init mutex */
  if (!flags) {
    lderr(handler->cct) << "register_method " << name << "." << mname
			<< " flags " << flags << " " << (void*)func
			<< " FAILED -- flags must be non-zero" << dendl;
    return NULL;
  }
  ldout(handler->cct, 10) << "register_method " << name << "." << mname << " flags " << flags << " " << (void*)func << dendl;
  [[maybe_unused]] auto [method, added] = methods_map.try_emplace(mname, mname, func, flags, this);
  return &method->second;
}

ClassHandler::ClassMethod *ClassHandler::ClassData::register_cxx_method(const char *mname,
                                                                        int flags,
									cls_method_cxx_call_t func)
{
  /* no need for locking, called under the class_init mutex */
  ldout(handler->cct, 10) << "register_cxx_method " << name << "." << mname << " flags " << flags << " " << (void*)func << dendl;
  [[maybe_unused]] auto [method, added] = methods_map.try_emplace(mname, mname, func, flags, this);
  return &method->second;
}

ClassHandler::ClassFilter *ClassHandler::ClassData::register_cxx_filter(
    const std::string &filter_name,
    cls_cxx_filter_factory_t fn)
{
  ClassFilter &filter = filters_map[filter_name];
  filter.fn = fn;
  filter.name = filter_name;
  filter.cls = this;
  return &filter;
}

ClassHandler::ClassMethod *ClassHandler::ClassData::_get_method(
    const std::string& mname)
{
  if (auto iter = methods_map.find(mname); iter != methods_map.end()) {
    return &(iter->second);
  } else {
    return nullptr;
  }
}

int ClassHandler::ClassData::get_method_flags(const std::string& mname)
{
  std::lock_guard l(handler->mutex);
  ClassMethod *method = _get_method(mname);
  if (!method)
    return -ENOENT;
  return method->flags;
}

void ClassHandler::ClassData::unregister_method(ClassHandler::ClassMethod *method)
{
  /* no need for locking, called under the class_init mutex */
   map<string, ClassMethod>::iterator iter = methods_map.find(method->name);
   if (iter == methods_map.end())
     return;
   methods_map.erase(iter);
}

void ClassHandler::ClassMethod::unregister()
{
  cls->unregister_method(this);
}

void ClassHandler::ClassData::unregister_filter(ClassHandler::ClassFilter *filter)
{
  /* no need for locking, called under the class_init mutex */
   map<string, ClassFilter>::iterator iter = filters_map.find(filter->name);
   if (iter == filters_map.end())
     return;
   filters_map.erase(iter);
}

void ClassHandler::ClassFilter::unregister()
{
  cls->unregister_filter(this);
}

int ClassHandler::ClassMethod::exec(cls_method_context_t ctx, bufferlist& indata, bufferlist& outdata)
{
  int ret = 0;
  std::visit([&](auto method) {
    using method_t = decltype(method);
    if constexpr (std::is_same_v<method_t, cls_method_cxx_call_t>) {
      // C++ call version
      ret = method(ctx, &indata, &outdata);
    } else if constexpr (std::is_same_v<method_t, cls_method_call_t>) {
      // C version
      char *out = nullptr;
      int olen = 0;
      ret = method(ctx, indata.c_str(), indata.length(), &out, &olen);
      if (out) {
        // assume *out was allocated via cls_alloc (which calls malloc!)
	ceph::buffer::ptr bp = ceph::buffer::claim_malloc(olen, out);
        outdata.push_back(bp);
      }
    } else {
      static_assert(std::is_same_v<method_t, void>);
    }
  }, func);
  return ret;
}

ClassHandler& ClassHandler::get_instance()
{
#ifdef WITH_SEASTAR
  // the context is being used solely for:
  //   1. random number generation (cls_gen_random_bytes)
  //   2. accessing the configuration
  //   3. logging
  static CephContext cct;
  static ClassHandler single(&cct);
#else
  static ClassHandler single(g_ceph_context);
#endif // WITH_SEASTAR
  return single;
}
