/**
 * collectd - src/utils_vl_lookup.c
 * Copyright (C) 2012       Florian Forster
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 * Authors:
 *   Florian Forster <octo at collectd.org>
 **/

#include "collectd.h"

#include <pthread.h>
#include <regex.h>

#include "utils/avltree/avltree.h"
#include "utils/common/common.h"
#include "utils/lookup/vl_lookup.h"

#if HAVE_KSTAT_H
#include <kstat.h>
#endif

#if HAVE_LIBKSTAT
kstat_ctl_t *kc;
#endif /* HAVE_LIBKSTAT */

#if BUILD_TEST
#define sstrncpy strncpy
#define plugin_log(s, ...)                                                     \
  do {                                                                         \
    printf("[severity %i] ", s);                                               \
    printf(__VA_ARGS__);                                                       \
    printf("\n");                                                              \
  } while (0)
#endif

/*
 * Types
 */
struct part_match_s {
  char str[DATA_MAX_NAME_LEN];
  regex_t regex;
  bool is_regex;
};
typedef struct part_match_s part_match_t;

struct identifier_match_s {
  part_match_t host;
  part_match_t plugin;
  part_match_t plugin_instance;
  part_match_t type;
  part_match_t type_instance;

  unsigned int group_by;
};
typedef struct identifier_match_s identifier_match_t;

struct lookup_s {
  c_avl_tree_t *by_type_tree;

  lookup_class_callback_t cb_user_class;
  lookup_obj_callback_t cb_user_obj;
  lookup_free_class_callback_t cb_free_class;
  lookup_free_obj_callback_t cb_free_obj;
};

struct user_obj_s;
typedef struct user_obj_s user_obj_t;
struct user_obj_s {
  void *user_obj;
  lookup_identifier_t ident;

  user_obj_t *next;
};

struct user_class_s {
  pthread_mutex_t lock;
  void *user_class;
  identifier_match_t match;
  user_obj_t *user_obj_list; /* list of user_obj */
};
typedef struct user_class_s user_class_t;

struct user_class_list_s;
typedef struct user_class_list_s user_class_list_t;
struct user_class_list_s {
  user_class_t entry;
  user_class_list_t *next;
};

struct by_type_entry_s {
  c_avl_tree_t *by_plugin_tree; /* plugin -> user_class_list_t */
  user_class_list_t *wildcard_plugin_list;
};
typedef struct by_type_entry_s by_type_entry_t;

/*
 * Private functions
 */
static bool lu_part_matches(part_match_t const *match, /* {{{ */
                            char const *str) {
  if (match->is_regex) {
    /* Short cut popular catch-all regex. */
    if (strcmp(".*", match->str) == 0)
      return true;

    int status = regexec(&match->regex, str,
                         /* nmatch = */ 0, /* pmatch = */ NULL,
                         /* flags = */ 0);
    if (status == 0)
      return true;
    else
      return false;
  } else if (strcmp(match->str, str) == 0)
    return true;
  else
    return false;
} /* }}} bool lu_part_matches */

static int lu_copy_ident_to_match_part(part_match_t *match_part, /* {{{ */
                                       char const *ident_part) {
  size_t len = strlen(ident_part);
  int status;

  if ((len < 3) || (ident_part[0] != '/') || (ident_part[len - 1] != '/')) {
    sstrncpy(match_part->str, ident_part, sizeof(match_part->str));
    match_part->is_regex = false;
    return 0;
  }

  /* Copy string without the leading slash. */
  sstrncpy(match_part->str, ident_part + 1, sizeof(match_part->str));
  assert(sizeof(match_part->str) > len);
  /* strip trailing slash */
  match_part->str[len - 2] = 0;

  status = regcomp(&match_part->regex, match_part->str,
                   /* flags = */ REG_EXTENDED);
  if (status != 0) {
    char errbuf[1024];
    regerror(status, &match_part->regex, errbuf, sizeof(errbuf));
    ERROR("utils_vl_lookup: Compiling regular expression \"%s\" failed: %s",
          match_part->str, errbuf);
    return EINVAL;
  }
  match_part->is_regex = true;

  return 0;
} /* }}} int lu_copy_ident_to_match_part */

static int lu_copy_ident_to_match(identifier_match_t *match, /* {{{ */
                                  lookup_identifier_t const *ident,
                                  unsigned int group_by) {
  memset(match, 0, sizeof(*match));

  match->group_by = group_by;

#define COPY_FIELD(field)                                                      \
  do {                                                                         \
    int status = lu_copy_ident_to_match_part(&match->field, ident->field);     \
    if (status != 0)                                                           \
      return status;                                                           \
  } while (0)

  COPY_FIELD(host);
  COPY_FIELD(plugin);
  COPY_FIELD(plugin_instance);
  COPY_FIELD(type);
  COPY_FIELD(type_instance);

#undef COPY_FIELD

  return 0;
} /* }}} int lu_copy_ident_to_match */

/* user_class->lock must be held when calling this function */
static void *lu_create_user_obj(lookup_t *obj, /* {{{ */
                                data_set_t const *ds, value_list_t const *vl,
                                user_class_t *user_class) {
  user_obj_t *user_obj;

  user_obj = calloc(1, sizeof(*user_obj));
  if (user_obj == NULL) {
    ERROR("utils_vl_lookup: calloc failed.");
    return NULL;
  }
  user_obj->next = NULL;

  user_obj->user_obj = obj->cb_user_class(ds, vl, user_class->user_class);
  if (user_obj->user_obj == NULL) {
    sfree(user_obj);
    WARNING("utils_vl_lookup: User-provided constructor failed.");
    return NULL;
  }

#define COPY_FIELD(field, group_mask)                                          \
  do {                                                                         \
    if (user_class->match.field.is_regex &&                                    \
        ((user_class->match.group_by & group_mask) == 0))                      \
      sstrncpy(user_obj->ident.field, "/.*/", sizeof(user_obj->ident.field));  \
    else                                                                       \
      sstrncpy(user_obj->ident.field, vl->field,                               \
               sizeof(user_obj->ident.field));                                 \
  } while (0)

  COPY_FIELD(host, LU_GROUP_BY_HOST);
  COPY_FIELD(plugin, LU_GROUP_BY_PLUGIN);
  COPY_FIELD(plugin_instance, LU_GROUP_BY_PLUGIN_INSTANCE);
  COPY_FIELD(type, 0);
  COPY_FIELD(type_instance, LU_GROUP_BY_TYPE_INSTANCE);

#undef COPY_FIELD

  if (user_class->user_obj_list == NULL) {
    user_class->user_obj_list = user_obj;
  } else {
    user_obj_t *last = user_class->user_obj_list;
    while (last->next != NULL)
      last = last->next;
    last->next = user_obj;
  }

  return user_obj;
} /* }}} void *lu_create_user_obj */

/* user_class->lock must be held when calling this function */
static user_obj_t *lu_find_user_obj(user_class_t *user_class, /* {{{ */
                                    value_list_t const *vl) {
  user_obj_t *ptr;

  for (ptr = user_class->user_obj_list; ptr != NULL; ptr = ptr->next) {
    if (user_class->match.host.is_regex &&
        (user_class->match.group_by & LU_GROUP_BY_HOST) &&
        (strcmp(vl->host, ptr->ident.host) != 0))
      continue;
    if (user_class->match.plugin.is_regex &&
        (user_class->match.group_by & LU_GROUP_BY_PLUGIN) &&
        (strcmp(vl->plugin, ptr->ident.plugin) != 0))
      continue;
    if (user_class->match.plugin_instance.is_regex &&
        (user_class->match.group_by & LU_GROUP_BY_PLUGIN_INSTANCE) &&
        (strcmp(vl->plugin_instance, ptr->ident.plugin_instance) != 0))
      continue;
    if (user_class->match.type_instance.is_regex &&
        (user_class->match.group_by & LU_GROUP_BY_TYPE_INSTANCE) &&
        (strcmp(vl->type_instance, ptr->ident.type_instance) != 0))
      continue;

    return ptr;
  }

  return NULL;
} /* }}} user_obj_t *lu_find_user_obj */

static int lu_handle_user_class(lookup_t *obj, /* {{{ */
                                data_set_t const *ds, value_list_t const *vl,
                                user_class_t *user_class) {
  user_obj_t *user_obj;
  int status;

  assert(strcmp(vl->type, user_class->match.type.str) == 0);
  assert(user_class->match.plugin.is_regex ||
         (strcmp(vl->plugin, user_class->match.plugin.str)) == 0);

  if (!lu_part_matches(&user_class->match.type_instance, vl->type_instance) ||
      !lu_part_matches(&user_class->match.plugin_instance,
                       vl->plugin_instance) ||
      !lu_part_matches(&user_class->match.plugin, vl->plugin) ||
      !lu_part_matches(&user_class->match.host, vl->host))
    return 1;

  pthread_mutex_lock(&user_class->lock);
  user_obj = lu_find_user_obj(user_class, vl);
  if (user_obj == NULL) {
    /* call lookup_class_callback_t() and insert into the list of user objects.
     */
    user_obj = lu_create_user_obj(obj, ds, vl, user_class);
    if (user_obj == NULL) {
      pthread_mutex_unlock(&user_class->lock);
      return -1;
    }
  }
  pthread_mutex_unlock(&user_class->lock);

  status = obj->cb_user_obj(ds, vl, user_class->user_class, user_obj->user_obj);
  if (status != 0) {
    ERROR("utils_vl_lookup: The user object callback failed with status %i.",
          status);
    /* Returning a negative value means: abort! */
    if (status < 0)
      return status;
    else
      return 1;
  }

  return 0;
} /* }}} int lu_handle_user_class */

static int lu_handle_user_class_list(lookup_t *obj, /* {{{ */
                                     data_set_t const *ds,
                                     value_list_t const *vl,
                                     user_class_list_t *user_class_list) {
  user_class_list_t *ptr;
  int retval = 0;

  for (ptr = user_class_list; ptr != NULL; ptr = ptr->next) {
    int status;

    status = lu_handle_user_class(obj, ds, vl, &ptr->entry);
    if (status < 0)
      return status;
    else if (status == 0)
      retval++;
  }

  return retval;
} /* }}} int lu_handle_user_class_list */

static by_type_entry_t *lu_search_by_type(lookup_t *obj, /* {{{ */
                                          char const *type,
                                          bool allocate_if_missing) {
  by_type_entry_t *by_type;
  char *type_copy;
  int status;

  status = c_avl_get(obj->by_type_tree, type, (void *)&by_type);
  if (status == 0)
    return by_type;

  if (!allocate_if_missing)
    return NULL;

  type_copy = strdup(type);
  if (type_copy == NULL) {
    ERROR("utils_vl_lookup: strdup failed.");
    return NULL;
  }

  by_type = calloc(1, sizeof(*by_type));
  if (by_type == NULL) {
    ERROR("utils_vl_lookup: calloc failed.");
    sfree(type_copy);
    return NULL;
  }
  by_type->wildcard_plugin_list = NULL;

  by_type->by_plugin_tree =
      c_avl_create((int (*)(const void *, const void *))strcmp);
  if (by_type->by_plugin_tree == NULL) {
    ERROR("utils_vl_lookup: c_avl_create failed.");
    sfree(by_type);
    sfree(type_copy);
    return NULL;
  }

  status = c_avl_insert(obj->by_type_tree,
                        /* key = */ type_copy, /* value = */ by_type);
  assert(status <= 0); /* >0 => entry exists => race condition. */
  if (status != 0) {
    ERROR("utils_vl_lookup: c_avl_insert failed.");
    c_avl_destroy(by_type->by_plugin_tree);
    sfree(by_type);
    sfree(type_copy);
    return NULL;
  }

  return by_type;
} /* }}} by_type_entry_t *lu_search_by_type */

static int lu_add_by_plugin(by_type_entry_t *by_type, /* {{{ */
                            user_class_list_t *user_class_list) {
  user_class_list_t *ptr = NULL;
  identifier_match_t const *match = &user_class_list->entry.match;

  /* Lookup user_class_list from the per-plugin structure. If this is the first
   * user_class to be added, the block returns immediately. Otherwise they will
   * set "ptr" to non-NULL. */
  if (match->plugin.is_regex) {
    if (by_type->wildcard_plugin_list == NULL) {
      by_type->wildcard_plugin_list = user_class_list;
      return 0;
    }

    ptr = by_type->wildcard_plugin_list;
  }    /* if (plugin is wildcard) */
  else /* (plugin is not wildcard) */
  {
    int status;

    status =
        c_avl_get(by_type->by_plugin_tree, match->plugin.str, (void *)&ptr);

    if (status != 0) /* plugin not yet in tree */
    {
      char *plugin_copy = strdup(match->plugin.str);

      if (plugin_copy == NULL) {
        ERROR("utils_vl_lookup: strdup failed.");
        sfree(user_class_list);
        return ENOMEM;
      }

      status =
          c_avl_insert(by_type->by_plugin_tree, plugin_copy, user_class_list);
      if (status != 0) {
        ERROR("utils_vl_lookup: c_avl_insert(\"%s\") failed with status %i.",
              plugin_copy, status);
        sfree(plugin_copy);
        sfree(user_class_list);
        return status;
      } else {
        return 0;
      }
    } /* if (plugin not yet in tree) */
  }   /* if (plugin is not wildcard) */

  assert(ptr != NULL);

  while (ptr->next != NULL)
    ptr = ptr->next;
  ptr->next = user_class_list;

  return 0;
} /* }}} int lu_add_by_plugin */

static void lu_destroy_user_obj(lookup_t *obj, /* {{{ */
                                user_obj_t *user_obj) {
  while (user_obj != NULL) {
    user_obj_t *next = user_obj->next;

    if (obj->cb_free_obj != NULL)
      obj->cb_free_obj(user_obj->user_obj);
    user_obj->user_obj = NULL;

    sfree(user_obj);
    user_obj = next;
  }
} /* }}} void lu_destroy_user_obj */

static void lu_destroy_user_class_list(lookup_t *obj, /* {{{ */
                                       user_class_list_t *user_class_list) {
  while (user_class_list != NULL) {
    user_class_list_t *next = user_class_list->next;

    if (obj->cb_free_class != NULL)
      obj->cb_free_class(user_class_list->entry.user_class);
    user_class_list->entry.user_class = NULL;

#define CLEAR_FIELD(field)                                                     \
  do {                                                                         \
    if (user_class_list->entry.match.field.is_regex) {                         \
      regfree(&user_class_list->entry.match.field.regex);                      \
      user_class_list->entry.match.field.is_regex = false;                     \
    }                                                                          \
  } while (0)

    CLEAR_FIELD(host);
    CLEAR_FIELD(plugin);
    CLEAR_FIELD(plugin_instance);
    CLEAR_FIELD(type);
    CLEAR_FIELD(type_instance);

#undef CLEAR_FIELD

    lu_destroy_user_obj(obj, user_class_list->entry.user_obj_list);
    user_class_list->entry.user_obj_list = NULL;
    pthread_mutex_destroy(&user_class_list->entry.lock);

    sfree(user_class_list);
    user_class_list = next;
  }
} /* }}} void lu_destroy_user_class_list */

static void lu_destroy_by_type(lookup_t *obj, /* {{{ */
                               by_type_entry_t *by_type) {

  while (42) {
    char *plugin = NULL;
    user_class_list_t *user_class_list = NULL;
    int status;

    status = c_avl_pick(by_type->by_plugin_tree, (void *)&plugin,
                        (void *)&user_class_list);
    if (status != 0)
      break;

    DEBUG("utils_vl_lookup: lu_destroy_by_type: Destroying plugin \"%s\".",
          plugin);
    sfree(plugin);
    lu_destroy_user_class_list(obj, user_class_list);
  }

  c_avl_destroy(by_type->by_plugin_tree);
  by_type->by_plugin_tree = NULL;

  lu_destroy_user_class_list(obj, by_type->wildcard_plugin_list);
  by_type->wildcard_plugin_list = NULL;

  sfree(by_type);
} /* }}} int lu_destroy_by_type */

/*
 * Public functions
 */
lookup_t *lookup_create(lookup_class_callback_t cb_user_class, /* {{{ */
                        lookup_obj_callback_t cb_user_obj,
                        lookup_free_class_callback_t cb_free_class,
                        lookup_free_obj_callback_t cb_free_obj) {
  lookup_t *obj = calloc(1, sizeof(*obj));
  if (obj == NULL) {
    ERROR("utils_vl_lookup: calloc failed.");
    return NULL;
  }

  obj->by_type_tree = c_avl_create((int (*)(const void *, const void *))strcmp);
  if (obj->by_type_tree == NULL) {
    ERROR("utils_vl_lookup: c_avl_create failed.");
    sfree(obj);
    return NULL;
  }

  obj->cb_user_class = cb_user_class;
  obj->cb_user_obj = cb_user_obj;
  obj->cb_free_class = cb_free_class;
  obj->cb_free_obj = cb_free_obj;

  return obj;
} /* }}} lookup_t *lookup_create */

void lookup_destroy(lookup_t *obj) /* {{{ */
{
  int status;

  if (obj == NULL)
    return;

  while (42) {
    char *type = NULL;
    by_type_entry_t *by_type = NULL;

    status = c_avl_pick(obj->by_type_tree, (void *)&type, (void *)&by_type);
    if (status != 0)
      break;

    DEBUG("utils_vl_lookup: lookup_destroy: Destroying type \"%s\".", type);
    sfree(type);
    lu_destroy_by_type(obj, by_type);
  }

  c_avl_destroy(obj->by_type_tree);
  obj->by_type_tree = NULL;

  sfree(obj);
} /* }}} void lookup_destroy */

int lookup_add(lookup_t *obj, /* {{{ */
               lookup_identifier_t const *ident, unsigned int group_by,
               void *user_class) {
  by_type_entry_t *by_type = NULL;
  user_class_list_t *user_class_obj;

  by_type = lu_search_by_type(obj, ident->type, /* allocate = */ true);
  if (by_type == NULL)
    return -1;

  user_class_obj = calloc(1, sizeof(*user_class_obj));
  if (user_class_obj == NULL) {
    ERROR("utils_vl_lookup: calloc failed.");
    return ENOMEM;
  }
  pthread_mutex_init(&user_class_obj->entry.lock, /* attr = */ NULL);
  user_class_obj->entry.user_class = user_class;
  lu_copy_ident_to_match(&user_class_obj->entry.match, ident, group_by);
  user_class_obj->entry.user_obj_list = NULL;
  user_class_obj->next = NULL;

  return lu_add_by_plugin(by_type, user_class_obj);
} /* }}} int lookup_add */

/* returns the number of successful calls to the callback function */
int lookup_search(lookup_t *obj, /* {{{ */
                  data_set_t const *ds, value_list_t const *vl) {
  by_type_entry_t *by_type = NULL;
  user_class_list_t *user_class_list = NULL;
  int retval = 0;
  int status;

  if ((obj == NULL) || (ds == NULL) || (vl == NULL))
    return -EINVAL;

  by_type = lu_search_by_type(obj, vl->type, /* allocate = */ false);
  if (by_type == NULL)
    return 0;

  status =
      c_avl_get(by_type->by_plugin_tree, vl->plugin, (void *)&user_class_list);
  if (status == 0) {
    status = lu_handle_user_class_list(obj, ds, vl, user_class_list);
    if (status < 0)
      return status;
    retval += status;
  }

  if (by_type->wildcard_plugin_list != NULL) {
    status =
        lu_handle_user_class_list(obj, ds, vl, by_type->wildcard_plugin_list);
    if (status < 0)
      return status;
    retval += status;
  }

  return retval;
} /* }}} lookup_search */
