/* @(#)dates.c	3.0	(c) copyright 3/01/90 (Dan Heller, Bart Schaefer) */

#include "mush.h"

/*
 *   %ld%3c%s	gmt_in_secs weekday orig_timezone
 * The standard "date format" stored in the msg data structure.
 */
char *day_names[] = {
    "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
};
char *month_names[] = {     /* imported in pick.c */
    "Jan", "Feb", "Mar", "Apr", "May", "Jun",
    "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
};

static int mtbl[] = { 0,31,59,90,120,151,181,212,243,273,304,334 };

/* Time Zone Stuff */
struct zoneoff {
    char *zname;
    int hr_off;
    int mn_off;
} time_zones[] = {
    /* Universal Time */
    { "UT",	  0,  0 },	{ "GMT",	  0,  0 },
    /* European Time */
    { "BST",	  1,  0 },				    /* Brit. Summer */
    { "EET",	  2,  0 },	{ "EEST",	  3,  0 },	/* Eastern */
    				{ "EET DST",	  3,  0 },
    { "MET",	  1,  0 },	{ "MEST",	  2,  0 },	/* Middle */
    				{ "MET DST",	  2,  0 },
    { "WET",	  0,  0 },	{ "WEST",	  1,  0 },	/* Western */
    				{ "WET DST",	  1,  0 },
    /* North American Time */
    { "NST",	 -3,-30 },				    /* Newfoundland */
    { "AST",	 -4,  0 },	{ "ADT",	 -3,  0 },	/* Atlantic */
    { "EST",	 -5,  0 },	{ "EDT",	 -4,  0 },	/* Eastern */
    { "CST",	 -6,  0 },	{ "CDT",	 -5,  0 },	/* Central */
    { "MST",	 -7,  0 },	{ "MDT",	 -6,  0 },	/* Mountain */
    { "PST",	 -8,  0 },	{ "PDT",	 -7,  0 },	/* Pacific */
    { "YST",	 -9,  0 },	{ "YDT",	 -8,  0 },	/* Yukon */
    { "HST",	-10,  0 },	{ "HDT",	 -9,  0 },	/* Hawaii */
    /* Japan and Australia Time */
    {"JST",	  9,  0 },					/* Japan */
    {"AEST",	 10,  0 },	{"AESST",	 11,  0 },	/* Eastern */	
    {"ACST",	  9, 30 },	{"ACSST",	 10, 30 },	/* Central */
    {"AWST",	  8,  0 },					/* Western */
    /* Military Time */
    { "A",	  1,  0 },	{ "N",		 -1,  0 },
    { "B",	  2,  0 },	{ "O",		 -2,  0 },
    { "C",	  3,  0 },	{ "P",		 -3,  0 },
    { "D",	  4,  0 },	{ "Q",		 -4,  0 },
    { "E",	  5,  0 },	{ "R",		 -5,  0 },
    { "F",	  6,  0 },	{ "S",		 -6,  0 },
    { "G",	  7,  0 },	{ "T",		 -7,  0 },
    { "H",	  8,  0 },	{ "U",		 -8,  0 },
    { "I",	  9,  0 },	{ "V",		 -9,  0 },
    { "K",	 10,  0 },	{ "W",		-10,  0 },
    { "L",	 11,  0 },	{ "X",		-11,  0 },
    { "M",	 12,  0 },	{ "Y",		-12,  0 },
    { "Z",	  0,  0 },
    /* Also legal is +/- followed by hhmm offset from UT */
    { 0, 0, 0 }
};

long
getzoff(zone)
char *zone;
{
    struct zoneoff *z;
    int hours, mins;
    char sign[2];

    if (!zone || !*zone)
	return 0;
    if (sscanf(zone, "%1[-+]%2d%2d", sign, &hours, &mins) == 3)
	return (hours * 3600 + mins * 60) * (*sign == '-' ? -1 : 1);
    for (z = time_zones; z->zname; z++)
	if (lcase_strncmp(zone, z->zname, -1) == 0)
	    return z->hr_off * 3600 + z->mn_off * 60;
    return 0;
}

/*
 * Kind of the reverse of localtime() and gmtime() -- converts a struct tm
 * to time in seconds since 1970.  Valid until 2038.
 * If the "zone" argument is present, it modifies the return value.
 * The zone should be a string, either +/-hhmm or symbolic (above).
 * The "how" argument should be -1 to convert FROM gmt, 1 to convert TO gmt,
 * and (as a "side-effect") 0 if the Zone parameter is to be ignored.
 *
 * Thanks to ktl@wag240.caltech.edu (Kian-Tat Lim) for similar algorithm
 * written in perl from which this was derived.
 */
long
time2gmt(tym, zone, how)
struct tm *tym;
char *zone;
int how;
{
    long year, julian;

    if (tym->tm_year < 100)
	year = tym->tm_year + 1900;
    if (year < 69)
	year += 100;

    julian = 365 * (year - 1970) + (int)((year - 1970 + 1) / 4) +
		mtbl[tym->tm_mon] + tym->tm_mday - 1;
		/* tym->tm_yday might not be valid */
    if (tym->tm_mon > 1 && year%4 == 0 && (year%100 != 0 || year%400 == 0))
	julian++;
    julian *= 86400;	/* convert to seconds */
    julian += (tym->tm_hour * 60 + tym->tm_min) * 60 + tym->tm_sec;
    return julian - getzoff(zone) * how;
}

struct tm *
time_n_zone(zone)
char *zone;
{
    struct tm *T;
    char *tz;
#if defined(SYSV) || defined(TIMEZONE)
    long	  x;

    (void) time(&x);
    T = localtime(&x);
#ifndef TIMEZONE
    {
	extern char *tzname[];
	tz = tzname[T->tm_isdst];
    }
#endif /* TIMEZONE */
#else /* SYSV || TIMEZONE */
    extern char     *timezone();
    struct timeval  mytime;
    struct timezone myzone;
    time_t	tt = mytime.tv_sec;

    (void) gettimeofday(&mytime, &myzone);
    T = localtime(&tt);
#ifdef sun
    /*
     * The timezone info in gettimeofday(2) is invalid (always 0) and there
     * is no way to set the kernel's notion of timezone via a 'config' 
     * parameter. You could adb the kernel and poke in _tz but I think this
     * is better.
     *
     * from gettimeofday(2):
     *		Also note that the offset of the local time  zone  from  GMT
     *		may  change  over time, as may the rules for Daylight Saving
     *		Time correction.  The localtime()  routine  (see  ctime(3V))
     *		obtains  this  information  from  a  file  rather  than from
     *		gettimeofday().  Programs should use localtime() to  convert
     *		dates  and  times;  the timezone() structure is filled in by
     *		gettimeofday() for backward compatibility with existing pro-
     *		grams.
     */
    tz = T->tm_zone;
#else
    tz = timezone(myzone.tz_minuteswest, (T->tm_isdst && myzone.tz_dsttime));
#endif
#endif /* !SYSV */

#ifdef TIMEZONE
#ifdef DAYLITETZ
    if (T->tm_isdst)
	tz = DAYLITETZ;
    else
#endif /* DAYLITETZ */
    tz = TIMEZONE;
#endif /* TIMEZONE */

    (void) strncpy(zone, tz, 7), zone[7] = 0;
    return T;
}

/* Time() returns a string according to criteria:
 *   if "now" is 0, then the current time is gotten and used.
 *       else, use the time described by now
 *   opts points to a string of args which is parsed until an unknown
 *       arg is found and opts will point to that upon return.
 *   valid args are T (time of day), D (day of week), M (month), Y (year),
 *       N (number of day in month -- couldn't think of a better letter).
 */
char *
Time(opts, now)
register char *opts;
long now;
{
    static char time_buf[30];
    struct tm 	  *T;
    register char *p = time_buf;
    long	  x;

    if (!opts)
	return NULL;
    if (now)
	x = now;
    else
	(void) time(&x);
    T = localtime(&x);
    for (;; opts++) {
	switch(*opts) {
	    case 'T':
		if (ison(glob_flags, MIL_TIME))
		    (void) sprintf(p, "%2d:%02d", T->tm_hour, T->tm_min);
		else
		    (void) sprintf(p, "%d:%02d", (T->tm_hour) ?
			  ((T->tm_hour <= 12) ? T->tm_hour : T->tm_hour - 12) :
			  12, T->tm_min);
	    when 'D': case 'W': (void) strcpy(p, day_names[T->tm_wday]);
	    when 'M': (void) strcpy(p, month_names[T->tm_mon]);
	    when 'y': (void) sprintf(p, "%d", T->tm_year);
	    when 'Y': (void) sprintf(p, "%d", T->tm_year + 1900);
	    when 'N': (void) sprintf(p, "%d", T->tm_mday);
	    otherwise: *--p = 0; return time_buf;
	}
	p += strlen(p);
	*p++ = ' ';
    }
}

/* parse date and return a string that looks like
 *   %ld%3c%s	gmt_in_secs weekday orig_timezone
 * This function is a bunch of scanfs on known date formats.  Don't
 * trust the "weekday" name fields because they may not be spelled
 * right, or have the correct punctuation.  Figure it out once the
 * year and month and date have been determined.
 */
char *
parse_date(p)
register char *p;
{
    /* When scanf-ing if month isn't a month, it could be a _long_ string.
     * this is also the static buffer whose address we return.
     */
    static char month[64];
    char Wkday[4], Zone[12], dst[4];
    char a_or_p;
    int Month = 0, Day = 0, Year = 0;
    int Hours = -1, Mins = -1, Secs = -1;
    struct tm T;

    Zone[0] = dst[0] = 0;
    skipspaces(0);

    /* programmer's note -- there are too many scanfs here for some compilers
     * to put them all into one if statement.  Use goto's :-(  Also reset
     * Zone[0] after any sscanf() that could corrupt it on a partial match.
     *
     * Not yet handling all possible combinations of mailers using two-word
     * time zones, e.g. MET DST instead of MEST.  Only the specific case
     * where this was reported has been handled here.
     */

    /* RFC822 formats and minor variations -- order important */

    /*   day_number month_name year_number time timezone */
    if (sscanf(p, "%d %s %d %d:%d:%d %7s %3s",
	    &Day, month, &Year, &Hours, &Mins, &Secs, Zone, dst) >= 6 && Day)
	goto gotit;
    Zone[0] = dst[0] = 0;
    if (sscanf(p, "%d %s %d %d:%d %7s",
	    &Day, month, &Year, &Hours, &Mins, Zone) >= 5 && Day) {
	Secs = 0;
	goto gotit;
    }
    Zone[0] = dst[0] = 0;
    /*   day_name day_number month_name year_number time timezone */
    if (sscanf(p, "%*s %d %s %d %d:%d:%d %7s %3s",
	    &Day, month, &Year, &Hours, &Mins, &Secs, Zone, dst) >= 6 && Day)
	goto gotit;
    Zone[0] = dst[0] = 0;
    if (sscanf(p, "%*s %d %s %d %d:%d %7s %3s",
	    &Day, month, &Year, &Hours, &Mins, Zone, dst) >= 5 && Day) {
	Secs = 0;
	goto gotit;
    }
    Zone[0] = dst[0] = 0;

    /* Ctime format (From_ lines) -- timezone almost never found */

    /*   day_name month_name day_number time timezone year_number */
    if (sscanf(p, "%*s %s %d %d:%d:%d %7s %d",
	    month, &Day, &Hours, &Mins, &Secs, Zone, &Year) == 7)
	goto gotit;
    Zone[0] = 0;
    /*   day_name month_name day_number time year_number */
    if (sscanf(p, "%*s %s %d %d:%d:%d %d",
	    month, &Day, &Hours, &Mins, &Secs, &Year) == 6)
	goto gotit;
    /*   day_name month_name day_number time timezone dst year_number */
    if (sscanf(p, "%*s %s %d %d:%d:%d %7s %3s %d",
	    month, &Day, &Hours, &Mins, &Secs, Zone, dst, &Year) == 8)
	goto gotit;
    Zone[0] = dst[0] = 0;

    /* Other common variants */

    /*   day_number month_name year_number time-timezone (day) */
    /*                                       ^no colon separator */
    if (sscanf(p, "%d %s %d %2d%2d%1[-+]%6[0123456789]",
	    &Day, month, &Year, &Hours, &Mins, &Secs, &Zone[0], &Zone[1]) == 8)
	goto gotit;
    if (sscanf(p, "%d %s %d %2d%2d-%7s",	/* Does this _ever_ hit? */
	    &Day, month, &Year, &Hours, &Mins, Zone) == 6) {
	Secs = 0;
	goto gotit;
    }
    Zone[0] = 0;

    /*   day_number month_name year_number time timezone	*/
    /*                                      ^no colon separator */
    /*   (This is the odd one in the RFC822 examples section;	*/
    /*    also catches the slop from partial hits above.)	*/
    if (sscanf(p, "%d %s %d %2d%2d %7s",
	    &Day, month, &Year, &Hours, &Mins, Zone) >= 5 && Day) {
	Secs = 0;
	goto gotit;
    }
    Zone[0] = 0;
    
    Zone[1] = 0;	/* Yes, Zone[1] -- tested below */

    /*   day_number month_name year_number, time "-" ?? */
    if (sscanf(p,"%d %s %d, %d:%d:%d %1[-+]%6[0123456789]",
	    &Day, month, &Year, &Hours, &Mins, &Secs,
	    &Zone[0], &Zone[1]) >= 6 && Day)
	goto gotit;

    /*   day_number month_name year_number 12_hour_time a_or_p */
    if (sscanf(p, "%d %s %d %d:%d:%d %cm %7s",
	    &Day, month, &Year, &Hours, &Mins, &Secs, &a_or_p, Zone) >= 7) {
	if (a_or_p == 'p')
	    Hours += 12;
	goto gotit;
    }

    /*   day_name month_name day_number year_number time */
    if (sscanf(p, "%*s %s %d %d %d:%d:%d %7s",
	    month, &Day, &Year, &Hours, &Mins, &Secs, Zone) >= 6)
	goto gotit;
    Zone[0] = 0;
    if (sscanf(p, "%*s %s %d %d %d:%d %7s",
	    month, &Day, &Year, &Hours, &Mins, Zone) >= 5) {
	Secs = 0;
	goto gotit;
    }
    Zone[0] = 0;

    /*   day_name month_name day_number time timezone year_number */
    if (sscanf(p, "%*s %s %d %d:%d:%d %7s %d",
	    month, &Day, &Hours, &Mins, &Secs, Zone, &Year) == 7)
	goto gotit;
    Zone[0] = 0;
    if (sscanf(p, "%*s %s %d %d:%d %7s %d",
	    month, &Day, &Hours, &Mins, Zone, &Year) == 6) {
	Secs = 0;
	goto gotit;
    }
    Zone[0] = 0;

    Secs = 0;	/* For the next 3 attempts */

    /*   day_number-month_name-year time */
    if (sscanf(p,"%d-%[^-]-%d %d:%d", &Day, month, &Year, &Hours, &Mins) == 5)
	goto gotit;

    /*   day_name, day_number-month_name-year time */
    if (sscanf(p,"%*s %d-%[^-]-%d %d:%d",
	    &Day, month, &Year, &Hours, &Mins) == 5)
	goto gotit;

    /*   year_number-month_number-day_number time */
    if (sscanf(p, "%d-%d-%d %d:%d", &Year, &Month, &Day, &Hours, &Mins) == 5)
	goto gotit;

    /*   month_name day_number time year Zone */
    /*   (ctime, but without the day name)    */
    if (sscanf(p, "%s %d %d:%d:%d %d %7s",
	    month, &Day, &Hours, &Mins, &Secs, &Year, Zone) >= 6)
	goto gotit;
    Zone[0] = 0;

    if (ison(glob_flags, WARNING))
	print("Unknown date format: %s\n", p);
    return NULL;

gotit:
    if (!lcase_strncmp(dst, "dst", -1)) {
	(void) strcat(Zone, " ");
	(void) strcat(Zone, dst);
    }
    if (Year > 1900)
	Year -= 1900;
    if (!Month && (Month = month_to_n(month)) == -1) {
	print("bad month: %s\n", p);
	return NULL;
    }
    if (Zone[0] == 0) {
	/* Use local time zone if none found -- important for date_recv */
	(void) time_n_zone(Zone);
    }
    {
	/* Lots of foolishness with casts for Xenix-286 16-bit ints */

	long days_ctr;	/* 16-bit ints overflowed Sept 12, 1989 */

    	days_ctr = ((long)Year * 365L) + ((Year + 3) / 4);
    	days_ctr += mtbl[Month-1] + Day + 6;
    	if (Month > 2 && (Year % 4 == 0))
	    days_ctr++;
    	(void) (sprintf(Wkday, "%.3s", day_names[(int)(days_ctr % 7L)]));
    }
    T.tm_sec = Secs;
    T.tm_min = Mins;
    T.tm_hour = Hours;
    T.tm_mday = Day;
    T.tm_mon = Month - 1;
    T.tm_year = Year;
    T.tm_wday = T.tm_yday = 0;	/* not used in time2gmt() */
    T.tm_isdst = 0;		/* determined from Zone */
    return sprintf(month, "%ld%s%s", time2gmt(&T, Zone, 1), Wkday, Zone);
}

/* pass a string in the standard date format, put into string.
 * return values in buffers provided they are not null.
 */
char *
date_to_string(Date, Yr, Mon, Day, Wkday, Tm, Zone, ret_buf)
char *Date, *Yr, *Mon, *Day, *Wkday, *Tm, *Zone, *ret_buf;
{
    long gmt;
    struct tm *T;
    char a_or_p, *p = ret_buf;

    Zone[0] = 0;
    (void) sscanf(Date, "%ld%3c%s", &gmt, Wkday, Zone);
    Wkday[3] = 0;
    gmt += getzoff(Zone);
    T = gmtime(&gmt);
    a_or_p = (T->tm_hour < 12)? 'a': 'p';

    (void) sprintf(Yr, "%d", T->tm_year + 1900);
    (void) sprintf(Day, "%d", T->tm_mday);
    (void) strcpy(Mon, month_names[T->tm_mon]);
    p += strlen(sprintf(p, "%s %2.d, ", Mon, T->tm_mday));

    if (ison(glob_flags, MIL_TIME))
	(void) sprintf(p, "%2d:%02d",T->tm_hour,T->tm_min);
    else
	(void) sprintf(p, "%2.d:%02d%cm",
	      (T->tm_hour)? (T->tm_hour <= 12)? T->tm_hour: T->tm_hour-12: 12,
	      T->tm_min, a_or_p);
    (void) strcpy(Tm, p);

    return ret_buf;
}

/* pass a string in the internal mush date format.
 * return pointer to static buffer holding ctime-format date.
 */
char *
date_to_ctime(Date)
char *Date;
{
    static char ret_buf[32];
    long gmt;

    ret_buf[0] = 0;
    (void) sscanf(Date, "%ld", &gmt);
    (void) strcpy(ret_buf, ctime(&gmt));

    return ret_buf;
}

/*
 * Build a date string according to the specification in the RFC for Date:
 */
char *
rfc_date(buf)
char buf[];
{
    struct tm *T;
    char zone[8];

    T = time_n_zone(zone);
#ifndef USA
    {
	long zoff_hr, zoff_sec = getzoff(zone);
	if (zoff_sec < 0) {
	    zone[0] = '-';
	    zoff_sec = -zoff_sec; 
	} else
	    zone[0] = '+';
	zoff_hr = zoff_sec / 3600;
	zoff_sec -= zoff_hr * 3600;
	(void) sprintf(&zone[1], "%02d%02d", zoff_hr, zoff_sec / 60);
    }
#endif /* !USA */

    return sprintf(buf, "%s, %d %s %d %02d:%02d:%02d %s",
	day_names[T->tm_wday],	/* day name */
	T->tm_mday,		/* day of the month */
	month_names[T->tm_mon],	/* month name */
	T->tm_year + 1900,	/* year number */
	T->tm_hour,		/* hours (24hr) */
	T->tm_min, T->tm_sec,	/* mins/secs */
	zone);			/* timezone */
}

#define JAN	1
#define FEB	2
#define MAR	3
#define APR	4
#define MAY	5
#define JUN	6
#define JUL	7
#define AUG	8
#define SEP	9
#define OCT	10
#define NOV	11
#define DEC	12

/* stolen direct from ELM */
month_to_n(name)
register char *name;
{
    /** return the month number given the month name... **/

    register char ch;

    switch (lower(*name)) {
	case 'a' : if ((ch = lower(name[1])) == 'p')
		       return(APR);
		   else if (ch == 'u')
		       return(AUG);
		   else return(-1);	/* error! */
	case 'd' : return(DEC);
	case 'f' : return(FEB);
	case 'j' : if ((ch = lower(name[1])) == 'a')
		       return(JAN);
		   else if (ch == 'u') {
		     if ((ch = lower(name[2])) == 'n')
			 return(JUN);
		     else if (ch == 'l')
			 return(JUL);
		     else return(-1);		/* error! */
		   }
		   else return(-1);		/* error */
	case 'm' : if ((ch = lower(name[2])) == 'r')
		       return(MAR);
		   else if (ch == 'y')
		       return(MAY);
		   else return(-1);		/* error! */
	case 'n' : return(NOV);
	case 'o' : return(OCT);
	case 's' : return(SEP);
	default  : return(-1);
    }
}
