/* loc.c: Interface to dbm index of object locations. */

#include <stdio.h>
#include <sys/types.h>
#include <sys/file.h>
#include <sys/stat.h>
#include <ndbm.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include "lookup.h"
#include "ident.h"
#include "log.h"
#include "ident.h"
#include "util.h"

#ifdef S_IRUSR
#define READ_WRITE		(S_IRUSR | S_IWUSR)
#define READ_WRITE_EXECUTE	(S_IRUSR | S_IWUSR | S_IXUSR)
#else
#define READ_WRITE 0600
#define READ_WRITE_EXECUTE 0700
#endif

#define NAME_CACHE_SIZE 503

static datum dbref_key(long dbref, Number_buf nbuf);
static datum name_key(long name);
static datum offset_size_value(off_t offset, int size, Number_buf nbuf);
static void parse_offset_size_value(datum value, off_t *offset, int *size);
static datum dbref_value(long dbref, Number_buf nbuf);
static void sync_name_cache(void);
static int store_name(long name, long dbref);
static int get_name(long name, long *dbref);

static DBM *dbp;

struct name_cache_entry {
    long name;
    long dbref;
    char dirty;
    char on_disk;
} name_cache[NAME_CACHE_SIZE];

void lookup_open(char *name, int cnew)
{
    int i;

    if (cnew)
	dbp = dbm_open(name, O_TRUNC | O_RDWR | O_CREAT, READ_WRITE);
    else
	dbp = dbm_open(name, O_RDWR, READ_WRITE);
    if (!dbp)
	fail_to_start("Cannot open dbm database file.");

    for (i = 0; i < NAME_CACHE_SIZE; i++)
	name_cache[i].name = NOT_AN_IDENT;
}

void lookup_close(void)
{
    sync_name_cache();
    dbm_close(dbp);
}

void lookup_sync(void)
{
    /* Only way to do this with ndbm is close and re-open. */
    sync_name_cache();
    dbm_close(dbp);
    dbp = dbm_open("binary/index", O_RDWR | O_CREAT, READ_WRITE);
    if (!dbp)
	panic("Cannot reopen dbm database file.");
}

int lookup_retrieve_dbref(long dbref, off_t *offset, int *size)
{
    datum key, value;
    Number_buf nbuf;

    /* Get the value for dbref from the database. */
    key = dbref_key(dbref, nbuf);
    value = dbm_fetch(dbp, key);
    if (!value.dptr)
	return 0;

    parse_offset_size_value(value, offset, size);
    return 1;
}

int lookup_store_dbref(long dbref, off_t offset, int size)
{
    datum key, value;
    Number_buf nbuf1, nbuf2;

    key = dbref_key(dbref, nbuf1);
    value = offset_size_value(offset, size, nbuf2);
    if (dbm_store(dbp, key, value, DBM_REPLACE)) {
	write_log("ERROR: Failed to store key %l.", dbref);
	return 0;
    }

    return 1;
}

int lookup_remove_dbref(long dbref)
{
    datum key;
    Number_buf nbuf;

    /* Remove the key from the database. */
    key = dbref_key(dbref, nbuf);
    if (dbm_delete(dbp, key)) {
	write_log("ERROR: Failed to delete key %l.", dbref);
	return 0;
    }
    return 1;
}

long lookup_first_dbref(void)
{
    datum key;

    key = dbm_firstkey(dbp);
    if (key.dptr == NULL)
	return NOT_AN_IDENT;
    if (key.dsize > 1 && *key.dptr == 0)
	return atoln(key.dptr + 1, key.dsize - 1);
    return lookup_next_dbref();
}

long lookup_next_dbref(void)
{
    datum key;

    key = dbm_nextkey(dbp);
    if (key.dptr == NULL)
	return NOT_AN_IDENT;
    if (key.dsize > 1 && *key.dptr == 0)
	return atoln(key.dptr + 1, key.dsize - 1);
    return lookup_next_dbref();
}

int lookup_retrieve_name(long name, long *dbref)
{
    int i = name % NAME_CACHE_SIZE;

    /* See if it's in the cache. */
    if (name_cache[i].name == name) {
	*dbref = name_cache[i].dbref;
	return 1;
    }

    /* Get it from the database. */
    if (!get_name(name, dbref))
	return 0;

    /* Discard the old cache entry if it exists. */
    if (name_cache[i].name != NOT_AN_IDENT) {
	if (name_cache[i].dirty)
	    store_name(name_cache[i].name, name_cache[i].dbref);
	/*write_log("##lookup_retreive_name %d %s", name_cache[i].name, ident_name(name_cache[i].name));*/
	ident_discard(name_cache[i].name);
    }

    /* Make a new cache entry. */
    name_cache[i].name = ident_dup(name);
    name_cache[i].dbref = *dbref;
    name_cache[i].dirty = 0;
    name_cache[i].on_disk = 1;

    return 1;
}

int lookup_store_name(long name, long dbref)
{
    int i = name % NAME_CACHE_SIZE;

    /* See if it's in the cache. */
    if (name_cache[i].name == name) {
	if (name_cache[i].dbref != dbref) {
	    name_cache[i].dbref = dbref;
	    name_cache[i].dirty = 1;
	}
	return 1;
    }

    /* Discard the old cache entry if it exists. */
    if (name_cache[i].name != NOT_AN_IDENT) {
	if (name_cache[i].dirty)
	    store_name(name_cache[i].name, name_cache[i].dbref);
	/*write_log("##lookup_store_name %d %s", name_cache[i].name, ident_name(name_cache[i].name));*/
	ident_discard(name_cache[i].name);
    }

    /* Make a new cache entry. */
    name_cache[i].name = ident_dup(name);
    name_cache[i].dbref = dbref;
    name_cache[i].dirty = 1;
    name_cache[i].on_disk = 0;

    return 1;
}

int lookup_remove_name(long name)
{
    datum key;
    int i = name % NAME_CACHE_SIZE;

    /* See if it's in the cache. */
    if (name_cache[i].name == name) {
	/* Delete it from the cache.  If it's not on disk, then we're done. */
	/*write_log("##lookup_remove_name %d %s", name_cache[i].name, ident_name(name_cache[i].name));*/
	ident_discard(name_cache[i].name);
	name_cache[i].name = NOT_AN_IDENT;
	if (!name_cache[i].on_disk)
	    return 1;
    }

    /* Remove the key from the database. */
    key = name_key(name);
    if (dbm_delete(dbp, key))
	return 0;
    return 1;
}

long lookup_first_name(void)
{
    datum key;

    sync_name_cache();
    key = dbm_firstkey(dbp);
    if (key.dptr == NULL)
	return NOT_AN_IDENT;
    if (key.dsize == 1 || *key.dptr != 0)
	return ident_get(key.dptr);
    return lookup_next_name();
}

long lookup_next_name(void)
{
    datum key;

    key = dbm_nextkey(dbp);
    if (key.dptr == NULL)
	return NOT_AN_IDENT;
    if (key.dsize == 1 || *key.dptr != 0)
	return ident_get(key.dptr);
    return lookup_next_name();
}

static datum dbref_key(long dbref, Number_buf nbuf)
{
    char *s;
    datum key;

    /* Set up a key for a dbref.  The first byte will be 0, distinguishing it
     * from a string. */
    s = long_to_ascii(dbref, nbuf);
    *--s = 0;
    key.dptr = s;
    key.dsize = strlen(s + 1) + 2;
    return key;
}

static datum offset_size_value(off_t offset, int size, Number_buf nbuf)
{
    char *s;
    Number_buf tmp_buf;
    datum value;

    /* Set up a value for the offset and size. */
    s = long_to_ascii(offset, tmp_buf);
    nbuf[0] = 0;
    strcpy(nbuf, s);
    strcat(nbuf, ";");
    s = long_to_ascii(size, tmp_buf);
    strcat(nbuf, s);
    value.dptr = nbuf;
    value.dsize = strlen(nbuf) + 1;
    return value;
}

static void parse_offset_size_value(datum value, off_t *offset, int *size)
{
    char *p;

    *offset = atol(value.dptr);
    p = strchr(value.dptr, ';');
    *size = atol(p + 1);
}

static datum name_key(long name)
{
    datum key;

    /* Set up a key for the name.  Include the 0 byte at the end. */
    key.dptr = ident_name(name);
    key.dsize = strlen(key.dptr) + 1;
    return key;
}

static datum dbref_value(long dbref, Number_buf nbuf)
{
    char *s;
    datum value;

    s = long_to_ascii(dbref, nbuf);
    value.dptr = s;
    value.dsize = strlen(s) + 1;
    return value;
}

static void sync_name_cache(void)
{
    int i;

    for (i = 0; i < NAME_CACHE_SIZE; i++) {
	if (name_cache[i].name != NOT_AN_IDENT && name_cache[i].dirty) {
	    store_name(name_cache[i].name, name_cache[i].dbref);
	    name_cache[i].dirty = 0;
	    name_cache[i].on_disk = 1;
	}
    }
}

static int store_name(long name, long dbref)
{
    datum key, value;
    Number_buf nbuf;

    /* Set up the value structure. */
    value = dbref_value(dbref, nbuf);

    key = name_key(name);
    if (dbm_store(dbp, key, value, DBM_REPLACE)) {
	write_log("ERROR: Failed to store key %s.", name);
	return 0;
    }

    return 1;
}

static int get_name(long name, long *dbref)
{
    datum key, value;

    /* Get the key from the database. */
    key = name_key(name);
    value = dbm_fetch(dbp, key);
    if (!value.dptr)
	return 0;

    *dbref = atol(value.dptr);
    return 1;
}