/* date - Display (or set) the date and time		Author: V. Archer */

#include <sys/types.h>
#include <ctype.h>
#include <stddef.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
#include <unistd.h>

#define	MIN	60L		/* # seconds in a minute */
#define	HOUR	(60 * MIN)	/* # seconds in an hour */
#define	DAY	(24 * HOUR)	/* # seconds in a day */
#define	YEAR	(365 * DAY)	/* # seconds in a (non-leap) year */

int qflag, uflag, sflag, Sflag;

/* Default output file descriptor.
 */
int outfd = 1;

_PROTOTYPE(int main, (int argc, char **argv));
_PROTOTYPE(void putchar, (int c));
_PROTOTYPE(void pstring, (char *s, int len));
_PROTOTYPE(void pldecimal, (unsigned long d, int digits));
_PROTOTYPE(void pdecimal, (int d, int digits));
_PROTOTYPE(void fmtdate, (char *format, time_t t, struct tm *p));
_PROTOTYPE(time_t make_time, (char *t));
_PROTOTYPE(struct tm *september, (time_t *tp));
_PROTOTYPE(void usage, (void));

/* Main module. Handles P1003.2 date and system administrator's date. The
 * date entered should be given GMT, regardless of the system's TZ!
 */
int main(argc, argv)
int argc;
char **argv;
{
  time_t t;
  struct tm *tm;
  char *format;
  char time_buf[40];
  int n;
  int i;

  time(&t);

  i = 1;
  while (i < argc && argv[i][0] == '-') {
	char *opt = argv[i++] + 1, *end;

	if (opt[0] == '-' && opt[1] == 0) break;

	while (*opt != 0) switch (*opt++) {
	case 'q':
		qflag = 1;
		break;
	case 's':
		sflag = 1;
		break;
	case 'u':
		uflag = 1;
		break;
	case 'S':
		Sflag = 1;
		break;
	case 't':
		/* (obsolete, now -r) */
	case 'r':
		if (*opt == 0) {
			if (i == argc) usage();
			opt = argv[i++];
		}
		t = strtoul(opt, &end, 10);
		if (*end != 0) usage();
		opt = "";
		break;
	default:
		usage();
	}
  }

  if (!qflag && i < argc && ('0' <= argv[i][0] && argv[i][0] <= '9')) {
	t = make_time(argv[i++]);
	sflag = 1;
  }

  format = "%c";
  if (i < argc && argv[i][0] == '+') format = argv[i++] + 1;

  if (i != argc) usage();

  if (qflag) {
	pstring("\nPlease enter date: MMDDYYhhmmss. Then hit the RETURN key.\n", -1);
	n = read(0, time_buf, sizeof(time_buf));
	if (n > 0 && time_buf[n-1] == '\n') n--;
	if (n >= 0) time_buf[n] = 0;
	t = make_time(time_buf);
	sflag = 1;
  }

  if (sflag && stime(&t) != 0) {
	outfd = 2;
	pstring("No permission to set time\n", -1);
	return(1);
  }

  tm = Sflag ? september(&t) : uflag ? gmtime(&t) : localtime(&t);

  fmtdate(format, t, tm);
  putchar('\n');
  return(0);
}

/* Replacement for stdio putchar().
 */
void putchar(c)
int c;
{
  static char buf[1024];
  static char *bp = buf;

  if (c != 0) *bp++ = c;
  if (c == 0 || c == '\n' || bp == buf + sizeof(buf)) {
	write(outfd, buf, bp - buf);
	bp = buf;
  }
}

/* Internal function that prints a n-digits number. Replaces stdio in our
 * specific case.
 */
void pldecimal(d, digits)
unsigned long d;
int digits;
{
  digits--;
  if (d > 9 || digits > 0) pldecimal(d / 10, digits);
  putchar('0' + (d % 10));
}

void pdecimal(d, digits)
int d, digits;
{
  pldecimal((unsigned long) d, digits);
}

/* Internal function that prints a fixed-size string. Replaces stdio in our
 * specific case.
 */
void pstring(s, len)
char *s;
int len;
{
  while (*s)
	if (len--)
		putchar(*s++);
	else
		break;
}

/* Format the date, using the given locale string. A special case is the
 * TZ which might be a sign followed by four digits (New format time zone).
 */
void fmtdate(format, t, p)
char *format;
time_t t;
struct tm *p;
{
  int i;
  char *s;
  static char *wday[] = {"Sunday", "Monday", "Tuesday", "Wednesday",
		       "Thursday", "Friday", "Saturday"};
  static char *month[] = {"January", "February", "March", "April",
			"May", "June", "July", "August",
		    "September", "October", "November", "December"};

  while (*format)
	if (*format == '%') {
		switch (*++format) {
		    case 'A':
			pstring(wday[p->tm_wday], -1);
			break;
		    case 'B':
			pstring(month[p->tm_mon], -1);
			break;
		    case 'D':
			pdecimal(p->tm_mon + 1, 2);
			putchar('/');
			pdecimal(p->tm_mday, 2);
			putchar('/');
		    case 'y':
			pdecimal(p->tm_year % 100, 2);
			break;
		    case 'H':
			pdecimal(p->tm_hour, 2);
			break;
		    case 'I':
			i = p->tm_hour % 12;
			pdecimal(i ? i : 12, 2);
			break;
		    case 'M':
			pdecimal(p->tm_min, 2);
			break;
		    case 'X':
		    case 'T':
			pdecimal(p->tm_hour, 2);
			putchar(':');
			pdecimal(p->tm_min, 2);
			putchar(':');
		    case 'S':
			pdecimal(p->tm_sec, 2);
			break;
		    case 'U':
			pdecimal((p->tm_yday - p->tm_wday + 13) / 7, 2);
			break;
		    case 'W':
			if (--(p->tm_wday) < 0) p->tm_wday = 6;
			pdecimal((p->tm_yday - p->tm_wday + 13) / 7, 2);
			if (++(p->tm_wday) > 6) p->tm_wday = 0;
			break;
		    case 'Y':
			pdecimal(p->tm_year + 1900, 4);
			break;
		    case 'Z':
			if (uflag) {
				s = "GMT";
			} else {
				s = (p->tm_isdst == 1) ? tzname[1] : tzname[0];
			}
			pstring(s, strlen(s));
			break;
		    case 'a':
			pstring(wday[p->tm_wday], 3);
			break;
		    case 'b':
		    case 'h':
			pstring(month[p->tm_mon], 3);
			break;
		    case 'c':
			if (!(s = getenv("LC_TIME")))
				s = "%a %b %e %T %Z %Y";
			fmtdate(s, t, p);
			break;
		    case 'd':
			pdecimal(p->tm_mday, 2);
			break;
		    case 'e':
			if (p->tm_mday < 10) putchar(' ');
			pdecimal(p->tm_mday, 1);
			break;
		    case 'j':
			pdecimal(p->tm_yday + 1, 3);
			break;
		    case 'm':
			pdecimal(p->tm_mon + 1, 2);
			break;
		    case 'n':	putchar('\n');	break;
		    case 'p':
			if (p->tm_hour < 12)
				putchar('A');
			else
				putchar('P');
			putchar('M');
			break;
		    case 'r':
			fmtdate("%I:%M:%S %p", t, p);
			break;
		    case 's':
			pldecimal((unsigned long) t, 0);
			break;
		    case 't':	putchar('\t');	break;
		    case 'w':
			putchar('0' + p->tm_wday);
			break;
		    case 'x':
			fmtdate("%B %e %Y", t, p);
			break;
		    case '%':	putchar('%');	break;
		    case '\0':	format--;
		}
		format++;
	} else
		putchar(*format++);
}

/* Convert a local date string into GMT time in seconds. */
time_t make_time(t)
char *t;
{
  struct tm tm;				/* user specified time */
  time_t now;				/* current time */
  int leap;				/* current year is leap year */
  int i;				/* general index */
  int fld;				/* number of fields */
  int f[6];				/* time fields */
  static int days_per_month[2][12] = {
  { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 },
  { 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }};

/* Get current time just in case */
  now = time((time_t *) 0);
  tm  = *localtime(&now);
  tm.tm_sec   = 0;
  tm.tm_mon++;
  tm.tm_year %= 100;

/* Parse the time */
#if '0'+1 != '1' || '1'+1 != '2' || '2'+1 != '3' || '3'+1 != '4' || \
    '4'+1 != '5' || '5'+1 != '6' || '6'+1 != '7' || '7'+1 != '8' || '8'+1 != '9'
  << Code unsuitable for character collating sequence >>
#endif

  for (fld = 0; fld < sizeof(f)/sizeof(f[0]); fld++) {
	if (*t == 0) break;
	f[fld] = 0;
	for (i = 0; i < 2; i++, t++) {
		if (*t < '0' || *t > '9') usage();
		f[fld] = f[fld] * 10 + *t - '0';
	}
  }

  switch (fld) {
  case 2:
	tm.tm_hour = f[0]; tm.tm_min  = f[1]; break;

  case 3:
	tm.tm_hour = f[0]; tm.tm_min  = f[1]; tm.tm_sec  = f[2];
	break;

  case 5:
  	tm.tm_mon  = f[0]; tm.tm_mday = f[1]; tm.tm_year = f[2];
	tm.tm_hour = f[3]; tm.tm_min  = f[4];
	break;

  case 6:
	tm.tm_mon  = f[0]; tm.tm_mday = f[1]; tm.tm_year = f[2];
	tm.tm_hour = f[3]; tm.tm_min  = f[4]; tm.tm_sec  = f[5];
	break;

  default:
	usage();
  }

/* Convert the time into seconds since 1 January 1970 */
  if (tm.tm_year < 70)
    tm.tm_year += 100;
  leap = (tm.tm_year % 4 == 0 && tm.tm_year % 400 != 0);
  if (tm.tm_mon  < 1  || tm.tm_mon  > 12 ||
      tm.tm_mday < 1  || tm.tm_mday > days_per_month[leap][tm.tm_mon-1] ||
      tm.tm_hour > 23 || tm.tm_min  > 59) {
    outfd = 2;
    pstring("Illegal date format\n", -1);
    exit(1);
  }

/* Convert the time into Minix time - zone independent code */
  {
    time_t utctime;			/* guess at unix time */
    time_t nextbit;			/* next bit to try */
    int rv;				/* result of try */
    struct tm *tmp;			/* local time conversion */

#define COMPARE(a,b)	((a) != (b)) ? ((a) - (b)) :

    utctime = 1;
    do {
      nextbit = utctime;
      utctime = nextbit << 1;
    } while (utctime >= 1);

    for (utctime = 0; ; nextbit >>= 1) {

      utctime |= nextbit;
      tmp = localtime(&utctime);
      if (tmp == 0) continue;

      rv = COMPARE(tmp->tm_year,    tm.tm_year)
           COMPARE(tmp->tm_mon + 1, tm.tm_mon)
	   COMPARE(tmp->tm_mday,    tm.tm_mday)
	   COMPARE(tmp->tm_hour,    tm.tm_hour)
	   COMPARE(tmp->tm_min,     tm.tm_min)
	   COMPARE(tmp->tm_sec,     tm.tm_sec)
	   0;

      if (rv > 0)
        utctime &= ~nextbit;
      else if (rv == 0)
        break;

      if (nextbit == 0) {
	uflag = 1;
        outfd = 2;
        pstring("Inexact conversion to UTC from ", -1);
        fmtdate("%c\n", utctime, localtime(&utctime) );
	exit(1);
      }
    }
    return utctime;
  }
}

/* Correct the time to the reckoning of Eternal September. */
struct tm *september(tp)
time_t *tp;
{
  time_t t;
  int days;
  struct tm *tm;

  tm = localtime(tp);

  t = *tp - (tm->tm_hour - 12) * 3600L;  /* No zone troubles around noon. */
  days = 0;

  while (tm->tm_year > 93 || (tm->tm_year == 93 && tm->tm_mon >= 8)) {
	/* Step back a year or a month. */
	days += tm->tm_year > 93 ? tm->tm_yday+1 : tm->tm_mday;
	t = *tp - days * 24 * 3600L;

	tm = localtime(&t);
  }

  if (days > 0) {
	tm = localtime(tp);
	tm->tm_mday = days;
	tm->tm_year = 93;
	tm->tm_mon = 8;
#if SANITY
	t = mktime(tm);
	tm = localtime(&t);
#endif
  }
  return tm;
}

/* (Extended) Posix prototype of date. */
void usage()
{
  outfd = 2;
  pstring("Usage: date [-qsuS] [-r seconds] [[MMDDYY]hhmm[ss]] [+format]\n", -1);
  exit(1);
}
