/* NeoStats - IRC Statistical Services 
** Copyright (c) 1999-2004 Adam Rutter, Justin Hammond
** http://www.neostats.net/
**
**  This program is free software; you can redistribute it and/or modify
**  it under the terms of the GNU General Public License as published by
**  the Free Software Foundation; either version 2 of the License, or
**  (at your option) any later version.
**
**  This program is distributed in the hope that it will be useful,
**  but WITHOUT ANY WARRANTY; without even the implied warranty of
**  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
**  GNU General Public License for more details.
**
**  You should have received a copy of the GNU General Public License
**  along with this program; if not, write to the Free Software
**  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
**  USA
**
** NeoStats CVS Identification
** $Id$
*/
/*
 * KEEPER: A configuration reading and writing library
 *
 * Copyright (C) 1999-2000 Miklos Szeredi
 * Email: mszeredi@inf.bme.hu
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the Free
 * Software Foundation, Inc., 59 Temple Place - Suite 330, Boston,
 * MA 02111-1307, USA
 */

#include "kp_util.h"

#include <errno.h>
#include <time.h>
#include <unistd.h>
#include <sys/stat.h>

/* One physical file's cached data */
typedef struct _kp_fil {
	kp_path kpp;
	kp_key *keys;
	int dirty;
	time_t modif;
	time_t use;
	time_t check;
	struct _kp_fil *next;
} kp_fil;

/* The cache */
static kp_fil *kp_cachef = NULL;

/* ------------------------------------------------------------------------- 
 * Free a key list
 * ------------------------------------------------------------------------- */
static void kp_free_keys(kp_key * k)
{
	kp_key *nextk;

	while (k != NULL) {
		nextk = k->next;
		kp_value_destroy(k);
		free(k);
		k = nextk;
	}
}

/* ------------------------------------------------------------------------- 
 * If the file is in the cache, return it, otherwise return NULL
 * ------------------------------------------------------------------------- */
static kp_fil *kp_get_cached_file(kp_path * kpp)
{
	kp_fil *fil;
	char *path = kpp->path;

	for (fil = kp_cachef; fil != NULL; fil = fil->next)
		if (strcmp(fil->kpp.path, path) == 0) {
#ifdef KPDEBUG
			printf("kp_get_cached_file: %s\n", fil->kpp.path);
#endif
			break;

		}
	return fil;
}

/* ------------------------------------------------------------------------- 
 * Create a new chaced file, and insert it into the cache
 * ------------------------------------------------------------------------- */
static kp_fil *kp_new_cached_file(kp_path * kpp)
{
	kp_fil *fil;

	fil = (kp_fil *) malloc_check(sizeof(kp_fil));
	fil->kpp.path = strdup_check(kpp->path);
	fil->kpp.dbindex = kpp->dbindex;
	fil->keys = NULL;
	fil->dirty = 0;
	fil->modif = 0;
	fil->use = 0;
	fil->check = 0;
	fil->next = kp_cachef;
	kp_cachef = fil;
#ifdef KPDEBUG
	printf("kp_new_cached_file: %s\n", fil->kpp.path);
#endif

	return fil;
}

/* ------------------------------------------------------------------------- 
 * Free data associated with a cached file
 * ------------------------------------------------------------------------- */
static void kp_free_file(kp_fil * fil)
{
#ifdef KPDEBUG
	printf("kp_free_file: %s\n", fil->kpp.path);
#endif
	free(fil->kpp.path);
	kp_free_keys(fil->keys);
	free(fil);
}

/* ------------------------------------------------------------------------- 
 * Remove a file from a cache and free it
 * ------------------------------------------------------------------------- */
static void kp_remove_file_from_cache(kp_fil * rfil)
{
	kp_fil **filp;

	for (filp = &kp_cachef; *filp != NULL; filp = &(*filp)->next) {
		if (*filp == rfil) {
			*filp = rfil->next;
#ifdef KPDEBUG
			printf("kp_remove_file_from_cache: %s\n", rfil->kpp.path);
#endif
			kp_free_file(rfil);
			return;
		}
	}

	/* Should never happen */
	fprintf(stderr, "keeper: Internal Error: Corrupted cache data\n");
	abort();
}

/* ------------------------------------------------------------------------- 
 * Remove all files from the cache
 * ------------------------------------------------------------------------- */
void _kp_clear_cache(void)
{
#ifdef KPDEBUG
	printf("kp_clear_cache\n");
#endif
	while (kp_cachef != NULL)
		kp_remove_file_from_cache(kp_cachef);
}

/* ------------------------------------------------------------------------- 
 * Remove all old, non-dirty files from the cache
 * ------------------------------------------------------------------------- */
static void kp_clean_up_cache(time_t now)
{
	kp_fil **filp;
	kp_fil *fil;
	time_t expire;

	expire = now - 10;

	for (filp = &kp_cachef; *filp != NULL;) {
		fil = *filp;
		if (fil->use < expire && !fil->dirty) {
			*filp = fil->next;
#ifdef KPDEBUG
			printf("kp_clean_up_cache: %d < %d (Dirty: %d): %s\n", fil->use, expire, fil->dirty, fil->kpp.path);
#endif
			kp_free_file(fil);
		} else
			filp = &(*filp)->next;
	}
}

/* ------------------------------------------------------------------------- 
 * If the key is in the cached file, return it, otherwise return NULL
 * ------------------------------------------------------------------------- */
static kp_key *kp_get_cached_key(kp_fil * fil, const char *name)
{
	kp_key *key;

	for (key = fil->keys; key != NULL; key = key->next)
		if (strcmp(key->name, name) == 0) {
#ifdef KPDEBUG
			printf("kp_get_cached_file: %s-%s\n", fil->kpp.path, name);
#endif
			break;
		} 
	return key;
}

/* ------------------------------------------------------------------------- 
 * Find a key in a cached file. If it exists return it. If there exists a
 * subkey of it, or it is a subkey of one, then return an error
 * ------------------------------------------------------------------------- */
static int kp_check_insert(kp_fil * fil, char *name, kp_key ** keyretp)
{
	kp_key *key;

	for (key = fil->keys; key != NULL; key = key->next) {
		if (strcmp(key->name, name) == 0) {
#ifdef KPDEBUG
			printf("kp_check_insert: Got key %s-%s\n", fil->kpp.path, name);
#endif
			break;
		}
		if (kp_is_subkey(name, key->name)
		    || kp_is_subkey(key->name, name))
			return KPERR_BADKEY;
	}
#ifdef KPDEBUG
	printf("kp_check_insert: Got it\n");
#endif
	*keyretp = key;
	return 0;
}

/* -------------------------------------------------------------------------
 * This function returns a cached file.  If the file was not in the cache or
 * was outdated, then it reads it into the cache. If the path was refering
 * to a directory, then it returns this in the *isdirp flag.
 * ------------------------------------------------------------------------- */
static int kp_get_file(kp_path * kpp, kp_fil ** fp, int *isdirp)
{
	kp_fil *fil;
	int res;
	struct stat stbuf;
	time_t now;

	*fp = NULL;
	*isdirp = 0;

	fil = kp_get_cached_file(kpp);
	now = time(NULL);

	/* This check-magic is needed, because stat()-ing nonexistent
	   files on NFS seems to be very expensive on some (Solaris)
	   architectures */

	/* If the file was checked less then one second ago, then we
	   believe it hasn't changed. */

	/* FIXME: even if the file is dirty, the non-dirty keys should be
	   updated from a changed file. This is not very important
	   though. */

	if (fil != NULL && !fil->dirty && now != fil->check) {
		res = stat(kpp->path, &stbuf);

		if (fil->keys == NULL && res == -1 && errno == ENOENT) {
#ifdef KPDEBUG
			printf("kp_get_file: does not exist: %s\n",kpp->path);
#endif 
			fil->check = now;
		} else if (res == -1 || !S_ISREG(stbuf.st_mode) ||
			 stbuf.st_mtime != fil->modif) {
#ifdef KPDEBUG
			printf("kp_get_file: not regular file: %s\n", kpp->path);
#endif
			kp_remove_file_from_cache(fil);
			fil = NULL;
		} else {
#ifdef KPDEBUG
			printf("kp_get_file: ok %s\n", kpp->path);
#endif
			fil->check = now;
		}
	}

	if (fil != NULL) {
#ifdef KPDEBUG
		printf("kp_get_file: existing file\n");
#endif
		fil->use = now;
	} else {
		kp_key *keys = NULL;

		/* Clean up old cached files */
		kp_clean_up_cache(now);

		res = stat(kpp->path, &stbuf);
		if (res != 0) {
#ifdef KPDEBUG
			printf("kp_get_file: stat failed\n");
#endif
			res = _kp_errno_to_kperr(errno);
		} else if (!S_ISREG(stbuf.st_mode)) {
			res = KPERR_BADKEY;
			if (S_ISDIR(stbuf.st_mode))
				*isdirp = 1;
		} else {
#ifdef KPDEBUG
			printf("kp_get_file: kp_read_file called\n");
#endif
			res = _kp_read_file(kpp->path, &keys);
			if (res == 0) {
				res = stat(kpp->path, &stbuf);
				if (res == -1) {
					kp_free_keys(keys);
					res = _kp_errno_to_kperr(errno);
				}
			}
		}

		/* We cache nonexistent files also */
		if (res != 0 && res != KPERR_NOKEY)
			return res;

		fil = kp_new_cached_file(kpp);
		now = time(NULL);

		fil->use = now;
		fil->check = now;

		if (res == 0) {
			fil->keys = keys;
			fil->modif = stbuf.st_mtime;
		}
	}
	*fp = fil;

	return 0;
}

/* ------------------------------------------------------------------------- 
 * Set a key in the cache. If it exists overwrite it, if not, then create
 * it. Removed (negative) keys are also inserted into the cache.
 * ------------------------------------------------------------------------- */
int _kp_cache_set(kp_path * kpp, kp_key * ck)
{
	kp_fil *fil;
	kp_key *key;
	int res;
	int isdir;

	res = kp_get_file(kpp, &fil, &isdir);
	if (res != 0)
		return res;

	if ((ck->flags & KPFL_REMOVED) != 0) {
#ifdef KPDEBUG
		printf("kp_cache_set: get cached key %s\n", kpp->path);
#endif
		key = kp_get_cached_key(fil, ck->name);

		if (key == NULL)
			return KPERR_NOKEY;
	}

	if (!(ck->flags & KPFL_REMOVED)) {
#ifdef KPDEBUG
		printf("Kp_cache_set: kp_check_insert %s\n", kpp->path);
#endif
		res = kp_check_insert(fil, ck->name, &key);
		if (res != 0)
			return res;
	}

	fil->dirty = 1;

	if (key != NULL)
		kp_value_destroy(key);
	else {
#ifdef KPDEBUG
		printf("kp_cache_set: create new %s\n", kpp->path);
#endif
		key = (kp_key *) malloc_check(sizeof(kp_key));
		key->next = fil->keys;
		fil->keys = key;
	}

	key->type = ck->type;
	key->len = ck->len;
	key->data = ck->data;
	key->name = ck->name;
	key->flags = ck->flags;
	key->flags |= KPFL_DIRTY;

	/* Data is MOVED to key, not COPIED */
	ck->data = NULL;
	ck->name = NULL;

	return 0;
}

/* ------------------------------------------------------------------------- 
 * Get the value of a key from a given section
 * ------------------------------------------------------------------------- */
int _kp_cache_get(kp_path * kpp, const char *keyname, kpval_t type,
		  kp_key * ck)
{
	kp_fil *fil;
	kp_key *key;
	int res;
	int isdir;
#ifdef KPDEBUG
	printf("kp_cache_get\n");
#endif

	res = kp_get_file(kpp, &fil, &isdir);
	if (res != 0)
		return res;

	key = kp_get_cached_key(fil, keyname);

	if (key == NULL || key->flags & KPFL_REMOVED)
		return KPERR_NOKEY;

	if (key->flags & KPFL_BADDB)
		return KPERR_BADDB;

	if (type != KPVAL_UNKNOWN && key->type != type)
		return KPERR_BADTYPE;

	/* Data is COPIED */
	kp_value_new(ck, key->type, key->len, key->data);

	return 0;
}

/* ------------------------------------------------------------------------- 
 * Get the type of a key from a given section
 * ------------------------------------------------------------------------- */
int _kp_cache_get_type(kp_path * kpp, char *keyname, int iskeyfile,
		       kpval_t * tp)
{
	kp_fil *fil;
	kp_key *key;
	int res;
	unsigned int keynamelen;
	int isdir;
#ifdef KPDEBUG
	printf("kp_cache_get_type\n");
#endif

	res = kp_get_file(kpp, &fil, &isdir);
	if (isdir) {
		if (iskeyfile)
			return KPERR_BADKEY;
		else {
			*tp = KPVAL_DIR;
			return 0;
		}
	}
	if (res != 0)
		return res;

	if (fil->keys == NULL)
		return KPERR_NOKEY;

	keynamelen = strlen(keyname);

	for (key = fil->keys; key != NULL; key = key->next) {
		if (strcmp(key->name, keyname) == 0) {

			if (key->flags & KPFL_REMOVED)
				return KPERR_NOKEY;

			if (key->flags & KPFL_BADDB)
				return KPERR_BADDB;

			if (iskeyfile && keynamelen == 0)
				return KPERR_BADKEY;

			*tp = key->type;
			return 0;
		}
		if (!(key->flags & KPFL_REMOVED)) {
			if (keynamelen == 0) {
				if (!iskeyfile)
					return KPERR_BADKEY;

				*tp = KPVAL_DIR;
				return 0;
			}
			if (strncmp(key->name, keyname, keynamelen) == 0 &&
			    key->name[keynamelen] == '/') {
				*tp = KPVAL_DIR;
				return 0;
			}
		}
	}

	return KPERR_NOKEY;
}

/* ------------------------------------------------------------------------- 
 * Collect the subkeys of a given key in a file
 * ------------------------------------------------------------------------- */
static int kp_get_subkeys_file(kp_path * kpp, const char *keyname,
			       struct key_array *keys)
{
	kp_fil *fil;
	kp_key *key;
	int res;
	unsigned int keynamelen;
	int found;
	char *s;
	char *ent;
	int isdir;
#ifdef KPDEBUG
	printf("kp_cache_get_subkeys_file\n");
#endif

	res = kp_get_file(kpp, &fil, &isdir);
	if (res != 0)
		return res;

	if (fil->keys == NULL)
		return KPERR_NOKEY;

	keynamelen = strlen(keyname);

	found = 0;
	for (key = fil->keys; key != NULL; key = key->next) {
		if ((key->flags & KPFL_REMOVED) != 0)
			continue;

		if (keynamelen == 0 ||
		    (strlen(key->name) > keynamelen
		     && key->name[keynamelen] == '/'
		     && strncmp(key->name, keyname, keynamelen) == 0)) {

			s = key->name + keynamelen;
			if (*s == '/')
				s++;

			ent = strdup_check(s);
			s = ent;
			while (*s && *s != '/')
				s++;

			*s = '\0';

			_kp_add_subkey_check(keys, ent);
			free(ent);

			found = 1;
		}
	}

	if (!found)
		return KPERR_NOKEY;

	return 0;
}

/* ------------------------------------------------------------------------- 
 * Get the subkeys of a given key, from a directory or from a file
 * ------------------------------------------------------------------------- */
int _kp_cache_get_subkeys(kp_path * kpp, const char *keypath,
			  int iskeyfile, struct key_array *keys)
{
	int res;
#ifdef KPDEBUG
	printf("kp_cache_get_subkeys\n");
#endif

	if (!iskeyfile) {
		res = _kp_get_subkeys_dir(kpp->path, keys);
	} else
		res = kp_get_subkeys_file(kpp, keypath, keys);

	return res;
}

/* ------------------------------------------------------------------------- 
 * Flush the dirty files in the cache to the disk
 * ------------------------------------------------------------------------- */
int _kp_cache_flush()
{
	int res;
	int finalres = 0;
	kp_fil *fil;
#ifdef KPDEBUG
	printf("kp_cache_flush\n");
#endif
	for (fil = kp_cachef; fil != NULL; fil = fil->next) {
		if (fil->dirty) {
			res = _kp_write_file(&fil->kpp, fil->keys);

			/* The return value of the flush will be the value of the
			   last error that occured */

			if (res != 0)
				finalres = res;

			fil->dirty = 0;
		}
	}

	return finalres;
}

/* End of kp_cache.c */