/*
 * dbtext.c  -  Text database routines
 *
 * Copyright (C) 2003-2007 Gero Kuhlmann <gero@gkminix.han.de>
 *
 *  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
 *  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., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 * $Id: dbtext.c,v 1.14 2007/01/06 18:31:38 gkminix Exp $
 */

#include <common.h>
#include <nblib.h>
#include "privlib.h"
#include "privdb.h"
#include "parsefile.h"



/*
 * Special values for current record pointer
 */
#define CURREC_EOF	(struct parserec *)(0)		/* end of file */
#define CURREC_BOF	(struct parserec *)(-1)		/* beginning of file */

#define currec_valid(id)	(id != CURREC_BOF && id != CURREC_EOF)



/*
 * Structure holding information about an open database
 */
struct dbtext {
	int               flags;	/* open flags */
	int               modified;	/* flag if contents modified */
	char             *lockfile;	/* lock file name */
	struct dbconfig  *config;	/* database configuration */
	struct parseloc  *mainloc;	/* location of main file */
	struct parserec  *currec;	/* current read record */
	struct parserec  *mark;		/* record mark */
	struct parseinfo *file;		/* file information */
};



/*
 * Local variables
 */
static int errors;



/*
 * Syntax error handler for parsefile()
 */
static void errorfile __F((msg, fname, lineno),
			const char *msg AND
			const char *fname AND
			int lineno)
{
  char *buf;

  errors++;
  buf = (char *)nbmalloc(strlen(fname) + strlen(msg) + 30);
  sprintf(buf, "[%s:%d] %s", fname, lineno, msg);
  nblib_db_error(buf, DBERR_CLASS_ERROR);
  free(buf);
}



/*
 * Delete all items in a record
 */
static void delitems __F((rec), struct parserec *rec)
{
  struct parseitem *pi, *pp;
  struct dbitem *di, *dp;

  if (rec != NULL) {
	/* Delete all old data items */
	di = rec->datalist;
	while (di != NULL) {
		dp = di;
		di = di->next;
		if (dp->name != NULL)
			free(dp->name);
		if (dp->type == item_string && dp->val.s != NULL)
			free(dp->val.s);
		free(dp);
	}

	/* Delete all old meta-data items */
	pi = rec->parselist;
	while (pi != NULL) {
		pp = pi;
		pi = pi->next;
		free(pp);
	}
  }
}



/*
 * Add new items to a record
 */
static void additems __F((rec, items),
			struct parserec *rec AND
			struct dbitem *items)
{
  struct dbitem *di;
  struct parseitem *pi, *pp;

  rec->datalist = di = items;
  rec->parselist = pi = NULL;
  while (di != NULL) {
	pp = (struct parseitem *)nbmalloc(sizeof(struct parseitem));
	pp->lineno = 0;
	pp->loc = rec->loc;
	pp->data = di;
	pp->next = NULL;
	if (rec->parselist == NULL)
		rec->parselist = pp;
	if (pi != NULL)
		pi->next = pp;
	pi = pp;
	rec->datalast = di;
	rec->parselast = pp;
	di = di->next;
  }
}



/*
 * Routine to determine the location of an error
 */
static char *text_errmsg __F((dbid, recname, item),
			DBID dbid AND
			const char *recname AND
			const struct dbitem *item)
{
  struct dbtext *db = (struct dbtext *)dbid;
  struct parserec *rec;
  struct parseitem *pi;
  char *cp, *fname = NULL;
  int len, lineno = 0;

  /* Check if the file is open */
  if (db->file == NULL)
	return(NULL);

  /* Find item or record in parse file description */
  rec = db->file->reclist;
  while (rec != NULL) {
	if (item != NULL) {
		pi = rec->parselist;
		while (pi != NULL && pi->data != item)
			pi = pi->next;
		if (pi != NULL) {
			if (pi->loc != NULL) {
				fname = pi->loc->fname;
				lineno = pi->lineno;
			}
			break;
		}
	} else if (!strcmp(rec->name, recname)) {
		if (rec->loc != NULL) {
			fname = rec->loc->fname;
			lineno = rec->lineno;
		}
		break;
	}
	rec = rec->next;
  }

  /*
   * Return location string. If it not possible to assign a file name
   * to the current item, use the default handling.
   */
  if (fname == NULL)
	return(NULL);
  len = strlen(fname) + 29;
  cp = (char *)nbmalloc(len);
  sprintf(cp, "[%s:%d]", fname, lineno);
  return(cp);
}



/*
 * Open a text database
 */
static DBID text_open __F((conf, flags),
			struct dbconfig *conf AND
			int flags)
{
  FILE *fd = NULL;
  struct dbtext *db = NULL;
  struct parseloc *loc;
  char *fname = NULL;
  char *lname = NULL;
  char *dirname = NULL;
  char *cp;
  int class;

  /* Check for flags */
  if (conf->mode == dbmode_ro) {
	/* This is just for safety - should have been done already */
	flags |= DBFLAGS_RO;
	flags &= ~DBFLAGS_ADD;
  }

  /* Check for file name */
  copystr(&fname, conf->config.text.fname);
  checkaccess(&fname, nbhomedir, ACCESS_FILE_READ);
  if (fname == NULL) {
	/* If the file is readonly and optional, we can return without error */
	if ((flags & (DBFLAGS_OPT | DBFLAGS_RO)) == (DBFLAGS_OPT | DBFLAGS_RO)) {
		class = DBERR_CLASS_INFO;
		goto openerr1;
	}

	/* If the file is readonly and non-optional, that's an error */
	if ((flags & (DBFLAGS_OPT | DBFLAGS_RO)) == DBFLAGS_RO) {
		class = DBERR_CLASS_ERROR;
openerr1:
		nblib_db_error("database file not found", class);
		goto openend;
	}

	/*
	 * Otherwise we have to create the file, so generate a suitable name.
	 * With the file descriptor being NULL the file parser routine just
	 * generates a file structure, but doesn't read anything. When we
	 * close the database lateron, we can create the database file and
	 * write the new contents.
	 */
	copystr(&fname, conf->config.text.fname);
	setpath(&fname, nbhomedir);
	assert(fname != NULL);
	if ((cp = strchr(fname, ':')) != NULL)
		*cp = '\0';
	if (!*fname) {
		class = DBERR_CLASS_ERROR;
		goto openerr1;
	}
  } else {
	/* Open the database file */
	fd = fopen(fname, "r");
	if (fd == NULL) {
		nblib_db_error("unable to open database file", DBERR_CLASS_FATAL);
		goto openend;
	}
  }

  /* Check for database file directory */
  copystr(&dirname, fname);
  if ((cp = strrchr(dirname, '/')) == NULL)
	copystr(&dirname, nbhomedir);
  else {
	*cp = '\0';
	checkaccess(&dirname, nbhomedir,
			((flags & DBFLAGS_RO) == 0 ?
					ACCESS_DIR_RW : ACCESS_DIR_READ));
  }
  if (dirname == NULL) {
	nblib_db_error("unable to access directory for database file",
							DBERR_CLASS_ERROR);
	goto openend;
  }

  /* Create a lock for the database file */
  copystr(&lname, conf->config.text.lockfile);
  if (lname != NULL) {
	setpath(&lname, dirname);
	assert(lname != NULL);
	if ((cp = strchr(lname, ':')) != NULL)
		*cp = '\0';
	if (strlen(lname) == 0 || (flags & DBFLAGS_RO) != 0) {
		free(lname);
		lname = NULL;
	} else {
		int lockret;

		lockret = filelock(lname);
		if (lockret == FILELOCK_ERROR) {
			nblib_db_error(NULL, DBERR_CLASS_FATAL);
			goto openend;
		} else if (lockret == FILELOCK_SET) {
			nblib_db_error("unable to lock database file",
							DBERR_CLASS_FATAL);
			goto openend;
		}
	}
  }

  /* Create a new database entity */
  assert(conf->type == dbtype_text);
  db = (struct dbtext *)nbmalloc(sizeof(struct dbtext));
  db->flags = flags;
  db->modified = FALSE;
  db->config = conf;
  db->lockfile = lname;
  lname = NULL;

  /* Read the entire file into memory */
  errors = 0;
  db->file = nblib_parse_file(fd, fname, &errorfile);

  /* Check for errors */
  if (db->file == NULL || nblib_parse_error != PARSESTATUS_SUCCESS) {
	if (nblib_parse_error == PARSESTATUS_SYNTAXERR && errors > 0) {
		cp = (char *)nbmalloc(56);
		sprintf(cp, "%d syntax errors in database file", errors);
		nblib_db_error(cp, DBERR_CLASS_ERROR);
		free(cp);
	} else {
		if (nblib_parse_error == PARSESTATUS_READERR)
			cp = "read error while processing database file";
		else
			cp = "unknown error while reading database file";
		nblib_db_error(cp, DBERR_CLASS_FATAL);
	}
	nblib_parse_free(db->file);
	free(db);
	db = NULL;
	goto openend;
  }

  /* Determine pointer to main location structure */
  loc = db->file->loclist;
  while (loc != NULL) {
	if (!strcmp(loc->fname, fname))
		break;
	loc = loc->next;
  }
  if (loc == NULL) {
	loc = (struct parseloc *)nbmalloc(sizeof(struct parseloc));
	copystr(&(loc->fname), fname);
	loc->next = db->file->loclist;
	db->file->loclist = loc;
  }
  db->mainloc = loc;
  db->currec = CURREC_BOF;
  db->mark = CURREC_EOF;

  /* Close the file again */
openend:
  if (fd != NULL)
	(void)fclose(fd);
  if (fname != NULL)
	free(fname);
  if (lname != NULL)
	free(lname);
  if (dirname != NULL)
	free(dirname);
  return(db == NULL ? DBID_NULL : (DBID)db);
}



/*
 * Close a text database file
 */
static void text_close __F((dbid), DBID dbid)
{
  FILE *fd;
  struct dbtext *db = (struct dbtext *)dbid;
  struct parserec *rec;
  struct dbitem *item;
  int status;

  /* If the contents has been modified, write everything */
  if ((db->flags & DBFLAGS_RO) == 0 && db->modified) {
	/* Check the database lock file */
	if (db->lockfile != NULL) {
		int lockret;

		lockret = checklock(db->lockfile);
		if (lockret == FILELOCK_ERROR) {
			nblib_db_error(NULL, DBERR_CLASS_FATAL);
			goto close_end;
		} else if (lockret != FILELOCK_SET) {
			nblib_db_error("invalid lock file for database",
							DBERR_CLASS_FATAL);
			goto close_end;
		}
	}

	/* Open database file for writing */
	if ((fd = fopen(db->mainloc->fname, "w")) == NULL) {
		nblib_db_error("unable to open database file for writing",
							DBERR_CLASS_FATAL);
		goto close_end;
	}

	/* Write every record into database file */
	rec = db->file->reclist;
	while (rec != NULL) {
		status = fprintf(fd, "[%s]\n", rec->name);
		item = rec->datalist;
		while (status >= 0 && item != NULL) {
			switch (item->type) {
				case item_string:
					if (item->val.s != NULL)
						status = fprintf(fd,
							"  %s = \"%s\"\n",
							item->name,
							item->val.s);
					break;
				case item_integer:
					status = fprintf(fd,
							"  %s = %ld\n",
							item->name,
							item->val.i);
					break;
				case item_boolean:
					status = fprintf(fd,
							"  %s = %s\n",
							item->name,
							(item->val.b ?
							   "TRUE" : "FALSE"));
					break;
				default:
					status = 0;
					break;
			}
			item = item->next;
			if (status >= 0 && item == NULL)
				status = fprintf(fd, "\n\n");
		}
		if (status < 0) {
			nblib_db_error("unable to write to database file",
							DBERR_CLASS_FATAL);
			goto close_end;
		}
		rec = rec->next;
	}
	if (fclose(fd) == -1) {
		nblib_db_error("unable to close database file",
							DBERR_CLASS_FATAL);
		goto close_end;
	}
  }

  /* Unlock the database file */
  if (db->lockfile != NULL) {
	if (!fileunlock(db->lockfile))
		nblib_db_error(NULL, DBERR_CLASS_FATAL);
  }

  /* Delete all data structures */
close_end:
  nblib_parse_free(db->file);
  if (db->lockfile != NULL)
	free(db->lockfile);
  free(db);
}



/*
 * Release a database lock. This is no-op here, because after releasing
 * the lock file for a text database, we would have to read the whole
 * contents again, which is extremely expensive. Text databases are not
 * intended for concurrent access anyway, so there is no use in releasing
 * the lock.
 */
static void text_release __F((dbid), DBID dbid)
{
  struct dbtext *db = (struct dbtext *)dbid;

  /* Just reset the current reading position */
  db->currec = CURREC_BOF;
  db->mark = CURREC_EOF;
}



/*
 * Find a record
 */
static int text_find __F((dbid, recname, first),
			DBID dbid AND
			const char *recname AND
			int first)
{
  struct dbtext *db = (struct dbtext *)dbid;
  struct parserec *rec;

  /* Set starting position and comparison length */
  if (first || db->currec == CURREC_BOF)
	rec = db->file->reclist;
  else if (db->currec == CURREC_EOF)
	return(FALSE);
  else
	rec = db->currec->next;

  /* Scan through all records */
  if (recname != NULL && *recname != '\0') {
	while (rec != NULL) {
		if (nblib_db_match(recname, rec->name))
			break;
		rec = rec->next;
	}
  }

  /* Prepare return value */
  db->currec = (rec == NULL ? CURREC_EOF : rec);
  return(rec != NULL ? TRUE : FALSE);
}



/*
 * Advance to next record
 */
static int text_next __F((dbid), DBID dbid)
{
  struct dbtext *db = (struct dbtext *)dbid;
  struct parserec *rec;

  /* Advance to next record */
  if (db->currec == CURREC_BOF)
	rec = db->file->reclist;
  else if (db->currec == CURREC_EOF)
	return(FALSE);
  else
	rec = db->currec->next;

  /* Prepare return value */
  db->currec = (rec == NULL ? CURREC_EOF : rec);
  return(rec != NULL ? TRUE : FALSE);
}



/*
 * Advance to previous record
 */
static int text_prev __F((dbid), DBID dbid)
{
  struct dbtext *db = (struct dbtext *)dbid;
  struct parserec *rec;

  /* Advance to previous record */
  if (db->currec == CURREC_BOF)
	return(FALSE);
  else if (db->currec == CURREC_EOF) {
	rec = db->file->reclist;
	while (rec != NULL && rec->next != NULL)
		rec = rec->next;
  } else {
	rec = db->file->reclist;
	while (rec != NULL && rec->next != db->currec)
		rec = rec->next;
  }

  /* Prepare return value */
  db->currec = (rec == NULL ? CURREC_BOF : rec);
  return(rec != NULL ? TRUE : FALSE);
}



/*
 * Advance to first record
 */
static int text_first __F((dbid), DBID dbid)
{
  struct dbtext *db = (struct dbtext *)dbid;
  struct parserec *rec;

  /* Advance to first record */
  rec = db->file->reclist;

  /* Prepare return value */
  db->currec = (rec == NULL ? CURREC_BOF : rec);
  return(rec != NULL ? TRUE : FALSE);
}



/*
 * Advance to last record
 */
static int text_last __F((dbid), DBID dbid)
{
  struct dbtext *db = (struct dbtext *)dbid;
  struct parserec *rec;

  /* Advance to last record */
  rec = db->file->reclist;
  while (rec != NULL && rec->next != NULL)
	rec = rec->next;

  /* Prepare return value */
  db->currec = (rec == NULL ? CURREC_EOF : rec);
  return(rec != NULL ? TRUE : FALSE);
}



/*
 * Return name of current record
 */
static char *text_curpos __F((dbid), DBID dbid)
{
  struct dbtext *db = (struct dbtext *)dbid;
  char *ret = NULL;

  /* Prepare return value */
  if (currec_valid(db->currec))
	copystr(&ret, db->currec->name);
  return(ret);
}



/*
 * Mark the current record
 */
static int text_mark __F((dbid, action), DBID dbid AND int action)
{
  struct dbtext *db = (struct dbtext *)dbid;

  /* Execute required action */
  switch (action) {
	case BOOKMARK_SET:
		/* Check if the current cursor position is valid */
		if (!currec_valid(db->currec)) {
			nblib_db_error("invalid database position for bookmark",
							DBERR_CLASS_ERROR);
			break;
		}

		/* Set new bookmark */
		db->mark = db->currec;
		return(TRUE);

	case BOOKMARK_RELEASE:
		/* Make the old bookmark invalid */
		db->mark = CURREC_EOF;
		return(TRUE);

	case BOOKMARK_JUMP:
		/* Check if we have anything to do */
		if (!currec_valid(db->mark))
			break;

		/* Set current cursor position with bookmark */
		db->currec = db->mark;
		db->mark = CURREC_EOF;
		return(TRUE);

	default:
		nblib_db_error("invalid bookmark action code",
							DBERR_CLASS_ERROR);
		break;
  }
  return(FALSE);
}



/*
 * Add a new record
 */
static void text_add __F((dbid, recname, items),
				DBID dbid AND
				const char *recname AND
				struct dbitem *items)
{
  struct dbtext *db = (struct dbtext *)dbid;
  struct parserec *newrec, *rec;

  /* This should have been checked in the upper levels already */
  assert((db->flags & (DBFLAGS_ADD | DBFLAGS_RO)) == DBFLAGS_ADD);

  /* Empty records are not allowed */
  assert(items != NULL);

  /* Create new record */
  newrec = (struct parserec *)nbmalloc(sizeof(struct parserec));
  newrec->lineno = 0;
  newrec->itemnum = 0;
  newrec->loc = db->mainloc;
  copystr(&(newrec->name), recname);

  /* Add new data items into the record */
  additems(newrec, items);

  /* Insert record at current position */
  if (db->currec == CURREC_BOF) {
	newrec->next = db->file->reclist;
	db->file->reclist = newrec;
  } else if (db->currec == CURREC_EOF) {
	newrec->next = NULL;
	rec = db->file->reclist;
	while (rec != NULL && rec->next != NULL)
		rec = rec->next;
	if (rec == NULL)
		db->file->reclist = newrec;
	else
		rec->next = newrec;
  } else {
	newrec->next = db->currec->next;
	db->currec->next = newrec;
  }
  db->currec = newrec;
  db->modified = TRUE;
}



/*
 * Delete current record
 */
static void text_delete __F((dbid), DBID dbid)
{
  struct dbtext *db = (struct dbtext *)dbid;
  struct parserec *rec;

  /* This should have been checked in the upper levels already */
  assert((db->flags & DBFLAGS_RO) == 0);

  /* Unable to delete the beginning and end of file indications */
  if (!currec_valid(db->currec)) {
	nblib_db_error("error deleting record", DBERR_CLASS_ERROR);
	return;
  }

  /* If we are going to delete the marked record, invalidate the mark */
  if (db->currec == db->mark)
	db->mark = CURREC_EOF;

  /* Remove the record from the record list */
  rec = db->file->reclist;
  if (rec == db->currec) {
	db->file->reclist = db->currec->next;
	db->currec = db->currec->next;
  } else if (rec != NULL) {
	while (rec->next != db->currec)
		rec = rec->next;
	if (rec != NULL) {
		rec->next = db->currec->next;
		rec = db->currec;
		db->currec = rec->next;
	}
  }

  /* Delete the current record */
  if (rec != NULL) {
	delitems(rec);
	if (rec->name != NULL)
		free(rec->name);
	free(rec);
  }
  db->modified = TRUE;
}



/*
 * Read a record from the current read position
 */
static struct dbitem *text_read __F((dbid, recname),
				DBID dbid AND
				char **recname)
{
  struct dbtext *db = (struct dbtext *)dbid;
  struct dbitem *ret;

  ret = NULL;
  if (!currec_valid(db->currec))
	nblib_db_error("error reading record", DBERR_CLASS_ERROR);
  else {
	copystr(recname, db->currec->name);
	ret = db->currec->datalist;
  }
  return(ret);
}



/*
 * Write a record into the current position
 */
static void text_write __F((dbid, items),
				DBID dbid AND
				struct dbitem *items)
{
  struct dbtext *db = (struct dbtext *)dbid;

  /* This should have been checked in the upper levels already */
  assert((db->flags & DBFLAGS_RO) == 0);

  /* Empty records are not allowed */
  assert(items != NULL);

  /* Check if we have a current read position */
  if (!currec_valid(db->currec)) {
	nblib_db_error("error writing record", DBERR_CLASS_ERROR);
	return;
  }

  /* Modify new record location */
  db->currec->lineno = 0;
  db->currec->loc = db->mainloc;

  /* Delete all old items */
  delitems(db->currec);

  /* Add new data items into the record */
  additems(db->currec, items);

  /* Mark database as modified */
  db->modified = TRUE;
}



/*
 * Structure to export all text database functions
 */
struct dbprocs db_textprocs = {
	&text_open,
	&text_find,
	&text_next,
	&text_prev,
	&text_first,
	&text_last,
	&text_curpos,
	&text_mark,
	&text_delete,
	&text_read,
	&text_write,
	&text_add,
	&text_release,
	&text_close,
	&text_errmsg
};

