/*
 *	db.c : Database routines for xkal.
 *	       Handles searching and updating the appointment graph.
 *	       See db.h for a description of the data structure.
 *
 *	George Ferguson, ferguson@cs.rochester.edu, 27 Oct 1990.
 *	Version 1.1 - 27 Feb 1991.
 *
 *	$Id: db.c,v 1.2 91/12/20 12:23:48 jik Exp $
 *
 * The following can be used in place of the ones from X11/Intrinsic.h:
 *	#define XtNew(T)	(T *)malloc(sizeof(T))
 *	#define XtFree(X)	if ((X) != NULL) free(X)
 *	#define XtNewString(S)	strcpy(malloc(strlen(S)+1),S)
 *
 */
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <sys/types.h>			/* for writeDb() backup only */
#include <sys/stat.h>			/*  "	  "	    "	  "  */
#include <X11/Intrinsic.h>
#include "db.h"
#include "util.h"
#include "date-strings.h"
#include "app-resources.h"		/* for levelDelim and daySlashMonth */
#ifdef USE_ALERT
#include "alert.h"
#endif
extern char *program;

/*
 * Functions defined here:
 */
void initDb();
Msgrec *addEntry();
Boolean deleteEntry();
int lookupEntry();
void readDb(), writeDb(), writeAppoint();

/*
 * Data defined here (could be static but we might hack on the DB later,
 * like we do with xkal2xremind and the like).
 */
Yearrec *Db[8];

/*	-	-	-	-	-	-	-	-	*/

/*VARARGS1*/
static void
dprintf(fmt,a1,a2,a3,a4,a5,a6,a7,a8,a9)
char *fmt,*a1,*a2,*a3,*a4,*a5,*a6,*a7,*a8,*a9;
{
#ifdef debug
    printf(fmt,a1,a2,a3,a4,a5,a6,a7,a8,a9);
#endif
}

/*	-	-	-	-	-	-	-	-	*/
/*
 * initDb() : Pretty lame, and unneeded for most UNIX boxes, but I put
 *	it here anyway. We never erase the db, so we don't have to
 *	worry about this.
 */
void
initDb()
{
    int i;

    for (i=0; i < 8; i++)
	Db[i] = NULL;		/* should free them all */
}

/*	-	-	-	-	-	-	-	-	*/
/*
 * addEntry() : Add an entry to the appropriate place in the database
 *	by traversing it and possibly adding nodes. Not much to it.
 */
Msgrec *
addEntry(dow,year,mon,day,hour,mins,text,flag,level)
int dow,year,mon,day,hour,mins;
char *text;
int flag,level;
{
    Yearrec *yp;
    Monrec *mp;
    Dayrec *dp;
    Msgrec *xp;

    if (Db[dow] == NULL) {
	Db[dow] = XtNew(Yearrec);
	Db[dow]->year = year;
	Db[dow]->mons = NULL;
	Db[dow]->next = NULL;
    }
    for (yp = Db[dow]; yp->year != year && yp->next != NULL; yp = yp->next) ;
    if (yp->year != year) {
	yp->next = XtNew(Yearrec);
	yp->next->year = year;
	yp->next->mons = NULL;
	yp->next->next = NULL;
	yp = yp->next;
    }
    if (yp->mons == NULL) {
	yp->mons = XtNew(Monrec);
	yp->mons->mon = mon;
	yp->mons->days = NULL;
	yp->mons->next = NULL;
    }
    for (mp = yp->mons; mp->mon != mon && mp->next != NULL; mp = mp->next) ;
    if (mp->mon != mon) {
	mp->next = XtNew(Monrec);
	mp->next->mon = mon;
	mp->next->days = NULL;
	mp->next->next=NULL;
	mp = mp->next;
    }

    if (mp->days == NULL) {
	mp->days = XtNew(Dayrec);
	mp->days->day = day;
	mp->days->msgs = NULL;
	mp->days->next = NULL;
    }
    for (dp = mp->days; dp->day != day && dp->next != NULL; dp = dp->next) ;
    if (dp->day != day) {
	dp->next = XtNew(Dayrec);
	dp->next->day = day;
	dp->next->msgs = NULL;
	dp->next->next=NULL;
	dp = dp->next;
    }

    if (dp->msgs == NULL) {
	dp->msgs = XtNew(Msgrec);
	dp->msgs->next = NULL;
    } else {
	xp = dp->msgs;
	dp->msgs = XtNew(Msgrec);
	dp->msgs->next = xp;
    }
    dp->msgs->dow = dow;
    dp->msgs->year = year;
    dp->msgs->month = mon;
    dp->msgs->day = day;
    dp->msgs->hour = hour;
    dp->msgs->mins = mins;
    dp->msgs->text = XtNewString(text);
    dp->msgs->system = flag;
    dp->msgs->level = level;
    return(dp->msgs);
}

/*
 * deleteEntry() : Removes an entry matching the given parameters from
 *	the database, and tidies up afterwards. Note that everything must
 *	match, the text as well.
 */
Boolean
deleteEntry(dow,year,mon,day,hour,mins,text)
int dow,year,mon,day,hour,mins;
char *text;
{
    Yearrec *yp,*yp2;
    Monrec *mp,*mp2;
    Dayrec *dp,*dp2;
    Msgrec *xp,*xp2;

    for (yp = Db[dow]; yp != NULL && yp->year != year; yp = yp->next) ;
    if (yp == NULL)
	return(False);
    for (mp = yp->mons; mp != NULL && mp->mon != mon; mp = mp->next) ;
    if (mp == NULL)
	return(False);
    for (dp = mp->days; dp != NULL && dp->day != day; dp = dp->next) ;
    if (dp == NULL)
	return(False);
    for (xp = dp->msgs; xp != NULL; xp = xp2) {
	xp2 = xp->next;
	if (xp->hour == hour && xp->mins == mins &&
						strcmp(xp->text, text) == 0) {
	    XtFree(xp->text);		/* free the text */
	    if (dp->msgs == xp)		/* and the msgrec */
		dp->msgs = xp->next;
	    else {
		for (xp2 = dp->msgs; xp2->next != xp; xp2 = xp2->next);
		xp2->next = xp->next;
	    }
	    XtFree((XtPointer) xp);
	    if (dp->msgs == NULL) {	/* if no more entries, free the day */
		if (mp->days == dp)
		    mp->days = dp->next;
		else {
		    for (dp2 = mp->days; dp2->next != dp; dp2 = dp2->next);
		    dp2->next = dp->next;
		}
		XtFree((XtPointer) dp);
	    }
	    if (mp->days == NULL) {	/* if no more days, free the month */
		if (yp->mons == mp)
		    yp->mons = mp->next;
		else {
		    for (mp2 = yp->mons; mp2->next != mp; mp2 = mp2->next);
		    mp2->next = mp->next;
		}
		XtFree((XtPointer) mp);
	    }
	    if (yp->mons == NULL) {	/* if no more months, free the year */
		if (Db[dow] == yp)
		    Db[dow] = Db[dow]->next;
		else {
		    for (yp2 = Db[dow]; yp2->next != yp; yp2 = yp2->next);
		    yp2->next = yp->next;
		}
		XtFree((XtPointer) yp);
	    }
	    return(True);
	}
    }
    return(False);
}

/*	-	-	-	-	-	-	-	-	*/
/*
 * lookupEntry() : Fills in at most max entries in result[] with pointers
 *	to messages for the given date/time. Returns the number of
 *	entries filled or -1 if there were more than max entries (but the
 *	ones that were filled, ie max of them, are valid). As a special
 *	case, if max is -1, then no entries are filled (result can be NULL)
 *	and the number of entries is returned. (If useLevel is True, then
 *	the total of the levels of all applicable appointments is returned
 *	rather than the number of appointments.)
 *
 *	The definition of a match is complicated by the fact that items in
 *	the database may be only partially specified. The approach is as
 *	follows:
 *	(a) First consider all db records which do not specify a dow (ie.
 *	    from Db[0]). Within these, the day, month, and year must match
 *	    if given, otherwise they are assumed to match. The hour and
 *	    minutes must match explicitly.
 *	(b) Then consider all db records specifying the dow of the given date.
 *	    If the record does not also specify a day, then the month and
 *	    year must match if given, as above, and the time must match
 *	    exactly. If the record specifies both dow and day, then the
 *	    interpretation is "the first dow after date", so we have to
 *	    check if the current date is within a week of the date in the
 *	    record.
 *
 *	Note that the strings are not copied.
 */
int
lookupEntry(day,mon,year,hour,mins,max,result,useLevel)
int day,mon,year,hour,mins,max;
Msgrec *result[];
{
    Yearrec *yp;
    Monrec *mp;
    Dayrec *dp;
    int num,n,i;

    dprintf("lookup for %d:%02d, %d %s %d...\n",hour,mins,day,
						shortMonthStr[mon-1],year);
    num = 0;
    dprintf("  checking DB with dow = 0...\n");
    for (yp = Db[0]; yp != NULL; yp = yp->next) {
	dprintf("    year = %d\n", yp->year);
	if (yp->year == year || yp->year == 0)
	    for (mp = yp->mons; mp != NULL; mp = mp->next) {
		dprintf("      mon = %d\n", mp->mon);
		if (mp->mon == mon || mp->mon == 0)
		    for (dp = mp->days; dp != NULL; dp = dp->next) {
			dprintf("\tday = %d\n", dp->day);
			if (dp->day == day || dp->day == 0) {
			    n = lookupEntryDay(dp,hour,mins,max-num,
							result+num,useLevel);
			    if (n < 0)
				return(-1);
			    else
				num += n;
			}
		}
	}
    }
    dprintf("  checking DB with real dow...\n");
    for (yp = Db[computeDOW(day,mon,year)]; yp != NULL; yp = yp->next) {
	dprintf("    year = %d\n", yp->year);
	for (mp = yp->mons; mp != NULL; mp = mp->next) {
	    dprintf("      mon = %d\n", mp->mon);
	    for (dp = mp->days; dp != NULL; dp = dp->next) {
		dprintf("\tday = %d\n", dp->day);
		if (dp->day == 0) {
		    if ((yp->year == 0 || yp->year == year) &&
			(mp->mon == 0 || mp->mon == mon)) {
			n = lookupEntryDay(dp,hour,mins,max-num,
							result+num,useLevel);
			if (n < 0)
			    return(-1);
			else
			    num += n;
		    }
		} else {	/* have dow and day */
		    int d,m,y;

		    d = day;
		    m = mon;
		    y = year;
		    dprintf("\tscanning back from %d %d %d\n",d,m,y);
		    for (i=0; i < 7; i++)
			if (dp->day == d && (yp->year == 0 || yp->year == y) &&
					    (mp->mon == 0 || mp->mon == m))
			    break;
			else
			    prevDay(&d,&m,&y);
		    if (i < 7) {
			n = lookupEntryDay(dp,hour,mins,max-num,
							result+num,useLevel);
			if (n < 0)
			    return(-1);
			else
			    num += n;
		    }
		}
	    }
	}
    }
    dprintf("returning %d\n", num);
    return(num);
}

/*
 * lookupEntryDay() : Scans the list of messages associated with the
 *	given Dayrec and fills in at most max entries of result[] with
 *	pointers to the messages. Returns the number of entries filled in,
 *	or -1 if there were more entries than max (but all that were filled
 *	in, ie. max of them, are valid).
 *
 *	If the given hour or mins is -1, then it's assumed to match, making
 *	it easy to get all the entries for a day.
 */
static int
lookupEntryDay(dp,hour,mins,max,result,useLevel)
Dayrec *dp;
int hour,mins,max;
Msgrec *result[];
Boolean useLevel;
{
    Msgrec *xp;
    int num;

    num = 0;
    for (xp = dp->msgs; xp != NULL; xp = xp->next) {
	dprintf("\t  h:m = %d:%02d\n",xp->hour,xp->mins);
	if ((hour == -1 || xp->hour == -1 || xp->hour == hour) &&
	    (mins == -1 || xp->mins == -1 || xp->mins == mins)) {
	    dprintf("\t    YES\n");
	    if (max >= 0 && num >= max)
		return (-1);
	    else if (max > 0)
		result[num] = xp;
	    if (useLevel)
		num += xp->level;
	    else
		num += 1;
	}
    }
    return(num);
}

/*	-	-	-	-	-	-	-	-	*/
/*
 * readDb() : Reads the given file into the database, setting the flag
 *	bit on new entries appropriately. This function understands
 *	comments starting with #, and the #include capability. It uses
 *	parseLine to actually decode the date spec.
 */
void
readDb(name,systemFlag)
char *name;
int systemFlag;
{
    FILE *fp;
    char *fname,buf[256];
    int line,i,n,c;
    int dow,y,m,d,h,mins,lev;
    char *t;

    fname = expandFilename(name);
    if ((fp=fopen(fname,"r")) == NULL) {
	if (errno != ENOENT)	/* appointment file may not exist */
	    perror(fname);
	return;
    }
    c = line = 0;
    while (c != EOF) {
	n = 0;
	line += 1;
	while (n < 255 && (c=getc(fp)) != EOF && c != '\n')
	    buf[n++] = c;
	buf[n] = '\0';
	if (n == 0) {
	    continue;
	} else if (c != EOF && c != '\n') {
	    fprintf(stderr,"%s: line %d too long, file %s\n",program,
								line,fname);
	    while ((c=getc(fp)) != EOF && c != '\n') ;
	}
	i = 0;
	while (isspace(buf[i]))
	    i += 1;
	if (buf[i] == '#') {
	    if (strncmp(buf+i+1,"include",7) == 0) {
		i += 8;
		while (isspace(buf[i]))
		    i += 1;
		readDb(buf+i,systemFlag);
	    }
	    continue;
	}
	parseLine(buf+i,&dow,&y,&m,&d,&h,&mins,&t,&lev);
#ifdef debug
	if (dow != 0)
	    printf("%s ",shortDowStr[dow-1]);
	if (d != 0)
	    printf("%2d ",d);
	if (m != 0)
	    printf("%s ",shortMonthStr[m-1]);
	if (y != 0)
	    printf("%4d ",y);
	if (h != -1)
	    printf("%2d:%02d ",h,mins);
	if (lev != 0)
	    printf("@%d@ ",lev);
	printf("%s\n",t);
#endif
	addEntry(dow,y,m,d,h,mins,t,systemFlag,lev);
    }
    fclose(fp);
}

/*
 * writeDb() : Writes any non-system database entries to the given file
 *	in the canonical format. If writeAll is True, then system entries are
 *	also written.
 */
void
writeDb(name,writeAll)
char *name;
Boolean writeAll;
{
    FILE *fp;
    char *fname,*backup;
    struct stat stbuf;
    int dow;
    Yearrec *yp;
    Monrec *mp;
    Dayrec *dp;
    Msgrec *xp;

    fname = expandFilename(name);
    if (*appResources.backupExtension && stat(fname,&stbuf) == 0) {
	backup = XtMalloc(strlen(fname)+strlen(appResources.backupExtension)+1);
	strcpy(backup,fname);
	strcat(backup,appResources.backupExtension);
	(void)unlink(backup);
	if (link(fname,backup) < 0) {
#ifdef USE_ALERT
	    alert("Error: Can't backup \"%s\"; save aborted.",fname);
#else
	    fprintf(stderr,"\007%s: can't backup \"%s\"; save aborted\n",program,fname);
#endif
	    XtFree(backup);
	    return;
	}
	(void)unlink(fname);
	XtFree(backup);
    }
    if ((fp=fopen(fname,"w")) == NULL) {
	perror(fname);
	return;
    }
    for (dow = 0; dow < 8; dow++)
      for (yp = Db[dow]; yp != NULL; yp = yp->next)
	for (mp = yp->mons; mp != NULL; mp = mp->next)
	    for (dp = mp->days; dp != NULL; dp = dp->next)
		for (xp = dp->msgs; xp != NULL; xp = xp->next)
		    if (!xp->system || writeAll)
			writeAppoint(fp,xp);

    fclose(fp);
}

/*
 * writeAppoint(fp,msg) : Formats the msg onto the given stream.
 *
 * The following escapes are possible in appResources.outputFormat:
 *	%w	short form of day of the week
 *	%W	long form of day of the week
 *	%d	day
 *	%m	short form of month
 *	%M	long form of month
 *	%n	numeric form of month
 *	%y	year
 *	%Y	year modulo 100
 *	%t	time
 *	%l	criticality level
 *	%~	space iff previous pattern was printed
 *	%/	slash iff previous pattern was printed
 * Any other character is simply printed. The text of the appointment follows,
 * then a newline.
 */
void
writeAppoint(fp,msg)
FILE *fp;
Msgrec *msg;
{
    char *s;
    Boolean flag;

    s = appResources.outputFormat;
    flag = False;
    while (*s) {
	if (*s == '%') {
	    s += 1;
	    switch (*s) {
		case '\0': fprintf(fp,"%");
			   break;
		case '~': if (flag)
			      fprintf(fp," ");
			   break;
		case '/': if (flag)
			      fprintf(fp,"/");
			   break;
		case 'w': if (msg->dow > 0)
			      fprintf(fp,"%s",shortDowStr[msg->dow-1]);
			  flag = (msg->dow > 0);
			  break;
		case 'W': if (msg->dow > 0)
			      fprintf(fp,"%s",longDowStr[msg->dow-1]);
			  flag = (msg->dow > 0);
			  break;
		case 'd': if (msg->day > 0)
			      fprintf(fp,"%d",msg->day);
			  flag = (msg->day > 0);
			  break;
		case 'm': if (msg->month > 0)
			      fprintf(fp,"%s",shortMonthStr[msg->month-1]);
			  flag = (msg->month > 0);
			  break;
		case 'M': if (msg->month > 0)
			      fprintf(fp,"%s",longMonthStr[msg->month-1]);
			  flag = (msg->month > 0);
			  break;
		case 'n': if (msg->month > 0)
			      fprintf(fp,"%d",msg->month);
			  flag = (msg->month > 0);
			  break;
		case 'y': if (msg->year > 0)
			      fprintf(fp,"%d",msg->year);
			  flag = (msg->year > 0);
			  break;
		case 'Y': if (msg->year > 0)
			      fprintf(fp,"%d",msg->year%100);
			  flag = (msg->year > 0);
			  break;
		case 't': if (msg->hour != -1)
			     fprintf(fp,"%s",timetostr(msg->hour*60+msg->mins));
			  flag = (msg->hour != -1);
			  break;
		case 'l': if (msg->level > 0) {
			      fprintf(fp,"%c",*appResources.levelDelim);
			      fprintf(fp,"%d",msg->level);
			      if (*(appResources.levelDelim+1))
				  fprintf(fp,"%c",*(appResources.levelDelim+1));
			      else
				  fprintf(fp,"%c",*appResources.levelDelim);
			  }
			  flag = (msg->level > 0);
			  break;
		default: fprintf(fp,"%c",*s);
	    } /* switch */
	} else {
	   fprintf(fp,"%c",*s);
	}
	if (*s)
	    s += 1;
    } /* while */
    fprintf(fp,"%s\n",msg->text);
}
