/* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only */
/* Copyright (c) 2023 Brett Sheffield <bacs@librecast.net> */

#include <canonpath.h>
#include <librecast_pvt.h>
#include <librecast/mdex.h>
#include <librecast/mtree.h>
#include <librecast/sync.h>
#include <dirent.h>
#include <libgen.h>
#include <string.h>
#include <sys/param.h>
#include <unistd.h>

static int mdex_addfile_root(mdex_t *mdex, const char *path, q_t *q, int flags, unsigned char *root);

char *mdex_sharepath(mdex_t *mdex, const char *restrict path)
{
	char *base, *rpath, *sharepath;
	size_t blen;
	int err = 0;
	rpath = canonpath(path);
	if (!rpath) return NULL;
	base = strdup(mdex->basedir);
	if (!base) goto err_free_rpath;
	blen = mdex->basedirlen - strlen(basename(base));
	free(base);
	sharepath = strdup(rpath + blen);
	if (!sharepath) goto err_free_rpath;
	free(rpath);
	return sharepath;
err_free_rpath:
	err = errno;
	free(rpath);
	errno = err;
	return NULL;
}

static void * mdex_stack_pop(mdex_stack_t *stack)
{
	return (stack->ptr) ? stack->stack[--(stack->ptr)] : NULL;
}

static int mdex_stack_push(mdex_stack_t *stack, void *ptr)
{
	if (!stack->ptr || stack->ptr >= MDEX_STACK_SZ) {
		stack->len += MDEX_STACK_SZ;
		stack->stack = realloc(stack->stack, stack->len * sizeof(void *));
		if (!stack->stack) return -1;
	}
	stack->stack[(stack->ptr)++] = ptr;
	return 0;
}

int mdex_get(mdex_t *mdex, unsigned char *hash, size_t hashlen, mdex_entry_t *entry)
{
	mdex_idx_t *idx;
	int rc;

	if (!mdex || !hash || !hashlen) return (errno = EINVAL), -1;
	idx = mdex->root;
	if (!idx) return (errno = ENOENT), -1;
	while ((rc = memcmp(idx->hash, hash, hashlen))) {
		idx = (rc < 0) ? idx->clown : idx->joker;
		if (!idx) return (errno = ENOENT), -1;
	}
	if (entry && idx->entry) {
		if (idx->entry->type == MDEX_ALIAS) {
			if (!memcmp(hash, idx->entry->ptr.data, hashlen))
				return (errno = ELOOP), -1;
			memcpy(hash, idx->entry->ptr.data, hashlen);
			return mdex_get(mdex, hash, hashlen, entry);
		}
		*entry = *idx->entry;
	}
	return 0;
}

void mdex_aliashash(const char *path, unsigned char *hash, size_t hashlen)
{
	hash_generic(hash, hashlen, (unsigned char *)path, strlen(path));
}

int mdex_getalias(mdex_t *mdex, const char *path, mdex_entry_t *entry)
{
	unsigned char hash[HASHSIZE];
	char *sharepath = mdex_sharepath(mdex, path);
	if (!sharepath) return -1;
	mdex_aliashash(sharepath, hash, sizeof hash);
	free(sharepath);
	return mdex_get(mdex, hash, sizeof hash, entry);
}

static int mdex_alloc(mdex_t *mdex)
{
	size_t pages;
	void *ptr;
	int err;

	if (!mdex->pagesz) return (errno = EINVAL), -1;
	if (!mdex->entries) mdex->entries++;
	pages = howmany(mdex->entries * sizeof(mdex_idx_t), mdex->pagesz);

	if (!pages) return (errno = EINVAL), -1;

	ptr = calloc(pages, mdex->pagesz);
	if (!ptr || mdex_stack_push(&mdex->alloc_stack, ptr)) goto exit_cleanup;
	if (!mdex->head) {
		mdex->head = ptr;
		mdex->root = mdex->head;
	}
	mdex->next = ptr;
	mdex->last = (mdex_idx_t *)((char *)ptr + mdex->entries * sizeof(mdex_idx_t));

	pages = howmany(mdex->entries * sizeof(mdex_entry_t), mdex->pagesz);
	ptr = calloc(pages, mdex->pagesz);
	if (!ptr || mdex_stack_push(&mdex->alloc_stack, ptr)) goto exit_cleanup;
	if (!mdex->head_entry) {
		mdex->head_entry = ptr;
		mdex->next_entry = mdex->head_entry;
	}
	mdex->next_entry = ptr;
	return 0;
exit_cleanup:
	err = errno;
	free(ptr);
	errno = err;
	return -1;
}

int mdex_put(mdex_t *mdex, unsigned char *hash, size_t hashlen, mdex_entry_t *entry)
{
	mdex_idx_t *new, *idx, **ptr;
	int rc;

	if (!mdex || !hash || !hashlen) return (errno = EINVAL), -1;
	new = mdex_stack_pop(&mdex->idx_stack);
	if (!new) {
		if (!mdex->next) {
			if (mdex_alloc(mdex) == -1) return -1;
		}
		new = mdex->next++;
		if (mdex->next >= mdex->last) mdex->next = NULL; /* force allocation */
	}
	idx = mdex->root;
	memcpy(new->hash, hash, hashlen);
	while ((rc = memcmp(idx->hash, hash, hashlen))) {
		ptr = (rc < 0) ? &idx->clown : &idx->joker;
		if (!*ptr) { *ptr = new; break; }
		idx = *ptr;
	}
	if (entry) {
		if (!rc && idx) new = idx; /* overwrite existing entry */
		new->entry = mdex_stack_pop(&mdex->entry_stack);
		if (!new->entry) new->entry = mdex->next_entry++;
		*new->entry = *entry;
		if ((new->entry->type & MDEX_FILE) && new->entry->file.file.name) {
			/* increment reference count for filename */
			new->entry->file.file.ref++;
		}
		entry->idx = idx;
	}
	return 0;
}

int mdex_alias(mdex_t *mdex, unsigned char *alias, size_t aliaslen,
		unsigned char *hash, size_t hashlen)
{
	mdex_entry_t entry = { .type = MDEX_ALIAS };
	mdex_idx_t *idx;
	int rc;

	if (!mdex || !hash || !hashlen || !alias || !aliaslen)
		return (errno = EINVAL), -1;
	idx = mdex->root;
	if (!idx) return (errno = ENOENT), -1;
	while ((rc = memcmp(idx->hash, hash, hashlen))) {
		idx = (rc < 0) ? idx->clown : idx->joker;
		if (!idx) return (errno = ENOENT), -1;
	}
	entry.ptr.size = hashlen;
	entry.ptr.data = idx->hash;
	return mdex_put(mdex, alias, aliaslen, &entry);
}

int mdex_alias_hash(mdex_t *mdex, const char *path, unsigned char *hash, size_t hashlen)
{
	unsigned char alias[HASHSIZE];
	char *sharepath = mdex_sharepath(mdex, path);
	if (!sharepath) return -1;
	hash_generic(alias, sizeof alias, (unsigned char *)sharepath, strlen(sharepath));
	free(sharepath);
	return mdex_alias(mdex, alias, sizeof alias, hash, hashlen);
}

/* build the entries hash from the path, attributes and subdirectory entries */
void mdex_hash_entry(unsigned char *hash, size_t hashlen, const char *path, struct stat *sb,
		mdex_hash_t *dir, int entries)
{
	hash_state state;
	memset(hash, 0, hashlen);
	hash_init(&state, NULL, 0, hashlen);
	if (path) {
		char *tmp = strdup(path);
		char *base = basename(tmp);
		hash_update(&state, (unsigned char *)base, strlen(base));
		free(tmp);
	}
	if (sb) {
		hash_update(&state, (unsigned char *)&sb->st_mode, sizeof(mode_t));
		hash_update(&state, (unsigned char *)&sb->st_uid, sizeof(uid_t));
		hash_update(&state, (unsigned char *)&sb->st_gid, sizeof(gid_t));
		hash_update(&state, (unsigned char *)&sb->st_mtim.tv_sec, sizeof(time_t));
		long nsec = sb->st_mtim.tv_nsec >> 3 << 3; /* reduce precision */
		hash_update(&state, (unsigned char *)&nsec, sizeof(long));
	}
	for (int i = 0; i < entries; i++) hash_update(&state, dir[i], hashlen);
	hash_final(&state, hash, hashlen);
}

void mdex_tree_hash_sb(unsigned char *hash, size_t hashlen, mtree_t *mtree, size_t n, struct stat *sb,
		const char *path)
{
	hash_state state;
	memset(hash, 0, hashlen);
	hash_init(&state, NULL, 0, hashlen);
	if (path) {
		/* remove first segment of sharepath (the "sharename") before
		 * including in hash (path has already been canonicalised) */
		const char *relsharepath = strchr(path, '/');
		if (!relsharepath) relsharepath = path;
		hash_update(&state, (unsigned char *)relsharepath, strlen(relsharepath));
	}
	if (mtree && mtree->len) {
		/* root hash + key */
		hash_update(&state, mtree_nnode(mtree, 0), hashlen);
		hash_update(&state, (unsigned char *)&n, sizeof(n));
	}
	if (sb) {
		hash_update(&state, (unsigned char *)&sb->st_mode, sizeof(mode_t));
		hash_update(&state, (unsigned char *)&sb->st_uid, sizeof(uid_t));
		hash_update(&state, (unsigned char *)&sb->st_gid, sizeof(gid_t));
		hash_update(&state, (unsigned char *)&sb->st_mtim.tv_sec, sizeof(time_t));
		long nsec = sb->st_mtim.tv_nsec >> 3 << 3; /* reduce precision */
		hash_update(&state, (unsigned char *)&nsec, sizeof(long));
	}
	hash_final(&state, hash, hashlen);
}

int mdex_tree_hash(unsigned char *hash, size_t hashlen, mtree_t *mtree, size_t n)
{
	return hash_generic_key(hash, hashlen, mtree_nnode(mtree, 0), HASHSIZE,
			(unsigned char *)&n, sizeof(n));
}

static int mdex_tree(mdex_t *mdex, mtree_t *mtree, const char *path, struct stat *sb)
{
	net_tree_t *data;
	unsigned char hash[HASHSIZE];
	char *sharepath = NULL;
	mdex_entry_t entry = {0};
	size_t hashsz = 0, off = 0, len, namesz = 0;
	int rc = -1;

	entry.type = MDEX_PTR | MDEX_OTI;
	if (mtree && mtree->len) {
		entry.ptr.size = HASHSIZE * mtree->nodes;
		hashsz = mtree->nodes * HASHSIZE;
	}
	if (path) {
		sharepath = mdex_sharepath(mdex, path);
		if (!sharepath) return -1;
		namesz = strlen(sharepath);
		if (namesz > UINT16_MAX) {
			free(sharepath);
			return (errno = ENAMETOOLONG), -1;
		}
	}
	len = sizeof (net_tree_t) + hashsz + namesz;
	data = malloc(len);
	if (!data || mdex_stack_push(&mdex->alloc_stack, data)) {
		free(data);
		free(sharepath);
		return -1;
	}
	memset(data, 0, len);
	if (mtree && mtree->len) {
		data->size = htobe64(mtree->len);
		memcpy(data->tree + namesz, mtree->tree, hashsz);
	}
	if (namesz) {
		data->namesz = htons(namesz);
		memcpy(data->tree, sharepath, namesz);
	}
	if (sb) {
		data->atime = sb->st_atim;
		data->mtime = sb->st_mtim;
		data->mode = sb->st_mode;
		data->uid = sb->st_uid;
		data->gid = sb->st_gid;
	}
	if (hashsz) memcpy(data->tree + namesz, mtree->tree, hashsz);
	size_t parts = howmany(len, MTREE_CHUNKSIZE);
	for (size_t z = 0; z < parts; z++) {
		mdex_tree_hash_sb(hash, HASHSIZE, mtree, z, sb, sharepath);
		entry.ptr.size = MIN(MTREE_CHUNKSIZE, len);
		entry.ptr.data = (char *)data + off;
		rc = mdex_put(mdex, hash, HASHSIZE, &entry);
		if (rc == -1) break;
		len -= MTREE_CHUNKSIZE, off += MTREE_CHUNKSIZE;
	}
	if (namesz) free(sharepath);
	return rc;
}

int mdex_add(mdex_t *mdex, void *data, size_t len, q_t *q, int flags)
{
	(void) flags; /* unused */
	mtree_t *mtree;
	unsigned char *hash;
	mdex_entry_t entry = {0};
	size_t min, max, off = 0;
	int err;

	mtree = malloc(sizeof (mtree_t));
	if (!mtree) return -1;
	if (mtree_init(mtree, len) == -1) goto exit_cleanup;
	if (mtree_build(mtree, data, q) == -1) goto exit_cleanup;

	/* index mtree */
	if (mdex_tree(mdex, mtree, NULL, NULL) == -1) goto exit_cleanup;

	/* write entries for each chunk */
	entry.type = MDEX_PTR;
	min = mtree_subtree_data_min(mtree->base, 0);
	max = min + mtree->chunks - 1;
	for (size_t z = min; z <= max; z++) {
		hash = mtree_nnode(mtree, z);
		entry.ptr.size = MIN(MTREE_CHUNKSIZE, len);
		entry.ptr.data = (char *)data + off;
		mdex_put(mdex, hash, HASHSIZE, &entry);
		len -= MTREE_CHUNKSIZE; off += MTREE_CHUNKSIZE;
	}
	mtree_free(mtree);
	free(mtree);
	return 0;
exit_cleanup:
	err = errno;
	mtree_free(mtree);
	free(mtree);
	errno = err;
	return -1;
}

static int mdex_createdir(mdex_t *mdex, const char *path, int flags, mdex_hash_t dir[],
		int entries, struct stat *sb, mdex_hash_t hash, int depth)
{
	mdex_entry_t entry = {0};
	int err = 0;

	/* do not include path in hash at top level */
	const char *hashpath = (depth) ? path : NULL;
	struct stat *ssb = (depth) ? sb: NULL;

	/* store path */
	entry.file.file.name = mdex_sharepath(mdex, path);
	if (!entry.file.file.name) return -1;
	if (mdex_stack_push(&mdex->alloc_stack, entry.file.file.name)) {
		goto err_free_path;
	}
	/* copy stat buffer */
	entry.file.file.sb = malloc(sizeof(struct stat));
	if (mdex_stack_push(&mdex->alloc_stack, entry.file.file.sb)) {
		err = errno;
		free(entry.file.file.sb);
		goto err_free_stack_path;
	}
	*entry.file.file.sb = *sb;
	entry.type = MDEX_DIR | MDEX_OTI | (flags & MDEX_RAND);
	entry.file.off = 0;
	entry.file.size = entries;
	entry.file.file.dir = dir;
	mdex_hash_entry(hash, sizeof(mdex_hash_t), hashpath, ssb, dir, entries);

	/* write directory entry */
	if (mdex_put(mdex, hash, HASHSIZE, &entry)) {
		perror("mdex_put");
	}

	/* write alias of directory name */
	if (mdex_alias_hash(mdex, path, hash, HASHSIZE)) {
		perror("mdex_alias");
	}
	return 0;
err_free_stack_path:
	free(mdex_stack_pop(&mdex->alloc_stack));
	errno = err;
	return -1;
err_free_path:
	err = errno;
	free(entry.file.file.name);
	errno = err;
	return -1;
}

static int scandirfilter(const struct dirent *dir)
{
	/* filter current and parent directories */
	return !(!strcmp(dir->d_name, ".") || !strcmp(dir->d_name, ".."));
}

/* alphasort_c - locale-independent sort
 * alphasort(3) uses strcoll(3) which is affected by locale
 * we need to be sure we get the same order regardless of locale */
static int alphasort_c(const struct dirent **a, const struct dirent **b)
{
	return strcmp((*a)->d_name, (*b)->d_name);
}

static int mdex_directory(mdex_t *mdex, const char *path, q_t *q, int flags, struct stat *sb, int depth,
		unsigned char *root)
{
	char dname[PATH_MAX]; // FIXME PATH_MAX
	mdex_hash_t *dir = NULL;
	struct dirent **namelist;
	int e, err = 0, n = 0;

	if (mdex->debug & MDEX_DEBUG_FILE) {
		char *sharepath = mdex_sharepath(mdex, path);
		if (sharepath) {
			fprintf(mdex->stream, "%s\n", sharepath);
			free(sharepath);
		}
	}
	e = scandir(path, &namelist, scandirfilter, alphasort_c);
	if (e == -1) return -1;
	else if (!e) goto empty_directory;
	dir = calloc(e, sizeof *dir); /* allocate memory for directory object */
	/* for any errors we set err but continue loop to free namelist entries */
	if (!dir) err = errno;
	else if (mdex_stack_push(&mdex->alloc_stack, dir)) err = errno;
	while (e--) {
		if (!err && snprintf(dname, sizeof dname, "%s/%s", path, namelist[e]->d_name) > 0) {
			struct stat lsb;
			if (lstat(dname, &lsb) == 0) {
				switch (namelist[e]->d_type) {
				case DT_DIR:
					/* without MDEX_RECURSE set, we still
					 * recurse into mdex_directory() once to
					 * write the top level directories */
					if (mdex->maxdepth != -1 && depth >= mdex->maxdepth) break;
					if (mdex_directory(mdex, dname, q, flags, &lsb, depth+1, dir[n++])) {
						err = errno;
						break;
					}
					break;
				case DT_LNK:
				case DT_REG:
					if (depth && !(flags & MDEX_RECURSE)) break;
					if (mdex_addfile_root(mdex, dname, q, MDEX_ALIAS, dir[n++]) == -1) {
						err = errno;
						break;
					}
					break;
				/* case DT_CHR: */
				/* case DT_BLK: */
				/* case DT_SOCK: */
				/* case DT_FIFO: */
				/* case DT_WHT: */
				/* case DT_UNKNOWN: */
				default:
					break;
				}
			}
		}
		free(namelist[e]);
	}
	free(namelist);
	if (err) return (errno = err), -1;
empty_directory:
	return mdex_createdir(mdex, path, flags, dir, n, sb, root, depth);
}

static int mdex_symlink(mdex_t *mdex, const char *sharepath, const char *path, struct stat *sb, int flags,
		unsigned char *hash)
{
	mdex_entry_t entry = {0};
	int rc = -1, err = 0;

	/* store path */
	entry.file.file.name = strdup(sharepath);
	if (!entry.file.file.name) return -1;
	if (mdex_stack_push(&mdex->alloc_stack, entry.file.file.name)) {
		err = errno; goto err_free_path;
	}
	/* copy stat buffer */
	entry.file.file.sb = malloc(sizeof(struct stat));
	if (mdex_stack_push(&mdex->alloc_stack, entry.file.file.sb)) {
		err = errno; goto err_free_sb;
	}
	*entry.file.file.sb = *sb;
	entry.type = MDEX_LINK | MDEX_OTI | (flags & MDEX_RAND);
	mdex_tree_hash_sb(hash, HASHSIZE, NULL, 0, sb, sharepath);
	/* write symlink entry */
	if (mdex_put(mdex, hash, HASHSIZE, &entry)) {
		err = errno; goto err_free_stack_path;
	}
	/* write alias of symlink name */
	return mdex_alias_hash(mdex, path, hash, HASHSIZE);
err_free_sb:
	free(entry.file.file.sb);
err_free_stack_path:
	free(mdex_stack_pop(&mdex->alloc_stack));
err_free_path:
	free(entry.file.file.name);
	if (err) errno = err;
	return rc;
}

/* map file, create mtree and add to mdex, returning copy of root hash if root != NULL */
static int mdex_addfile_root(mdex_t *mdex, const char *path, q_t *q, int flags, unsigned char *root)
{
	struct stat sb = {0};
	char *data = NULL, *rpath, *sharepath;
	mtree_t *mtree = NULL;
	unsigned char *hash;
	mdex_entry_t entry = {0};
	size_t len, min, max, sz = 0;
	int err;

	rpath = canonpath(path);
	if (!rpath) return -1;
	sharepath = mdex_sharepath(mdex, rpath);
	if (!sharepath) goto exit_cleanup;
	data = lc_mmapfile(rpath, &sz, PROT_READ, MAP_PRIVATE, 0, &sb);
	mdex->files++;
	if (data == MAP_FAILED) {
		switch (errno) {
			case ENODATA:
				goto store_path;
			case EMLINK:
				err = mdex_symlink(mdex, sharepath, path, &sb, flags, root);
				break;
			case EISDIR:
				if (!(flags & MDEX_RECURSE)) mdex->maxdepth = 1;
				err = mdex_directory(mdex, rpath, q, flags, &sb, 0, root);
				break;
			default:
				goto exit_cleanup;
		}
		free(sharepath);
		free(rpath);
		return err;
	}
	free(rpath);
	rpath = NULL;
	mtree = malloc(sizeof (mtree_t));
	if (!mtree) goto exit_cleanup;
	if (mtree_init(mtree, sz) == -1) goto exit_cleanup;
	if (mtree_build(mtree, data, q) == -1) goto exit_cleanup;
store_path:
	/* index mtree */
	if (mdex_tree(mdex, mtree, path, &sb) == -1) goto exit_cleanup;

	/* store path */
	if (mdex->debug & MDEX_DEBUG_FILE) fprintf(mdex->stream, "%s\n", sharepath);
	entry.file.file.name = sharepath;
	if (!entry.file.file.name) goto exit_cleanup;
	if (mdex_stack_push(&mdex->alloc_stack, entry.file.file.name))
		goto exit_cleanup;

	/* copy stat buffer */
	entry.file.file.sb = malloc(sizeof sb);
	if (!entry.file.file.sb) goto exit_cleanup;
	if (mdex_stack_push(&mdex->alloc_stack, entry.file.file.sb))
		goto exit_cleanup;
	*entry.file.file.sb = sb;

	/* write alias for root (hash filename) */
	if (flags & MDEX_ALIAS) {
		mdex_tree_hash_sb(root, HASHSIZE, mtree, 0, &sb, sharepath);
		if (mdex_alias_hash(mdex, path, root, HASHSIZE) == -1)
			goto exit_cleanup;
		if (!mtree) return 0; /* empty file */
	}

	/* write entries for each chunk */
	entry.type = MDEX_FILE | (flags & MDEX_RAND);
	entry.file.off = 0;
	len = mtree->len;
	min = mtree_subtree_data_min(mtree->base, 0);
	max = min + mtree->chunks - 1;
	for (size_t z = min; z <= max; z++) {
		hash = mtree_nnode(mtree, z);
		entry.file.size = MIN(MTREE_CHUNKSIZE, len);
		mdex_put(mdex, hash, HASHSIZE, &entry);
		len -= MTREE_CHUNKSIZE;
		entry.file.off += MTREE_CHUNKSIZE;
	}
	mtree_free(mtree);
	free(mtree);
	munmap(data, sz);
	return 0;
exit_cleanup:
	err = errno;
	if (mtree) {
		mtree_free(mtree);
		free(mtree);
	}
	if (data) munmap(data, sz);
	free(sharepath);
	free(rpath);
	errno = err;
	return -1;
}

static int mdex_basedir(mdex_t *mdex, const char *basedir)
{
	mdex->basedir = canonpath(basedir);
	if (!mdex->basedir) return -1;
	mdex->basedirlen = strlen(mdex->basedir);
	char *tmp = strdup(mdex->basedir);
	if (!tmp) goto err_free_basedir;
	mdex->rootdir = strdup(dirname(tmp));
	if (!mdex->rootdir) goto err_free_tmp;
	mdex->rootdirlen = strlen(mdex->rootdir);
	free(tmp);
	return 0;
err_free_tmp:
	free(tmp);
err_free_basedir:
	free(mdex->basedir);
	return -1;
}

int mdex_addfile(mdex_t *mdex, const char *path, q_t *q, int flags)
{
	unsigned char root[HASHSIZE] = "";
	if (mdex_basedir(mdex, path) == -1) return -1;
	return mdex_addfile_root(mdex, path, q, flags, root);
}

/* convenience API function to return the root hash of a directory from the
 * supplied mdex */
int mdex_directory_root(mdex_t *mdex, const char *dir, unsigned char *root)
{
	mdex_entry_t entry = {0};
	if (mdex_getalias(mdex, dir, &entry)) return -1;
	assert(entry.idx);
	memcpy(root, entry.idx->hash, HASHSIZE);
	return 0;
}

/* calculate the directory root hash */
int mdex_get_directory_root(const char *dir, unsigned char *root)
{
	mdex_t *mdex;
	int rc = -1;
	mdex = mdex_init(0);
	if (!mdex) return -1;
	if (mdex_basedir(mdex, dir) == -1) goto err_free_mdex;
	rc = mdex_addfile_root(mdex, dir, NULL, MDEX_RECURSE, root);
err_free_mdex:
	mdex_free(mdex);
	return rc;
}

int mdex_del(mdex_t *mdex, unsigned char *hash, size_t hashlen)
{
	mdex_idx_t *idx;
	mdex_idx_t **ptr = NULL;
	int rc;

	if (!mdex || !hash || !hashlen) return (errno = EINVAL), -1;
	idx = mdex->root;
	while ((rc = memcmp(idx->hash, hash, hashlen))) {
		ptr = (rc < 0) ? &idx->clown : &idx->joker;
		if (!*ptr) return (errno = ENOENT), -1;
		idx = *ptr;
	}
	if ((idx->entry->type & MDEX_FILE) && idx->entry->file.file.name) {
		/* decrement reference count for filename */
		if (--idx->entry->file.file.ref == 0) {
			free(idx->entry->file.file.name);
		}
		mdex->files--;
	}
	/* push deleted idx + entry onto the stack */
	if (mdex_stack_push(&mdex->idx_stack, idx)) return -1;
	if (mdex_stack_push(&mdex->entry_stack, idx->entry)) return -1;
	if ((!idx->clown) && (!idx->joker)) {
		*ptr = NULL;       /* no children, just remove */
	}
	else if (idx->clown && !idx->joker) {
		*ptr = idx->clown; /* hoist left child */
	}
	else if (!idx->clown && idx->joker) {
		*ptr = idx->joker; /* hoist right child */
	}
	else {                     /* two children */
		/* find successor (smallest node in right subtree) */
		mdex_idx_t *s;
		for (s = idx->joker, ptr = &idx->joker; s && s->clown; ptr = &s->clown, s = s->clown);
		memcpy(idx->hash, s->hash, hashlen);
		idx->entry = s->entry;
		*ptr = s->joker; /* delete successor from tree */
	}
	return 0;
}

mdex_t *mdex_init(size_t entries)
{
	mdex_t *mdex = calloc(1, sizeof (mdex_t));
	if (mdex) {
		mdex->pagesz = (size_t)sysconf(_SC_PAGESIZE); /* cannot be < 1 */
		mdex->entries = entries;
		if (entries) mdex_alloc(mdex);
		mdex->maxdepth = MDEX_DEFAULT_DEPTH;
	}
	return mdex;
}

void mdex_free(mdex_t *mdex)
{
	void *ptr;
	while ((ptr = mdex_stack_pop(&mdex->alloc_stack))) free(ptr);
	free(mdex->alloc_stack.stack);
	free(mdex->entry_stack.stack);
	free(mdex->idx_stack.stack);
	free(mdex->basedir);
	free(mdex->rootdir);
	free(mdex);
}
