/*
 * sched.c - execute commands at scheduled times
 *
 * This file is part of zsh, the Z shell.
 *
 * Copyright (c) 1992-1997 Paul Falstad
 * All rights reserved.
 *
 * Permission is hereby granted, without written agreement and without
 * license or royalty fees, to use, copy, modify, and distribute this
 * software and to distribute modified versions of this software for any
 * purpose, provided that the above copyright notice and the following
 * two paragraphs appear in all copies of this software.
 *
 * In no event shall Paul Falstad or the Zsh Development Group be liable
 * to any party for direct, indirect, special, incidental, or consequential
 * damages arising out of the use of this software and its documentation,
 * even if Paul Falstad and the Zsh Development Group have been advised of
 * the possibility of such damage.
 *
 * Paul Falstad and the Zsh Development Group specifically disclaim any
 * warranties, including, but not limited to, the implied warranties of
 * merchantability and fitness for a particular purpose.  The software
 * provided hereunder is on an "as is" basis, and Paul Falstad and the
 * Zsh Development Group have no obligation to provide maintenance,
 * support, updates, enhancements, or modifications.
 *
 */

#include "sched.mdh"
#include "sched.pro"

/* node in sched list */

typedef struct schedcmd  *Schedcmd;

/* Flags for each scheduled event */
enum schedflags {
    /* Trash zle if necessary when event is activated */
    SCHEDFLAG_TRASH_ZLE = 1
};

struct schedcmd {
    struct schedcmd *next;
    char *cmd;			/* command to run */
    time_t time;		/* when to run it */
    int flags;			/* flags as above */
};

/* the list of sched jobs pending */

static struct schedcmd *schedcmds;

/* flag that timed event is running (via addtimedfn())*/
static int schedcmdtimed;

/* Use addtimedfn() to add a timed event for sched's use */

/**/
static void
schedaddtimed(time_t t)
{
    /*
     * The following code shouldn't be necessary and indicates
     * a bug.  However, the DPUTS() in the caller should pick
     * this up so we can detect and fix it, and the following
     * Makes The World Safe For Timed Events in non-debugging shells.
     */
    if (schedcmdtimed)
	scheddeltimed();
    schedcmdtimed = 1;
    addtimedfn(checksched, schedcmds->time);
}

/* Use deltimedfn() to remove the sched timed event */

/**/
static void
scheddeltimed(void)
{
    if (schedcmdtimed)
    {
	deltimedfn(checksched);
	schedcmdtimed = 0;
    }
}


/* Check scheduled commands; call this function from time to time. */

/**/
static void
checksched(void)
{
    time_t t;
    struct schedcmd *sch;

    if(!schedcmds)
	return;
    t = time(NULL);
    /*
     * List is ordered, so we only need to consider the
     * head element.
     */
    while (schedcmds && schedcmds->time <= t) {
	/*
	 * Remove the entry to be executed from the list
	 * before execution:  this makes quite sure that
	 * the entry hasn't been monkeyed with when we
	 * free it.
	 */
	sch = schedcmds;
	schedcmds = sch->next;
	/*
	 * Delete from the timed function list now in case
	 * the called code reschedules.
	 */
	scheddeltimed();

	if ((sch->flags & SCHEDFLAG_TRASH_ZLE) && zleactive)
	    zleentry(ZLE_CMD_TRASH);
	execstring(sch->cmd, 0, 0, "sched");
	zsfree(sch->cmd);
	zfree(sch, sizeof(struct schedcmd));

	/*
	 * Fix time for future events.
	 * I had this outside the loop, for a little extra efficiency.
	 * However, it then occurred to me that having the list of
	 * forthcoming entries up to date could be regarded as
	 * a feature, and the inefficiency is negligible.
	 *
	 * Careful in case the code we called has already set
	 * up a timed event; if it has, that'll be up to date since
	 * we haven't changed the list here.
	 */
	if (schedcmds && !schedcmdtimed) {
	    /*
	     * We've already delete the function from the list.
	     */
	    DPUTS(timedfns && firstnode(timedfns),
		  "BUG: already timed fn (1)");
	    schedaddtimed(schedcmds->time);
	}
    }
}

/**/
static int
bin_sched(char *nam, char **argv, UNUSED(Options ops), UNUSED(int func))
{
    char *s, **argptr;
    time_t t;
    long h, m, sec;
    struct tm *tm;
    struct schedcmd *sch, *sch2, *schl;
    int sn, flags = 0;

    /* If the argument begins with a -, remove the specified item from the
    schedule. */
    for (argptr = argv; *argptr && **argptr == '-'; argptr++) {
	char *arg = *argptr + 1;
	if (idigit(*arg)) {
	    sn = atoi(arg);

	    if (!sn) {
		zwarnnam("sched", "usage for delete: sched -<item#>.");
		return 1;
	    }
	    for (schl = NULL, sch = schedcmds, sn--;
		 sch && sn; sch = (schl = sch)->next, sn--);
	    if (!sch) {
		zwarnnam("sched", "not that many entries");
		return 1;
	    }
	    if (schl)
		schl->next = sch->next;
	    else {
		scheddeltimed();
		schedcmds = sch->next;
		if (schedcmds) {
		    DPUTS(timedfns && firstnode(timedfns), "BUG: already timed fn (2)");
		    schedaddtimed(schedcmds->time);
		}
	    }
	    zsfree(sch->cmd);
	    zfree(sch, sizeof(struct schedcmd));

	    return 0;
	} else if (*arg == '-') {
	    /* end of options */
	    argptr++;
	    break;
	} else if (!strcmp(arg, "o")) {
	    flags |= SCHEDFLAG_TRASH_ZLE;
	} else {
	    if (*arg)
		zwarnnam(nam, "bad option: -%c", *arg);
	    else
		zwarnnam(nam, "option expected");
	    return 1;
	}
    }

    /* given no arguments, display the schedule list */
    if (!*argptr) {
	for (sn = 1, sch = schedcmds; sch; sch = sch->next, sn++) {
	    char tbuf[60], *flagstr, *endstr;
	    time_t t;
	    struct tm *tmp;

	    t = sch->time;
	    tmp = localtime(&t);
	    ztrftime(tbuf, 40, "%a %b %e %k:%M:%S", tmp, 0L);
	    if (sch->flags & SCHEDFLAG_TRASH_ZLE)
		flagstr = "-o ";
	    else
		flagstr = "";
	    if (*sch->cmd == '-')
		endstr = "-- ";
	    else
		endstr = "";
	    printf("%3d %s %s%s%s\n", sn, tbuf, flagstr, endstr,
		   unmeta(sch->cmd));
	}
	return 0;
    } else if (!argptr[1]) {
	/* other than the two cases above, sched *
	 *requires at least two arguments        */
	zwarnnam("sched", "not enough arguments");
	return 1;
    }

    /* The first argument specifies the time to schedule the command for.  The
    remaining arguments form the command. */
    s = *argptr++;
    if (*s == '+') {
	/*
	 * + introduces a relative time.  The rest of the argument may be an
	 * hour:minute offset from the current time.  Once the hour and minute
	 * numbers have been extracted, and the format verified, the resulting
	 * offset is simply added to the current time.
	 */
	zlong zl = zstrtol(s + 1, &s, 10);
	if (*s == ':') {
	    m = (long)zstrtol(s + 1, &s, 10);
	    if (*s == ':')
		sec = (long)zstrtol(s + 1, &s, 10);
	    else
		sec = 0;
	    if (*s) {
		zwarnnam("sched", "bad time specifier");
		return 1;
	    }
	    t = time(NULL) + (long)zl * 3600 + m * 60 + sec;
	} else if (!*s) {
	    /*
	     * Alternatively, it may simply be a number of seconds.
	     * This is here for consistency with absolute times.
	     */
	    t = time(NULL) + (time_t)zl;
	} else {
	    zwarnnam("sched", "bad time specifier");
	    return 1;
	}
    } else {
	/*
	 * If there is no +, an absolute time must have been given.
	 * This may be in hour:minute format, optionally followed by a string
	 * starting with `a' or `p' (for a.m. or p.m.).  Characters after the
	 * `a' or `p' are ignored.
	 */
	zlong zl = zstrtol(s, &s, 10);
	if (*s == ':') {
	    h = (long)zl;
	    m = (long)zstrtol(s + 1, &s, 10);
	    if (*s == ':')
		sec = (long)zstrtol(s + 1, &s, 10);
	    else
		sec = 0;
	    if (*s && *s != 'a' && *s != 'A' && *s != 'p' && *s != 'P') {
		zwarnnam("sched", "bad time specifier");
		return 1;
	    }
	    t = time(NULL);
	    tm = localtime(&t);
	    t -= tm->tm_sec + tm->tm_min * 60 + tm->tm_hour * 3600;
	    if (*s == 'p' || *s == 'P')
		h += 12;
	    t += h * 3600 + m * 60 + sec;
	    /*
	     * If the specified time is before the current time, it must refer
	     * to tomorrow.
	     */
	    if (t < time(NULL))
		t += 3600 * 24;
	} else if (!*s) {
	    /*
	     * Otherwise, it must be a raw time specifier.
	     */
	    t = (long)zl;
	} else {
	    zwarnnam("sched", "bad time specifier");
	    return 1;
	}
    }
    /* The time has been calculated; now add the new entry to the linked list
    of scheduled commands. */
    sch = (struct schedcmd *) zalloc(sizeof *sch);
    sch->time = t;
    sch->cmd = zjoin(argptr, ' ', 0);
    sch->flags = flags;
    /* Insert into list in time order */
    if (schedcmds) {
	if (sch->time < schedcmds->time) {
	    scheddeltimed();
	    sch->next = schedcmds;
	    schedcmds = sch;
	    DPUTS(timedfns && firstnode(timedfns), "BUG: already timed fn (3)");
	    schedaddtimed(t);
	} else {
	    for (sch2 = schedcmds;
		 sch2->next && sch2->next->time < sch->time;
		 sch2 = sch2->next)
		;
	    sch->next = sch2->next;
	    sch2->next = sch;
	}
    } else {
	sch->next = NULL;
	schedcmds = sch;
	DPUTS(timedfns && firstnode(timedfns), "BUG: already timed fn (4)");
	schedaddtimed(t);
    }
    return 0;
}


/**/
static char **
schedgetfn(UNUSED(Param pm))
{
    int i;
    struct schedcmd *sch;
    char **ret, **aptr;

    for (i = 0, sch = schedcmds; sch; sch = sch->next, i++)
	;

    aptr = ret = zhalloc(sizeof(char *) * (i+1));
    for (sch = schedcmds; sch; sch = sch->next, aptr++) {
	char tbuf[40], *flagstr;
	time_t t;

	t = sch->time;
	sprintf(tbuf, "%ld", t);
	if (sch->flags & SCHEDFLAG_TRASH_ZLE)
	    flagstr = "-o";
	else
	    flagstr = "";
	*aptr = (char *)zhalloc(5 + strlen(tbuf) + strlen(sch->cmd));
	sprintf(*aptr, "%s:%s:%s", tbuf, flagstr, sch->cmd);
    }
    *aptr = NULL;

    return ret;
}


static struct builtin bintab[] = {
    BUILTIN("sched", 0, bin_sched, 0, -1, 0, NULL, NULL),
};

static const struct gsu_array sched_gsu =
{ schedgetfn, arrsetfn, stdunsetfn };

static struct paramdef partab[] = {
    SPECIALPMDEF("zsh_scheduled_events", PM_ARRAY|PM_READONLY,
		 &sched_gsu, NULL, NULL)
};

static struct features module_features = {
    bintab, sizeof(bintab)/sizeof(*bintab),
    NULL, 0,
    NULL, 0,
    partab, sizeof(partab)/sizeof(*partab),
    0
};

/**/
int
setup_(UNUSED(Module m))
{
    return 0;
}

/**/
int
features_(Module m, char ***features)
{
    *features = featuresarray(m, &module_features);
    return 0;
}

/**/
int
enables_(Module m, int **enables)
{
    return handlefeatures(m, &module_features, enables);
}

/**/
int
boot_(Module m)
{
    addprepromptfn(&checksched);
    return 0;
}

/**/
int
cleanup_(Module m)
{
    struct schedcmd *sch, *schn;

    if (schedcmds)
	scheddeltimed();
    for (sch = schedcmds; sch; sch = schn) {
	schn = sch->next;
	zsfree(sch->cmd);
	zfree(sch, sizeof(*sch));
    }
    delprepromptfn(&checksched);
    return setfeatureenables(m, &module_features, NULL);
}

/**/
int
finish_(UNUSED(Module m))
{
    return 0;
}
