/* sdb - MIT - Copyright 2011-2026 - pancake */

#include <fcntl.h>
#include <errno.h>
#include <sys/stat.h>
#include "sdb_private.h"

static inline SdbKv *next_kv(HtPP *ht, SdbKv *kv) {
	return (SdbKv *)((uintptr_t)kv + ht->opt.elem_size);
}

#define BUCKET_FOREACH(ht, bt, j, kv)					\
	for ((j) = 0, (kv) = (SdbKv *)(bt)->arr; j < (bt)->count; (j)++, (kv) = next_kv (ht, kv))

#define BUCKET_FOREACH_SAFE(ht, bt, j, count, kv)			\
	if ((bt)->arr)							\
		for ((j) = 0, (kv) = (SdbKv *)(bt)->arr, (count) = (ht)->count; \
		     (j) < (bt)->count;					\
		     (j) = (count) == (ht)->count? j + 1: j, (kv) = (count) == (ht)->count? next_kv (ht, kv): kv, (count) = (ht)->count)

static inline int nextcas(SdbKv const *kv) {
	if (!kv->cas) {
		return 1;
	}
	return kv->cas + 1;
}

SDB_API Sdb* sdb_new0(void) {
	return sdb_new (NULL, NULL, 0);
}

SDB_API Sdb* sdb_new(const char *path, const char *name, int lock) {
	Sdb* s = (Sdb*)sdb_gh_calloc (1, sizeof (Sdb));
	if (!s) {
		return NULL;
	}
	s->db.fd = -1;
	s->fd = -1;
	s->journal = -1;
	s->refs = 1;
	s->ht = sdb_ht_new ();
	if (!s->ht) {
		sdb_gh_free (s);
		return NULL;
	}
	if (path && !*path) {
		path = NULL;
	}
	if (name && *name && strcmp (name, "-")) {
		if (path && *path) {
			size_t plen = strlen (path);
			size_t nlen = strlen (name);
			s->dir = (char *)sdb_gh_malloc (plen + nlen + 2);
			if (!s->dir) {
				sdb_gh_free (s);
				return NULL;
			}
			memcpy (s->dir, path, plen);
			s->dir[plen] = '/';
			memcpy (s->dir + plen + 1, name, nlen + 1);
			s->path = sdb_strdup (path);
		} else {
			s->dir = sdb_strdup (name);
		}
		if (lock && !sdb_lock_db (s, lock)) {
			goto fail;
		}
		if (sdb_open (s, s->dir) == -1) {
			s->last = s->timestamped? sdb_now (): 0LL;
			// TODO: must fail if we cant open for write in sync
		}
		s->name = sdb_strdup (name);
	} else {
		s->last = s->timestamped? sdb_now (): 0LL;
		s->fd = -1;
	}
	s->journal = -1;
	s->fdump = -1;
	s->depth = 0;
	s->ndump = NULL;
	s->ns = ls_new (); // TODO: should be NULL
	if (!s->ns) {
		goto fail;
	}
	s->ns->free = NULL;
	cdb_init (&s->db, s->fd);
	return s;
fail:
	sdb_unlock_db (s);
	if (s->fd != -1) {
		close (s->fd);
		s->fd = -1;
	}
	sdb_gh_free (s->dir);
	sdb_gh_free (s->name);
	sdb_gh_free (s->path);
	sdb_gh_free (s);
	return NULL;
}

// XXX: this is wrong. stuff not stored in memory is lost
SDB_API void sdb_file(Sdb* s, const char *dir) {
	const int lock = s->lock;
	sdb_unlock_db (s);
	sdb_gh_free (s->dir);
	s->dir = (dir && *dir)? sdb_strdup (dir): NULL;
	sdb_lock_db (s, lock);
}

static bool sdb_merge_cb(void *user, const char *k, const char *v) {
	sdb_set ((Sdb*)user, k, v, 0);
	return true;
}

SDB_API bool sdb_merge(Sdb* d, Sdb *s) {
	return sdb_foreach (s, sdb_merge_cb, d);
}

SDB_API bool sdb_isempty(Sdb *s) {
	if (s) {
		if (s->db.fd != -1) {
			sdb_dump_begin (s);
			while (sdb_dump_hasnext (s)) {
				return false;
			}
		}
		if (s->ht && s->ht->count > 0) {
			return false;
		}
	}
	return true;
}

SDB_API int sdb_count(Sdb *s) {
	int count = 0;
	if (s) {
		if (s->db.fd != -1) {
			sdb_dump_begin (s);
			while (sdb_dump_hasnext (s)) {
				count++;
			}
		}
		if (s->ht) {
			count += s->ht->count;
		}
	}
	return count;
}

static void sdb_fini(Sdb* s, bool donull) {
	if (!s) {
		return;
	}
	sdb_hook_free (s);
	cdb_fini (&s->db);
	sdb_unlock_db (s);
	sdb_ns_free (s);
	s->refs = 0;
	sdb_journal_close (s);
	sdb_gh_free (s->name);
	sdb_gh_free (s->path);
	ls_free (s->ns);
	sdb_ht_free (s->ht);
	if (s->fd != -1) {
		close (s->fd);
		s->fd = -1;
	}
	sdb_gh_free (s->ndump);
	sdb_gh_free (s->dir);
	sdb_gh_free (sdbkv_value (&s->tmpkv));
	s->tmpkv.base.value_len = 0;
	if (donull) {
		memset (s, 0, sizeof (Sdb));
	}
}

SDB_API bool sdb_free(Sdb* s) {
	if (s && s->ht && s->refs) {
		s->refs--;
		if (s->refs < 1) {
			s->refs = 0;
			sdb_fini (s, false);
			s->ht = NULL;
			sdb_gh_free (s);
			return true;
		}
	}
	return false;
}

SDB_API const char *sdb_const_get_len(Sdb* s, const char *key, int *vlen, ut32 *cas) {
	ut32 pos, len;
	ut64 now = 0LL;
	bool found;

	if (cas) {
		*cas = 0;
	}
	if (vlen) {
		*vlen = 0;
	}
	if (!s || !key) {
		return NULL;
	}
	// TODO: optimize, iterate once
	size_t keylen = strlen (key);

	/* search in memory */
	if (s->ht) {
		SdbKv *kv = (SdbKv*) sdb_ht_find_kvp (s->ht, key, &found);
		if (found) {
			char *kvv = sdbkv_value (kv);
			if (!kvv || !*kvv) {
				return NULL;
			}
			if (s->timestamped && kv->expire) {
				if (!now) {
					now = sdb_now ();
				}
				if (now > kv->expire) {
					sdb_unset (s, key, 0);
					return NULL;
				}
			}
			if (cas) {
				*cas = kv->cas;
			}
			if (vlen) {
				*vlen = sdbkv_value_len (kv);
			}
			return sdbkv_value (kv);
		}
	}
	/* search in gperf */
	if (s->gp && s->gp->get) {
		return s->gp->get (key);
	}
	/* search in disk */
	if (s->fd == -1) {
		return NULL;
	}
	(void) cdb_findstart (&s->db);
	if (!s->ht || cdb_findnext (&s->db, s->ht->opt.hashfn (key), key, keylen) < 1) {
		return NULL;
	}
	len = cdb_datalen (&s->db);
	if (len < SDB_MIN_VALUE || len >= SDB_MAX_VALUE) {
		return NULL;
	}
	if (vlen) {
		*vlen = len;
	}
	pos = cdb_datapos (&s->db);
	return s->db.map + pos;
}

SDB_API const char *sdb_const_get(Sdb* s, const char *key, ut32 *cas) {
	return sdb_const_get_len (s, key, NULL, cas);
}

// Format a key into a SDB_MAX_KEY-sized buffer; fails on truncation or error
SDB_IPI bool sdb_vfmtkey(char key[SDB_MAX_KEY], const char *fmt, va_list ap) {
	if (!fmt) {
		return false;
	}
	int len = vsnprintf (key, SDB_MAX_KEY, fmt, ap);
	return len >= 0 && len < SDB_MAX_KEY;
}

SDB_IPI const char *sdb_const_vgetf(Sdb *s, ut32 *cas, const char *fmt, va_list ap) {
	char key[SDB_MAX_KEY];
	if (sdb_vfmtkey (key, fmt, ap)) {
		return sdb_const_get (s, key, cas);
	}
	if (cas) {
		*cas = 0;
	}
	return NULL;
}

SDB_API const char *sdb_const_getf(Sdb *s, ut32 *cas, const char *fmt, ...) {
	va_list ap;
	va_start (ap, fmt);
	const char *value = sdb_const_vgetf (s, cas, fmt, ap);
	va_end (ap);
	return value;
}

SDB_API char *sdb_get_len(Sdb* s, const char *key, int *vlen, ut32 *cas) {
	const char *value = sdb_const_get_len (s, key, vlen, cas);
	return value ? sdb_strdup (value) : NULL;
}

SDB_API char *sdb_get(Sdb* s, const char *key, ut32 *cas) {
	return sdb_get_len (s, key, NULL, cas);
}

SDB_API char *sdb_getf(Sdb *s, ut32 *cas, const char *fmt, ...) {
	va_list ap;
	va_start (ap, fmt);
	const char *value = sdb_const_vgetf (s, cas, fmt, ap);
	va_end (ap);
	return value? sdb_strdup (value): NULL;
}

SDB_API char *sdb_nget(Sdb* s, ut64 nkey, ut32 *cas) {
	char buf[SDB_NUM_BUFSZ];
	const char *key = sdb_itoa (nkey, 16, buf, sizeof (buf));
	return sdb_get_len (s, key, NULL, cas);
}

SDB_API bool sdb_unset(Sdb* s, const char *key, ut32 cas) {
	return key? sdb_set (s, key, "", cas): 0;
}

SDB_IPI bool sdb_vsetf(Sdb *s, const char *val, ut32 cas, const char *fmt, va_list ap) {
	char key[SDB_MAX_KEY];
	return sdb_vfmtkey (key, fmt, ap)? sdb_set (s, key, val, cas): 0;
}

SDB_API bool sdb_unsetf(Sdb *s, ut32 cas, const char *fmt, ...) {
	va_list ap;
	va_start (ap, fmt);
	int res = sdb_vsetf (s, "", cas, fmt, ap);
	va_end (ap);
	return res;
}

SDB_API bool sdb_nunset(Sdb* s, ut64 nkey, ut32 cas) {
	return sdb_nset (s, nkey, "", cas);
}

SDB_API bool sdb_rename(Sdb *s, const char *oldkey, const char *newkey, ut32 cas) {
	if (!s || !oldkey || !newkey) {
		return false;
	}
	char *value = sdb_get (s, oldkey, NULL);
	if (!value) {
		return false;
	}
	if (!sdb_unset (s, oldkey, cas)) {
		sdb_gh_free (value);
		return false;
	}
	return sdb_set_owned (s, newkey, value, 0) != 0;
}

typedef struct {
	const char *prefix;
	size_t len;
	SdbList *keys;
} RenamePrefixData;

static bool rename_prefix_cb(void *user, const char *k, const char *v) {
	RenamePrefixData *rpd = (RenamePrefixData *)user;
	(void)v;
	if (strncmp (k, rpd->prefix, rpd->len)) {
		return true;
	}
	char *key = sdb_strdup (k);
	if (!key) {
		return false;
	}
	if (!ls_append (rpd->keys, key)) {
		sdb_gh_free (key);
		return false;
	}
	return true;
}

static char *rename_prefix_key(const char *newprefix, const char *suffix) {
	size_t plen = strlen (newprefix);
	size_t slen = strlen (suffix);
	if (SZT_ADD_OVFCHK (plen, slen) || SZT_ADD_OVFCHK (plen + slen, 1)) {
		return NULL;
	}
	char *key = (char *)sdb_gh_malloc (plen + slen + 1);
	if (key) {
		memcpy (key, newprefix, plen);
		memcpy (key + plen, suffix, slen + 1);
	}
	return key;
}

SDB_API int sdb_rename_prefix(Sdb *s, const char *oldprefix, const char *newprefix) {
	if (!s || !oldprefix || !newprefix) {
		return 0;
	}
	SdbList *keys = ls_newf ((SdbListFree)sdb_gh_free);
	if (!keys) {
		return 0;
	}
	RenamePrefixData rpd = {
		.prefix = oldprefix,
		.len = strlen (oldprefix),
		.keys = keys
	};
	if (!sdb_foreach (s, rename_prefix_cb, &rpd)) {
		ls_free (keys);
		return 0;
	}
	int count = 0;
	SdbListIter *iter;
	char *key;
	ls_foreach_cast (keys, iter, char *, key) {
		char *newkey = rename_prefix_key (newprefix, key + rpd.len);
		if (newkey) {
			if (sdb_rename (s, key, newkey, 0)) {
				count++;
			}
			sdb_gh_free (newkey);
		}
	}
	ls_free (keys);
	return count;
}

// alias for '-key=str'.. '+key=str' concats
SDB_API bool sdb_uncat(Sdb *s, const char *key, const char *value, ut32 cas) {
	// remove 'value' from current key value.
	ut32 kas = 0;
	int vlen = 0, valen;
	char *p, *v = sdb_get_len (s, key, &vlen, &kas);
	int mod = 0;
	if (!v || !key || !value || (cas && kas != cas)) {
		sdb_gh_free (v);
		return 0;
	}
	valen = strlen (value);
	if (valen > 0) {
		while ((p = strstr (v, value))) {
			memmove (p, p + valen, strlen (p + valen) + 1);
			mod = 1;
		}
	}
	if (mod) {
		return sdb_set_owned (s, key, v, cas);
	}
	sdb_gh_free (v);
	return 0;
}

SDB_API bool sdb_concat(Sdb *s, const char *key, const char *value, ut32 cas) {
	int kl, vl;
	const char *p;
	char *o;
	if (!s || !key || !*key || !value || !*value) {
		return 0;
	}
	p = sdb_const_get_len (s, key, &kl, 0);
	if (!p) {
		return sdb_set (s, key, value, cas);
	}
	vl = strlen (value);
	o = (char *)sdb_gh_malloc (kl + vl + 1);
	if (o) {
		memcpy (o, p, kl);
		memcpy (o + kl, value, vl + 1);
		return sdb_set_owned (s, key, o, cas);
	}
	return 0;
}

// set if not defined
SDB_API bool sdb_add(Sdb* s, const char *key, const char *val, ut32 cas) {
	if (sdb_exists (s, key)) {
		return 0;
	}
	return sdb_set (s, key, val, cas);
}

SDB_API bool sdb_nadd(Sdb* s, ut64 nkey, const char *val, ut32 cas) {
	char buf[SDB_NUM_BUFSZ];
	const char *key = sdb_itoa (nkey, 16, buf, sizeof (buf));
	return sdb_add (s, key, val, cas);
}

SDB_API bool sdb_existsf(Sdb *s, const char *fmt, ...) {
	char key[SDB_MAX_KEY];
	va_list ap;
	va_start (ap, fmt);
	bool ok = sdb_vfmtkey (key, fmt, ap);
	va_end (ap);
	return ok? sdb_exists (s, key): false;
}

SDB_API bool sdb_exists(Sdb* s, const char *key) {
	ut32 pos;
	char ch;
	bool found;
	if (!s || !key) {
		return false;
	}
	size_t klen = strlen (key);
	SdbKv *kv = (SdbKv*)sdb_ht_find_kvp (s->ht, key, &found);
	if (found && kv) {
		char *v = sdbkv_value (kv);
		return v && *v;
	}
	if (s->fd == -1) {
		return false;
	}
	(void)cdb_findstart (&s->db);
	if (cdb_findnext (&s->db, sdb_hash (key), key, klen)) {
		pos = cdb_datapos (&s->db);
		cdb_read (&s->db, &ch, 1, pos);
		return ch != 0;
	}
	return false;
}

SDB_API int sdb_open_gperf(Sdb *s, SdbGperf *gp) {
	if (!s || !gp) {
		return -1;
	}
	s->gp = gp;
	return 0;
}

static int sdb_open_text(Sdb *s, const char *file) {
	if (!sdb_text_load (s, file)) {
		return -1;
	}
	return s->fd;
}

SDB_API int sdb_open(Sdb *s, const char *file) {
        struct stat st;
	if (!s) {
		return -1;
	}
	if (file) {
		if (sdb_text_check (s, file)) {
			return sdb_open_text (s, file);
		}
		if (s->fd != -1) {
			close (s->fd);
			s->fd = -1;
		}
		s->db.fd = -1; // aliases s->fd, cdb_init must not close it again
		s->fd = sdb_file_open (file, O_RDONLY, 0);
		if (file != s->dir) {
			sdb_gh_free (s->dir);
			s->dir = sdb_strdup (file);
			s->path = NULL; // TODO: path is important
		}
	}
	s->last = 0LL;
	if (s->fd != -1 && fstat (s->fd, &st) != -1) {
		if ((S_IFREG & st.st_mode) != S_IFREG) {
			close (s->fd);
			s->fd = -1;
			return -1;
		}
		s->last = st.st_mtime;
	}
	if (s->fd != -1) {
		cdb_init (&s->db, s->fd);
	}
	return s->fd;
}

SDB_API void sdb_close(Sdb *s) {
	if (s) {
		if (s->fd != -1) {
			if (s->db.fd != -1 && s->db.fd == s->fd) {
				/* close db fd as well */
				s->db.fd = -1;
			}
			close (s->fd);
			s->fd = -1;
		}
		if (s->dir) {
			sdb_gh_free (s->dir);
			s->dir = NULL;
		}
		s->gp = NULL;
	}
}

SDB_API void sdb_reset(Sdb* s) {
	if (!s) {
		return;
	}
	/* ignore disk cache, file is not removed, but we will ignore
	 * its values when syncing again */
	sdb_close (s);
	/* empty memory hashtable, keeping the old one if the alloc fails */
	HtPP *ht = sdb_ht_new ();
	if (ht) {
		sdb_ht_free (s->ht);
		s->ht = ht;
	}
}

static char lastChar(const char *str) {
	size_t len = strlen (str);
	return str[(len > 0)? len - 1: 0];
}

static bool match(const char *str, const char *expr) {
	const bool startsWith = *expr == '^';
	const bool endsWith = lastChar (expr) == '$';
	size_t str_len = strlen (str);
	size_t expr_len = strlen (expr);
	if (startsWith && endsWith) {
		return str_len == expr_len - 2 && \
			!strncmp (str, expr + 1, expr_len - 2);
	}
	if (startsWith) {
		return !strncmp (str, expr + 1, expr_len - 1);
	}
	if (endsWith) {
		size_t blen = expr_len - 1;
		if (str_len <= blen) {
			return false;
		}
		const char *a = str + str_len - blen;
		return (!strncmp (a, expr, blen));
	}
	return strstr (str, expr);
}

SDB_API bool sdbkv_match(SdbKv *kv, const char *expr) {
	// TODO: add syntax to negate condition
	// TODO: add syntax to OR k/v instead of AND
	// [^]str[$]=[^]str[$]
	const char *eq = strchr (expr, '=');
	if (eq) {
		char *e = sdb_strdup (expr);
		char *ep = e + (eq - expr);
		*ep++ = 0;
		bool res = !*e || match (sdbkv_key (kv), e);
		bool res2 = !*ep || match (sdbkv_value (kv), ep);
		sdb_gh_free (e);
		return res && res2;
	}
	return match (sdbkv_key (kv), expr);
}

SDB_API SdbKv* sdbkv_new(const char *k, const char *v) {
	return sdbkv_new2 (k, strlen (k), v, strlen (v));
}

SDB_API SdbKv* sdbkv_new2(const char *k, int kl, const char *v, int vl) {
	SdbKv *kv;
	if (v) {
		if (vl >= SDB_VSZ) {
			return NULL;
		}
	} else {
		vl = 0;
	}
	if (kl >= SDB_KSZ) {
		return NULL;
	}
	kv = R_NEW0 (SdbKv);
	if (!kv) {
		return NULL;
	}
	kv->base.key_len = kl;
	kv->base.key = sdb_gh_malloc (kv->base.key_len + 1);
	if (!kv->base.key) {
		sdb_gh_free (kv);
		return NULL;
	}
	memcpy (kv->base.key, k, kv->base.key_len + 1);
	kv->base.value_len = vl;
	if (vl) {
		kv->base.value = sdb_gh_malloc (vl + 1);
		if (!kv->base.value) {
			sdb_gh_free (kv->base.key);
			sdb_gh_free (kv);
			return NULL;
		}
		memcpy (kv->base.value, v, vl + 1);
	} else {
		kv->base.value = NULL;
		kv->base.value_len = 0;
	}
	kv->cas = nextcas (kv);
	kv->expire = 0LL;
	return kv;
}

SDB_API void sdbkv_free(SdbKv *kv) {
	if (kv) {
		sdb_gh_free (sdbkv_key (kv));
		sdb_gh_free (sdbkv_value (kv));
		R_FREE (kv);
	}
}

static ut32 sdb_set_internal(Sdb* s, const char *key, char *val, bool owned, ut32 cas) {
	ut32 vlen, klen;
	bool found;
	if (!s || !key) {
		return 0;
	}
	if (!val) {
		if (owned) {
			val = sdb_strdup ("");
		} else {
			val = (char *)"";
		}
	}
	// XXX strlen computed twice.. because of check_*()
	klen = strlen (key);
	vlen = strlen (val);
	if (klen >= SDB_KSZ || vlen >= SDB_VSZ) {
		if (owned) {
			sdb_gh_free (val);
		}
		return 0;
	}
	if (s->journal != -1) {
		sdb_journal_log (s, key, val);
	}
	cdb_findstart (&s->db);
	SdbKv *kv = sdb_ht_find_kvp (s->ht, key, &found);
	if (found && sdbkv_value (kv)) {
		bool free_val = owned;
		if (cdb_findnext (&s->db, sdb_hash (key), key, klen)) {
			if (cas && kv->cas != cas) {
				if (owned) {
					sdb_gh_free (val);
				}
				return 0;
			}
			if (vlen == sdbkv_value_len (kv) && !strcmp (sdbkv_value (kv), val)) {
				sdb_hook_call (s, key, val);
				if (owned) {
					sdb_gh_free (val);
				}
				return kv->cas;
			}
			kv->cas = cas = nextcas (kv);
			if (owned) {
				kv->base.value_len = vlen;
				sdb_gh_free (kv->base.value);
				kv->base.value = val; // owned
				free_val = false;
			} else {
				if ((ut32)vlen > kv->base.value_len) {
					sdb_gh_free (kv->base.value);
					kv->base.value = sdb_gh_malloc (vlen + 1);
				}
				memcpy (kv->base.value, val, vlen + 1);
				kv->base.value_len = vlen;
			}
		} else {
			cas = nextcas (kv);
			sdb_ht_delete (s->ht, key);
		}
		sdb_hook_call (s, key, val);
		if (free_val) {
			sdb_gh_free (val);
		}
		return cas;
	}
	// empty values are also stored
	// TODO store only the ones that are in the CDB
	if (owned) {
		kv = sdbkv_new2 (key, klen, NULL, 0);
		if (kv) {
			kv->base.value = val;
			kv->base.value_len = vlen;
		}
	} else {
		kv = sdbkv_new2 (key, klen, val, vlen);
	}
	if (kv) {
		cas = kv->cas = nextcas (kv);
		sdb_ht_insert_kvp (s->ht, kv, true /*update*/);
		sdb_hook_call (s, key, val);
		sdb_gh_free (kv);
		return cas;
	}
	// kv set failed, no need to callback	sdb_hook_call (s, key, val);
	return 0;
}

SDB_API bool sdb_set_owned(Sdb* s, const char *key, char *val, ut32 cas) {
	return sdb_set_internal (s, key, val, true, cas);
}

SDB_API bool sdb_set(Sdb* s, const char *key, const char *val, ut32 cas) {
	return sdb_set_internal (s, key, (char *)val, false, cas);
}

SDB_API bool sdb_setf(Sdb *s, const char *val, ut32 cas, const char *fmt, ...) {
	va_list ap;
	va_start (ap, fmt);
	int res = sdb_vsetf (s, val, cas, fmt, ap);
	va_end (ap);
	return res;
}

SDB_API bool sdb_nset(Sdb* s, ut64 nkey, const char *val, ut32 cas) {
	char buf[SDB_NUM_BUFSZ];
	const char *key = sdb_itoa (nkey, 16, buf, sizeof (buf));
	return sdb_set_internal (s, key, (char *)val, false, cas);
}

SDB_API bool sdb_num_nset(Sdb* s, ut64 nkey, ut64 nval, ut32 cas) {
	char buf[SDB_NUM_BUFSZ];
	const char *key = sdb_itoa (nkey, 16, buf, sizeof (buf));
	return sdb_num_set (s, key, nval, cas);
}

SDB_API ut64 sdb_num_nget(Sdb *s, ut64 nkey, ut32 *cas) {
	char buf[SDB_NUM_BUFSZ];
	const char *key = sdb_itoa (nkey, 16, buf, sizeof (buf));
	return sdb_num_get (s, key, cas);
}

static bool sdb_foreach_list_cb(void *user, const char *k, const char *v) {
	SdbList *list = (SdbList *)user;
	SdbKv *kv = R_NEW0 (SdbKv);
	if (kv) {
		/* seems like some k/v are constructed in the stack and cant be used after returning */
		kv->base.key = sdb_strdup (k);
		kv->base.value = sdb_strdup (v);
		ls_append (list, kv);
		return true;
	}
	return false;
}

static int __cmp_asc(const void *a, const void *b) {
	const SdbKv *ka = (SdbKv *)a;
	const SdbKv *kb = (SdbKv *)b;
	return strcmp (sdbkv_key (ka), sdbkv_key (kb));
}

SDB_API SdbList *sdb_foreach_list(Sdb* s, bool sorted) {
	SdbList *list = ls_newf ((SdbListFree)sdbkv_free);
	sdb_foreach (s, sdb_foreach_list_cb, list);
	if (sorted) {
		ls_sort (list, __cmp_asc);
	}
	return list;
}

struct foreach_list_filter_t {
	SdbForeachCallback filter;
	SdbList *list;
};

static bool sdb_foreach_list_filter_cb(void *user, const char *k, const char *v) {
	struct foreach_list_filter_t *u = (struct foreach_list_filter_t *)user;
	SdbList *list = u->list;
	SdbKv *kv = NULL;

	if (!u->filter || u->filter (NULL, k, v)) {
		kv = R_NEW0 (SdbKv);
		if (!kv) {
			goto err;
		}
		kv->base.key = sdb_strdup (k);
		kv->base.value = sdb_strdup (v);
		if (!kv->base.key || !kv->base.value) {
			goto err;
		}
		ls_append (list, kv);
	}
	return true;
 err:
	sdbkv_free (kv);
	return false;
}

SDB_API SdbList *sdb_foreach_list_filter(Sdb* s, SdbForeachCallback filter, bool sorted) {
	struct foreach_list_filter_t u;
	SdbList *list = ls_newf ((SdbListFree)sdbkv_free);

	if (!list) {
		return NULL;
	}
	u.filter = filter;
	u.list = list;
	sdb_foreach (s, sdb_foreach_list_filter_cb, &u);
	if (sorted) {
		ls_sort (list, __cmp_asc);
	}
	return list;
}

typedef struct {
	const char *expr;
	SdbList *list;
	bool single;
} _match_sdb_user;

static bool sdb_foreach_match_cb(void *user, const char *k, const char *v) {
	_match_sdb_user *o = (_match_sdb_user*)user;
	SdbKv tkv = {{0}};
	tkv.base.key = (char *)k;
	tkv.base.value = (char *)v;
	if (sdbkv_match (&tkv, o->expr)) {
		SdbKv *kv = R_NEW0 (SdbKv);
		if (!kv) {
			return false;
		}
		kv->base.key = sdb_strdup (k);
		kv->base.value = sdb_strdup (v);
		ls_append (o->list, kv);
		if (o->single) {
			return false;
		}
	}
	return true;
}

SDB_API SdbList *sdb_foreach_match(Sdb* s, const char *expr, bool single) {
	SdbList *list = ls_newf ((SdbListFree)sdbkv_free);
	_match_sdb_user o = { expr, list, single };
	sdb_foreach (s, sdb_foreach_match_cb, &o);
	return list;
}

static int getbytes(Sdb *s, char *b, int len) {
	if (!cdb_read (&s->db, b, len, s->pos)) {
		return -1;
	}
	s->pos += len;
	return len;
}

static bool sdb_foreach_end(Sdb *s, bool result) {
	s->depth--;
	return result;
}

static bool sdb_foreach_cdb(Sdb *s, SdbForeachCallback cb, SdbForeachCallback cb2, void *user) {
	char *v = NULL;
	char k[SDB_MAX_KEY] = {0};
	bool found;
	sdb_dump_begin (s);
	while (sdb_dump_dupnext (s, k, &v, NULL)) {
		SdbKv *kv = sdb_ht_find_kvp (s->ht, k, &found);
		if (found) {
			sdb_gh_free (v);
			if (kv && sdbkv_key (kv) && sdbkv_value (kv)) {
				if (!cb (user, sdbkv_key (kv), sdbkv_value (kv))) {
					return false;
				}
				if (cb2 && !cb2 (user, k, sdbkv_value (kv))) {
					return false;
				}
			}
		} else {
			if (!cb (user, k, v)) {
				sdb_gh_free (v);
				return false;
			}
			sdb_gh_free (v);
		}
	}
	return true;
}

SDB_API bool sdb_foreach(Sdb* s, SdbForeachCallback cb, void *user) {
	if (!s) {
		return false;
	}
	if (s->gp) {
		return s->gp->foreach ((GperfForeachCallback)cb, user);
	}
	s->depth++;
	bool result = sdb_foreach_cdb (s, cb, NULL, user);
	if (!result) {
		return sdb_foreach_end (s, false);
	}

	ut32 i;
	for (i = 0; i < s->ht->size; i++) {
		HtPPBucket *bt = &s->ht->table[i];
		SdbKv *kv;
		ut32 j, count;

		BUCKET_FOREACH_SAFE (s->ht, bt, j, count, kv) {
			if (kv) {
				const char *kvv = sdbkv_value (kv);
				if (kvv && *kvv && !cb (user, sdbkv_key (kv), kvv)) {
					return sdb_foreach_end (s, false);
				}
			}
		}
	}
	return sdb_foreach_end (s, true);
}

typedef struct {
	Sdb *s;
	HtPP *persisted;
} SdbSync;

static bool _sync_has_persisted(SdbSync *sync, const char *key) {
	return ht_pp_find (sync->persisted, key, NULL) != NULL;
}

static bool _insert_into_disk(void *user, const char *key, const char *value) {
	SdbSync *sync = (SdbSync *)user;
	if (!sync || !sync->s) {
		return false;
	}
	// An updated key may occur more than once in an existing CDB.
	return _sync_has_persisted (sync, key)
		|| sdb_disk_insert (sync->s, key, value);
}

static bool _mark_persisted(void *user, const char *key, const char *value) {
	SdbSync *sync = (SdbSync *)user;
	(void)value;
	return sync && (_sync_has_persisted (sync, key)
		|| ht_pp_insert (sync->persisted, key, (void *)1));
}

static bool _remove_persisted(void *user, const void *key, const void *value) {
	Sdb *s = (Sdb *)user;
	(void)value;
	(void)sdb_ht_delete (s->ht, (const char *)key);
	return true;
}

SDB_API bool sdb_sync(Sdb* s) {
	ut32 i;

	if (!s) {
		return false;
	}
	HtPP *persisted = ht_pp_new0 ();
	if (!persisted) {
		return false;
	}
	SdbSync sync = { s, persisted };
	if (!sdb_disk_create (s)) {
		ht_pp_free (persisted);
		return false;
	}
	if (!sdb_foreach_cdb (s, _insert_into_disk, _mark_persisted, &sync)) {
		goto fail;
	}

	/* append new keyvalues */
	for (i = 0; i < s->ht->size; i++) {
		HtPPBucket *bt = &s->ht->table[i];
		SdbKv *kv;
		ut32 j, count;

		BUCKET_FOREACH_SAFE (s->ht, bt, j, count, kv) {
			if (sdbkv_key (kv)) {
				const char *kvv = sdbkv_value (kv);
				if (kvv && *kvv && !kv->expire
					&& !_sync_has_persisted (&sync, sdbkv_key (kv))) {
					if (!sdb_disk_insert (s, sdbkv_key (kv), kvv)
						|| !_mark_persisted (&sync, sdbkv_key (kv), kvv)) {
						goto fail;
					}
				}
			}
		}
	}
	if (!sdb_disk_finish (s)) {
		goto fail;
	}
	// Dirty entries remain available for retry until the replacement succeeds.
	ht_pp_foreach (persisted, _remove_persisted, s);
	ht_pp_free (persisted);
	(void)sdb_journal_clear (s);
	return true;

fail:
	sdb_disk_abort (s);
	ht_pp_free (persisted);
	return false;
}

SDB_API void sdb_dump_begin(Sdb* s) {
	if (s->fd != -1) {
		s->pos = sizeof (((struct cdb_make *)0)->final);
		seek_set (s->fd, s->pos);
	} else {
		s->pos = 0;
	}
}

SDB_API SdbKv *sdb_dump_next(Sdb* s) {
	char *v = NULL;
	char k[SDB_MAX_KEY] = {0};
	int vl = 0;
	// we dont need to malloc, because all values are null terminated in memory.
	if (!sdb_dump_dupnext (s, k, &v, &vl)) {
		return NULL;
	}
	vl--;
	snprintf (sdbkv_key (&s->tmpkv), SDB_KSZ, "%s", k);
	sdb_gh_free (sdbkv_value (&s->tmpkv));
	s->tmpkv.base.value = v;
	s->tmpkv.base.value_len = vl;
	return &s->tmpkv;
}

SDB_API bool sdb_dump_hasnext(Sdb* s) {
	ut32 k, v;
	if (!cdb_getkvlen (&s->db, &k, &v, s->pos)) {
		return false;
	}
	if (k < 1 || v < 1) {
		return false;
	}
	s->pos += k + v + 4;
	return true;
}

SDB_API bool sdb_stats(Sdb *s, ut32 *disk, ut32 *mem) {
	if (!s) {
		return false;
	}
	if (disk) {
		ut32 count = 0;
		if (s->fd != -1) {
			sdb_dump_begin (s);
			while (sdb_dump_hasnext (s)) {
				count ++;
			}
		}
		*disk = count;
	}
	if (mem) {
		*mem = s->ht->count;
	}
	return disk || mem;
}

// TODO: make it static? internal api?
SDB_API bool sdb_dump_dupnext(Sdb* s, char *key, char **value, int *_vlen) {
	ut32 vlen = 0, klen = 0;
	if (value) {
		*value = NULL;
	}
	if (_vlen) {
		*_vlen = 0;
	}
	if (!cdb_getkvlen (&s->db, &klen, &vlen, s->pos)) {
		return false;
	}
	s->pos += 4;
	if (klen < 1 || vlen < 1) {
		return false;
	}
	if (_vlen) {
		*_vlen = vlen;
	}
	if (key) {
		key[0] = 0;
		if (klen > SDB_MIN_KEY && klen < SDB_MAX_KEY) {
			if (getbytes (s, key, klen) == -1) {
				return 0;
			}
			key[klen] = 0;
		}
	}
	if (value) {
		*value = 0;
		if (vlen < SDB_MAX_VALUE) {
			*value = (char *)sdb_gh_malloc (vlen + 10);
			if (!*value) {
				return false;
			}
			if (getbytes (s, *value, vlen) == -1) {
				sdb_gh_free (*value);
				*value = NULL;
				return false;
			}
			(*value)[vlen] = 0;
		}
	}
	return true;
}

static inline ut64 parse_expire(ut64 e) {
	const ut64 month = 30 * 24 * 60 * 60;
	if (e > 0 && e < month) {
		e += sdb_now ();
	}
	return e;
}

SDB_API bool sdb_expire_set(Sdb* s, const char *key, ut64 expire, ut32 cas) {
	char *buf;
	ut32 pos, len;
	SdbKv *kv;
	bool found;
	if (!s) {
		return false;
	}
	s->timestamped = true;
	if (!key) {
		s->expire = parse_expire (expire);
		return true;
	}
	kv = (SdbKv*)sdb_ht_find_kvp (s->ht, key, &found);
	if (found && kv) {
		if (*sdbkv_value (kv)) {
			if (!cas || cas == kv->cas) {
				kv->expire = parse_expire (expire);
				return true;
			}
		}
		return false;
	}
	if (s->fd == -1) {
		return false;
	}
	(void) cdb_findstart (&s->db);
	if (!cdb_findnext (&s->db, sdb_hash (key), key, strlen (key))) {
		return false;
	}
	pos = cdb_datapos (&s->db);
	len = cdb_datalen (&s->db);
	if (len < 1 || len >= INT32_MAX) {
		return false;
	}
	if (!(buf = (char *)sdb_gh_calloc (1, len + 1))) {
		return false;
	}
	cdb_read (&s->db, buf, len, pos);
	buf[len] = 0;
	sdb_set_owned (s, key, buf, cas);
	return sdb_expire_set (s, key, expire, cas); // recursive
}

SDB_API ut64 sdb_expire_get(Sdb* s, const char *key, ut32 *cas) {
	bool found = false;
	SdbKv *kv = (SdbKv*)sdb_ht_find_kvp (s->ht, key, &found);
	if (found && kv && *sdbkv_value (kv)) {
		if (cas) {
			*cas = kv->cas;
		}
		return kv->expire;
	}
	return 0LL;
}

SDB_API bool sdb_hook(Sdb* s, SdbHook cb, void* user) {
	int i = 0;
	SdbHook hook;
	SdbListIter *iter;
	if (!s || !cb) {
		return false;
	}
	if (s->hooks) {
		ls_foreach_cast (s->hooks, iter, SdbHook, hook) {
			if (!(i % 2) && (hook == cb)) {
				return false;
			}
			i++;
		}
	} else {
		s->hooks = ls_new ();
		if (!s->hooks) {
			return false;
		}
		s->hooks->free = NULL;
	}
	ls_append (s->hooks, (void*)cb);
	ls_append (s->hooks, user);
	return true;
}

SDB_API bool sdb_unhook(Sdb* s, SdbHook h) {
	int i = 0;
	SdbHook hook;
	SdbListIter *iter, *iter2;
	ls_foreach_cast (s->hooks, iter, SdbHook, hook) {
		if (!(i % 2) && (hook == h)) {
			iter2 = iter->n;
			ls_delete (s->hooks, iter);
			ls_delete (s->hooks, iter2);
			return true;
		}
		i++;
	}
	return false;
}

SDB_API int sdb_hook_call(Sdb *s, const char *k, const char *v) {
	SdbListIter *iter;
	SdbHook hook;
	int i = 0;
	if (s->timestamped && s->last) {
		s->last = sdb_now ();
	}
	ls_foreach_cast (s->hooks, iter, SdbHook, hook) {
		if (!(i % 2) && k && iter->n) {
			void *u = iter->n->data;
			hook (s, u, k, v);
		}
		i++;
	}
	return i >> 1;
}

SDB_API void sdb_hook_free(Sdb *s) {
	ls_free (s->hooks);
	s->hooks = NULL;
}

SDB_API void sdb_config(Sdb *s, int options) {
	if (!s) {
		return;
	}
	s->options = options;
	if (options & SDB_OPTION_SYNC) {
		// sync on every query
	}
	if (options & SDB_OPTION_JOURNAL) {
		// sync on every query
		sdb_journal_open (s);
		// load journaling if exists
		sdb_journal_load (s);
		sdb_journal_clear (s);
	} else {
		sdb_journal_close (s);
	}
	if (options & SDB_OPTION_NOSTAMP) {
		// sync on every query
		s->last = 0LL;
	}
	if (options & SDB_OPTION_FS) {
		// have access to fs (handle '.' or not in query)
	}
}

SDB_API bool sdb_unlink(Sdb* s) {
	// unlink first, sdb_fini frees s->dir
	bool ret = sdb_disk_unlink (s);
	sdb_fini (s, true);
	return ret;
}

SDB_API void sdb_drain(Sdb *s, Sdb *f) {
	if (s && f) {
		f->refs = s->refs;
		sdb_fini (s, true);
		*s = *f;
		sdb_gh_free (f);
	}
}

static bool copy_foreach_cb(void *user, const char *k, const char *v) {
	Sdb *dst = (Sdb *)user;
	sdb_set (dst, k, v, 0);
	return true;
}

SDB_API void sdb_copy(Sdb *src, Sdb *dst) {
	if (!src || !dst) {
		return;
	}
	sdb_foreach (src, copy_foreach_cb, dst);
	SdbListIter *it;
	SdbNs *ns;
	ls_foreach_cast (src->ns, it, SdbNs*, ns) {
		sdb_copy (ns->sdb, sdb_ns (dst, ns->name, true));
	}
}

typedef struct {
	Sdb *sdb;
	const char *key;
	int count;
} UnsetCallbackData;

static bool unset_cb(void *user, const char *k, const char *v) {
	UnsetCallbackData *ucd = (UnsetCallbackData *)user;
	if (sdb_match (k, ucd->key)) {
		sdb_unset (ucd->sdb, k, 0);
		ucd->count++;
	}
	return true;
}

// returns the number of unset keys
SDB_API int sdb_unset_like(Sdb *s, const char *k) {
	UnsetCallbackData ucd = { s, k, 0 };
	sdb_foreach (s, unset_cb, &ucd);
	return ucd.count;
}

typedef struct {
	Sdb *sdb;
	const char *key;
	const char *val;
	SdbForeachCallback cb;
	const char **array;
	int array_index;
	int array_size;
} LikeCallbackData;

static bool like_cb(void *user, const char *k, const char *v) {
	LikeCallbackData *lcd = (LikeCallbackData *)user;
	if (!user) {
		return false;
	}
	if (k && lcd->key && !sdb_match (k, lcd->key)) {
		return true;
	}
	if (v && lcd->val && !sdb_match (v, lcd->val)) {
		return true;
	}
	if (lcd->array) {
		int idx = lcd->array_index;
		int newsize = lcd->array_size + sizeof (char*) * 2;
		const char **newarray = (const char **)sdb_gh_realloc ((void*)lcd->array, newsize);
		if (!newarray) {
			return false;
		}
		lcd->array = newarray;
		lcd->array_size = newsize;
		// concatenate in array
		lcd->array[idx] = k;
		lcd->array[idx + 1] = v;
		lcd->array[idx + 2] = NULL;
		lcd->array[idx + 3] = NULL;
		lcd->array_index = idx+2;
	} else {
		if (lcd->cb) {
			lcd->cb (lcd->sdb, k, v);
		}
	}
	return true;
}

SDB_API char** sdb_like(Sdb *s, const char *k, const char *v, SdbForeachCallback cb) {
	LikeCallbackData lcd = { s, k, v, cb, NULL, 0, 0 };
	if (cb) {
		sdb_foreach (s, like_cb, &lcd);
		return NULL;
	}
	if (k && !*k) {
		lcd.key = NULL;
	}
	if (v && !*v) {
		lcd.val = NULL;
	}
	lcd.array_size = sizeof (char*) * 2;
	lcd.array = (const char **)sdb_gh_calloc (lcd.array_size, 1); // XXX shouldnt be const
	if (!lcd.array) {
		return NULL;
	}
	lcd.array_index = 0;
	sdb_foreach (s, like_cb, &lcd);
	if (lcd.array_index == 0) {
		sdb_gh_free ((void*)lcd.array);
		return NULL;
	}
	return (char**)lcd.array;
}
