/*
 * @author Evgeny Khramtsov <ekhramtsov@process-one.net>
 * @copyright (C) 2002-2019 ProcessOne, SARL. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

#include <erl_nif.h>
#include <stdio.h>
#include <errno.h>
#include "uthash.h"

void __free(void *ptr, size_t size) {
  enif_free(ptr);
}

#undef uthash_malloc
#undef uthash_free
#define uthash_malloc enif_alloc
#define uthash_free __free

/****************************************************************
 *               Structures/Globals definitions                 *
 ****************************************************************/
typedef struct __tree_t {
  char *key;
  char *val;
  int refc;
  struct __tree_t *sub;
  UT_hash_handle hh;
} tree_t;

typedef struct {
  tree_t *tree;
  char *name;
  ErlNifRWLock *lock;
} state_t;

typedef struct {
  char *name;
  state_t *state;
  UT_hash_handle hh;
} registry_t;

static ErlNifResourceType *tree_state_t = NULL;
static registry_t *registry = NULL;
static ErlNifRWLock *registry_lock = NULL;

/****************************************************************
 *                   MQTT Tree Manipulation                     *
 ****************************************************************/
tree_t *tree_new(char *key, size_t len) {
  tree_t *tree = enif_alloc(sizeof(tree_t));
  if (tree) {
    memset(tree, 0, sizeof(tree_t));
    if (key && len) {
      tree->key = enif_alloc(len);
      if (tree->key) {
        memcpy(tree->key, key, len);
      } else {
        enif_free(tree);
        tree = NULL;
      }
    }
  }
  return tree;
}

void tree_free(tree_t *t) {
  tree_t *found, *iter;
  if (t) {
    enif_free(t->key);
    enif_free(t->val);
    HASH_ITER(hh, t->sub, found, iter) {
      HASH_DEL(t->sub, found);
      tree_free(found);
    }
    memset(t, 0, sizeof(tree_t));
    enif_free(t);
  }
}

void tree_clear(tree_t *root) {
  tree_t *found, *iter;
  HASH_ITER(hh, root->sub, found, iter) {
    HASH_DEL(root->sub, found);
    tree_free(found);
  }
}

int tree_add(tree_t *root, char *path, size_t size) {
  int i = 0;
  size_t len;
  tree_t *t = root;
  tree_t *found, *new;

  while (i<=size) {
    len = strlen(path+i) + 1;
    HASH_FIND_STR(t->sub, path+i, found);
    if (found) {
      i += len;
      t = found;
    } else {
      new = tree_new(path+i, len);
      if (new) {
        HASH_ADD_STR(t->sub, key, new);
        i += len;
        t = new;
      } else
        return ENOMEM;
    }
  }

  if (!t->val) {
    t->val = enif_alloc(size+1);
    if (t->val) {
      t->val[size] = 0;
      for (i=0; i<size; i++) {
        char c = path[i];
        t->val[i] = c ? c : '/';
      }
    } else
      return ENOMEM;
  }
  t->refc++;
  return 0;
}

int tree_del(tree_t *root, char *path, size_t i, size_t size) {
  tree_t *found;

  if (i<=size) {
    HASH_FIND_STR(root->sub, path+i, found);
    if (found) {
      i += strlen(path+i) + 1;
      int deleted = tree_del(found, path, i, size);
      if (deleted) {
        HASH_DEL(root->sub, found);
        tree_free(found);
      }
    }
  } else if (root->refc) {
    root->refc--;
    if (!root->refc) {
      enif_free(root->val);
      root->val = NULL;
    }
  }

  return !root->refc && !root->sub;
}

void tree_size(tree_t *tree, size_t *size) {
  tree_t *found, *iter;

  HASH_ITER(hh, tree->sub, found, iter) {
    if (found->refc) (*size)++;
    tree_size(found, size);
  }
}

int tree_refc(tree_t *tree, char *path, size_t i, size_t size) {
  tree_t *found;

  if (i<=size) {
    HASH_FIND_STR(tree->sub, path+i, found);
    if (found) {
      i += strlen(path+i) + 1;
      return tree_refc(found, path, i, size);
    } else {
      return 0;
    }
  } else
    return tree->refc;
}

/****************************************************************
 *                        Registration                          *
 ****************************************************************/
void delete_registry_entry(registry_t *entry) {
  /* registry_lock must be RW-locked! */
  HASH_DEL(registry, entry);
  entry->state->name = NULL;
  enif_release_resource(entry->state);
  enif_free(entry->name);
  enif_free(entry);
}

int register_tree(char *name, state_t *state) {
  registry_t *entry, *found;

  entry = enif_alloc(sizeof(registry_t));
  if (!entry) return ENOMEM;

  entry->name = enif_alloc(strlen(name) + 1);
  if (!entry->name) {
    free(entry);
    return ENOMEM;
  }

  entry->state = state;
  strcpy(entry->name, name);
  enif_rwlock_rwlock(registry_lock);
  HASH_FIND_STR(registry, name, found);
  if (found) {
    enif_rwlock_rwunlock(registry_lock);
    enif_free(entry->name);
    enif_free(entry);
    return EINVAL;
  } else {
    if (state->name) {
      /* Unregistering previously registered name */
      HASH_FIND_STR(registry, state->name, found);
      if (found)
        delete_registry_entry(found);
    }
    enif_keep_resource(state);
    HASH_ADD_STR(registry, name, entry);
    state->name = entry->name;
    enif_rwlock_rwunlock(registry_lock);
    return 0;
  }
}

int unregister_tree(char *name) {
  registry_t *entry;
  int ret;

  enif_rwlock_rwlock(registry_lock);
  HASH_FIND_STR(registry, name, entry);
  if (entry) {
    delete_registry_entry(entry);
    ret = 0;
  } else {
    ret = EINVAL;
  }
  enif_rwlock_rwunlock(registry_lock);

  return ret;
}

/****************************************************************
 *                        NIF helpers                           *
 ****************************************************************/
static ERL_NIF_TERM cons(ErlNifEnv *env, char *str, ERL_NIF_TERM tail)
{
  if (str) {
    size_t len = strlen(str);
    ERL_NIF_TERM head;
    unsigned char *buf = enif_make_new_binary(env, len, &head);
    if (buf) {
      memcpy(buf, str, len);
      return enif_make_list_cell(env, head, tail);
    }
  }
  return tail;
}

static void match(ErlNifEnv *env, tree_t *root,
                  char *path, size_t i, size_t size, ERL_NIF_TERM *acc)
{
  tree_t *found;
  size_t len = 0;

  if (i<=size) {
    HASH_FIND_STR(root->sub, path+i, found);
    if (found) {
      len = strlen(path+i) + 1;
      match(env, found, path, i+len, size, acc);
    };
    if (i || path[0] != '$') {
      HASH_FIND_STR(root->sub, "+", found);
      if (found) {
	len = strlen(path+i) + 1;
	match(env, found, path, i+len, size, acc);
      }
      HASH_FIND_STR(root->sub, "#", found);
      if (found) {
	*acc = cons(env, found->val, *acc);
      }
    }
  } else {
    *acc = cons(env, root->val, *acc);
    HASH_FIND_STR(root->sub, "#", found);
    if (found)
      *acc = cons(env, found->val, *acc);
  }
}

static void to_list(ErlNifEnv *env, tree_t *root, ERL_NIF_TERM *acc)
{
  tree_t *found, *iter;

  HASH_ITER(hh, root->sub, found, iter) {
    if (found->val) {
      size_t len = strlen(found->val);
      ERL_NIF_TERM refc = enif_make_int(env, found->refc);
      ERL_NIF_TERM val;
      unsigned char *buf = enif_make_new_binary(env, len, &val);
      if (buf) {
        memcpy(buf, found->val, len);
        *acc = enif_make_list_cell(env, enif_make_tuple2(env, val, refc), *acc);
      }
    };
    to_list(env, found, acc);
  }
}

static ERL_NIF_TERM dump(ErlNifEnv *env, tree_t *tree)
{
  tree_t *found, *iter;
  ERL_NIF_TERM tail, head;

  tail = enif_make_list(env, 0);
  HASH_ITER(hh, tree->sub, found, iter) {
    head = dump(env, found);
    tail = enif_make_list_cell(env, head, tail);
  }
  if (tree->key) {
    ERL_NIF_TERM part, path;
    part = enif_make_string(env, tree->key, ERL_NIF_LATIN1);
    if (tree->val)
      path = enif_make_string(env, tree->val, ERL_NIF_LATIN1);
    else
      path = enif_make_atom(env, "none");
    return enif_make_tuple4(env, part, path, enif_make_int(env, tree->refc), tail);
  } else
    return tail;
}

static ERL_NIF_TERM raise(ErlNifEnv *env, int err)
{
  switch (err) {
  case ENOMEM:
    return enif_raise_exception(env, enif_make_atom(env, "enomem"));
  default:
    return enif_make_badarg(env);
  }
}

void prep_path(char *path, ErlNifBinary *bin) {
  int i;
  unsigned char c;
  path[bin->size] = 0;
  for (i=0; i<bin->size; i++) {
    c = bin->data[i];
    path[i] = (c == '/') ? 0 : c;
  }
}

/****************************************************************
 *                 Constructors/Destructors                     *
 ****************************************************************/
static state_t *init_tree_state(ErlNifEnv *env) {
  state_t *state = enif_alloc_resource(tree_state_t, sizeof(state_t));
  if (state) {
    memset(state, 0, sizeof(state_t));
    state->tree = tree_new(NULL, 0);
    state->lock = enif_rwlock_create("mqtree_lock");
    if (state->tree && state->lock)
      return state;
    else
      enif_release_resource(state);
  }
  return NULL;
}

static void destroy_tree_state(ErlNifEnv *env, void *data) {
  state_t *state = (state_t *) data;
  if (state) {
    tree_free(state->tree);
    if (state->lock) enif_rwlock_destroy(state->lock);
  }
  memset(state, 0, sizeof(state_t));
}

/****************************************************************
 *                      NIF definitions                         *
 ****************************************************************/
static int load(ErlNifEnv* env, void** priv, ERL_NIF_TERM max) {
  registry_lock = enif_rwlock_create("mqtree_registry");
  if (registry_lock) {
    ErlNifResourceFlags flags = ERL_NIF_RT_CREATE | ERL_NIF_RT_TAKEOVER;
    tree_state_t = enif_open_resource_type(env, NULL, "mqtree_state",
                                           destroy_tree_state,
                                           flags, NULL);
    return 0;
  }
  return ENOMEM;
}

static void unload(ErlNifEnv* env, void* priv) {
  if (registry_lock) {
    enif_rwlock_destroy(registry_lock);
    registry_lock = NULL;
  }
}

static ERL_NIF_TERM new_0(ErlNifEnv* env, int argc,
                          const ERL_NIF_TERM argv[])
{
  ERL_NIF_TERM result;
  state_t *state = init_tree_state(env);
  if (state) {
    result = enif_make_resource(env, state);
    enif_release_resource(state);
  } else
    result = raise(env, ENOMEM);

  return result;
}

static ERL_NIF_TERM insert_2(ErlNifEnv* env, int argc,
                             const ERL_NIF_TERM argv[])
{
  state_t *state;
  ErlNifBinary path_bin;

  if (!enif_get_resource(env, argv[0], tree_state_t, (void *) &state) ||
      !enif_inspect_iolist_as_binary(env, argv[1], &path_bin))
    return raise(env, EINVAL);

  if (!path_bin.size)
    return enif_make_atom(env, "ok");

  char path[path_bin.size+1];
  prep_path(path, &path_bin);
  enif_rwlock_rwlock(state->lock);
  int ret = tree_add(state->tree, path, path_bin.size);
  enif_rwlock_rwunlock(state->lock);

  if (!ret)
    return enif_make_atom(env, "ok");
  else
    return raise(env, ret);
}

static ERL_NIF_TERM delete_2(ErlNifEnv* env, int argc,
                             const ERL_NIF_TERM argv[])
{
  state_t *state;
  ErlNifBinary path_bin;

  if (!enif_get_resource(env, argv[0], tree_state_t, (void *) &state) ||
      !enif_inspect_iolist_as_binary(env, argv[1], &path_bin))
    return raise(env, EINVAL);

  if (!path_bin.size)
    return enif_make_atom(env, "ok");

  char path[path_bin.size+1];
  prep_path(path, &path_bin);
  enif_rwlock_rwlock(state->lock);
  tree_del(state->tree, path, 0, path_bin.size);
  enif_rwlock_rwunlock(state->lock);

  return enif_make_atom(env, "ok");
}

static ERL_NIF_TERM match_2(ErlNifEnv* env, int argc,
                            const ERL_NIF_TERM argv[])
{
  state_t *state;
  ErlNifBinary path_bin;
  ERL_NIF_TERM result = enif_make_list(env, 0);

  if (!enif_get_resource(env, argv[0], tree_state_t, (void *) &state) ||
      !enif_inspect_iolist_as_binary(env, argv[1], &path_bin))
    return raise(env, EINVAL);

  if (!path_bin.size)
    return result;

  char path[path_bin.size+1];
  prep_path(path, &path_bin);
  enif_rwlock_rlock(state->lock);
  match(env, state->tree, path, 0, path_bin.size, &result);
  enif_rwlock_runlock(state->lock);

  return result;
}

static ERL_NIF_TERM refc_2(ErlNifEnv* env, int argc,
                           const ERL_NIF_TERM argv[])
{
  state_t *state;
  ErlNifBinary path_bin;

  if (!enif_get_resource(env, argv[0], tree_state_t, (void *) &state) ||
      !enif_inspect_iolist_as_binary(env, argv[1], &path_bin))
    return raise(env, EINVAL);

  if (!path_bin.size)
    return enif_make_int(env, 0);

  char path[path_bin.size+1];
  prep_path(path, &path_bin);
  enif_rwlock_rlock(state->lock);
  int refc = tree_refc(state->tree, path, 0, path_bin.size);
  enif_rwlock_runlock(state->lock);

  return enif_make_int(env, refc);
}

static ERL_NIF_TERM clear_1(ErlNifEnv* env, int argc,
                            const ERL_NIF_TERM argv[])
{
  state_t *state;
  if (!enif_get_resource(env, argv[0], tree_state_t, (void *) &state))
    return raise(env, EINVAL);

  enif_rwlock_rwlock(state->lock);
  tree_clear(state->tree);
  enif_rwlock_rwunlock(state->lock);

  return enif_make_atom(env, "ok");
}

static ERL_NIF_TERM size_1(ErlNifEnv* env, int argc,
                           const ERL_NIF_TERM argv[])
{
  state_t *state;
  size_t size = 0;
  if (!enif_get_resource(env, argv[0], tree_state_t, (void *) &state))
    return raise(env, EINVAL);

  enif_rwlock_rlock(state->lock);
  tree_size(state->tree, &size);
  enif_rwlock_runlock(state->lock);

  return enif_make_uint64(env, (ErlNifUInt64) size);
}

static ERL_NIF_TERM is_empty_1(ErlNifEnv* env, int argc,
                               const ERL_NIF_TERM argv[])
{
  state_t *state;
  if (!enif_get_resource(env, argv[0], tree_state_t, (void *) &state))
    return raise(env, EINVAL);

  enif_rwlock_rlock(state->lock);
  char *ret = state->tree->sub ? "false" : "true";
  enif_rwlock_runlock(state->lock);

  return enif_make_atom(env, ret);
}

static ERL_NIF_TERM to_list_1(ErlNifEnv* env, int argc,
                              const ERL_NIF_TERM argv[])
{
  state_t *state;
  ERL_NIF_TERM result = enif_make_list(env, 0);

  if (!enif_get_resource(env, argv[0], tree_state_t, (void *) &state))
    return raise(env, EINVAL);

  enif_rwlock_rlock(state->lock);
  to_list(env, state->tree, &result);
  enif_rwlock_runlock(state->lock);

  return result;
}

static ERL_NIF_TERM dump_1(ErlNifEnv* env, int argc,
                           const ERL_NIF_TERM argv[])
{
  state_t *state;
  if (!enif_get_resource(env, argv[0], tree_state_t, (void *) &state))
    return raise(env, EINVAL);

  enif_rwlock_rlock(state->lock);
  ERL_NIF_TERM result = dump(env, state->tree);
  enif_rwlock_runlock(state->lock);

  return result;
}

static ERL_NIF_TERM register_2(ErlNifEnv* env, int argc,
                               const ERL_NIF_TERM argv[])
{
  state_t *state;
  unsigned int len;
  int ret;

  if (!enif_get_atom_length(env, argv[0], &len, ERL_NIF_LATIN1) ||
      !enif_get_resource(env, argv[1], tree_state_t, (void *) &state))
    return raise(env, EINVAL);

  char name[len+1];
  enif_get_atom(env, argv[0], name, len+1, ERL_NIF_LATIN1);
  if (!strcmp(name, "undefined"))
    return raise(env, EINVAL);

  ret = register_tree(name, state);
  if (ret)
    return raise(env, ret);
  else
    return enif_make_atom(env, "ok");
}

static ERL_NIF_TERM unregister_1(ErlNifEnv* env, int argc,
                                 const ERL_NIF_TERM argv[])
{
  unsigned int len;
  int ret;

  if (!enif_get_atom_length(env, argv[0], &len, ERL_NIF_LATIN1))
    return raise(env, EINVAL);

  char name[len+1];
  enif_get_atom(env, argv[0], name, len+1, ERL_NIF_LATIN1);
  ret = unregister_tree(name);
  if (ret)
    return raise(env, ret);
  else
    return enif_make_atom(env, "ok");
}

static ERL_NIF_TERM whereis_1(ErlNifEnv* env, int argc,
                              const ERL_NIF_TERM argv[])
{
  unsigned int len;
  registry_t *entry;
  ERL_NIF_TERM result;

  if (!enif_get_atom_length(env, argv[0], &len, ERL_NIF_LATIN1))
    return raise(env, EINVAL);

  char name[len+1];
  enif_get_atom(env, argv[0], name, len+1, ERL_NIF_LATIN1);
  enif_rwlock_rlock(registry_lock);
  HASH_FIND_STR(registry, name, entry);
  if (entry)
    result = enif_make_resource(env, entry->state);
  else
    result = enif_make_atom(env, "undefined");
  enif_rwlock_runlock(registry_lock);

  return result;
}

static ERL_NIF_TERM registered_0(ErlNifEnv* env, int argc,
                                 const ERL_NIF_TERM argv[])
{
  registry_t *entry, *iter;
  ERL_NIF_TERM result = enif_make_list(env, 0);

  enif_rwlock_rlock(registry_lock);
  HASH_ITER(hh, registry, entry, iter) {
    result = enif_make_list_cell(env, enif_make_atom(env, entry->name), result);
  }
  enif_rwlock_runlock(registry_lock);

  return result;
}

static ErlNifFunc nif_funcs[] =
  {
    {"new", 0, new_0},
    {"insert", 2, insert_2},
    {"delete", 2, delete_2},
    {"match", 2, match_2},
    {"refc", 2, refc_2},
    {"clear", 1, clear_1},
    {"size", 1, size_1},
    {"is_empty", 1, is_empty_1},
    {"to_list", 1, to_list_1},
    {"dump", 1, dump_1},
    {"register", 2, register_2},
    {"unregister", 1, unregister_1},
    {"whereis", 1, whereis_1},
    {"registered", 0, registered_0}
  };

ERL_NIF_INIT(mqtree, nif_funcs, load, NULL, NULL, unload)
