#ifndef lint
static char    *sccsid = "@(#)xcal_alarm.c	1.25 (Hillside Systems) 8/9/94";
static char    *copyright = "@(#)Copyright 1989,1990,1993 Mark Majhor, Peter Collinson";
#endif				/* lint */
/***

* module name:
	xcal_alarm.c
* function:
	Deal with editable days
	This is derived from xcalendar's view of how to store things
* history:
	Original idea and code from
	Mark Majhor, Sequent written August 1990, received Sept 1990
	I have reworked most of the code and algorithms
	Peter Collinson
	Hillside Systems

* (C) Copyright: 1989/1990 Hillside Systems/Peter Collinson/Mark Majhor
	
	For full permissions and copyright notice - see xcal.c
	
***/
#include <X11/IntrinsicP.h>	/* for toolkit stuff */
#include <X11/Intrinsic.h>
#include <X11/Xos.h>
#include <X11/StringDefs.h>	/* for useful atom names */
#include <X11/Shell.h>
#include <X11/Xaw/AsciiText.h>
#include <X11/Xaw/Text.h>
#include <X11/Xaw/Command.h>
#include <X11/Xaw/Label.h>
#include <X11/Xaw/Form.h>
#include <X11/Xaw/Paned.h>
#include <stdio.h>		/* for printing error messages */
#include <ctype.h>
#include <sys/stat.h>		/* for stat() */
#include <pwd.h>		/* for getting username */
#include <errno.h>
#include "xcal.h"

#ifndef wordBreak
#define wordBreak	0x01
#define scrollVertical	0x02
#endif

static char    *LocalMapStem;
static int	LocalMapStemMade;	/* day when LocalMapStem made last */
static struct stat localstat;
static char    *TodayFile;
static struct stat todaystat;
static Alarm    head;
static int     *countDown;
static int      countDownCt;

static XtCallbackRec callbacks[] = {
	{NULL, NULL},
	{NULL, NULL}
};

#define	dprintf	if (appResources.alarmScan) printf

#define	MINUTES(hr, mn)	((hr)*60 + (mn))

#define argLD(N,V) { XtSetArg(args[nargs], N, V); nargs++; }

/*
 * Local routines
 */
static void     AlarmScan();
static void     FreeAlarmList();
static void     setCall();
static void     beep();
static void     AddAlarm();
static void     DisplayAlarmWindow();
static void     DestroyAlarm();
static void     HoldAlarm();
static void     AutoDestroy();
static void	AlarmEvent();
static void	ClockTick();
static void	AlarmsOff();
static int      readDataLine();
static String	CreateCommand();
static int	ReloadAlarms();

/*
 * Initialise variables for the alarm mechanism
 */
void
InitAlarms()
{
	register char  *str;
	register char  *wk;
	register        ct;
	register int   *p;
	char           *XtMalloc();

	if (!MyCalendar)
		return;

	if (appResources.alarms == False) {
		dprintf("Alarms not enabled\n");
		return;
	}
	dprintf("Alarms on\n");
	/*
	 * Interpret the countDown string established
	 * by user - turn this into a vector
	 * countDown -> points to a vector of integers
	 *	indicating the number of mins each alarm will be set
	 * countDownCt - the number of integers in the vector
	 */
	if (str = appResources.countdown) {
		for (wk = str, ct = 0; *wk; wk++)
			if (*wk == ',')
				ct++;
		ct++;		/* no of things to store */

		countDown = (int *) XtMalloc(sizeof(int) * (ct + 2));

		p = countDown;
		while (*str)
			if (!isdigit(*str))
				str++;
			else
				break;
		while (*str) {
			*p = 0;
			ct = 0;
			while (isdigit(*str))
				ct = ct * 10 + *str++ - '0';
			*p++ = ct;
			countDownCt++;
			while (*str && !isdigit(*str))
				str++;
		}
	}
	if (appResources.update)
		ClockTick(0, 0);
	else
		AlarmFilePoll(0);
}


/*
 * ClockTick is polled every minute (update) to see if the data file has
 * altered - if so then a new data list is built
 */
static void
ClockTick(client_data, id)
	caddr_t         client_data;
	XtIntervalId   *id;
{
	long            secs;
	time_t          ti;
	struct tm      *tm;


	ti = time(0);
	tm = localtime(&ti);

	dprintf("ClockTick %d:%02d:%02d\n", tm->tm_hour, tm->tm_min, tm->tm_sec);

	secs = ClockSync(tm, appResources.update);
	(void) XtAppAddTimeOut(appContext, secs * 1000,
			    (XtTimerCallbackProc) ClockTick,
			    (caddr_t) NULL);
	AlarmFilePoll(tm);
	MemoPoll();
}

void
AlarmFilePoll(tm)
	struct tm      *tm;
{
	time_t          ti;
	int		mnow;
	int             files, changed;
	struct stat	oldstat;
	char           *home;
	char            buf[256];
	Alarm		*old;

	if (appResources.alarms == False)
		return;

	if (tm == NULL) {
		ti = time(0);
		tm = localtime(&ti);
	}
	/*
	 * Create the name of the data file in a cache to save energy
	 */
	if (LocalMapStemMade != tm->tm_mday) {
		LocalMapStemMade = tm->tm_mday;
		if (appResources.calCompat == False) {
			(void) sprintf(buf, "%s/xy%d/xc%d%s%d",
				       MapStem,
				       tm->tm_year + 1900,
				       tm->tm_mday,
				       appResources.smon[tm->tm_mon],
				       tm->tm_year + 1900);
		} else {
			(void) sprintf(buf, "%s/xc%d%s%d",
				       MapStem,
			 	       tm->tm_mday,
				       appResources.smon[tm->tm_mon],
				       tm->tm_year + 1900);
		}
		if (LocalMapStem)
			XtFree(LocalMapStem);
		LocalMapStem = XtNewString(buf);
		dprintf("Todays Daily Filename = %s\n", LocalMapStem);

		if (TodayFile)
			XtFree(TodayFile);

		TodayFile = XtNewString(MakeWeeklyName(tm->tm_wday));

		memset(&localstat, '\0', sizeof (struct stat));
		memset(&todaystat, '\0', sizeof (struct stat));
	}
	files = 0;
	changed = 0;
	/*
	 * check for file existence
	 */
	if (access(LocalMapStem, F_OK) == 0) {
		files = 1;
		oldstat = localstat;
		(void) stat(LocalMapStem, &localstat);
		if (oldstat.st_mtime != localstat.st_mtime ||
		    oldstat.st_ctime != localstat.st_ctime)
			changed = 1;
	}
	if (access(TodayFile, F_OK) == 0) {
		files |= 2;
		oldstat = todaystat;
		(void) stat(TodayFile, &todaystat);
		if (oldstat.st_mtime != todaystat.st_mtime ||
		    oldstat.st_ctime != todaystat.st_ctime)
			changed |= 2;
	}

	old = NULL;
	if (changed) {
		old = head.next;
		head.next = NULL;
	}
	if (files) {
		if (changed) {
			mnow = MINUTES(tm->tm_hour, tm->tm_min);
			if (files & 1)
				AlarmScan(LocalMapStem, tm, mnow);
			if (files & 2)
				AlarmScan(TodayFile, tm, mnow);
			/* check on any extant alarm */
			if (ReloadAlarms(old, mnow))
				AlarmsOff();
			if (appResources.interval_id == 0 && head.next)
				setCall();
		}
	} else
		dprintf("No files to scan\n");

	if (old)
		FreeAlarmList(old);

	UpdateMemo();
}

static int
ReloadAlarms(old, mnow)
	Alarm	       *old;
	int		mnow;
{
	Alarm	       *prev;

	dprintf("Checking old alarm details\n");
	if (head.next == NULL) {
		if (old) {
			dprintf("No new alarms, stop old running\n");
			return 1;
		}
	}
	else if (head.next->alarm_mm <= mnow && 
	         (old == NULL ||
	 	  old->alarm_mm > head.next->alarm_mm)) {
		/* 
		 * this looks different but let's be careful
		 * if the entries on the new list are earlier
		 * than those on the old - then we have
		 * serviced those requests and we need to
		 * ensure that we don't fire them again
		 */
		while (head.next && head.next->alarm_mm <= mnow &&
		       (old == NULL ||
		        old->alarm_mm > head.next->alarm_mm)) {
			dprintf("Discard alarm\n");
			XtFree(head.next->alarm);
			XtFree(head.next->what);
			prev = head.next;
			head.next = prev->next;
			XtFree((char *)prev);
		}
		return (ReloadAlarms(old, mnow));
	}
	else if (old && old->alarm_mm != head.next->alarm_mm) {
		dprintf("First new alarm differs from old, stop old alarm\n");
		dprintf("Old: %d %s %s\n", old->alarm_mm, old->alarm, old->what);
		dprintf("New: %d %s %s\n", head.next->alarm_mm, head.next->alarm, head.next->what);
		return 1;
	}
	return 0;
}

static void
AlarmScan(file, tm, mnow)
	String          file;
	struct tm      *tm;
	int             mnow;
{
	FILE           *fp;
	char            hrstr[16];
	char            remline[256];
	char           *rem;
	int             hr, mn;
	Boolean         isAction;


	dprintf("Scanning data file %s\n", file);

	if ((fp = fopen(file, "r")) == NULL) {
		fprintf(stderr, "Unexpected failure to open: %s\n", file);
		exit(1);
	}
	while (readDataLine(fp, &hr, &mn, hrstr, remline)) {
		/* see if we have an action string to do */
		isAction = False;
		rem = remline;
		if (*rem == '!' || strncmp(remline, "%cron", 5) == 0) {
			if (appResources.execalarms == False)
				continue;
			isAction = True;
			if (*rem == '!')
				rem++;
			else	rem += 5;
			while (isspace(*rem))
				rem++;
			if (*rem == '\0')
				continue;
		}
		AddAlarm(mnow, isAction, hr, mn, hrstr, rem);
	}
	(void) fclose(fp);
}

/*
 * The idea here is to generate a sorted event list - one element for each
 * event. We will discard anything that has already happened
 */
static void
AddAlarm(mnow, exec, hr, mn, hrstr, rem)
	int             mnow;
	Boolean         exec;
	int             hr;
	int             mn;
	char           *hrstr;
	char           *rem;
{

	Alarm          *al, *prev, *new;
	char           *XtMalloc();
	int             al_hr, al_mn, mm;
	int             loop;
	int             zero = 0;
	int            *p;

	if (exec || countDownCt == 0) {
		loop = 1;
		p = &zero;
	} else {
		loop = countDownCt;
		p = countDown;
	}

	for (; loop--; p++) {
		al_hr = hr;
		al_mn = mn;
		if (*p) {
			al_mn -= *p;
			if (al_mn < 0) {
				al_mn += 60;
				al_hr--;
				if (al_hr < 0)
					continue;
			}
		}
		if ((mm = MINUTES(al_hr, al_mn)) < mnow)
			continue;

		new = (Alarm *) XtMalloc(sizeof(Alarm));
		new->alarm = XtNewString(hrstr);
		new->what = XtNewString(rem);
		new->alarm_mm = mm;
		new->alarm_state = *p;
		new->isAction = exec;
		new->next = NULL;

		/* now insert into correct place in List */
		if (head.next == NULL) {
			head.next = new;
			dprintf("%s - %s; alarm at %02d:%02d\n",
				hrstr, rem, al_hr, al_mn);
		} else {
			for (prev = &head, al = head.next; al; prev = al, al = prev->next)
				if (mm < al->alarm_mm)
					break;
			prev->next = new;
			new->next = al;
			dprintf("%s - %s; alarm at %02d:%02d\n",
				hrstr, rem, al_hr, al_mn);
		}
	}
}

/*
 * read a line looking for a time spec and some data
 * return 1 if found
 * return 0 at end
 * Time spec is: hhmm hmm
 * hh:mm hh.mm
 * h:mm h.mm
 * all above may be optionally followed by:
 * A a AM am Am aM P p PM pm Pm pM
 * the string is terminated by a space or a tab
 */
static int
readDataLine(fin, hrp, minp, timestr, remline)
	FILE           *fin;
	int            *hrp;
	int            *minp;
	char           *timestr;
	char           *remline;
{
	register enum readState {
		Ignore, Hr1, Hr2, HrSep,
		Mn1, Mn2, AmPm, LoseM,
		LookSp, LoseSp, Store, AllDone
	}               state;
	register int    c = 0;
	int             hr = 0, mn = 0;
	char           *destp;
	Boolean		foundampm = False;

	if (feof(fin))
		return 0;

	state = Hr1;

	while (state != AllDone) {
		if ((c = getc(fin)) == EOF) {
			if (state == Store)
				break;
			return 0;
		}
		switch (state) {
		case Ignore:
			if (c == '\n')
				state = Hr1;
			continue;
		case Hr1:
			destp = timestr;
			if (isdigit(c)) {
				hr = c - '0';
				mn = 0;
				destp = timestr;
				*destp = '\0';
				state = Hr2;
				break;
			}
			state = (c == '\n' ? Hr1 : Ignore);
			continue;
		case Hr2:
			if (isdigit(c)) {
				hr = hr * 10 + c - '0';
				state = HrSep;
				break;
			}
			/* Falls through to .. */
		case HrSep:
			if (c == ':' || c == '.') {
				state = Mn1;
				break;
			}
			/* Falls through to .. */
		case Mn1:
			if (isdigit(c)) {
				mn = c - '0';
				state = Mn2;
				break;
			}
			/* Falls through to .. */
		case Mn2:
			if (isdigit(c)) {
				mn = mn * 10 + c - '0';
				state = AmPm;
				break;
			} else if (state == Mn2) {
				state = (c == '\n' ? Hr1 : Ignore);
				continue;
			}
			/* Falls through to .. */
		case AmPm:
			if (c == 'a' || c == 'A') {
				if (hr == 12)
					hr = 0;
				foundampm = True;
				state = LoseM;
				break;
			} else if (c == 'p' || c == 'P') {
				if (hr < 12)
					hr += 12;
				foundampm = True;
				state = LoseM;
				break;
			}
			/* Falls through to .. */
		case LoseM:
			if (c == 'M' || c == 'm') {
				state = LookSp;
				break;
			}
			/* Falls through to .. */
		case LookSp:
			if (c == ' ' || c == '\t') {
				state = LoseSp;
				if (hr >= 24 || mn >= 60)
					state = Ignore;
				destp = remline;
				*destp = '\0';
				continue;
			} else {
				state = (c == '\n' ? Hr1 : Ignore);
				continue;
			}
			break;
		case LoseSp:
			if (c == ' ' || c == '\t')
				continue;
			state = Store;
			/* Falls through to .. */
		case Store:
			if (c == '\n') {
				state = AllDone;
				continue;
			}
			break;
		}
		*destp++ = c;
		*destp = '\0';
	}
	/*
	 * idea from Marc Pawliger
	 * if you don't give an am or pm, then hours from 0 to minAlarmWarp
	 * is treated as a pm time
	 */
	if (appResources.alarmWarp == True &&
	    hr <= appResources.minAlarmWarp &&
	    appResources.minAlarmWarp < 12 &&
	    foundampm == False)
		hr += 12;
	*hrp = hr;
	*minp = mn;
	return 1;
}

/*
 * set up the timeout for the next event
 */
static void
setCall()
{
	time_t          ti;
	long		mnow;
	struct tm      *tm;
	Alarm          *sc;
	int             togo;

	ti = time(0);
	tm = localtime(&ti);
	mnow = MINUTES(tm->tm_hour, tm->tm_min);

	for (sc = head.next; sc; sc = sc->next) {
		togo = sc->alarm_mm - mnow;
		if (togo == 0)
		{	AlarmEvent(1, 0);	/* don't miss any current alarms */
			sc = &head;
			continue;
		}
		if (togo < 0)
			continue;
		appResources.interval_id = XtAppAddTimeOut(appContext, 
			  togo * 60 * 1000 - tm->tm_sec * 1000,
			  (XtTimerCallbackProc) AlarmEvent, (caddr_t) NULL);
		dprintf("Alarm in %d mins (less %d secs)\n", togo, tm->tm_sec);
		break;
	}
}


static void
AlarmEvent(client_data, id)
	caddr_t         client_data;
	XtIntervalId   *id;
{
	int             tnow;
	long            ti;
	struct tm      *tm;
	Alarm          *al, *action, *actionend;
	String		cmd;

	appResources.interval_id = 0;

	ti = time(0);
	tm = localtime(&ti);
	tnow = MINUTES(tm->tm_hour, tm->tm_min);

	dprintf("Alarm %d:%02d:%02d\n", tm->tm_hour, tm->tm_min, tm->tm_sec);

	/*
	 * Look for alarms that have fired and remove them
	 * from the list
	 */
	actionend = NULL;
	for (al = head.next; al; al = al->next) {
		if (al->alarm_mm <= tnow)
			actionend = al;
		else break;
	}
	if (actionend) {
		al = head.next;
		head.next = actionend->next;
		actionend->next = NULL;	
		/*
		 * now process the list
		 */
		while (al) {
			if (tnow == al->alarm_mm) {

				if (appResources.cmd != NULL) {
					cmd = CreateCommand(appResources.cmd, al->what);
					system(cmd);
					XtFree(cmd);
				}
				if (al->isAction == False) {
					DisplayAlarmWindow(al->alarm_state, al->alarm, al->what);
					beep();	/* notify the user */
				} else {
					cmd = CreateCommand(al->what, NULL);
					system(cmd);
					XtFree(cmd);
				}
			}
			actionend = al->next;
			XtFree(al->alarm);
			XtFree(al->what);
			XtFree((char *) al);
			al = actionend;
		}
	}
	if (client_data == NULL)
		setCall();
}

static String
CreateCommand(cmd, arg)
	String	cmd;
	String	arg;
{
	String	buf;
	char	*s, *d;
	char	*fmt;
	int	cmdlen;
	int	arglen;
	
	cmdlen = strlen(cmd);
	arglen = arg ? strlen(arg) : 0;

	buf = XtMalloc(cmdlen + arglen + 200);
	if (arglen == 0)
		fmt = "%s&";
	else	fmt = "%s '";
	(void) sprintf(buf, fmt, cmd);
	/*
	 * copy the arguments worrying about any embedded quote
	 */
#define	QUOTE	'\''
#define BACKSLASH '\\'
	if (arglen) {
		d = buf + strlen(buf);
		for(s = arg; *s; s++) {
			if (*s == QUOTE) {
				*d++ = QUOTE;
				*d++ = BACKSLASH;
				*d++ = QUOTE;
			}
			*d++ = *s;
		}
		*d++ = QUOTE;
		*d++ = '&';
		*d++ = '\0';
	}
	return buf;
}

static void
beep()
{
	register Display *dpy = XtDisplay(toplevel);
	int             i;

	for (i = 0; i < appResources.nbeeps; i++) {
		XBell(dpy, appResources.volume);
		sleep(1);
	}
}

static void
FreeAlarmList(al)
	register Alarm *al;
{
	register Alarm *nx;

	for (; al; al = nx) {
		nx = al->next;
		XtFree(al->alarm);
		XtFree(al->what);
		XtFree((char *) al);
	}
}

static void
AlarmsOff()
{
	if (appResources.interval_id) {
		XtRemoveTimeOut(appResources.interval_id);
		appResources.interval_id = 0;
	}
}
	
typedef struct {
	Widget          sa_top;
	XtIntervalId    sa_id;
} AlarmStatus;

static void
DisplayAlarmWindow(tleft, str1, str2)
	int             tleft;
	String          str1, str2;
{
	Widget          shell, form, title, aq, ah;
	Arg             args[10];
	Cardinal        nargs;
	char           *fmt;
	char            buf[255];
	AlarmStatus    *als;

	/*
	 * making the top level shell the override widget class causes it to
	 * popup without window manager interaction or window manager
	 * handles.  this also means that it pops on the foreground of an
	 * xlocked terminal and is not resizable by the window manager.  If
	 * any one finds that to be desirable behavior, then change the
	 * transient class below to override.
	 * 
	 * For now transient class is much better behaved
	 */
	nargs = 0;
	argLD(XtNallowShellResize, True);
	argLD(XtNinput, True);
	argLD(XtNsaveUnder, TRUE);
	shell = XtCreatePopupShell("alarm", transientShellWidgetClass,
				   toplevel, args, nargs);

	form = XtCreateManagedWidget("alarmPanel", panedWidgetClass,
				     shell, NULL, 0);
	/*
	 * create alarm status save area
	 */
	als = (AlarmStatus *) XtMalloc(sizeof(AlarmStatus));

	als->sa_top = shell;
	als->sa_id = (XtIntervalId) NULL;
	if (appResources.autoquit) {
		als->sa_id = XtAppAddTimeOut(appContext,
				   	     appResources.autoquit * 1000,
					     AutoDestroy, (caddr_t) als);
	}
	nargs = 0;
	argLD(XtNshowGrip, False);
	argLD(XtNdefaultDistance, 2);
	title = XtCreateManagedWidget("alarmForm", formWidgetClass,
				      form, args, nargs);
	/*
	 * Exit button Take "Quit" from resources
	 */
	callbacks[0].callback = DestroyAlarm;
	callbacks[0].closure = (caddr_t) als;
	nargs = 0;
	argLD(XtNcallback, callbacks);
	argLD(XtNfromHoriz, NULL);
	argLD(XtNleft, XtChainLeft);
	argLD(XtNright, XtChainLeft);
	aq = XtCreateManagedWidget("alarmQuit", commandWidgetClass,
				   title, args, nargs);

	/*
	 * Hold button Take "Hold" from resources
	 */
	callbacks[0].callback = HoldAlarm;
	callbacks[0].closure = (caddr_t) als;
	nargs = 0;
	argLD(XtNcallback, callbacks);
	argLD(XtNfromHoriz, aq);
	if (!appResources.autoquit) {
		argLD(XtNsensitive, False);
	}
	argLD(XtNleft, XtChainLeft);
	argLD(XtNright, XtChainLeft);
	ah = XtCreateManagedWidget("alarmHold", commandWidgetClass,
				   title, args, nargs);

	if (tleft == 0)
		fmt = appResources.alarmnow;
	else
		fmt = appResources.alarmleft;

	if (fmt && *fmt) {
		nargs = 0;
		argLD(XtNfromHoriz, ah);
		argLD(XtNleft, XtChainLeft);
		argLD(XtNright, XtChainLeft);
		argLD(XtNborderWidth, 0);
		argLD(XtNfromVert, NULL);
		argLD(XtNvertDistance, 3);
		(void) sprintf(buf, fmt, tleft);
		argLD(XtNlabel, buf);
		(void) XtCreateManagedWidget("alarmTitle", labelWidgetClass, title, args, nargs);
	}
	/*
	 * Now the text which is the remainder of the panel
	 */
	(void) sprintf(buf, "%s\n%s\n", str1, str2);
	nargs = 0;
	argLD(XtNshowGrip, False);
	argLD(XtNstring, buf);
	argLD(XtNdisplayCaret, False);
	(void) XtCreateManagedWidget("alarmText", asciiTextWidgetClass,
				     form, args, nargs);

	XtPopup(shell, XtGrabNone);
}

/* ARGSUSED */
static void
DestroyAlarm(w, als, call_data)
	Widget          w;
	AlarmStatus    *als;
	caddr_t         call_data;
{
	if (als->sa_id)
		XtRemoveTimeOut(als->sa_id);
	AutoDestroy(als, NULL);
}

/* ARGSUSED */
static void
HoldAlarm(w, als, call_data)
	Widget          w;
	AlarmStatus    *als;
	caddr_t         call_data;
{
	if (als->sa_id)
		XtRemoveTimeOut(als->sa_id);
	XtSetSensitive(w, FALSE);
}

/* ARGSUSED */
static void
AutoDestroy(als, id)
	AlarmStatus    *als;
	XtIntervalId   *id;
{

	XtDestroyWidget(als->sa_top);
	XtFree((char *) als);
}
