/*
 *  Unix-like Date providers
 *
 *  Generally useful Unix / POSIX / ANSI Date providers.
 */

#include "duk_internal.h"

/* The necessary #includes are in place in duk_config.h. */

/* Buffer sizes for some UNIX calls.  Larger than strictly necessary
 * to avoid Valgrind errors.
 */
#define DUK__STRPTIME_BUF_SIZE  64
#define DUK__STRFTIME_BUF_SIZE  64

#if defined(DUK_USE_DATE_NOW_GETTIMEOFDAY)
/* Get current Ecmascript time (= UNIX/Posix time, but in milliseconds). */
DUK_INTERNAL duk_double_t duk_bi_date_get_now_gettimeofday(duk_context *ctx) {
	duk_hthread *thr = (duk_hthread *) ctx;
	struct timeval tv;
	duk_double_t d;

	if (gettimeofday(&tv, NULL) != 0) {
		DUK_ERROR_INTERNAL_DEFMSG(thr);
	}

	d = ((duk_double_t) tv.tv_sec) * 1000.0 +
	    ((duk_double_t) (tv.tv_usec / 1000));
	DUK_ASSERT(DUK_FLOOR(d) == d);  /* no fractions */

	return d;
}
#endif  /* DUK_USE_DATE_NOW_GETTIMEOFDAY */

#if defined(DUK_USE_DATE_NOW_TIME)
/* Not a very good provider: only full seconds are available. */
DUK_INTERNAL duk_double_t duk_bi_date_get_now_time(duk_context *ctx) {
	time_t t;

	DUK_UNREF(ctx);
	t = time(NULL);
	return ((duk_double_t) t) * 1000.0;
}
#endif  /* DUK_USE_DATE_NOW_TIME */

#if defined(DUK_USE_DATE_TZO_GMTIME) || defined(DUK_USE_DATE_TZO_GMTIME_R)
/* Get local time offset (in seconds) for a certain (UTC) instant 'd'. */
DUK_INTERNAL duk_int_t duk_bi_date_get_local_tzoffset_gmtime(duk_double_t d) {
	time_t t, t1, t2;
	duk_int_t parts[DUK_DATE_IDX_NUM_PARTS];
	duk_double_t dparts[DUK_DATE_IDX_NUM_PARTS];
	struct tm tms[2];
#ifdef DUK_USE_DATE_TZO_GMTIME
	struct tm *tm_ptr;
#endif

	/* For NaN/inf, the return value doesn't matter. */
	if (!DUK_ISFINITE(d)) {
		return 0;
	}

	/* If not within Ecmascript range, some integer time calculations
	 * won't work correctly (and some asserts will fail), so bail out
	 * if so.  This fixes test-bug-date-insane-setyear.js.  There is
	 * a +/- 24h leeway in this range check to avoid a test262 corner
	 * case documented in test-bug-date-timeval-edges.js.
	 */
	if (!duk_bi_date_timeval_in_leeway_range(d)) {
		DUK_DD(DUK_DDPRINT("timeval not within valid range, skip tzoffset computation to avoid integer overflows"));
		return 0;
	}

	/*
	 *  This is a bit tricky to implement portably.  The result depends
	 *  on the timestamp (specifically, DST depends on the timestamp).
	 *  If e.g. UNIX APIs are used, they'll have portability issues with
	 *  very small and very large years.
	 *
	 *  Current approach:
	 *
	 *  - Stay within portable UNIX limits by using equivalent year mapping.
	 *    Avoid year 1970 and 2038 as some conversions start to fail, at
	 *    least on some platforms.  Avoiding 1970 means that there are
	 *    currently DST discrepancies for 1970.
	 *
	 *  - Create a UTC and local time breakdowns from 't'.  Then create
	 *    a time_t using gmtime() and localtime() and compute the time
	 *    difference between the two.
	 *
	 *  Equivalent year mapping (E5 Section 15.9.1.8):
	 *
	 *    If the host environment provides functionality for determining
	 *    daylight saving time, the implementation of ECMAScript is free
	 *    to map the year in question to an equivalent year (same
	 *    leap-year-ness and same starting week day for the year) for which
	 *    the host environment provides daylight saving time information.
	 *    The only restriction is that all equivalent years should produce
	 *    the same result.
	 *
	 *  This approach is quite reasonable but not entirely correct, e.g.
	 *  the specification also states (E5 Section 15.9.1.8):
	 *
	 *    The implementation of ECMAScript should not try to determine
	 *    whether the exact time was subject to daylight saving time, but
	 *    just whether daylight saving time would have been in effect if
	 *    the _current daylight saving time algorithm_ had been used at the
	 *    time.  This avoids complications such as taking into account the
	 *    years that the locale observed daylight saving time year round.
	 *
	 *  Since we rely on the platform APIs for conversions between local
	 *  time and UTC, we can't guarantee the above.  Rather, if the platform
	 *  has historical DST rules they will be applied.  This seems to be the
	 *  general preferred direction in Ecmascript standardization (or at least
	 *  implementations) anyway, and even the equivalent year mapping should
	 *  be disabled if the platform is known to handle DST properly for the
	 *  full Ecmascript range.
	 *
	 *  The following has useful discussion and links:
	 *
	 *    https://bugzilla.mozilla.org/show_bug.cgi?id=351066
	 */

	duk_bi_date_timeval_to_parts(d, parts, dparts, DUK_DATE_FLAG_EQUIVYEAR /*flags*/);
	DUK_ASSERT(parts[DUK_DATE_IDX_YEAR] >= 1970 && parts[DUK_DATE_IDX_YEAR] <= 2038);

	d = duk_bi_date_get_timeval_from_dparts(dparts, 0 /*flags*/);
	DUK_ASSERT(d >= 0 && d < 2147483648.0 * 1000.0);  /* unsigned 31-bit range */
	t = (time_t) (d / 1000.0);
	DUK_DDD(DUK_DDDPRINT("timeval: %lf -> time_t %ld", (double) d, (long) t));

	DUK_MEMZERO((void *) tms, sizeof(struct tm) * 2);

#if defined(DUK_USE_DATE_TZO_GMTIME_R)
	(void) gmtime_r(&t, &tms[0]);
	(void) localtime_r(&t, &tms[1]);
#elif defined(DUK_USE_DATE_TZO_GMTIME)
	tm_ptr = gmtime(&t);
	DUK_MEMCPY((void *) &tms[0], tm_ptr, sizeof(struct tm));
	tm_ptr = localtime(&t);
	DUK_MEMCPY((void *) &tms[1], tm_ptr, sizeof(struct tm));
#else
#error internal error
#endif
	DUK_DDD(DUK_DDDPRINT("gmtime result: tm={sec:%ld,min:%ld,hour:%ld,mday:%ld,mon:%ld,year:%ld,"
	                     "wday:%ld,yday:%ld,isdst:%ld}",
	                     (long) tms[0].tm_sec, (long) tms[0].tm_min, (long) tms[0].tm_hour,
	                     (long) tms[0].tm_mday, (long) tms[0].tm_mon, (long) tms[0].tm_year,
	                     (long) tms[0].tm_wday, (long) tms[0].tm_yday, (long) tms[0].tm_isdst));
	DUK_DDD(DUK_DDDPRINT("localtime result: tm={sec:%ld,min:%ld,hour:%ld,mday:%ld,mon:%ld,year:%ld,"
	                     "wday:%ld,yday:%ld,isdst:%ld}",
	                     (long) tms[1].tm_sec, (long) tms[1].tm_min, (long) tms[1].tm_hour,
	                     (long) tms[1].tm_mday, (long) tms[1].tm_mon, (long) tms[1].tm_year,
	                     (long) tms[1].tm_wday, (long) tms[1].tm_yday, (long) tms[1].tm_isdst));

	/* tm_isdst is both an input and an output to mktime(), use 0 to
	 * avoid DST handling in mktime():
	 * - https://github.com/svaarala/duktape/issues/406
	 * - http://stackoverflow.com/questions/8558919/mktime-and-tm-isdst
	 */
	tms[0].tm_isdst = 0;
	tms[1].tm_isdst = 0;
	t1 = mktime(&tms[0]);  /* UTC */
	t2 = mktime(&tms[1]);  /* local */
	if (t1 == (time_t) -1 || t2 == (time_t) -1) {
		/* This check used to be for (t < 0) but on some platforms
		 * time_t is unsigned and apparently the proper way to detect
		 * an mktime() error return is the cast above.  See e.g.:
		 * http://pubs.opengroup.org/onlinepubs/009695299/functions/mktime.html
		 */
		goto error;
	}
	DUK_DDD(DUK_DDDPRINT("t1=%ld (utc), t2=%ld (local)", (long) t1, (long) t2));

	/* Compute final offset in seconds, positive if local time ahead of
	 * UTC (returned value is UTC-to-local offset).
	 *
	 * difftime() returns a double, so coercion to int generates quite
	 * a lot of code.  Direct subtraction is not portable, however.
	 * XXX: allow direct subtraction on known platforms.
	 */
#if 0
	return (duk_int_t) (t2 - t1);
#endif
	return (duk_int_t) difftime(t2, t1);

 error:
	/* XXX: return something more useful, so that caller can throw? */
	DUK_D(DUK_DPRINT("mktime() failed, d=%lf", (double) d));
	return 0;
}
#endif  /* DUK_USE_DATE_TZO_GMTIME */

#if defined(DUK_USE_DATE_PRS_STRPTIME)
DUK_INTERNAL duk_bool_t duk_bi_date_parse_string_strptime(duk_context *ctx, const char *str) {
	struct tm tm;
	time_t t;
	char buf[DUK__STRPTIME_BUF_SIZE];

	/* copy to buffer with spare to avoid Valgrind gripes from strptime */
	DUK_ASSERT(str != NULL);
	DUK_MEMZERO(buf, sizeof(buf));  /* valgrind whine without this */
	DUK_SNPRINTF(buf, sizeof(buf), "%s", (const char *) str);
	buf[sizeof(buf) - 1] = (char) 0;

	DUK_DDD(DUK_DDDPRINT("parsing: '%s'", (const char *) buf));

	DUK_MEMZERO(&tm, sizeof(tm));
	if (strptime((const char *) buf, "%c", &tm) != NULL) {
		DUK_DDD(DUK_DDDPRINT("before mktime: tm={sec:%ld,min:%ld,hour:%ld,mday:%ld,mon:%ld,year:%ld,"
		                     "wday:%ld,yday:%ld,isdst:%ld}",
		                     (long) tm.tm_sec, (long) tm.tm_min, (long) tm.tm_hour,
		                     (long) tm.tm_mday, (long) tm.tm_mon, (long) tm.tm_year,
		                     (long) tm.tm_wday, (long) tm.tm_yday, (long) tm.tm_isdst));
		tm.tm_isdst = -1;  /* negative: dst info not available */

		t = mktime(&tm);
		DUK_DDD(DUK_DDDPRINT("mktime() -> %ld", (long) t));
		if (t >= 0) {
			duk_push_number(ctx, ((duk_double_t) t) * 1000.0);
			return 1;
		}
	}

	return 0;
}
#endif  /* DUK_USE_DATE_PRS_STRPTIME */

#if defined(DUK_USE_DATE_PRS_GETDATE)
DUK_INTERNAL duk_bool_t duk_bi_date_parse_string_getdate(duk_context *ctx, const char *str) {
	struct tm tm;
	duk_small_int_t rc;
	time_t t;

	/* For this to work, DATEMSK must be set, so this is not very
	 * convenient for an embeddable interpreter.
	 */

	DUK_MEMZERO(&tm, sizeof(struct tm));
	rc = (duk_small_int_t) getdate_r(str, &tm);
	DUK_DDD(DUK_DDDPRINT("getdate_r() -> %ld", (long) rc));

	if (rc == 0) {
		t = mktime(&tm);
		DUK_DDD(DUK_DDDPRINT("mktime() -> %ld", (long) t));
		if (t >= 0) {
			duk_push_number(ctx, (duk_double_t) t);
			return 1;
		}
	}

	return 0;
}
#endif  /* DUK_USE_DATE_PRS_GETDATE */

#if defined(DUK_USE_DATE_FMT_STRFTIME)
DUK_INTERNAL duk_bool_t duk_bi_date_format_parts_strftime(duk_context *ctx, duk_int_t *parts, duk_int_t tzoffset, duk_small_uint_t flags) {
	char buf[DUK__STRFTIME_BUF_SIZE];
	struct tm tm;
	const char *fmt;

	DUK_UNREF(tzoffset);

	/* If the platform doesn't support the entire Ecmascript range, we need
	 * to return 0 so that the caller can fall back to the default formatter.
	 *
	 * For now, assume that if time_t is 8 bytes or more, the whole Ecmascript
	 * range is supported.  For smaller time_t values (4 bytes in practice),
	 * assumes that the signed 32-bit range is supported.
	 *
	 * XXX: detect this more correctly per platform.  The size of time_t is
	 * probably not an accurate guarantee of strftime() supporting or not
	 * supporting a large time range (the full Ecmascript range).
	 */
	if (sizeof(time_t) < 8 &&
	   (parts[DUK_DATE_IDX_YEAR] < 1970 || parts[DUK_DATE_IDX_YEAR] > 2037)) {
		/* be paranoid for 32-bit time values (even avoiding negative ones) */
		return 0;
	}

	DUK_MEMZERO(&tm, sizeof(tm));
	tm.tm_sec = parts[DUK_DATE_IDX_SECOND];
	tm.tm_min = parts[DUK_DATE_IDX_MINUTE];
	tm.tm_hour = parts[DUK_DATE_IDX_HOUR];
	tm.tm_mday = parts[DUK_DATE_IDX_DAY];       /* already one-based */
	tm.tm_mon = parts[DUK_DATE_IDX_MONTH] - 1;  /* one-based -> zero-based */
	tm.tm_year = parts[DUK_DATE_IDX_YEAR] - 1900;
	tm.tm_wday = parts[DUK_DATE_IDX_WEEKDAY];
	tm.tm_isdst = 0;

	DUK_MEMZERO(buf, sizeof(buf));
	if ((flags & DUK_DATE_FLAG_TOSTRING_DATE) && (flags & DUK_DATE_FLAG_TOSTRING_TIME)) {
		fmt = "%c";
	} else if (flags & DUK_DATE_FLAG_TOSTRING_DATE) {
		fmt = "%x";
	} else {
		DUK_ASSERT(flags & DUK_DATE_FLAG_TOSTRING_TIME);
		fmt = "%X";
	}
	(void) strftime(buf, sizeof(buf) - 1, fmt, &tm);
	DUK_ASSERT(buf[sizeof(buf) - 1] == 0);

	duk_push_string(ctx, buf);
	return 1;
}
#endif  /* DUK_USE_DATE_FMT_STRFTIME */

#undef DUK__STRPTIME_BUF_SIZE
#undef DUK__STRFTIME_BUF_SIZE
