/*
  Copyright (C) 2012-2018  Brazil
  Copyright (C) 2022-2024  Sutou Kouhei <kou@clear-code.com>

  This library is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either
  version 2.1 of the License, or (at your option) any later version.

  This library 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
  Lesser General Public License for more details.

  You should have received a copy of the GNU Lesser General Public
  License along with this library; if not, write to the Free Software
  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
*/

#include "grn.h"
#include "grn_ctx_impl_mrb.h"
#include "grn_encoding.h"
#include "grn_proc.h"
#include <groonga/plugin.h>

#include <stdarg.h>
#include <stdio.h>
#include <string.h>

#include <sys/stat.h>
#ifdef HAVE_DIRENT_H
#  include <dirent.h>
#endif /* HAVE_DIRENT_H */

#ifndef S_ISREG
#  ifdef _S_IFREG
#    define S_ISREG(mode) (mode & _S_IFREG)
#  endif /* _S_IFREG */
#endif   /* !S_ISREG */

#include "grn_db.h"
#include "grn_plugin.h"
#include "grn_ctx_impl.h"
#include "grn_util.h"

#ifdef GRN_WITH_MRUBY
#  include <mruby.h>
#endif /* GRN_WITH_MRUBY */

static grn_hash *grn_plugins = NULL;
static grn_critical_section grn_plugins_lock;
static grn_ctx grn_plugins_ctx;
static grn_obj grn_plugins_path;

#define GRN_PLUGINS_DIR_EACH_BEGIN(dir, dir_length)                            \
  do {                                                                         \
    unsigned int i_;                                                           \
    const unsigned int n_dirs_ =                                               \
      grn_vector_size(&grn_plugins_ctx, &grn_plugins_path);                    \
    for (i_ = 0; i_ < n_dirs_; i_++) {                                         \
      const char *dir = NULL;                                                  \
      unsigned int dir_length;                                                 \
      dir_length = grn_vector_get_element(&grn_plugins_ctx,                    \
                                          &grn_plugins_path,                   \
                                          i_,                                  \
                                          &dir,                                \
                                          NULL,                                \
                                          NULL);
#define GRN_PLUGINS_DIR_EACH_END()                                             \
  }                                                                            \
  }                                                                            \
  while (false)

#ifdef HAVE_DLFCN_H
#  include <dlfcn.h>
#  define grn_dl_open(filename)      dlopen(filename, RTLD_LAZY | RTLD_LOCAL)
#  define grn_dl_open_error_label()  dlerror()
#  define grn_dl_close(dl)           (dlclose(dl) == 0)
#  define grn_dl_close_error_label() dlerror()
#  define grn_dl_sym(dl, symbol)     dlsym(dl, symbol)
#  define grn_dl_sym_error_label()   dlerror()
#  define grn_dl_clear_error()       dlerror()
#else
#  define grn_dl_open(filename)      LoadLibrary(filename)
#  define grn_dl_open_error_label()  "LoadLibrary"
#  define grn_dl_close(dl)           (FreeLibrary(dl) != 0)
#  define grn_dl_close_error_label() "FreeLibrary"
#  define grn_dl_sym(dl, symbol)     ((void *)GetProcAddress(dl, symbol))
#  define grn_dl_sym_error_label()   "GetProcAddress"
#  define grn_dl_clear_error()
#endif

#define GRN_PLUGIN_KEY_SIZE(filename) (strlen((filename)) + 1)

static char grn_plugins_path_env[GRN_ENV_BUFFER_SIZE];
static char grn_plugins_dir_env[GRN_ENV_BUFFER_SIZE];

void
grn_plugin_init_from_env(void)
{
  grn_getenv("GRN_PLUGINS_PATH", grn_plugins_path_env, GRN_ENV_BUFFER_SIZE);
  grn_getenv("GRN_PLUGINS_DIR", grn_plugins_dir_env, GRN_ENV_BUFFER_SIZE);
}

static int
compute_name_size(const char *name, int name_size)
{
  if (name_size < 0) {
    if (name) {
      name_size = (int)strlen(name);
    } else {
      name_size = 0;
    }
  }
  return name_size;
}

grn_id
grn_plugin_reference(grn_ctx *ctx, const char *filename)
{
  grn_id id;
  grn_plugin **plugin = NULL;

  CRITICAL_SECTION_ENTER(grn_plugins_lock);
  id = grn_hash_get(&grn_plugins_ctx,
                    grn_plugins,
                    filename,
                    (unsigned int)GRN_PLUGIN_KEY_SIZE(filename),
                    (void **)&plugin);
  if (plugin) {
    (*plugin)->refcount++;
  }
  CRITICAL_SECTION_LEAVE(grn_plugins_lock);

  return id;
}

const char *
grn_plugin_path(grn_ctx *ctx, grn_id id)
{
  const char *path;
  grn_plugin *plugin;
  int value_size;

  if (id == GRN_ID_NIL) {
    return NULL;
  }

  CRITICAL_SECTION_ENTER(grn_plugins_lock);
  value_size = grn_hash_get_value(&grn_plugins_ctx, grn_plugins, id, &plugin);
  CRITICAL_SECTION_LEAVE(grn_plugins_lock);

  if (!plugin) {
    return NULL;
  }

  path = plugin->path;
  GRN_PLUGINS_DIR_EACH_BEGIN(dir, dir_length)
  {
    if (strncmp(dir, path, dir_length) == 0) {
      const char *plugin_name = path + dir_length;
      while (plugin_name[0] == '/') {
        plugin_name++;
      }
      /* TODO: remove suffix too? */
      return plugin_name;
    }
  }
  GRN_PLUGINS_DIR_EACH_END();

  return path;
}

#define GRN_PLUGIN_FUNC_PREFIX "grn_plugin_impl_"

static grn_rc
grn_plugin_call_init(grn_ctx *ctx, grn_id id)
{
  grn_plugin *plugin;
  int size;

  size = grn_hash_get_value(&grn_plugins_ctx, grn_plugins, id, &plugin);
  if (size == 0) {
    return GRN_INVALID_ARGUMENT;
  }

  if (plugin->init_func) {
    return plugin->init_func(ctx);
  }

  return GRN_SUCCESS;
}

#ifdef GRN_WITH_MRUBY
static grn_rc
grn_plugin_call_register_mrb(grn_ctx *ctx, grn_id id, grn_plugin *plugin)
{
  grn_mrb_data *data;
  mrb_state *mrb;
  struct RClass *module;
  struct RClass *plugin_loader_class;
  int arena_index;

  grn_ctx_impl_mrb_ensure_init(ctx);
  if (ctx->rc != GRN_SUCCESS) {
    return ctx->rc;
  }

  data = &(ctx->impl->mrb);
  mrb = data->state;
  module = data->module;

  {
    int added;
    grn_hash_add(ctx,
                 ctx->impl->mrb.registered_plugins,
                 &id,
                 sizeof(grn_id),
                 NULL,
                 &added);
    if (!added) {
      return ctx->rc;
    }
  }

  arena_index = mrb_gc_arena_save(mrb);
  plugin_loader_class = mrb_class_get_under(mrb, module, "PluginLoader");
  {
    const char *utf8_path;
    mrb_value mrb_path;
    utf8_path = grn_encoding_convert_to_utf8_from_locale(ctx,
                                                         ctx->impl->plugin_path,
                                                         -1,
                                                         NULL);
    mrb_path = mrb_str_new_cstr(mrb, utf8_path);
    grn_encoding_converted_free(ctx, utf8_path);
    mrb_funcall(mrb,
                mrb_obj_value(plugin_loader_class),
                "load_file",
                1,
                mrb_path);
  }
  mrb_gc_arena_restore(mrb, arena_index);
  return ctx->rc;
}
#endif /*GRN_WITH_MRUBY */

static grn_rc
grn_plugin_call_register(grn_ctx *ctx, grn_id id)
{
  grn_plugin *plugin;
  int size;

  CRITICAL_SECTION_ENTER(grn_plugins_lock);
  size = grn_hash_get_value(&grn_plugins_ctx, grn_plugins, id, &plugin);
  CRITICAL_SECTION_LEAVE(grn_plugins_lock);

  if (size == 0) {
    return GRN_INVALID_ARGUMENT;
  }

#ifdef GRN_WITH_MRUBY
  if (!plugin->dl) {
    return grn_plugin_call_register_mrb(ctx, id, plugin);
  }
#endif /* GRN_WITH_MRUBY */

  if (plugin->register_func) {
    return plugin->register_func(ctx);
  }

  return GRN_SUCCESS;
}

static grn_rc
grn_plugin_call_fin(grn_ctx *ctx, grn_id id)
{
  grn_plugin *plugin;
  int size;

  size = grn_hash_get_value(&grn_plugins_ctx, grn_plugins, id, &plugin);
  if (size == 0) {
    return GRN_INVALID_ARGUMENT;
  }

  if (plugin->fin_func) {
    return plugin->fin_func(ctx);
  }

  return GRN_SUCCESS;
}

static grn_rc
grn_plugin_initialize(
  grn_ctx *ctx, grn_plugin *plugin, grn_dl dl, grn_id id, const char *path)
{
  plugin->dl = dl;

#define GET_SYMBOL(type)                                                       \
  do {                                                                         \
    grn_dl_clear_error();                                                      \
    plugin->type##_func = grn_dl_sym(dl, GRN_PLUGIN_FUNC_PREFIX #type);        \
    if (!plugin->type##_func) {                                                \
      const char *label;                                                       \
      label = grn_dl_sym_error_label();                                        \
      SERR("%s", label);                                                       \
    }                                                                          \
  } while (0)

  GET_SYMBOL(init);
  GET_SYMBOL(register);
  GET_SYMBOL(fin);

#undef GET_SYMBOL

  if (!plugin->init_func || !plugin->register_func || !plugin->fin_func) {
    ERR(GRN_INVALID_FORMAT,
        "init func (%s) %sfound, "
        "register func (%s) %sfound and "
        "fin func (%s) %sfound",
        GRN_PLUGIN_FUNC_PREFIX "init",
        plugin->init_func ? "" : "not ",
        GRN_PLUGIN_FUNC_PREFIX "register",
        plugin->register_func ? "" : "not ",
        GRN_PLUGIN_FUNC_PREFIX "fin",
        plugin->fin_func ? "" : "not ");
  }

  if (!ctx->rc) {
    ctx->impl->plugin_path = path;
    grn_plugin_call_init(ctx, id);
    ctx->impl->plugin_path = NULL;
  }

  return ctx->rc;
}

#ifdef GRN_WITH_MRUBY
static grn_id
grn_plugin_open_mrb(grn_ctx *ctx, const char *filename, size_t filename_size)
{
  grn_ctx *plugins_ctx = &grn_plugins_ctx;
  grn_id id = GRN_ID_NIL;
  grn_plugin **plugin = NULL;

  grn_ctx_impl_mrb_ensure_init(ctx);
  if (ctx->rc != GRN_SUCCESS) {
    return GRN_ID_NIL;
  }

  if (!ctx->impl->mrb.state) {
    ERR(GRN_FUNCTION_NOT_IMPLEMENTED, "mruby support isn't enabled");
    return GRN_ID_NIL;
  }

  id = grn_hash_add(plugins_ctx,
                    grn_plugins,
                    filename,
                    filename_size,
                    (void **)&plugin,
                    NULL);
  if (!id) {
    return id;
  }

  {
    grn_ctx *ctx = plugins_ctx;
    *plugin = GRN_CALLOC(sizeof(grn_plugin));
  }
  if (!*plugin) {
    grn_hash_delete_by_id(plugins_ctx, grn_plugins, id, NULL);
    return GRN_ID_NIL;
  }

  grn_memcpy((*plugin)->path, filename, filename_size);
  (*plugin)->dl = NULL;
  (*plugin)->init_func = NULL;
  (*plugin)->register_func = NULL;
  (*plugin)->fin_func = NULL;
  (*plugin)->refcount = 1;

  return id;
}
#endif /* GRN_WITH_MRUBY */

grn_id
grn_plugin_open(grn_ctx *ctx, const char *filename)
{
  grn_ctx *plugins_ctx = &grn_plugins_ctx;
  grn_id id = GRN_ID_NIL;
  grn_dl dl;
  grn_plugin **plugin = NULL;
  unsigned int filename_size;

  filename_size = (unsigned int)GRN_PLUGIN_KEY_SIZE(filename);

  CRITICAL_SECTION_ENTER(grn_plugins_lock);
  if ((id = grn_hash_get(plugins_ctx,
                         grn_plugins,
                         filename,
                         filename_size,
                         (void **)&plugin))) {
    (*plugin)->refcount++;
    goto exit;
  }

#ifdef GRN_WITH_MRUBY
  {
    const char *mrb_suffix;
    mrb_suffix = grn_plugin_get_ruby_suffix();
    if (filename_size > strlen(mrb_suffix) &&
        strcmp(filename + (strlen(filename) - strlen(mrb_suffix)),
               mrb_suffix) == 0) {
      id = grn_plugin_open_mrb(ctx, filename, filename_size);
      goto exit;
    }
  }
#endif /* GRN_WITH_MRUBY */

  if ((dl = grn_dl_open(filename))) {
    if ((id = grn_hash_add(plugins_ctx,
                           grn_plugins,
                           filename,
                           filename_size,
                           (void **)&plugin,
                           NULL))) {
      {
        grn_ctx *ctx = plugins_ctx;
        *plugin = GRN_CALLOC(sizeof(grn_plugin));
      }
      if (*plugin) {
        grn_memcpy((*plugin)->path, filename, filename_size);
        if (grn_plugin_initialize(ctx, *plugin, dl, id, filename)) {
          {
            grn_ctx *ctx = plugins_ctx;
            GRN_FREE(*plugin);
          }
          *plugin = NULL;
        }
      }
      if (!*plugin) {
        grn_hash_delete_by_id(plugins_ctx, grn_plugins, id, NULL);
        if (grn_dl_close(dl)) {
          /* Now, __FILE__ set in plugin is invalid. */
          ctx->errline = 0;
          ctx->errfile = NULL;
        } else {
          const char *label;
          label = grn_dl_close_error_label();
          SERR("%s", label);
        }
        id = GRN_ID_NIL;
      } else {
        (*plugin)->refcount = 1;
      }
    } else {
      if (!grn_dl_close(dl)) {
        const char *label;
        label = grn_dl_close_error_label();
        SERR("%s", label);
      }
    }
  } else {
    const char *label;
    label = grn_dl_open_error_label();
    SERR("%s: <%.*s>", label, (int)filename_size, filename);
  }

exit:
  CRITICAL_SECTION_LEAVE(grn_plugins_lock);

  return id;
}

grn_rc
grn_plugin_close(grn_ctx *ctx, grn_id id)
{
  grn_ctx *plugins_ctx = &grn_plugins_ctx;
  grn_rc rc;
  grn_plugin *plugin;

  if (id == GRN_ID_NIL) {
    return GRN_INVALID_ARGUMENT;
  }

  CRITICAL_SECTION_ENTER(grn_plugins_lock);
  if (!grn_hash_get_value(plugins_ctx, grn_plugins, id, &plugin)) {
    rc = GRN_INVALID_ARGUMENT;
    goto exit;
  }
  if (--plugin->refcount) {
    rc = GRN_SUCCESS;
    goto exit;
  }
  if (plugin->dl) {
    grn_plugin_call_fin(ctx, id);
    if (!grn_dl_close(plugin->dl)) {
      const char *label;
      label = grn_dl_close_error_label();
      SERR("%s", label);
    }
  }
  {
    grn_ctx *ctx = plugins_ctx;
    GRN_FREE(plugin);
  }
  rc = grn_hash_delete_by_id(plugins_ctx, grn_plugins, id, NULL);

exit:
  CRITICAL_SECTION_LEAVE(grn_plugins_lock);

  return rc;
}

void *
grn_plugin_sym(grn_ctx *ctx, grn_id id, const char *symbol)
{
  grn_plugin *plugin;
  grn_dl_symbol func;

  if (id == GRN_ID_NIL) {
    return NULL;
  }

  CRITICAL_SECTION_ENTER(grn_plugins_lock);
  if (!grn_hash_get_value(&grn_plugins_ctx, grn_plugins, id, &plugin)) {
    func = NULL;
    goto exit;
  }
  grn_dl_clear_error();
  if (!(func = grn_dl_sym(plugin->dl, symbol))) {
    const char *label;
    label = grn_dl_sym_error_label();
    SERR("%s", label);
  }

exit:
  CRITICAL_SECTION_LEAVE(grn_plugins_lock);

  return func;
}

#ifdef WIN32
static void
grn_plugins_init_path(grn_ctx *ctx, grn_obj *path, const char *path_env)
{
  /* TODO */
}
#else  /* WIN32 */
static void
grn_plugins_init_path(grn_ctx *ctx, grn_obj *path, const char *path_env)
{
  const char separator = ':';
  const char *start = path_env;
  const char *current;

  for (current = path_env; current[0]; current++) {
    if (current[0] == separator) {
      if (current - start > 0) {
        grn_vector_add_element(&grn_plugins_ctx,
                               &grn_plugins_path,
                               start,
                               (uint32_t)(current - start),
                               0,
                               GRN_DB_TEXT);
      }
      start = current + 1;
    }
  }

  if (start[0] && current - start > 0) {
    grn_vector_add_element(&grn_plugins_ctx,
                           &grn_plugins_path,
                           start,
                           (uint32_t)(current - start),
                           0,
                           GRN_DB_TEXT);
  }
}
#endif /* WIN32 */

#ifdef WIN32
static char *windows_plugins_dir = NULL;
static char windows_plugins_dir_buffer[PATH_MAX];
static const char *
grn_plugin_get_default_system_plugins_dir(void)
{
  if (!windows_plugins_dir) {
    const char *base_dir;
    const char *relative_path = GRN_RELATIVE_PLUGINS_DIR;
    size_t base_dir_length;

    base_dir = grn_windows_base_dir();
    base_dir_length = strlen(base_dir);
    grn_strcpy(windows_plugins_dir_buffer, PATH_MAX, base_dir);
    grn_strcat(windows_plugins_dir_buffer, PATH_MAX, "/");
    grn_strcat(windows_plugins_dir_buffer, PATH_MAX, relative_path);
    windows_plugins_dir = windows_plugins_dir_buffer;
  }
  return windows_plugins_dir;
}

#else  /* WIN32 */
static const char *
grn_plugin_get_default_system_plugins_dir(void)
{
  return GRN_PLUGINS_DIR;
}
#endif /* WIN32 */

const char *
grn_plugin_get_system_plugins_dir(void)
{
  if (grn_plugins_dir_env[0]) {
    return grn_plugins_dir_env;
  } else {
    return grn_plugin_get_default_system_plugins_dir();
  }
}

grn_rc
grn_plugins_init(void)
{
  CRITICAL_SECTION_INIT(grn_plugins_lock);
  grn_ctx_init(&grn_plugins_ctx, 0);
  GRN_TEXT_INIT(&grn_plugins_path, GRN_OBJ_VECTOR);
  grn_plugins = grn_hash_create(&grn_plugins_ctx,
                                NULL,
                                PATH_MAX,
                                sizeof(grn_plugin *),
                                GRN_OBJ_KEY_VAR_SIZE);
  if (!grn_plugins) {
    GRN_OBJ_FIN(&grn_plugins_ctx, &grn_plugins_path);
    grn_ctx_fin(&grn_plugins_ctx);
    return GRN_NO_MEMORY_AVAILABLE;
  }
  if (grn_plugins_path_env[0]) {
    grn_plugins_init_path(&grn_plugins_ctx,
                          &grn_plugins_path,
                          grn_plugins_path_env);
  }
  {
    const char *system_plugins_dir = grn_plugin_get_system_plugins_dir();
    grn_vector_add_element(&grn_plugins_ctx,
                           &grn_plugins_path,
                           system_plugins_dir,
                           (uint32_t)strlen(system_plugins_dir),
                           0,
                           GRN_DB_TEXT);
  }
  return GRN_SUCCESS;
}

grn_rc
grn_plugins_fin(void)
{
  grn_rc rc;
  if (!grn_plugins) {
    return GRN_INVALID_ARGUMENT;
  }
  GRN_HASH_EACH(&grn_plugins_ctx, grn_plugins, id, NULL, NULL, NULL, {
    grn_plugin_close(&grn_plugins_ctx, id);
  });
  rc = grn_hash_close(&grn_plugins_ctx, grn_plugins);
  GRN_OBJ_FIN(&grn_plugins_ctx, &grn_plugins_path);
  grn_ctx_fin(&grn_plugins_ctx);
  CRITICAL_SECTION_FIN(grn_plugins_lock);
  return rc;
}

const char *
grn_plugin_get_suffix(void)
{
  return GRN_PLUGIN_SUFFIX;
}

const char *
grn_plugin_get_ruby_suffix(void)
{
  return ".rb";
}

grn_rc
grn_plugin_register_by_path(grn_ctx *ctx, const char *path)
{
  grn_obj *db;
  if (!ctx || !ctx->impl || !(db = ctx->impl->db)) {
    ERR(GRN_INVALID_ARGUMENT, "db not initialized");
    return ctx->rc;
  }
  GRN_API_ENTER;
  if (GRN_DB_P(db)) {
    grn_id id;
    id = grn_plugin_open(ctx, path);
    if (id) {
      ctx->impl->plugin_path = path;
      ctx->rc = grn_plugin_call_register(ctx, id);
      ctx->impl->plugin_path = NULL;
      grn_plugin_close(ctx, id);
    }
  } else {
    ERR(GRN_INVALID_ARGUMENT, "invalid db assigned");
  }
  GRN_API_RETURN(ctx->rc);
}

static char *
grn_plugin_find_path_raw(grn_ctx *ctx, const char *path)
{
  struct stat path_stat;

  if (stat(path, &path_stat) != 0) {
    return NULL;
  }

  if (!S_ISREG(path_stat.st_mode)) {
    return NULL;
  }

  return GRN_STRDUP(path);
}

#ifdef GRN_WITH_MRUBY
static char *
grn_plugin_find_path_mrb(grn_ctx *ctx, const char *path, size_t path_len)
{
  char mrb_path[PATH_MAX];
  const char *mrb_suffix;
  size_t mrb_path_len;

  grn_ctx_impl_mrb_ensure_init(ctx);
  if (ctx->rc != GRN_SUCCESS) {
    return NULL;
  }

  if (!ctx->impl->mrb.state) {
    return NULL;
  }

  mrb_suffix = grn_plugin_get_ruby_suffix();
  mrb_path_len = path_len + strlen(mrb_suffix);
  if (mrb_path_len >= PATH_MAX) {
    ERR(GRN_FILENAME_TOO_LONG,
        "too long plugin path: <%s%s>",
        path,
        mrb_suffix);
    return NULL;
  }

  grn_strcpy(mrb_path, PATH_MAX, path);
  grn_strcat(mrb_path, PATH_MAX, mrb_suffix);
  return grn_plugin_find_path_raw(ctx, mrb_path);
}
#else  /* GRN_WITH_MRUBY */
static char *
grn_plugin_find_path_mrb(grn_ctx *ctx, const char *path, size_t path_len)
{
  return NULL;
}
#endif /* GRN_WITH_MRUBY */

static char *
grn_plugin_find_path_so(grn_ctx *ctx, const char *path, size_t path_len)
{
  char so_path[PATH_MAX];
  const char *so_suffix;
  size_t so_path_len;

  so_suffix = grn_plugin_get_suffix();
  so_path_len = path_len + strlen(so_suffix);
  if (so_path_len >= PATH_MAX) {
    ERR(GRN_FILENAME_TOO_LONG, "too long plugin path: <%s%s>", path, so_suffix);
    return NULL;
  }

  grn_strcpy(so_path, PATH_MAX, path);
  grn_strcat(so_path, PATH_MAX, so_suffix);
  return grn_plugin_find_path_raw(ctx, so_path);
}

static char *
grn_plugin_find_path_libs_so(grn_ctx *ctx, const char *path, size_t path_len)
{
  char libs_so_path[PATH_MAX];
  const char *base_name;
  const char *so_suffix;
  const char *libs_path = "/.libs";
  size_t libs_so_path_len;

  base_name = strrchr(path, '/');
  if (!base_name) {
    return NULL;
  }

  so_suffix = grn_plugin_get_suffix();
  libs_so_path_len = (size_t)(base_name - path) + strlen(libs_path) +
                     strlen(base_name) + strlen(so_suffix);
  if (libs_so_path_len >= PATH_MAX) {
    ERR(GRN_FILENAME_TOO_LONG,
        "too long plugin path: <%.*s/.libs%s%s>",
        (int)(base_name - path),
        path,
        base_name,
        so_suffix);
    return NULL;
  }

  libs_so_path[0] = '\0';
  grn_strncat(libs_so_path, PATH_MAX, path, (size_t)(base_name - path));
  grn_strcat(libs_so_path, PATH_MAX, libs_path);
  grn_strcat(libs_so_path, PATH_MAX, base_name);
  grn_strcat(libs_so_path, PATH_MAX, so_suffix);
  return grn_plugin_find_path_raw(ctx, libs_so_path);
}

static char *
grn_plugin_find_path_one(grn_ctx *ctx, const char *path)
{
  char *found_path = NULL;
  size_t path_len;

  found_path = grn_plugin_find_path_raw(ctx, path);
  if (found_path) {
    goto exit;
  }

  path_len = strlen(path);

  found_path = grn_plugin_find_path_so(ctx, path, path_len);
  if (found_path) {
    goto exit;
  }
  if (ctx->rc) {
    goto exit;
  }

  found_path = grn_plugin_find_path_libs_so(ctx, path, path_len);
  if (found_path) {
    goto exit;
  }
  if (ctx->rc) {
    goto exit;
  }

  found_path = grn_plugin_find_path_mrb(ctx, path, path_len);
  if (found_path) {
    goto exit;
  }
  if (ctx->rc) {
    goto exit;
  }

exit:
  return found_path;
}

static bool
grn_plugin_find_path_is_absolute_path(grn_ctx *ctx, const char *name)
{
  /* UNIX */
  if (name[0] == '/') {
    return true;
  }

  /* Windows */
  if ((('a' <= name[0] && name[0] <= 'z') ||
       ('A' <= name[0] && name[0] <= 'Z')) &&
      name[1] == ':' && name[2] == '/') {
    return true;
  }

  return false;
}

char *
grn_plugin_find_path(grn_ctx *ctx, const char *name)
{
  char *found_path = NULL;

  GRN_API_ENTER;

  if (grn_plugin_find_path_is_absolute_path(ctx, name)) {
    found_path = grn_plugin_find_path_one(ctx, name);
  } else {
    GRN_PLUGINS_DIR_EACH_BEGIN(dir, dir_length)
    {
      char dir_last_char;
      char path[PATH_MAX];
      size_t name_length, max_name_length;

      grn_strncpy(path, PATH_MAX, dir, dir_length);
      path[dir_length] = '\0';

      dir_last_char = dir[dir_length - 1];
      if (dir_last_char != '/') {
        grn_strcat(path, PATH_MAX, "/");
      }

      name_length = strlen(name);
      max_name_length = PATH_MAX - strlen(path) - 1;
      if (name_length > max_name_length) {
        ERR(GRN_INVALID_ARGUMENT,
            "plugin name is too long: %" GRN_FMT_SIZE " (max: %" GRN_FMT_SIZE
            ") <%s%s>",
            name_length,
            max_name_length,
            path,
            name);
        break;
      }
      grn_strcat(path, PATH_MAX, name);

      found_path = grn_plugin_find_path_one(ctx, path);
      if (found_path) {
        break;
      }
      if (ctx->rc != GRN_SUCCESS) {
        break;
      }
    }
    GRN_PLUGINS_DIR_EACH_END();
  }

  GRN_API_RETURN(found_path);
}

static void
grn_plugin_set_name_resolve_error(grn_ctx *ctx,
                                  const char *name,
                                  const char *tag)
{
  if (name[0] == '/') {
    ERR(GRN_NO_SUCH_FILE_OR_DIRECTORY,
        "%s cannot find plugin file: <%s>",
        tag,
        name);
  } else {
    const char *grn_encoding_plugins_dir;
    size_t grn_encoding_plugins_dir_length;
    const char *plugins_dir_path_separator;

    grn_encoding_plugins_dir =
      grn_encoding_convert_from_locale(ctx,
                                       grn_plugin_get_system_plugins_dir(),
                                       -1,
                                       &grn_encoding_plugins_dir_length);
    if (grn_encoding_plugins_dir[grn_encoding_plugins_dir_length - 1] != '/') {
      plugins_dir_path_separator = "/";
    } else {
      plugins_dir_path_separator = "";
    }
    ERR(GRN_NO_SUCH_FILE_OR_DIRECTORY,
        "%s cannot find plugin file: <%s%s%s%s>",
        tag,
        grn_encoding_plugins_dir,
        plugins_dir_path_separator,
        name,
        grn_plugin_get_suffix());
    grn_encoding_converted_free(ctx, grn_encoding_plugins_dir);
  }
}

grn_rc
grn_plugin_register(grn_ctx *ctx, const char *name)
{
  grn_rc rc;
  char *path;

  GRN_API_ENTER;
  path = grn_plugin_find_path(ctx, name);
  if (path) {
    rc = grn_plugin_register_by_path(ctx, path);
    GRN_FREE(path);
  } else {
    if (ctx->rc == GRN_SUCCESS) {
      grn_plugin_set_name_resolve_error(ctx, name, "[plugin][register]");
    }
    rc = ctx->rc;
  }
  GRN_API_RETURN(rc);
}

grn_rc
grn_plugin_unregister_by_path(grn_ctx *ctx, const char *path)
{
  grn_obj *db;
  grn_id plugin_id;

  if (!ctx || !ctx->impl) {
    ERR(GRN_INVALID_ARGUMENT, "[plugin][unregister] ctx isn't initialized");
    return ctx->rc;
  }

  db = ctx->impl->db;
  if (!db) {
    ERR(GRN_INVALID_ARGUMENT, "[plugin][unregister] DB isn't initialized");
    return ctx->rc;
  }

  GRN_API_ENTER;

  CRITICAL_SECTION_ENTER(grn_plugins_lock);
  plugin_id = grn_hash_get(&grn_plugins_ctx,
                           grn_plugins,
                           path,
                           (uint32_t)GRN_PLUGIN_KEY_SIZE(path),
                           NULL);
  CRITICAL_SECTION_LEAVE(grn_plugins_lock);

  if (plugin_id == GRN_ID_NIL) {
    GRN_API_RETURN(ctx->rc);
  }

  {
    grn_table_cursor *cursor;
    grn_id id;

    cursor =
      grn_table_cursor_open(ctx, db, NULL, 0, NULL, 0, 0, -1, GRN_CURSOR_BY_ID);
    if (!cursor) {
      GRN_API_RETURN(ctx->rc);
    }

    while ((id = grn_table_cursor_next(ctx, cursor))) {
      grn_obj *obj;
      obj = grn_ctx_at(ctx, id);
      if (!obj) {
        continue;
      }
      if (obj->header.type == GRN_PROC && DB_OBJ(obj)->range == plugin_id) {
        grn_obj_remove(ctx, obj);
      } else {
        grn_obj_unlink(ctx, obj);
      }
    }
    grn_table_cursor_close(ctx, cursor);
  }

  GRN_API_RETURN(ctx->rc);
}

grn_rc
grn_plugin_unregister(grn_ctx *ctx, const char *name)
{
  grn_rc rc;
  char *path;

  GRN_API_ENTER;
  path = grn_plugin_find_path(ctx, name);
  if (path) {
    rc = grn_plugin_unregister_by_path(ctx, path);
    GRN_FREE(path);
  } else {
    if (ctx->rc == GRN_SUCCESS) {
      grn_plugin_set_name_resolve_error(ctx, name, "[plugin][unregister]");
    }
    rc = ctx->rc;
  }
  GRN_API_RETURN(rc);
}

void
grn_plugin_ensure_registered(grn_ctx *ctx, grn_obj *proc)
{
#ifdef GRN_WITH_MRUBY
  grn_id plugin_id;
  grn_plugin *plugin = NULL;

  if (!(proc->header.flags & GRN_OBJ_CUSTOM_NAME)) {
    return;
  }

  plugin_id = DB_OBJ(proc)->range;
  CRITICAL_SECTION_ENTER(grn_plugins_lock);
  {
    const char *value;
    value = grn_hash_get_value_(&grn_plugins_ctx, grn_plugins, plugin_id, NULL);
    if (value) {
      plugin = *((grn_plugin **)value);
    }
  }
  CRITICAL_SECTION_LEAVE(grn_plugins_lock);

  if (!plugin) {
    return;
  }

  if (plugin->dl) {
    return;
  }

  grn_ctx_impl_mrb_ensure_init(ctx);
  if (ctx->rc != GRN_SUCCESS) {
    return;
  }

  if (!ctx->impl->mrb.state) {
    return;
  }

  {
    grn_id id;
    int added;
    id = DB_OBJ(proc)->id;
    grn_hash_add(ctx,
                 ctx->impl->mrb.checked_procs,
                 &id,
                 sizeof(grn_id),
                 NULL,
                 &added);
    if (!added) {
      return;
    }
  }

  ctx->impl->plugin_path = plugin->path;
  grn_plugin_call_register_mrb(ctx, plugin_id, plugin);
  ctx->impl->plugin_path = NULL;
#endif /* GRN_WITH_MRUBY */
}

grn_rc
grn_plugin_get_names(grn_ctx *ctx, grn_obj *names)
{
  grn_hash *processed_paths;
  const char *system_plugins_dir;
  const char *native_plugin_suffix;
  const char *ruby_plugin_suffix;
  bool is_close_opened_object_mode = false;

  GRN_API_ENTER;

  if (ctx->rc) {
    GRN_API_RETURN(ctx->rc);
  }

  if (grn_thread_get_limit() == 1) {
    is_close_opened_object_mode = true;
  }

  processed_paths =
    grn_hash_create(ctx,
                    NULL,
                    GRN_TABLE_MAX_KEY_SIZE,
                    0,
                    GRN_OBJ_TABLE_HASH_KEY | GRN_OBJ_KEY_VAR_SIZE);
  if (!processed_paths) {
    GRN_API_RETURN(ctx->rc);
  }

  system_plugins_dir = grn_plugin_get_system_plugins_dir();
  native_plugin_suffix = grn_plugin_get_suffix();
  ruby_plugin_suffix = grn_plugin_get_ruby_suffix();

  GRN_TABLE_EACH_BEGIN_FLAGS(ctx,
                             grn_ctx_db(ctx),
                             cursor,
                             id,
                             GRN_CURSOR_BY_ID | GRN_CURSOR_ASCENDING)
  {
    void *name;
    int name_size;
    grn_obj *object;
    const char *path;
    grn_id processed_path_id;

    if (grn_id_is_builtin(ctx, id)) {
      continue;
    }

    name_size = grn_table_cursor_get_key(ctx, cursor, &name);
    if (grn_obj_name_is_column(ctx, name, name_size)) {
      continue;
    }

    if (is_close_opened_object_mode) {
      grn_ctx_push_temporary_open_space(ctx);
    }

    object = grn_ctx_at(ctx, id);
    if (!object) {
      ERRCLR(ctx);
      goto next_loop;
    }

    if (!grn_obj_is_proc(ctx, object)) {
      goto next_loop;
    }

    path = grn_obj_path(ctx, object);
    if (!path) {
      goto next_loop;
    }

    processed_path_id =
      grn_hash_get(ctx, processed_paths, path, (uint32_t)strlen(path), NULL);
    if (processed_path_id != GRN_ID_NIL) {
      goto next_loop;
    }

    grn_hash_add(ctx,
                 processed_paths,
                 path,
                 (uint32_t)strlen(path),
                 NULL,
                 NULL);

    {
      const char *relative_path;
      const char *libs_path = "/.libs/";
      const char *start_libs;
      char name[PATH_MAX];

      name[0] = '\0';
      if (strncmp(path, system_plugins_dir, strlen(system_plugins_dir)) == 0) {
        relative_path = path + strlen(system_plugins_dir);
      } else {
        relative_path = path;
      }
      start_libs = strstr(relative_path, libs_path);
      if (start_libs) {
        grn_strncat(name,
                    PATH_MAX,
                    relative_path,
                    (size_t)(start_libs - relative_path));
        grn_strcat(name, PATH_MAX, "/");
        grn_strcat(name, PATH_MAX, start_libs + strlen(libs_path));
      } else {
        grn_strcat(name, PATH_MAX, relative_path);
      }
      if (strlen(name) > strlen(native_plugin_suffix) &&
          strcmp(name + strlen(name) - strlen(native_plugin_suffix),
                 native_plugin_suffix) == 0) {
        name[strlen(name) - strlen(native_plugin_suffix)] = '\0';
      } else if (strlen(name) > strlen(ruby_plugin_suffix) &&
                 strcmp(name + strlen(name) - strlen(ruby_plugin_suffix),
                        ruby_plugin_suffix) == 0) {
        name[strlen(name) - strlen(ruby_plugin_suffix)] = '\0';
      }
      grn_vector_add_element(ctx,
                             names,
                             name,
                             (uint32_t)strlen(name),
                             0,
                             GRN_DB_TEXT);
    }

  next_loop:
    if (is_close_opened_object_mode) {
      grn_ctx_pop_temporary_open_space(ctx);
    }
  }
  GRN_TABLE_EACH_END(ctx, cursor);

  grn_hash_close(ctx, processed_paths);

  GRN_API_RETURN(ctx->rc);
}

void *
grn_plugin_malloc(
  grn_ctx *ctx, size_t size, const char *file, int line, const char *func)
{
  return grn_malloc(ctx, size, file, line, func);
}

void *
grn_plugin_calloc(
  grn_ctx *ctx, size_t size, const char *file, int line, const char *func)
{
  return grn_calloc(ctx, size, file, line, func);
}

void *
grn_plugin_realloc(grn_ctx *ctx,
                   void *ptr,
                   size_t size,
                   const char *file,
                   int line,
                   const char *func)
{
  return grn_realloc(ctx, ptr, size, file, line, func);
}

void
grn_plugin_free(
  grn_ctx *ctx, void *ptr, const char *file, int line, const char *func)
{
  grn_free(ctx, ptr, file, line, func);
}

void
grn_plugin_set_error(grn_ctx *ctx,
                     grn_log_level level,
                     grn_rc error_code,
                     const char *file,
                     int line,
                     const char *func,
                     const char *format,
                     ...)
{
  char old_error_message[GRN_CTX_MSGSIZE];

  ctx->errlvl = (unsigned char)level;
  ctx->rc = error_code;
  ctx->errfile = file;
  ctx->errline = (unsigned int)line;
  ctx->errfunc = func;

  grn_strcpy(old_error_message, GRN_CTX_MSGSIZE, ctx->errbuf);

  {
    va_list ap;
    va_start(ap, format);
    grn_ctx_logv(ctx, format, ap);
    va_end(ap);
  }

  if (grn_ctx_impl_should_log(ctx)) {
    grn_ctx_impl_set_current_error_message(ctx);
    if (grn_logger_pass(ctx, level)) {
      char new_error_message[GRN_CTX_MSGSIZE];
      grn_strcpy(new_error_message, GRN_CTX_MSGSIZE, ctx->errbuf);
      grn_strcpy(ctx->errbuf, GRN_CTX_MSGSIZE, old_error_message);
      {
        va_list ap;
        va_start(ap, format);
        grn_logger_putv(ctx, level, file, line, func, format, ap);
        va_end(ap);
      }
      grn_strcpy(ctx->errbuf, GRN_CTX_MSGSIZE, new_error_message);
    }
    if (level <= GRN_LOG_ERROR) {
      grn_plugin_logtrace(ctx, level);
    }
  }
}

void
grn_plugin_clear_error(grn_ctx *ctx)
{
  ERRCLR(ctx);
}

void
grn_plugin_backtrace(grn_ctx *ctx)
{
}

void
grn_plugin_logtrace(grn_ctx *ctx, grn_log_level level)
{
  grn_ctx_log_back_trace(ctx, level);
}

struct _grn_plugin_mutex {
  grn_critical_section critical_section;
};

grn_plugin_mutex *
grn_plugin_mutex_open(grn_ctx *ctx)
{
  grn_plugin_mutex *const mutex =
    GRN_PLUGIN_MALLOC(ctx, sizeof(grn_plugin_mutex));
  if (mutex != NULL) {
    CRITICAL_SECTION_INIT(mutex->critical_section);
  }
  return mutex;
}

grn_plugin_mutex *
grn_plugin_mutex_create(grn_ctx *ctx)
{
  return grn_plugin_mutex_open(ctx);
}

void
grn_plugin_mutex_close(grn_ctx *ctx, grn_plugin_mutex *mutex)
{
  if (mutex != NULL) {
    CRITICAL_SECTION_FIN(mutex->critical_section);
    GRN_PLUGIN_FREE(ctx, mutex);
  }
}

void
grn_plugin_mutex_destroy(grn_ctx *ctx, grn_plugin_mutex *mutex)
{
  grn_plugin_mutex_close(ctx, mutex);
}

void
grn_plugin_mutex_lock(grn_ctx *ctx, grn_plugin_mutex *mutex)
{
  if (mutex != NULL) {
    CRITICAL_SECTION_ENTER(mutex->critical_section);
  }
}

void
grn_plugin_mutex_unlock(grn_ctx *ctx, grn_plugin_mutex *mutex)
{
  if (mutex != NULL) {
    CRITICAL_SECTION_LEAVE(mutex->critical_section);
  }
}

grn_obj *
grn_plugin_proc_alloc(grn_ctx *ctx,
                      grn_user_data *user_data,
                      grn_id domain,
                      unsigned char flags)
{
  return grn_proc_alloc(ctx, user_data, domain, flags);
}

grn_obj *
grn_plugin_proc_get_vars(grn_ctx *ctx, grn_user_data *user_data)
{
  return grn_proc_get_vars(ctx, user_data);
}

grn_obj *
grn_plugin_proc_get_var(grn_ctx *ctx,
                        grn_user_data *user_data,
                        const char *name,
                        int name_size)
{
  name_size = compute_name_size(name, name_size);
  return grn_proc_get_var(ctx, user_data, name, (unsigned int)name_size);
}

bool
grn_plugin_proc_get_var_bool(grn_ctx *ctx,
                             grn_user_data *user_data,
                             const char *name,
                             int name_size,
                             bool default_value)
{
  grn_obj *var;

  var = grn_plugin_proc_get_var(ctx, user_data, name, name_size);
  return grn_proc_option_value_bool(ctx, var, default_value);
}

int32_t
grn_plugin_proc_get_var_int32(grn_ctx *ctx,
                              grn_user_data *user_data,
                              const char *name,
                              int name_size,
                              int32_t default_value)
{
  grn_obj *var;

  var = grn_plugin_proc_get_var(ctx, user_data, name, name_size);
  return grn_proc_option_value_int32(ctx, var, default_value);
}

double
grn_plugin_proc_get_var_double(grn_ctx *ctx,
                               grn_user_data *user_data,
                               const char *name,
                               int name_size,
                               double default_value)
{
  grn_obj *var;

  var = grn_plugin_proc_get_var(ctx, user_data, name, name_size);
  return grn_proc_option_value_double(ctx, var, default_value);
}

const char *
grn_plugin_proc_get_var_string(grn_ctx *ctx,
                               grn_user_data *user_data,
                               const char *name,
                               int name_size,
                               size_t *size)
{
  grn_obj *var;

  var = grn_plugin_proc_get_var(ctx, user_data, name, name_size);
  return grn_proc_option_value_string(ctx, var, size);
}

grn_content_type
grn_plugin_proc_get_var_content_type(grn_ctx *ctx,
                                     grn_user_data *user_data,
                                     const char *name,
                                     int name_size,
                                     grn_content_type default_value)
{
  grn_obj *var;

  var = grn_plugin_proc_get_var(ctx, user_data, name, name_size);
  return grn_proc_option_value_content_type(ctx, var, default_value);
}

grn_log_level
grn_plugin_proc_get_var_log_level(grn_ctx *ctx,
                                  grn_user_data *user_data,
                                  const char *name,
                                  int name_size,
                                  grn_log_level default_value)
{
  grn_obj *var;

  var = grn_plugin_proc_get_var(ctx, user_data, name, name_size);
  return grn_proc_option_value_log_level(ctx, var, default_value);
}

grn_obj *
grn_plugin_proc_get_var_by_offset(grn_ctx *ctx,
                                  grn_user_data *user_data,
                                  unsigned int offset)
{
  return grn_proc_get_var_by_offset(ctx, user_data, offset);
}

grn_obj *
grn_plugin_proc_get_caller(grn_ctx *ctx, grn_user_data *user_data)
{
  grn_obj *caller = NULL;
  GRN_API_ENTER;
  grn_proc_get_info(ctx, user_data, NULL, NULL, &caller);
  GRN_API_RETURN(caller);
}

const char *
grn_plugin_win32_base_dir(void)
{
  return grn_plugin_windows_base_dir();
}

const char *
grn_plugin_windows_base_dir(void)
{
#ifdef WIN32
  return grn_windows_base_dir();
#else  /* WIN32 */
  return NULL;
#endif /* WIN32 */
}

/*
  grn_plugin_charlen() takes the length of a string, unlike grn_charlen_().
 */
int
grn_plugin_charlen(grn_ctx *ctx,
                   const char *str_ptr,
                   unsigned int str_length,
                   grn_encoding encoding)
{
  return grn_charlen_(ctx, str_ptr, str_ptr + str_length, encoding);
}

/*
  grn_plugin_isspace() takes the length of a string, unlike grn_isspace().
 */
int
grn_plugin_isspace(grn_ctx *ctx,
                   const char *str_ptr,
                   unsigned int str_length,
                   grn_encoding encoding)
{
  if ((str_ptr == NULL) || (str_length == 0)) {
    return 0;
  }
  switch ((unsigned char)str_ptr[0]) {
  case ' ':
  case '\f':
  case '\n':
  case '\r':
  case '\t':
  case '\v':
    return 1;
  case 0x81:
    if ((encoding == GRN_ENC_SJIS) && (str_length >= 2) &&
        ((unsigned char)str_ptr[1] == 0x40)) {
      return 2;
    }
    break;
  case 0xA1:
    if ((encoding == GRN_ENC_EUC_JP) && (str_length >= 2) &&
        ((unsigned char)str_ptr[1] == 0xA1)) {
      return 2;
    }
    break;
  case 0xE3:
    if ((encoding == GRN_ENC_UTF8) && (str_length >= 3) &&
        ((unsigned char)str_ptr[1] == 0x80) &&
        ((unsigned char)str_ptr[2] == 0x80)) {
      return 3;
    }
    break;
  default:
    break;
  }
  return 0;
}

grn_rc
grn_plugin_expr_var_init(grn_ctx *ctx,
                         grn_expr_var *var,
                         const char *name,
                         int name_size)
{
  var->name = name;
  var->name_size = (unsigned int)compute_name_size(name, name_size);
  GRN_TEXT_INIT(&var->value, 0);
  return GRN_SUCCESS;
}

grn_obj *
grn_plugin_command_create(grn_ctx *ctx,
                          const char *name,
                          int name_size,
                          grn_proc_func func,
                          unsigned int n_vars,
                          grn_expr_var *vars)
{
  grn_obj *proc;
  name_size = compute_name_size(name, name_size);
  proc = grn_proc_create(ctx,
                         name,
                         name_size,
                         GRN_PROC_COMMAND,
                         func,
                         NULL,
                         NULL,
                         n_vars,
                         vars);
  return proc;
}

bool
grn_plugin_proc_get_value_bool(grn_ctx *ctx,
                               grn_obj *value,
                               bool default_value,
                               const char *tag)
{
  return grn_proc_get_value_bool(ctx, value, default_value, tag);
}

int32_t
grn_plugin_proc_get_value_int32(grn_ctx *ctx,
                                grn_obj *value,
                                int32_t default_value_raw,
                                const char *tag)
{
  return grn_proc_get_value_int32(ctx, value, default_value_raw, tag);
}

int64_t
grn_plugin_proc_get_value_int64(grn_ctx *ctx,
                                grn_obj *value,
                                int64_t default_value_raw,
                                const char *tag)
{
  return grn_proc_get_value_int64(ctx, value, default_value_raw, tag);
}

double
grn_plugin_proc_get_value_double(grn_ctx *ctx,
                                 grn_obj *value,
                                 double default_value_raw,
                                 const char *tag)
{
  return grn_proc_get_value_double(ctx, value, default_value_raw, tag);
}

grn_operator
grn_plugin_proc_get_value_mode(grn_ctx *ctx,
                               grn_obj *value,
                               grn_operator default_mode,
                               const char *tag)
{
  return grn_proc_get_value_mode(ctx, value, default_mode, tag);
}

grn_operator
grn_plugin_proc_get_value_operator(grn_ctx *ctx,
                                   grn_obj *value,
                                   grn_operator default_operator,
                                   const char *tag)
{
  return grn_proc_get_value_operator(ctx, value, default_operator, tag);
}
