
/*
 * sophia database
 * sphia.org
 *
 * Copyright (c) Dmitry Simonenko
 * BSD License
*/

#include <sp.h>

static inline int sphot
cmpstd(char *a, size_t asz, char *b, size_t bsz, void *arg spunused) {
	register size_t sz = (asz < bsz ? asz : bsz);
	register int rc = memcmp(a, b, sz);
	if (rc == 0)
		return (asz == bsz) ? 0 : (asz > bsz ? 1 : -1);
	return rc > 0 ? 1 : -1;
}

static inline void sp_envinit(spenv *e) {
	e->m = SPMENV;
	e->inuse = 0;
	sp_einit(&e->e);
	e->alloc = sp_allocstd;
	e->allocarg = NULL;
	e->cmp = cmpstd;
	e->cmparg = NULL;
	e->page = 2048;
	e->dir = NULL;
	e->flags = 0;
	e->mergewm = 100000;
	e->merge = 1;
	e->dbnewsize = 2 * 1024 * 1024;
	e->dbgrow = 1.4;
	e->gc = 1;
	e->gcfactor = 0.5;
}

static inline void sp_envfree(spenv *e) {
	if (e->dir) {
		free(e->dir);
		e->dir = NULL;
	}
	sp_efree(&e->e);
}

static inline int sp_envvalidate(spenv *e)
{
	/* check if environment is not already
	 * in use.
	 * do not set other environment error status 
	 * in that case.
	 */
	if (e->inuse)
		return -1;
	if (e->dir == NULL)
		return sp_ee(e, SPE, "directory is not specified");
	if (e->mergewm < 2)
		return sp_ee(e, SPE, "bad merge watermark count");
	if (e->page < 2)
		return sp_ee(e, SPE, "bad page size");
	if ((e->page % 2) > 0)
		return sp_ee(e, SPE, "bad page size must be even");
	return 0;
}

void *sp_env(void) {
	spenv *e = malloc(sizeof(spenv));
	if (spunlikely(e == NULL))
		return NULL;
	sp_envinit(e);
	return e;
}

static int sp_ctlenv(spenv *e, spopt opt, va_list args)
{
	if (e->inuse)
		return sp_ee(e, SPEOOM, "can't change env opts while in-use");
	switch (opt) {
	case SPDIR: {
		uint32_t flags = va_arg(args, uint32_t);
		char *path = va_arg(args, char*);
		char *p = strdup(path);
		if (spunlikely(p == NULL))
			return sp_ee(e, SPEOOM, "failed to allocate memory");
		if (spunlikely(e->dir)) {
			free(e->dir);
			e->dir = NULL;
		}
		e->dir = p;
		e->flags = flags;
		break;
	}
	case SPALLOC:
		e->alloc = va_arg(args, spallocf);
		e->allocarg = va_arg(args, void*);
		break;
	case SPCMP:
		e->cmp = va_arg(args, spcmpf);
		e->cmparg = va_arg(args, void*);
		break;
	case SPPAGE:
		e->page = va_arg(args, uint32_t);
		break;
	case SPGC:
		e->gc = va_arg(args, int);
		break;
	case SPGCF:
		e->gcfactor = va_arg(args, double);
		break;
	case SPGROW:
		e->dbnewsize = va_arg(args, uint32_t);
		e->dbgrow = va_arg(args, double);
		break;
	case SPMERGE:
		e->merge = va_arg(args, int);
		break;
	case SPMERGEWM:
		e->mergewm = va_arg(args, uint32_t);
		break;
	default:
		return sp_ee(e, SPE, "bad arguments");
	}
	return 0;
}

static int sp_ctldb(sp *s, spopt opt, va_list args spunused)
{
	switch (opt) {
	case SPMERGEFORCE:
		if (s->env->merge)
			return sp_e(s, SPE, "force merge doesn't work with merger thread active");
		return sp_merge(s);
	default:
		return sp_e(s, SPE, "bad arguments");
	}
	return 0;
}

int sp_ctl(void *o, spopt opt, ...)
{
	va_list args;
	va_start(args, opt);
	spmagic *magic = (spmagic*)o;
	int rc;
	if (opt == SPVERSION) {
		uint32_t *major = va_arg(args, uint32_t*);
		uint32_t *minor = va_arg(args, uint32_t*);
		*major = SP_VERSION_MAJOR;
		*minor = SP_VERSION_MINOR;
		return 0;
	}
	switch (*magic) {
	case SPMENV: rc = sp_ctlenv(o, opt, args);
		break;
	case SPMDB: rc = sp_ctldb(o, opt, args);
		break;
	default: rc = -1;
		break;
	}
	va_end(args);
	return rc;
}

int sp_rotate(sp *s, spe *err)
{
	int rc;
	sp_repepochincrement(&s->rep);
	/* allocate new epoch */
	spepoch *e = sp_repalloc(&s->rep, sp_repepoch(&s->rep));
	if (spunlikely(s == NULL))
		return sp_ef(err, SPEOOM, "failed to allocate repository");
	/* create log file */
	rc = sp_lognew(&e->log, s->env->dir, sp_repepoch(&s->rep));
	if (spunlikely(rc == -1)) {
		sp_free(&s->a, e);
		return sp_ef(err, SPEIO, e->epoch, "failed to create log file");
	}
	splogh h;
	h.magic = SPMAGIC;
	h.version[0] = SP_VERSION_MAJOR;
	h.version[1] = SP_VERSION_MINOR;
	rc = sp_logwrite(&e->log, &h, sizeof(h));
	if (spunlikely(rc == -1)) {
		sp_logclose(&e->log);
		sp_free(&s->a, e);
		return sp_ef(err, SPEIO, e->epoch, "failed to write log file");
	}
	/* attach epoch and mark it is as live */
	sp_repattach(&s->rep, e);
	sp_repset(&s->rep, e, SPLIVE);
	return 0;
}

static inline int sp_closerep(sp *s)
{
	int rcret = 0;
	int rc = 0;
	splist *i, *n;
	sp_listforeach_safe(&s->rep.l, i, n) {
		spepoch *e = spcast(i, spepoch, link);
		switch (e->type) {
		case SPUNDEF:
			/* this type is true to a epoch that has beed
			 * scheduled for a recovery, but not happen to
			 * proceed yet. */
			break;
		case SPLIVE:
			if (e->nupdate == 0) {
				rc = sp_logunlink(&e->log);
				if (spunlikely(rc == -1))
					rcret = sp_e(s, SPEIO, e->epoch, "failed to unlink log file");
				rc = sp_logclose(&e->log);
				if (spunlikely(rc == -1))
					rcret = sp_e(s, SPEIO, e->epoch, "failed to close log file");
				break;
			} else {
				rc = sp_logeof(&e->log);
				if (spunlikely(rc == -1))
					rcret = sp_e(s, SPEIO, e->epoch, "failed to write eof marker");
			}
		case SPXFER:
			rc = sp_logcomplete(&e->log);
			if (spunlikely(rc == -1))
				rcret = sp_e(s, SPEIO, e->epoch, "failed to complete log file");
			rc = sp_logclose(&e->log);
			if (spunlikely(rc == -1))
				rcret = sp_e(s, SPEIO, e->epoch, "failed to close log file");
			break;
		case SPDB:
			rc = sp_mapclose(&e->db);
			if (spunlikely(rc == -1))
				rcret = sp_e(s, SPEIO, e->epoch, "failed to close db file");
			break;
		}
		sp_free(&s->a, e);
	}
	return rcret;
}

static inline int sp_close(sp *s)
{
	int rcret = 0;
	int rc = 0;
	s->stop = 1;
	if (s->env->merge) {
		rc = sp_taskstop(&s->merger);
		if (spunlikely(rc == -1))
			rcret = sp_e(s, SPESYS, "failed to stop merger thread");
	}
	sp_refsetfree(&s->refs, &s->a);
	rc = sp_closerep(s);
	if (spunlikely(rc == -1))
		rcret = -1;
	rc = sp_recoverunlock(s);
	if (spunlikely(rc == -1))
		rcret = -1;
	sp_ifree(&s->i0);
	sp_ifree(&s->i1);
	sp_ifree(&s->itxn); /* equal to rollback */
	sp_catfree(&s->s);
	s->env->inuse = 0;
	sp_lockfree(&s->lockr);
	sp_lockfree(&s->locks);
	sp_lockfree(&s->locki);
	sp_efree(&s->e);
	sp_efree(&s->em);
	return rcret;
}

static void *merger(void *arg)
{
	sptask *self = arg;
	sp *s = self->arg;
	do {
		sp_lock(&s->locki);
		int merge = s->i->count > s->env->mergewm;
		sp_unlock(&s->locki);
		if (! merge)
			continue;
		int rc = sp_merge(s);
		if (spunlikely(rc == -1)) {
			sp_taskdone(self);
			return NULL;
		}
	} while (sp_taskwait(self));

	return NULL;
}

void *sp_open(void *e)
{
	spenv *env = e;
	assert(env->m == SPMENV);
	int rc = sp_envvalidate(env);
	if (spunlikely(rc == -1))
		return NULL;
	spa a;
	sp_allocinit(&a, env->alloc, env->allocarg);
	sp *s = sp_malloc(&a, sizeof(sp));
	if (spunlikely(s == NULL)) {
		sp_ee(env, SPEOOM, "failed to allocate db handle");
		return NULL;
	}
	memset(s, 0, sizeof(sp));
	sp_einit(&s->e);
	sp_einit(&s->em);
	s->m = SPMDB;
	s->env = env;
	s->env->inuse = 1;
	memcpy(&s->a, &a, sizeof(s->a));
	/* init locks */
	sp_fileinit(&s->lockdb, &s->a);
	sp_lockinit(&s->lockr);
	sp_lockinit(&s->locks);
	sp_lockinit(&s->locki);
	s->lockc = 0;
	/* init key index */
	rc = sp_iinit(&s->i0, &s->a, 1024, s->env->cmp, s->env->cmparg);
	if (spunlikely(rc == -1)) {
		sp_e(s, SPEOOM, "failed to allocate key index");
		goto e0;
	}
	rc = sp_iinit(&s->i1, &s->a, 1024, s->env->cmp, s->env->cmparg);
	if (spunlikely(rc == -1)) {
		sp_e(s, SPEOOM, "failed to allocate key index");
		goto e1;
	}
	s->i = &s->i0;
	/* init transaction index */
	rc = sp_iinit(&s->itxn, &s->a, 1024, s->env->cmp, s->env->cmparg);
	if (spunlikely(rc == -1)) {
		sp_e(s, SPEOOM, "failed to allocate transaction index");
		goto e2;
	}
	/* set current transaction state as single-stmt */
	s->txn = SPTSS;
	/* init page index */
	s->psn = 0;
	rc = sp_catinit(&s->s, &s->a, 512, s->env->cmp, s->env->cmparg);
	if (spunlikely(rc == -1)) {
		sp_e(s, SPEOOM, "failed to allocate page index");
		goto e2;
	}
	sp_repinit(&s->rep, &s->a);
	rc = sp_recover(s);
	if (spunlikely(rc == -1))
		goto e3;
	/* do not create new live epoch in read-only mode */
	if (! (s->env->flags & SPO_RDONLY)) {
		rc = sp_rotate(s, &s->e);
		if (spunlikely(rc == -1))
			goto e3;
	}
	s->stop = 0;
	rc = sp_refsetinit(&s->refs, &s->a, s->env->page);
	if (spunlikely(rc == -1)) {
		sp_e(s, SPEOOM, "failed to allocate key buffer");
		goto e3;
	}
	if (s->env->merge) {
		rc = sp_taskstart(&s->merger, merger, s);
		if (spunlikely(rc == -1)) {
			sp_e(s, SPESYS, "failed to start merger thread");
			goto e4;
		}
		sp_taskwakeup(&s->merger);
	}
	return s;
e4:
	sp_refsetfree(&s->refs, &s->a);
e3:
	sp_closerep(s);
	sp_recoverunlock(s);
	sp_catfree(&s->s);
e2:
	sp_ifree(&s->itxn);
	sp_ifree(&s->i1);
e1:
	sp_ifree(&s->i0);
e0:
	s->env->inuse = 0;
	sp_lockfree(&s->lockr);
	sp_lockfree(&s->locks);
	sp_lockfree(&s->locki);
	sp_edup(&env->e, &s->e);
	sp_efree(&s->e);
	sp_efree(&s->em);
	sp_free(&a, s);
	return NULL;
}

int sp_destroy(void *o)
{
	spmagic *magic = (spmagic*)o;
	spa *a = NULL;
	int rc = 0;
	switch (*magic) {
	case SPMNONE:
		assert(0);
		return -1;
	case SPMENV: {
		spenv *env = (spenv*)o;
		if (env->inuse)
			return -1;
		sp_envfree(env);
		*magic = SPMNONE;
		free(o);
		return 0;
	}
	case SPMCUR: {
		spc *c = (spc*)o;
		a = &c->s->a;
		sp_cursorclose(c);
		break;
	}
	case SPMDB: {
		sp *s = (sp*)o;
		a = &s->a;
		rc = sp_close(s);
		break;
	}
	default:
		return -1;
	}
	*magic = SPMNONE;
	sp_free(a, o);
	return rc;
}

char *sp_error(void *o)
{
	spmagic *magic = (spmagic*)o;
	spenv *env;
	switch (*magic) {
	case SPMENV:
		env = o;
		if (! sp_eis(&env->e))
			return NULL;
		return env->e.e;
	case SPMDB: break;
	default:
		assert(0);
		return NULL;
	}
	sp *s = o;
	if (sp_eis(&s->em))
		return s->em.e;
	if (sp_eis(&s->e))
		return s->e.e;
	return NULL;
}

int sp_begin(void *o)
{
	sp *s = o;
	assert(s->m == SPMDB);
	if (spunlikely(sp_evalidate(s)))
		return -1;
	if (spunlikely(s->txn == SPTMS))
		return -1;
	if (spunlikely(s->lockc))
		return sp_e(s, SPE, "begin with open cursor");
	s->txn = SPTMS;
	return 0;
}

int sp_commit(void *o)
{
	sp *s = o;
	assert(s->m == SPMDB);
	if (spunlikely(sp_evalidate(s)))
		return -1;
	if (spunlikely(s->txn == SPTSS))
		return sp_e(s, SPE, "no active transaction to commit");
	if (spunlikely(s->lockc))
		return sp_e(s, SPE, "commit with open cursor");
	if (spunlikely(s->itxn.count == 0)) {
		s->txn = SPTSS;
		return 0;
	}

	/* prepare to write the transaction
	 * to the log */
	int n = s->itxn.count;

	sp_lock(&s->lockr);
	sp_lock(&s->locki);

	spepoch *live = sp_replive(&s->rep);
	sp_filesvp(&live->log);

	char hbuf[sizeof(spvh) * 512];
	unsigned int hpos = 0;

	spii it;
	sp_iopen(&it, &s->itxn);
	int rc;
	do {
		spv *v = sp_ival(&it);

		if (spunlikely(! sp_batchensure(&s->lb, 3))) {
			rc = sp_logput(&live->log, &s->lb);
			if (spunlikely(rc == -1)) {
				sp_e(s, SPEIO|SPEF, live->epoch, "failed to write log file");
				goto abort;
			}
			hpos = 0;
		}

		v->epoch = live->epoch;
		assert(hpos < sizeof(hbuf));
		spvh *hp = (spvh*)(hbuf + hpos);
		hp->crc     = 0;
		hp->size    = v->size;
		hp->voffset = 0;
		hp->vsize   = sp_vvsize(v);
		hp->flags   = v->flags;
		hp->crc     = sp_crc32c(v->crc, &hp->size, sizeof(spvh) - sizeof(uint32_t));
		sp_batchadd(&s->lb, hp, sizeof(spvh));
		sp_batchadd(&s->lb, v->key, v->size);
		sp_batchadd(&s->lb, sp_vv(v), hp->vsize);
		hpos += sizeof(spvh);

		spv *old = NULL;
		rc = sp_iset(s->i, v, &old);
		if (spunlikely(rc == -1)) {
			sp_e(s, SPEOOM|SPEF, "failed to allocate key index page");
			goto abort;
		}
		if (old)
			sp_free(&s->a, old);

	} while (sp_inext(&it));

	if (sp_batchhas(&s->lb)) {
		rc = sp_logput(&live->log, &s->lb);
		if (spunlikely(rc == -1)) {
			sp_e(s, SPEIO|SPEF, live->epoch, "failed to write log file");
			goto abort;
		}
	}

	/* clean up transaction index (pages only) */
	sp_ireset(&s->itxn);

	sp_unlock(&s->locki);
	sp_unlock(&s->lockr);

	/* set transaction as single-stmt */
	s->txn = SPTSS;

	/* wake up merger if necessary */
	live->nupdate += n;
	if (live->nupdate >= s->env->mergewm) {
		if (splikely(s->env->merge))
			sp_taskwakeup(&s->merger);
	}
	return 0;

abort:
	sp_rollback(o);
	sp_logrlb(&live->log);
	sp_unlock(&s->locki);
	sp_unlock(&s->lockr);
	return -1;
}

int sp_rollback(void *o)
{
	sp *s = o;
	assert(s->m == SPMDB);
	if (spunlikely(sp_evalidate(s)))
		return -1;
	if (spunlikely(s->txn == SPTSS))
		return sp_e(s, SPE, "no active transaction to rollback");
	if (spunlikely(s->lockc))
		return sp_e(s, SPE, "rollback with open cursor");
	int rc = sp_itruncate(&s->itxn);
	if (spunlikely(rc == -1))
		return sp_e(s, SPEOOM, "failed to allocate key index page");
	s->txn = SPTSS;
	return 0;
}

static inline int
sp_do(sp *s, int op, void *k, size_t ksize, void *v, size_t vsize)
{
	/* allocate new version.
	 *
	 * try to reduce lock contention by making the alloc and
	 * the crc calculation before log write. 
	*/
	spv *n = sp_vnewv(s, k, ksize, v, vsize);
	if (spunlikely(n == NULL))
		return sp_e(s, SPEOOM, "failed to allocate version");
	/* prepare log record */
	spvh h = {
		.crc     = 0,
		.size    = ksize,
		.voffset = 0,
		.vsize   = vsize,
		.flags   = op 
	};
	/* calculate crc */
	uint32_t crc;
 	crc   = sp_crc32c(0, k, ksize);
	crc   = sp_crc32c(crc, v, vsize);
	h.crc = sp_crc32c(crc, &h.size, sizeof(spvh) - sizeof(uint32_t));

	n->flags = op;
	n->crc = crc;

	/* in case of multi-stmt transaction, simply add version to the
	 * transaction index only. */
	int rc;
	if (s->txn == SPTMS) {
		spv *old = NULL;
		rc = sp_iset(&s->itxn, n, &old);
		if (spunlikely(rc == -1)) {
			sp_free(&s->a, n);
			return sp_e(s, SPEOOM, "failed to allocate transacton key index page");
		}
		if (old)
			sp_free(&s->a, old);
		return 0;
	}

	sp_lock(&s->lockr);
	sp_lock(&s->locki);

	/* write to current live epoch log */
	spepoch *live = sp_replive(&s->rep);
	sp_filesvp(&live->log);
	sp_batchadd(&s->lb, &h, sizeof(spvh));
	sp_batchadd(&s->lb, k, ksize);
	sp_batchadd(&s->lb, v, vsize);
	rc = sp_logput(&live->log, &s->lb);
	if (spunlikely(rc == -1)) {
		sp_free(&s->a, n);
		rc = sp_logrlb(&live->log);
		if (spunlikely(rc == -1))
			sp_esetfatal(&s->e);
		sp_unlock(&s->locki);
		sp_unlock(&s->lockr);
		return sp_e(s, SPEIO, live->epoch, "failed to write log file");
	}

	/* add new version to the index */
	n->epoch = live->epoch;
	spv *old = NULL;
	rc = sp_iset(s->i, n, &old);
	if (spunlikely(rc == -1)) {
		sp_free(&s->a, n);
		rc = sp_logrlb(&live->log);
		if (spunlikely(rc == -1))
			sp_esetfatal(&s->e);
		sp_unlock(&s->locki);
		sp_unlock(&s->lockr);
		return (spunlikely(rc == -1)) ? -1 :
		        sp_e(s, SPEOOM, "failed to allocate key index page");
	}

	sp_unlock(&s->locki);
	sp_unlock(&s->lockr);

	if (old)
		sp_free(&s->a, old);

	/* wake up merger on merge watermark reached */
	live->nupdate++;
	if ((live->nupdate % s->env->mergewm) == 0) {
		if (splikely(s->env->merge))
			sp_taskwakeup(&s->merger);
	}
	return 0;
}

int sp_set(void *o, const void *k, size_t ksize, const void *v, size_t vsize)
{
	sp *s = o;
	assert(s->m == SPMDB);
	if (spunlikely(sp_evalidate(s)))
		return -1;
	if (spunlikely(s->env->flags & SPO_RDONLY))
		return sp_e(s, SPE, "db handle is read-only");
	if (spunlikely(ksize > UINT16_MAX))
		return sp_e(s, SPE, "key size limit reached");
	if (spunlikely(vsize > UINT32_MAX))
		return sp_e(s, SPE, "value size limit reached");
	if (spunlikely(s->lockc))
		return sp_e(s, SPE, "modify with open cursor");
	return sp_do(s, SPSET, (char*)k, ksize, (char*)v, vsize);
}

int sp_delete(void *o, const void *k, size_t ksize)
{
	sp *s = o;
	assert(s->m == SPMDB);
	if (spunlikely(sp_evalidate(s)))
		return -1;
	if (spunlikely(s->env->flags & SPO_RDONLY))
		return sp_e(s, SPE, "db handle is read-only");
	if (spunlikely(ksize > UINT16_MAX))
		return sp_e(s, SPE, "key size limit reached");
	if (spunlikely(s->lockc))
		return sp_e(s, SPE, "modify with open cursor");
	return sp_do(s, SPDEL, (char*)k, ksize, NULL, 0);
}

int sp_get(void *o, const void *k, size_t ksize, void **v, size_t *vsize)
{
	sp *s = o;
	assert(s->m == SPMDB);
	if (spunlikely(sp_evalidate(s)))
		return -1;
	if (spunlikely(ksize > UINT16_MAX))
		return sp_e(s, SPE, "key size limit reached");
	return sp_match(s, (char*)k, ksize, v, vsize);
}

void *sp_cursor(void *o, sporder order, const void *k, size_t ksize)
{
	sp *s = o;
	assert(s->m == SPMDB);
	if (spunlikely(sp_evalidate(s)))
		return NULL;
	if (spunlikely(ksize > UINT16_MAX)) {
		sp_e(s, SPE, "key size limit reached");
		return NULL;
	}
	spc *c = sp_malloc(&s->a, sizeof(spc));
	if (spunlikely(c == NULL)) {
		sp_e(s, SPEOOM, "failed to allocate cursor handle");
		return NULL;
	}
	memset(c, 0, sizeof(spc));
	sp_cursoropen(c, s, order, (char*)k, ksize);
	return c;
}

int sp_fetch(void *o) {
	spc *c = o;
	assert(c->m == SPMCUR);
	if (spunlikely(sp_evalidate(c->s)))
		return -1;
	return sp_iterate(c);
}

const char *sp_key(void *o)
{
	spc *c = o;
	assert(c->m == SPMCUR);
	return sp_refk(&c->r);
}

size_t sp_keysize(void *o)
{
	spc *c = o;
	assert(c->m == SPMCUR);
	return sp_refksize(&c->r);
}

const char *sp_value(void *o)
{
	spc *c = o;
	assert(c->m == SPMCUR);
	return sp_refv(&c->r, (char*)c->ph);
}

size_t sp_valuesize(void *o)
{
	spc *c = o;
	assert(c->m == SPMCUR);
	return sp_refvsize(&c->r);
}

void sp_stat(void *o, spstat *stat)
{
	spmagic *magic = (spmagic*)o;
	if (*magic != SPMDB) {
		memset(stat, 0, sizeof(*stat));
		return;
	}
	sp *s = o;
	sp_lock(&s->lockr);
	sp_lock(&s->locki);
	sp_lock(&s->locks);
	stat->epoch = s->rep.epoch;
	stat->psn = s->psn;
	stat->repn = s->rep.n;
	stat->repndb = s->rep.ndb;
	stat->repnxfer = s->rep.nxfer;
	stat->catn = s->s.count;
	stat->indexn = s->i->count;
	stat->indexpages = s->i->icount;
	sp_unlock(&s->locks);
	sp_unlock(&s->locki);
	sp_unlock(&s->lockr);
}
