/* mktime, localtime, gmtime */
/* written by ERS and placed in the public domain */

#include <stddef.h>
#include <stdlib.h>
#include <time.h>
#include <ctype.h>
#ifndef _COMPILER_H
#include <compiler.h>
#endif

#if 0
static void
DEBUG_TM(nm, tm)
	char *nm;
	struct tm *tm;
{
	char buf[100];

	(void)strftime(buf, 100, "%c %z", tm);
	printf("%s: %s\n", nm, buf);
}
#else
#define DEBUG_TM(nm, tm)
#endif

#define SECS_PER_MIN    (60L)
#define SECS_PER_HOUR   (3600L)
#define SECS_PER_DAY    (86400L)
#define SECS_PER_YEAR   (31536000L)
#define SECS_PER_LEAPYEAR (SECS_PER_DAY + SECS_PER_YEAR)

time_t _timezone = -1;	/* holds # seconds west of GMT */

static int
days_per_mth[12] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

static int
mth_start[13] = { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 };

static time_t tzoffset __PROTO((char *s, int *hasdst));
static int indst __PROTO((const struct tm *t));

static int dst = -1;	/* whether dst holds in current timezone */

/*
 * FIXME: none of these routines is very efficient. Also, none of them
 * handle dates before Jan 1, 1970.
 *
 */

/*
 * mktime: take a time structure representing the local time (such as is
 *  returned by localtime() and convert it into the standard representation
 *  (as seconds since midnight Jan. 1 1970, GMT).
 *
 * Note that time() sends us such a structure with tm_yday undefined, so
 * we shouldn't count on tm_yday being correct.
 */

time_t
mktime(t)
        const struct tm *t;
{
        time_t s;
        int y;

DEBUG_TM("mktime", t);
        if (t->tm_year < 70)      /* year before 1970 */
                return (time_t) -1;

/* calculate tm_yday here */
	y = (t->tm_mday - 1) + mth_start[t->tm_mon] + /* leap year correction */
		( ( (t->tm_year % 4) != 0 ) ? 0 : (t->tm_mon > 1) );

	s = (t->tm_sec)+(t->tm_min*SECS_PER_MIN)+(t->tm_hour*SECS_PER_HOUR) +
		(y*SECS_PER_DAY)+((t->tm_year - 70)*SECS_PER_YEAR) +
		((t->tm_year - 69)/4)*SECS_PER_DAY;

/* Now adjust for the time zone and possible daylight savings time */
/* note that we have to call tzset() every time; see 1003.1 sect 8.1.1 */
	tzset();

        s += _timezone;
        if (dst == 1 && indst(t))
                s -= 3600;

        return s;
}


struct tm *_gmtime(t, stm)
        const time_t *t;
	struct tm *stm;
{
        time_t  time = *t;
        int     year, mday, i;

        if (time < 0)   /* negative times are bad */
                return 0;
        stm->tm_wday = ((time/SECS_PER_DAY) + 4) % 7;

        year = 70;
        for (;;) {
                if (time < SECS_PER_YEAR) break;
                if ((year % 4) == 0) {
                        if (time < SECS_PER_LEAPYEAR)
                                break;
                        else
                                time -= SECS_PER_LEAPYEAR;
                }
                else
                        time -= SECS_PER_YEAR;
                year++;
        }
        stm->tm_year = year;
        mday = stm->tm_yday = time/SECS_PER_DAY;

        days_per_mth[1] = (year % 4) ? 28 : 29;
        for (i = 0; mday >= days_per_mth[i]; i++)
                mday -= days_per_mth[i];
        stm->tm_mon = i;
        stm->tm_mday = mday + 1;

        time = time % SECS_PER_DAY;
        stm->tm_hour = time/SECS_PER_HOUR;
        time = time % SECS_PER_HOUR;
        stm->tm_min = time/SECS_PER_MIN;
        stm->tm_sec = time % SECS_PER_MIN;
        stm->tm_isdst = 0;

DEBUG_TM("gmtime", stm);
        return stm;
}

struct tm *gmtime(t)
	const time_t *t;
{
	static struct tm gtime;

	return _gmtime(t, &gtime);
}

/* given a standard time, convert it to a local time */

struct tm *localtime(t)
        const time_t *t;
{
	static struct tm ltim;
        struct tm *stm;
        time_t offset;  /* seconds between local time and GMT */

	tzset();
        offset = *t - _timezone;
        stm = _gmtime(&offset, &ltim);
	if (stm == NULL) return stm;		/* check for illegal time */
        stm->tm_isdst = (dst == -1) ? -1 : 0;

        if (dst == 1 && indst((const struct tm *)stm)) {   /* daylight savings time in effect */
                stm->tm_isdst = 1;
                if (++stm->tm_hour > 23) {
                        stm->tm_hour -= 24;
                        stm->tm_wday = (stm->tm_wday + 1) % 7;
                        stm->tm_yday++;
                        stm->tm_mday++;
                        if (stm->tm_mday > days_per_mth[stm->tm_mon]) {
                                stm->tm_mday = 1;
                                stm->tm_mon++;
                        }
                }
        }
DEBUG_TM("localtime", stm);
        return stm;
}

/*
 * THIS IS A DELIBERATE VIOLATION OF THE ANSI STANDARD:
 * there appears to be a conflict between Posix and ANSI; the former
 * mandates a "tzset()" function that gets called whenever time()
 * does, and which sets some global variables. ANSI wants none of
 * this. Several Unix implementations have tzset(), and few people are
 * going to be hurt by it, so it's included here.
 */

/* set the timezone and dst flag to the local rules. Also sets the
   global variable tzname to the names of the timezones
 */

char *tzname[2] = {"UCT", "UCT"};

void
tzset()
{
	_timezone = tzoffset(getenv("TZ"), &dst);
}

/*
 * determine the difference, in seconds, between the given time zone
 * and Greenwich Mean. As a side effect, the integer pointed to
 * by hasdst is set to 1 if the given time zone follows daylight
 * savings time, 0 if there is no DST.
 *
 * Time zones are given as strings of the form
 * "[TZNAME][h][:m][TZDSTNAME]" where h:m gives the hours:minutes
 * east of GMT for the timezone (if [:m] does not appear, 0 is assumed).
 * If the final field, TZDSTNAME, appears, then the time zone follows
 * daylight savings time.
 *
 * Example: EST5EDT would represent the N. American Eastern time zone
 *          CST6CDT would represent the N. American Central time zone
 *          NFLD3:30NFLD would represent Newfoundland time (one and a
 *              half hours ahead of Eastern).
 *          OZCST-9:30 would represent the Australian central time zone.
 *              (which, so I hear, doesn't have DST).
 *
 * NOTE: support for daylight savings time is currently very bogus.
 * It's probably best to do without, unless you live in North America.
 *
 */
#define TZNAMLEN	8	/* max. length of time zone name */

static
time_t 
tzoffset(s, hasdst)
        char *s;
        int  *hasdst;
{
        time_t off = 0;
        int x, sgn = 1;
	static char stdname[TZNAMLEN+1], dstname[TZNAMLEN+1];
	static char unknwn[4] = "???";

	char *n;

        *hasdst = -1;                   /* Assume unknown */
        if (!s || !*s)
                return 0;               /* Assume GMT */
 
       *hasdst = 0;

	n = stdname;
        while (*s && isalpha(*s)) {
		*n++ = *s++;        /* skip name */
	}
	*n++ = 0;

/* now figure out the offset */

        x = 0;
        if (*s == '-') {
                sgn = -1;
                s++;
        }
        while (isdigit(*s)) {
                x = 10 * x + toint(*s);
                s++;
        }
        off = x * SECS_PER_HOUR;
        if (*s == ':') {
                x = 0;
                s++;
                while (isdigit(*s)) {
                        x = 10 * x + toint(*s);
                        s++;
                }
	        off += (x * SECS_PER_MIN);
        }

	n = dstname;
        if (isalpha(*s)) {
                *hasdst = 1;
		while (*s && isalpha(*s)) *n++ = *s++;
	}
	*n++ = 0;

	if (stdname[0])
		tzname[0] = stdname;
	else
		tzname[0] = unknwn;

	if (dstname[0])
		tzname[1] = dstname;
	else
		tzname[1] = stdname;

        return sgn * off;
}

/*
 * Given a tm struct representing the local time, determine whether
 * DST is currently in effect. This should only be
 * called if it is known that the time zone indeed supports DST.
 *
 * FIXME: For now, assume that everyone follows the North American
 *   time zone rules, all the time. This means daylight savings
 *   time is assumed to be in effect from the first Sunday in April
 *   to the last Sunday in October. Prior to 1987, the old rules
 *   (last Sunday in April to last Sunday in Oct.) are used, even when
 *   (as in 1974) they're not applicable. Sorry.
 *
 */

static
int indst(t)
        const struct tm *t;
{
        if (t->tm_mon == 3) {           /* April */
/* before 1987, see if there's another sunday in the month */
                if (t->tm_year < 87 && t->tm_wday + 30 - t->tm_mday < 7)
                        return 1;       /* no there isn't */
/* after 1987, see if a sunday has happened yet */
                if (t->tm_wday - t->tm_mday < 0)
                        return 1;       /* yep */
                return 0;
        }
        if (t->tm_mon == 9) {           /* October */
                if (t->tm_wday + 31 - t->tm_mday < 7)
                        return 0;       /* there are no more sundays */
                return 1;
        }
/* Otherwise, see if it's a month between April and October exclusive */
        return (t->tm_mon > 3 && t->tm_mon < 9);
}

