/* -*- c-basic-offset: 2 -*- */
/*
  Copyright(C) 2012-2016 Brazil

  This library is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License version 2.1 as published by the Free Software Foundation.

  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_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;

#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_dir[GRN_ENV_BUFFER_SIZE];

void
grn_plugin_init_from_env(void)
{
  grn_getenv("GRN_PLUGINS_DIR",
             grn_plugins_dir,
             GRN_ENV_BUFFER_SIZE);
}

static int
compute_name_size(const char *name, int name_size)
{
  if (name_size < 0) {
    if (name) {
      name_size = 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, 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;
  const char *system_plugins_dir;
  size_t system_plugins_dir_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;
  system_plugins_dir = grn_plugin_get_system_plugins_dir();
  system_plugins_dir_size = strlen(system_plugins_dir);
  if (strncmp(system_plugins_dir, path, system_plugins_dir_size) == 0) {
    const char *plugin_name = path + system_plugins_dir_size;
    while (plugin_name[0] == '/') {
      plugin_name++;
    }
    /* TODO: remove suffix too? */
    return plugin_name;
  } else {
    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 = &(ctx->impl->mrb);
  mrb_state *mrb = data->state;
  struct RClass *module = data->module;
  struct RClass *plugin_loader_class;
  int arena_index;

  {
    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");
  mrb_funcall(mrb, mrb_obj_value(plugin_loader_class),
              "load_file", 1, mrb_str_new_cstr(mrb, ctx->impl->plugin_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_MALLOCN(grn_plugin, 1);
  }
  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;
  size_t filename_size;

  filename_size = 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_MALLOCN(grn_plugin, 1);
      }
      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", label);
  }

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;
}

grn_rc
grn_plugins_init(void)
{
  CRITICAL_SECTION_INIT(grn_plugins_lock);
  grn_ctx_init(&grn_plugins_ctx, 0);
  grn_plugins = grn_hash_create(&grn_plugins_ctx, NULL,
                                PATH_MAX, sizeof(grn_plugin *),
                                GRN_OBJ_KEY_VAR_SIZE);
  if (!grn_plugins) {
    grn_ctx_fin(&grn_plugins_ctx);
    return GRN_NO_MEMORY_AVAILABLE;
  }
  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_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);
}

#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[0]) {
    return grn_plugins_dir;
  } else {
    return grn_plugin_get_default_system_plugins_dir();
  }
}

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 =
    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, 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);
}

char *
grn_plugin_find_path(grn_ctx *ctx, const char *name)
{
  const char *plugins_dir;
  char dir_last_char;
  char path[PATH_MAX];
  int name_length, max_name_length;
  char *found_path = NULL;
  size_t path_len;

  GRN_API_ENTER;
  if (name[0] == '/') {
    path[0] = '\0';
  } else {
    plugins_dir = grn_plugin_get_system_plugins_dir();
    grn_strcpy(path, PATH_MAX, plugins_dir);

    dir_last_char = plugins_dir[strlen(path) - 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: %d (max: %d) <%s%s>",
        name_length, max_name_length,
        path, name);
    goto exit;
  }
  grn_strcat(path, PATH_MAX, name);

  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 :
  GRN_API_RETURN(found_path);
}

static void
grn_plugin_set_name_resolve_error(grn_ctx *ctx, const char *name,
                                  const char *tag)
{
  const char *prefix, *prefix_separator, *suffix;

  if (name[0] == '/') {
    prefix = "";
    prefix_separator = "";
    suffix = "";
  } else {
    prefix = grn_plugin_get_system_plugins_dir();
    if (prefix[strlen(prefix) - 1] != '/') {
      prefix_separator = "/";
    } else {
      prefix_separator = "";
    }
    suffix = grn_plugin_get_suffix();
  }
  ERR(GRN_NO_SUCH_FILE_OR_DIRECTORY,
      "%s cannot find plugin file: <%s%s%s%s>",
      tag, prefix, prefix_separator, name, suffix);
}

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, 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;
    uint32_t value_size;
    value = grn_hash_get_value_(&grn_plugins_ctx, grn_plugins, plugin_id,
                                &value_size);
    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_obj *db;
  grn_table_cursor *cursor;
  grn_id id;
  grn_hash *processed_paths;
  const char *system_plugins_dir;
  const char *native_plugin_suffix;
  const char *ruby_plugin_suffix;

  GRN_API_ENTER;

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

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

  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_table_cursor_close(ctx, cursor);
    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();
  while ((id = grn_table_cursor_next(ctx, cursor)) != GRN_ID_NIL) {
    grn_obj *object;
    const char *path;
    grn_id processed_path_id;

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

    if (!grn_obj_is_proc(ctx, object)) {
      grn_obj_unlink(ctx, object);
      continue;
    }

    if (grn_obj_is_builtin(ctx, object)) {
      grn_obj_unlink(ctx, object);
      continue;
    }

    path = grn_obj_path(ctx, object);
    if (!path) {
      grn_obj_unlink(ctx, object);
      continue;
    }

    processed_path_id = grn_hash_get(ctx, processed_paths,
                                     path, strlen(path),
                                     NULL);
    if (processed_path_id != GRN_ID_NIL) {
      grn_obj_unlink(ctx, object);
      continue;
    }

    grn_hash_add(ctx, processed_paths,
                 path, 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, 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, strlen(name),
                             0, GRN_DB_TEXT);
    }
  }
  grn_table_cursor_close(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);
}

/*
  grn_plugin_ctx_log() is a clone of grn_ctx_log() in ctx.c. The only
  difference is that grn_plugin_ctx_log() uses va_list instead of `...'.
 */
static void
grn_plugin_ctx_log(grn_ctx *ctx, const char *format, va_list ap)
{
  vsnprintf(ctx->errbuf, GRN_CTX_MSGSIZE, format, ap);
}

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, ...)
{
  ctx->errlvl = level;
  ctx->rc = error_code;
  ctx->errfile = file;
  ctx->errline = line;
  ctx->errfunc = func;

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

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

void
grn_plugin_backtrace(grn_ctx *ctx)
{
  BACKTRACE(ctx);
}

void
grn_plugin_logtrace(grn_ctx *ctx, grn_log_level level)
{
  if (level <= GRN_LOG_ERROR) {
    LOGTRACE(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, grn_obj_flags 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, name_size);
}

grn_bool
grn_plugin_proc_get_var_bool(grn_ctx *ctx,
                             grn_user_data *user_data,
                             const char *name,
                             int name_size,
                             grn_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);
}

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_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);
}

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 = 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;
}
