/*-
 * Copyright (c) 2003, 2004 Lev Walkin <vlm@lionet.info>. All rights reserved.
 * Redistribution and modifications are permitted subject to BSD license.
 */
#define	_POSIX_PTHREAD_SEMANTICS	/* for Sun */
#define	_REENTRANT			/* for Sun */
#include <asn_internal.h>
#include <GeneralizedTime.h>
#include <errno.h>

#ifdef	__CYGWIN__
#include "/usr/include/time.h"
#else
#include <time.h>
#endif	/* __CYGWIN__ */

#if	defined(WIN32)
#pragma message( "PLEASE STOP AND READ!")
#pragma message( "  localtime_r is implemented via localtime(), which may be not thread-safe.")
#pragma message( "  gmtime_r is implemented via gmtime(), which may be not thread-safe.")
#pragma message( "  ")
#pragma message( "  You must fix the code by inserting appropriate locking")
#pragma message( "  if you want to use asn_GT2time() or asn_UT2time().")
#pragma message( "PLEASE STOP AND READ!")

static struct tm *localtime_r(const time_t *tloc, struct tm *result) {
	struct tm *tm;
	if((tm = localtime(tloc)))
		return memcpy(result, tm, sizeof(struct tm));
	return 0;
}

static struct tm *gmtime_r(const time_t *tloc, struct tm *result) {
	struct tm *tm;
	if((tm = gmtime(tloc)))
		return memcpy(result, tm, sizeof(struct tm));
	return 0;
}

#define	tzset()	_tzset()
#define	putenv(c)	_putenv(c)
#define	_EMULATE_TIMEGM

#endif	/* WIN32 */

#if	defined(sun) || defined(_sun_) || defined(__solaris__)
#define	_EMULATE_TIMEGM
#endif

/*
 * Where to look for offset from GMT, Phase I.
 * Several platforms are known.
 */
#if defined(__FreeBSD__)				\
	|| (defined(__GNUC__) && defined(__APPLE_CC__))	\
	|| (defined __GLIBC__ && __GLIBC__ >= 2)
#undef	HAVE_TM_GMTOFF
#define	HAVE_TM_GMTOFF
#endif	/* BSDs and newer glibc */

/*
 * Where to look for offset from GMT, Phase II.
 */
#ifdef	HAVE_TM_GMTOFF
#define	GMTOFF(tm)	((tm).tm_gmtoff)
#else	/* HAVE_TM_GMTOFF */
#define	GMTOFF(tm)	(-timezone)
#endif	/* HAVE_TM_GMTOFF */

#if	(defined(_EMULATE_TIMEGM) || !defined(HAVE_TM_GMTOFF))
#warning "PLEASE STOP AND READ!"
#warning "  timegm() is implemented via getenv(\"TZ\")/setenv(\"TZ\"), which may be not thread-safe."
#warning "  "
#warning "  You must fix the code by inserting appropriate locking"
#warning "  if you want to use asn_GT2time() or asn_UT2time()."
#warning "PLEASE STOP AND READ!"
#endif	/* _EMULATE_TIMEGM */

/*
 * Override our GMTOFF decision for other known platforms.
 */
#ifdef __CYGWIN__
#undef	GMTOFF
static long GMTOFF(struct tm a){
	struct tm *lt;
	time_t local_time, gmt_time;
	long zone;

	tzset();
	gmt_time = time (NULL);

	lt = gmtime(&gmt_time);

	local_time = mktime(lt);
	return (gmt_time - local_time);
}

#endif	/* __CYGWIN__ */

#ifdef __MINGW32__

#define	ATZVARS do {							\
	char tzoldbuf[64];						\
	char *tzold
#define	ATZSAVETZ do {							\
	tzold = getenv("TZ");						\
	if(tzold) {							\
		size_t tzlen = strlen(tzold);				\
		if(tzlen < sizeof(tzoldbuf)) {				\
			tzold = memcpy(tzoldbuf, tzold, tzlen + 1);	\
		} else {						\
			char *dupptr = tzold;				\
			tzold = MALLOC(tzlen + 1);			\
			if(tzold) memcpy(tzold, dupptr, tzlen + 1);	\
		}							\
		putenv("TZ= UTC");					\
	}								\
	tzset();							\
} while(0)
#define	ATZOLDTZ do {							\
	if (tzold) {							\
		putenv("TZ= UTC");					\
		*tzoldbuf = 0;						\
		if(tzold != tzoldbuf)					\
			FREEMEM(tzold);					\
	} else {							\
		putenv("TZ= ");						\
	}								\
	tzset();							\
} while(0); } while(0);

#else //******* not defined __MinGW32__  ***/

#define	ATZVARS do {							\
	char tzoldbuf[64];						\
	char *tzold
#define	ATZSAVETZ do {							\
	tzold = getenv("TZ");						\
	if(tzold) {							\
		size_t tzlen = strlen(tzold);				\
		if(tzlen < sizeof(tzoldbuf)) {				\
			tzold = memcpy(tzoldbuf, tzold, tzlen + 1);	\
		} else {						\
			char *dupptr = tzold;				\
			tzold = MALLOC(tzlen + 1);			\
			if(tzold) memcpy(tzold, dupptr, tzlen + 1);	\
		}							\
		setenv("TZ", "UTC", 1);					\
	}								\
	tzset();							\
} while(0)
#define	ATZOLDTZ do {							\
	if (tzold) {							\
		setenv("TZ", tzold, 1);					\
		*tzoldbuf = 0;						\
		if(tzold != tzoldbuf)					\
			FREEMEM(tzold);					\
	} else {							\
		unsetenv("TZ");						\
	}								\
	tzset();							\
} while(0); } while(0);

#endif 

#ifdef	_EMULATE_TIMEGM
static time_t timegm(struct tm *tm) {
	time_t tloc;
	ATZVARS;
	ATZSAVETZ;
	tloc = mktime(tm);
	ATZOLDTZ;
	return tloc;
}
#endif	/* _EMULATE_TIMEGM */


#ifndef	__ASN_INTERNAL_TEST_MODE__

/*
 * GeneralizedTime basic type description.
 */
static ber_tlv_tag_t asn_DEF_GeneralizedTime_tags[] = {
	(ASN_TAG_CLASS_UNIVERSAL | (24 << 2)),	/* [UNIVERSAL 24] IMPLICIT ...*/
	(ASN_TAG_CLASS_UNIVERSAL | (26 << 2)),  /* [UNIVERSAL 26] IMPLICIT ...*/
	(ASN_TAG_CLASS_UNIVERSAL | (4 << 2))    /* ... OCTET STRING */
};
static asn_per_constraints_t asn_DEF_GeneralizedTime_constraints = {
	{ APC_CONSTRAINED, 7, 7, 0x20, 0x7e },  /* Value */
	{ APC_SEMI_CONSTRAINED, -1, -1, 0, 0 }, /* Size */
	0, 0
};
asn_TYPE_descriptor_t asn_DEF_GeneralizedTime = {
	"GeneralizedTime",
	"GeneralizedTime",
	OCTET_STRING_free,
	GeneralizedTime_print,
	GeneralizedTime_constraint, /* Check validity of time */
	OCTET_STRING_decode_ber,    /* Implemented in terms of OCTET STRING */
	GeneralizedTime_encode_der,
	OCTET_STRING_decode_xer_utf8,
	GeneralizedTime_encode_xer,
	OCTET_STRING_decode_uper,
	OCTET_STRING_encode_uper,
	0, /* Use generic outmost tag fetcher */
	asn_DEF_GeneralizedTime_tags,
	sizeof(asn_DEF_GeneralizedTime_tags)
	  / sizeof(asn_DEF_GeneralizedTime_tags[0]) - 2,
	asn_DEF_GeneralizedTime_tags,
	sizeof(asn_DEF_GeneralizedTime_tags)
	  / sizeof(asn_DEF_GeneralizedTime_tags[0]),
	&asn_DEF_GeneralizedTime_constraints,
	0, 0,	/* No members */
	0	/* No specifics */
};

#endif	/* __ASN_INTERNAL_TEST_MODE__ */

/*
 * Check that the time looks like the time.
 */
int
GeneralizedTime_constraint(asn_TYPE_descriptor_t *td, const void *sptr,
		asn_app_constraint_failed_f *ctfailcb, void *app_key) {
	const GeneralizedTime_t *st = (const GeneralizedTime_t *)sptr;
	time_t tloc;

	errno = EPERM;			/* Just an unlikely error code */
	tloc = asn_GT2time(st, 0, 0);
	if(tloc == -1 && errno != EPERM) {
		_ASN_CTFAIL(app_key, td, sptr,
			"%s: Invalid time format: %s (%s:%d)",
			td->name, strerror(errno), __FILE__, __LINE__);
		// return -1;
		/* FIXME: reading FEF files from CNSystem's TaskForceMonitor (TFM) is failing here.
		   Date and time information (recording time, birthday, etc.) is not properly decoded.
		   Other than that, it seems the files can be read.
		   At this time, it is not clear whether the problem is with the File format or with the ASN1 converter used here.
		   As workaround, we do not trigger this as error, but report only the following information.
		*/
		fprintf(stderr,"ERROR %s L%d: char[%d]<%s> date-time is not properly decoded\n", __FILE__, __LINE__, ((OCTET_STRING_t*)sptr)->size, (char*)(((OCTET_STRING_t*)sptr)->buf));
	}

	return 0;
}

asn_enc_rval_t
GeneralizedTime_encode_der(asn_TYPE_descriptor_t *td, void *sptr,
	int tag_mode, ber_tlv_tag_t tag,
	asn_app_consume_bytes_f *cb, void *app_key) {
	GeneralizedTime_t *st = (GeneralizedTime_t *)sptr;
	asn_enc_rval_t erval;
	int fv, fd;	/* seconds fraction value and number of digits */
	struct tm tm;
	time_t tloc;

	/*
	 * Encode as a canonical DER.
	 */
	errno = EPERM;
	tloc = asn_GT2time_frac(st, &fv, &fd, &tm, 1);	/* Recognize time */
	if(tloc == -1 && errno != EPERM)
		/* Failed to recognize time. Fail completely. */
		_ASN_ENCODE_FAILED;

	st = asn_time2GT_frac(0, &tm, fv, fd, 1); /* Save time canonically */
	if(!st) _ASN_ENCODE_FAILED;	/* Memory allocation failure. */

	erval = OCTET_STRING_encode_der(td, st, tag_mode, tag, cb, app_key);

	FREEMEM(st->buf);
	FREEMEM(st);

	return erval;
}

#ifndef	__ASN_INTERNAL_TEST_MODE__

asn_enc_rval_t
GeneralizedTime_encode_xer(asn_TYPE_descriptor_t *td, void *sptr,
	int ilevel, enum xer_encoder_flags_e flags,
		asn_app_consume_bytes_f *cb, void *app_key) {

	if(flags & XER_F_CANONICAL) {
		GeneralizedTime_t *gt;
		asn_enc_rval_t rv;
		int fv, fd;		/* fractional parts */
		struct tm tm;

		errno = EPERM;
		if(asn_GT2time_frac((GeneralizedTime_t *)sptr,
					&fv, &fd, &tm, 1) == -1
				&& errno != EPERM)
			_ASN_ENCODE_FAILED;

		gt = asn_time2GT_frac(0, &tm, fv, fd, 1);
		if(!gt) _ASN_ENCODE_FAILED;
	
		rv = OCTET_STRING_encode_xer_utf8(td, sptr, ilevel, flags,
			cb, app_key);
		ASN_STRUCT_FREE(asn_DEF_GeneralizedTime, gt);
		return rv;
	} else {
		return OCTET_STRING_encode_xer_utf8(td, sptr, ilevel, flags,
			cb, app_key);
	}
}

#endif	/* __ASN_INTERNAL_TEST_MODE__ */

int
GeneralizedTime_print(asn_TYPE_descriptor_t *td, const void *sptr, int ilevel,
	asn_app_consume_bytes_f *cb, void *app_key) {
	const GeneralizedTime_t *st = (const GeneralizedTime_t *)sptr;

	(void)td;	/* Unused argument */
	(void)ilevel;	/* Unused argument */

	if(st && st->buf) {
		char buf[32];
		struct tm tm;
		int ret;

		errno = EPERM;
		if(asn_GT2time(st, &tm, 1) == -1 && errno != EPERM)
			return (cb("<bad-value>", 11, app_key) < 0) ? -1 : 0;

		ret = snprintf(buf, sizeof(buf),
			"%04d-%02d-%02d %02d:%02d:%02d (GMT)",
			tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
			tm.tm_hour, tm.tm_min, tm.tm_sec);
		assert(ret > 0 && ret < (int)sizeof(buf));
		return (cb(buf, ret, app_key) < 0) ? -1 : 0;
	} else {
		return (cb("<absent>", 8, app_key) < 0) ? -1 : 0;
	}
}

time_t
asn_GT2time(const GeneralizedTime_t *st, struct tm *ret_tm, int as_gmt) {
	return asn_GT2time_frac(st, 0, 0, ret_tm, as_gmt);
}

time_t
asn_GT2time_prec(const GeneralizedTime_t *st, int *frac_value, int frac_digits, struct tm *ret_tm, int as_gmt) {
	time_t tloc;
	int fv, fd = 0;

	if(frac_value)
		tloc = asn_GT2time_frac(st, &fv, &fd, ret_tm, as_gmt);
	else
		return asn_GT2time_frac(st, 0, 0, ret_tm, as_gmt);
	if(fd == 0 || frac_digits <= 0) {
		*frac_value = 0;
	} else {
		while(fd > frac_digits)
			fv /= 10, fd--;
		while(fd < frac_digits) {
			int new_fv = fv * 10;
			if(new_fv / 10 != fv) {
				/* Too long precision request */
				fv = 0;
				break;
			}
			fv = new_fv, fd++;
		}

		*frac_value = fv;
	}

	return tloc;
}

time_t
asn_GT2time_frac(const GeneralizedTime_t *st, int *frac_value, int *frac_digits, struct tm *ret_tm, int as_gmt) {
	struct tm tm_s;
	uint8_t *buf;
	uint8_t *end;
	int gmtoff_h = 0;
	int gmtoff_m = 0;
	int gmtoff = 0;	/* h + m */
	int offset_specified = 0;
	int fvalue = 0;
	int fdigits = 0;
	time_t tloc;

	if(!st || !st->buf) {
		errno = EINVAL;
		return -1;
	} else {
		buf = st->buf;
		end = buf + st->size;
	}

	if(st->size < 10) {
		errno = EINVAL;
		return -1;
	}

	/*
	 * Decode first 10 bytes: "AAAAMMJJhh"
	 */
	memset(&tm_s, 0, sizeof(tm_s));
#undef	B2F
#undef	B2T
#define	B2F(var)	do {					\
		unsigned ch = *buf;				\
		if(ch < 0x30 || ch > 0x39) {			\
			errno = EINVAL;				\
			return -1;				\
		} else {					\
			var = var * 10 + (ch - 0x30);		\
			buf++;					\
		}						\
	} while(0)
#define	B2T(var)	B2F(tm_s.var)

	B2T(tm_year);	/* 1: A */
	B2T(tm_year);	/* 2: A */
	B2T(tm_year);	/* 3: A */
	B2T(tm_year);	/* 4: A */
	B2T(tm_mon);	/* 5: M */
	B2T(tm_mon);	/* 6: M */
	B2T(tm_mday);	/* 7: J */
	B2T(tm_mday);	/* 8: J */
	B2T(tm_hour);	/* 9: h */
	B2T(tm_hour);	/* 0: h */

	if(buf == end) goto local_finish;

	/*
	 * Parse [mm[ss[(.|,)ffff]]]
	 *        ^^
	 */
	switch(*buf) {
	case 0x30: case 0x31: case 0x32: case 0x33: case 0x34:
	case 0x35: case 0x36: case 0x37: case 0x38: case 0x39:
		tm_s.tm_min = (*buf++) - 0x30;
		if(buf == end) { errno = EINVAL; return -1; }
		B2T(tm_min);
		break;
	case 0x2B: case 0x2D:	/* +, - */
		goto offset;
	case 0x5A:		/* Z */
		goto utc_finish;
	default:
		errno = EINVAL;
		return -1;
	}

	if(buf == end) goto local_finish;

	/*
	 * Parse [mm[ss[(.|,)ffff]]]
	 *           ^^
	 */
	switch(*buf) {
	case 0x30: case 0x31: case 0x32: case 0x33: case 0x34:
	case 0x35: case 0x36: case 0x37: case 0x38: case 0x39:
		tm_s.tm_sec = (*buf++) - 0x30;
		if(buf == end) { errno = EINVAL; return -1; }
		B2T(tm_sec);
		break;
	case 0x2B: case 0x2D:	/* +, - */
		goto offset;
	case 0x5A:		/* Z */
		goto utc_finish;
	default:
		errno = EINVAL;
		return -1;
	}

	if(buf == end) goto local_finish;

	/*
	 * Parse [mm[ss[(.|,)ffff]]]
	 *               ^ ^
	 */
	switch(*buf) {
	case 0x2C: case 0x2E: /* (.|,) */
		/*
		 * Process fractions of seconds.
		 */
		for(buf++; buf < end; buf++) {
			int v = *buf;
			int new_fvalue;
			switch(v) {
			case 0x30: case 0x31: case 0x32: case 0x33: case 0x34:
			case 0x35: case 0x36: case 0x37: case 0x38: case 0x39:
				new_fvalue = fvalue * 10 + (v - 0x30);
				if(new_fvalue / 10 != fvalue) {
					/* Not enough precision, ignore */
				} else {
					fvalue = new_fvalue;
					fdigits++;
				}
				continue;
			default:
				break;
			}
			break;
		}
	}

	if(buf == end) goto local_finish;

	switch(*buf) {
	case 0x2B: case 0x2D:	/* +, - */
		goto offset;
	case 0x5A:		/* Z */
		goto utc_finish;
	default:
		errno = EINVAL;
		return -1;
	}


offset:

	if(end - buf < 3) {
		errno = EINVAL;
		return -1;
	}
	buf++;
	B2F(gmtoff_h);
	B2F(gmtoff_h);
	if(buf[-3] == 0x2D)	/* Negative */
		gmtoff = -1;
	else
		gmtoff = 1;

	if((end - buf) == 2) {
		B2F(gmtoff_m);
		B2F(gmtoff_m);
	} else if(end != buf) {
		errno = EINVAL;
		return -1;
	}

	gmtoff = gmtoff * (3600 * gmtoff_h + 60 * gmtoff_m);

	/* Fall through */
utc_finish:

	offset_specified = 1;

	/* Fall through */
local_finish:

	/*
	 * Validation.
	 */
	if((tm_s.tm_mon > 12 || tm_s.tm_mon < 1)
	|| (tm_s.tm_mday > 31 || tm_s.tm_mday < 1)
	|| (tm_s.tm_hour > 23)
	|| (tm_s.tm_sec > 60)
	) {
		errno = EINVAL;
		return -1;
	}

	/* Canonicalize */
	tm_s.tm_mon -= 1;	/* 0 - 11 */
	tm_s.tm_year -= 1900;
	tm_s.tm_isdst = -1;

	tm_s.tm_sec -= gmtoff;

	/*** AT THIS POINT tm_s is either GMT or local (unknown) ****/

	if(offset_specified) {
		tloc = timegm(&tm_s);
	} else {
		/*
		 * Without an offset (or "Z"),
		 * we can only guess that it is a local zone.
		 * Interpret it in this fashion.
		 */
		tloc = mktime(&tm_s);
	}
	if(tloc == -1) {
		errno = EINVAL;
		return -1;
	}

	if(ret_tm) {
		if(as_gmt) {
			if(offset_specified) {
				*ret_tm = tm_s;
			} else {
				if(gmtime_r(&tloc, ret_tm) == 0) {
					errno = EINVAL;
					return -1;
				}
			}
		} else {
			if(localtime_r(&tloc, ret_tm) == 0) {
				errno = EINVAL;
				return -1;
			}
		}
	}

	/* Fractions of seconds */
	if(frac_value) *frac_value = fvalue;
	if(frac_digits) *frac_digits = fdigits;

	return tloc;
}

GeneralizedTime_t *
asn_time2GT(GeneralizedTime_t *opt_gt, const struct tm *tm, int force_gmt) {
	return asn_time2GT_frac(opt_gt, tm, 0, 0, force_gmt);
}

GeneralizedTime_t *
asn_time2GT_frac(GeneralizedTime_t *opt_gt, const struct tm *tm, int frac_value, int frac_digits, int force_gmt) {
	struct tm tm_s;
	long gmtoff;
	const unsigned int buf_size =
		4 + 2 + 2	/* yyyymmdd */
		+ 2 + 2 + 2	/* hhmmss */
		+ 1 + 6		/* .ffffff */
		+ 1 + 4		/* +hhmm */
		+ 1		/* '\0' */
		;
	char *buf;
	char *p;
	int size;

	/* Check arguments */
	if(!tm) {
		errno = EINVAL;
		return 0;
	}

	/* Pre-allocate a buffer of sufficient yet small length */
	buf = (char *)MALLOC(buf_size);
	if(!buf) return 0;

	gmtoff = GMTOFF(*tm);

	if(force_gmt && gmtoff) {
		tm_s = *tm;
		tm_s.tm_sec -= gmtoff;
		timegm(&tm_s);	/* Fix the time */
		tm = &tm_s;
#ifdef	HAVE_TM_GMTOFF
		assert(!GMTOFF(tm_s));	/* Will fix itself */
#else	/* !HAVE_TM_GMTOFF */
		gmtoff = 0;
#endif
	}

	size = snprintf(buf, buf_size, "%04d%02d%02d%02d%02d%02d",
		tm->tm_year + 1900,
		tm->tm_mon + 1,
		tm->tm_mday,
		tm->tm_hour,
		tm->tm_min,
		tm->tm_sec
	);
	if(size != 14) {
		/* Could be assert(size == 14); */
		FREEMEM(buf);
		errno = EINVAL;
		return 0;
	}

	p = buf + size;

	/*
	 * Deal with fractions.
	 */
	if(frac_value > 0 && frac_digits > 0) {
		char *end = p + 1 + 6;	/* '.' + maximum 6 digits */
		char *z = p;
		long fbase;
		*z++ = '.';

		/* Place bounds on precision */
		while(frac_digits-- > 6)
			frac_value /= 10;

		/* emulate fbase = pow(10, frac_digits) */
		for(fbase = 1; frac_digits--;)
			fbase *= 10;

		do {
			int digit = frac_value / fbase;
			if(digit > 9) { z = 0; break; }
			*z++ = digit + 0x30;
			frac_value %= fbase;
			fbase /= 10;
		} while(fbase > 0 && frac_value > 0 && z < end);
		if(z) {
			for(--z; *z == 0x30; --z);	/* Strip zeroes */
			p = z + (*z != '.');
			size = p - buf;
		}
	}

	if(force_gmt) {
		*p++ = 0x5a;	/* "Z" */
		*p++ = 0;
		size++;
	} else {
		int ret;
		gmtoff %= 86400;
		ret = snprintf(p, buf_size - size, "%+03ld%02ld",
			gmtoff / 3600, labs(gmtoff % 3600));
		if(ret != 5) {
			FREEMEM(buf);
			errno = EINVAL;
			return 0;
		}
		size += ret;
	}

	if(opt_gt) {
		if(opt_gt->buf)
			FREEMEM(opt_gt->buf);
	} else {
		opt_gt = (GeneralizedTime_t *)CALLOC(1, sizeof *opt_gt);
		if(!opt_gt) { FREEMEM(buf); return 0; }
	}

	opt_gt->buf = (unsigned char *)buf;
	opt_gt->size = size;

	return opt_gt;
}


