#ifndef lint
static char    *sccsid = "@(#)xcal_alarm.c	1.13 (Hillside Systems) 1/16/91";
static char    *copyright = "@(#)Copyright 1989/1990 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 char	*TodayFile;
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))

/*
 *	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 XtTimerCallbackProc AlarmEvent();
static XtTimerCallbackProc ClockTick();
static int readDataLine();

/*
 *	Initialise variables for the alarm mechanism
 */
void
InitAlarms()
{
	register char *str;
	register char *wk;
	register ct;
	register int *p;
	char     *XtMalloc();
	
	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));
		if (countDown == (int *)0)
			Fatal("Error allocating memory\n");

		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);
}
	

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

	dprintf("ClockTick\n");

	ti = time(0);
	tm = localtime(&ti);
	
	/* syncronise on the clock if this makes sense */
	if (secs && 60%secs == 0 && tm->tm_sec%secs)
		secs -= tm->tm_sec%secs;
	
	/* install a new timer */
	(void) XtAddTimeOut(secs * 1000, ClockTick, (caddr_t) NULL);

	AlarmFilePoll(tm);
	MemoPoll();
}

void
AlarmFilePoll(tm)
	struct	tm	*tm;
{
	time_t	ti;
	int	files;
	char	*home;
	char	buf[256];
	char	*getenv();
	
	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 (LocalMapStem == NULL || (tm->tm_hour == 0 && tm->tm_min == 0))
	{	home = getenv("HOME");
		if (home == NULL)
		{	/* should do things with password files */
			/* but for now we will simply die */
			Fatal("Xcal needs HOME defined in the environment");
		}

		if (appResources.calCompat == False)
		{	(void) sprintf(buf, "%s/%s/xy%d/xc%d%s%d",
				       home, appResources.directory,
				       tm->tm_year + 1900, tm->tm_mday,
				       appResources.smon[tm->tm_mon],
				       tm->tm_year + 1900);
		}
		else
		{	(void) sprintf(buf, "%s/%s/xc%d%s%d",
				       home, appResources.directory,
				 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 = MakeWeeklyName(today.wday);
	}

	files = 0;
	/*
	 *	check for file existence
	 */
	if (access(LocalMapStem, F_OK) == 0)
		files = 1;
	if (access(TodayFile, F_OK) == 0)
		files |= 2;

	FreeAlarmList();

	if (files)
	{	if (files&1)
			AlarmScan(LocalMapStem, tm, MINUTES(tm->tm_hour, tm->tm_min));
		if (files&2)
			AlarmScan(TodayFile, tm, MINUTES(tm->tm_hour, tm->tm_min));
	}
	else
		dprintf("No files to scan");

	UpdateMemo();
}

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 == '!')
		{	isAction = True;
			rem++;
			while (*rem == ' ' || *rem == '\t')
			{	if (*rem == '\0')
					goto abort;
				rem++;
			}
		}
		else
		if (strncmp(remline, "%cron", 5) == 0)
		{	isAction = True;
			rem += 5;
			while (*rem == ' ' || *rem == '\t')
			{	if (*rem == '\0')
					goto abort;
				rem++;
			}
		}
		AddAlarm(mnow, isAction, hr, mn, hrstr, rem);
abort:
		;
	}
	(void) fclose(fp);

	/*
	 *	if we have a timeout - then set up an alarm for it
	 */
	if (head.next)
		setCall(tm, mnow);
}

/*
 *	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));
		if (new == NULL)
			Fatal("Error allocating alarm memory\n");
		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;

	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;
				state = LoseM;
				break;
			}
			else
			if (c == 'p' || c == 'P')
			{	if (hr < 12)
					hr += 12;
				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';
	}
	*hrp = hr;
	*minp = mn;
	return 1;
}

/*
 *	set up the timeout for the next event
 */
static void
setCall(tm, mnow)
	struct	tm *tm;
	int	mnow;
{
	Alarm	*sc;
	int	togo;
	
	for (sc = head.next; sc; sc = sc->next)
	{	togo = sc->alarm_mm - mnow;
		if (togo <= 0)
			continue;
		appResources.interval_id = XtAddTimeOut(togo*60*1000 - tm->tm_sec*1000,
						AlarmEvent, (caddr_t) NULL);
		dprintf("Alarm in %d mins\n", togo);
		break;
	}
}


static XtTimerCallbackProc
AlarmEvent(client_data, id)
	caddr_t         client_data;
	XtIntervalId   *id;
{
	int		tnow;
	long            ti;
	struct tm      *tm;
	Alarm		*al, *prev;
	char            buf[255];

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

	dprintf("Alarm %d:%02d\n", tm->tm_hour, tm->tm_min);
	
	for (prev = &head, al = head.next; al; prev = al, al = al->next)
	{	
		if (tnow == al->alarm_mm)
		{	if (appResources.cmd != NULL)
			{	(void) sprintf(buf, "%s %s &", appResources.cmd, al->what);
				system(buf);
			}
			if (al->isAction == False)
			{	DisplayAlarmWindow(al->alarm_state, al->alarm, al->what);
				beep();	/* notify the user */
			}
			else
				system(al->what);

		}

		if (al->alarm_mm <= tnow)
		{	prev->next = al->next;
			XtFree(al->alarm);
			XtFree(al->what);
			XtFree(al);
			al = prev;
		}
		else
			break;
	}
	setCall(tm, tnow);
}

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()
{
	register Alarm *al, *nx;

	for (al = head.next; al; al = nx)
	{	nx = al->next;
		XtFree(al->alarm);
		XtFree(al->what);
		XtFree(al);
	}
	head.next = NULL;
	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;
	XtSetArg(args[nargs], XtNallowShellResize, True); nargs++;
	XtSetArg(args[nargs], XtNinput, True); nargs++;
	XtSetArg(args[nargs], XtNsaveUnder, TRUE); nargs++;
	shell = XtCreatePopupShell("alarm", transientShellWidgetClass,
				   toplevel, args, nargs);

	form = XtCreateManagedWidget("alarmPanel", panedWidgetClass,
				     shell, NULL, 0);
	/*
	 * create alarm status save area
	 */
	als = (AlarmStatus *)XtMalloc(sizeof (AlarmStatus));
	if (als == NULL)
		Fatal("Out of memory\n");

	als->sa_top = shell;
	als->sa_id = NULL;
	if (appResources.autoquit)
	{	als->sa_id = XtAddTimeOut(appResources.autoquit*1000,
					AutoDestroy, (caddr_t) als);
	}
	 
	nargs = 0;
	XtSetArg(args[nargs], XtNshowGrip, False); nargs++;
	XtSetArg(args[nargs], XtNdefaultDistance, 2); nargs++;
	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;
	XtSetArg(args[nargs], XtNcallback, callbacks); nargs++;
	XtSetArg(args[nargs], XtNfromHoriz, NULL); nargs++;
	XtSetArg(args[nargs], XtNleft, XtChainLeft); nargs++;
	XtSetArg(args[nargs], XtNright, XtChainLeft); nargs++;
	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;
	XtSetArg(args[nargs], XtNcallback, callbacks); nargs++;
	XtSetArg(args[nargs], XtNfromHoriz, aq); nargs++;
	if (!appResources.autoquit) {
	  XtSetArg(args[nargs], XtNsensitive, False); nargs++;
	}
	XtSetArg(args[nargs], XtNleft, XtChainLeft); nargs++;
	XtSetArg(args[nargs], XtNright, XtChainLeft); nargs++;
	ah = XtCreateManagedWidget("alarmHold", commandWidgetClass,
				     title, args, nargs);
	
	if (tleft == 0)
		fmt = appResources.alarmnow;
	else
		fmt = appResources.alarmleft;

	if (fmt && *fmt)
	{	nargs = 0;
		XtSetArg(args[nargs], XtNfromHoriz, ah); nargs++;
		XtSetArg(args[nargs], XtNleft, XtChainLeft); nargs++;
		XtSetArg(args[nargs], XtNright, XtChainLeft); nargs++;
		XtSetArg(args[nargs], XtNborderWidth, 0); nargs++;
		XtSetArg(args[nargs], XtNfromVert, NULL); nargs++;
		XtSetArg(args[nargs], XtNvertDistance, 3); nargs++;
		(void) sprintf(buf, fmt, tleft);
		XtSetArg(args[nargs], XtNlabel, buf); nargs++;
		(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;
	XtSetArg(args[nargs], XtNshowGrip, False); nargs++;
	XtSetArg(args[nargs], XtNstring, buf); nargs++;
	XtSetArg(args[nargs], XtNdisplayCaret, False); nargs++;
	(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(als);
}
