/*
 * This module provides time conversion routines to convert unix 'binary'
 * times (longwords representing seconds since 1 jan 1970) between the
 * current timzone and GMT (UTC).
 *
 * If using VMS version 7 or later and DECC V5 or later, the time-based
 * C RTL will return GMT by default.  You must ovverride this default
 * to continue to use this routine.
 *
 * Only times in the range 1-jan-1970 to 1-jan-2038 may be converted.
 *
 * The conversion is governed by the following logicals:
 *    SYS$LOCALTIME		If defined, must point to a binary data
 *				containing the timezone transitions for the
 *				current local timezone. see <tzfile.h> for
 *				description of format.
 *
 *    SYS$TIMEZONE_DIFFERENTIAL If defined, contains the seconds that must
 *				currently be added to local time to get
 *				GMT time (Eastern Std. time is -18000).
 *				This differential is applied iff sys$localtime
 *				does not exist or cannot be interpreted or
 *				defined times are out of range.
 *
 * Author:	David Jones
 * Date:	12-FEB-1997
 * Revised:	17-JUL-1997	Added futc_decode_vms_date and
 *				fuct_current_time functions.
 */
#include "pthread_1c_np.h"
#include "tutil.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <descrip.h>

#define timezone_file "SYS$LOCALTIME"
#define timezone_diff "SYS$TIMEZONE_DIFFERENTIAL"

#include "fast_utc.h"

/*
 * The zone_def struct is an in-memory structure for holding zone information
 * files.
 */
struct tdef { int offset;		/* offset from GMT */
    unsigned char is_dst;		/* if true, daylight savings */
    unsigned char abbrev_off;
    unsigned char trans_is_std;		/* if true, transition is GMT */
    char *abbrev;			/* timezone abbreviation (e.g. edt) */
};

struct zone_def {
    int type_count;		/* number of transition types */
    int leapsec_count;		/* not used */
    int trans_count;		/* size of transition array */
    int abbreviation_char;	/* bytes of storage allocated to abbrev. */
    struct tdef *trans_def;	/* Timezone info */
    time_t *transition_time;	/* Time transition starts (sec since 1970 GMT)*/
    unsigned char *trans_flag;  /* index into trans_def */
    char *abbreviation;
    int default_offset;		/* offset if type_count zero */
    int default_dst;		/* if true, apply default US daylight savings */
};
static struct zone_def *local_zone;
static long zero_time[2];		/* epoch start time in VMS format */
static long ticks_per_second;		/* 100-nanosecond ticks in 1 second */
static pthread_once_t futc_setup = PTHREAD_ONCE_INIT;
static pthread_mutex_t cache_lock;
#define EPOCH_START "1-JAN-1970 00:00:00.0"
extern int lib$emul(), lib$ediv(), lib$addx(), lib$subx();
extern int sys$bintim(), sys$gettim(), sys$numtim();

/*****************************************************************************/
/* Utility routines for reversing byte order.
 */
static int ntoh_l ( unsigned char *value )
{
    union { unsigned char b[4]; int l; } store;
    store.b[0] = value[3]; store.b[1] = value[2];
    store.b[2] = value[1]; store.b[3] = value[0];
    return store.l;
}
static unsigned int ntoh_ul ( unsigned char *value )
{
    union { unsigned char b[4]; unsigned int l; } store;
    store.b[0] = value[3]; store.b[1] = value[2];
    store.b[2] = value[1]; store.b[3] = value[0];
    return store.l;
}
/****************************************************************************/
/* Allocate a zone structure with ancillary storage and initialize the 
 * default offset.  The .trans_def array will be allocated and the address of
 * an array of var_size bytes will be stored in the transition_time element.
 */
static struct zone_def *alloc_zone ( int type_count, int var_size )
{
    struct zone_def *zone;
    char *buffer;
    int size;
    zone = (struct zone_def *) malloc ( var_size + sizeof(struct zone_def) +
	type_count * sizeof(struct tdef) );
    if ( zone ) {
	zone->type_count = type_count;
	zone->leapsec_count = 0;
	zone->trans_count = 0;
	zone->abbreviation_char = 0;
	zone->trans_def = (struct tdef *) &zone[1];
	zone->transition_time = (time_t *) &zone->trans_def[type_count];
	zone->trans_flag = (unsigned char *) 0;
	zone->abbreviation = "";
	/*
	 * set default offset.
	 */
	buffer = getenv ( "SYS$TIMEZONE_DIFFERENTIAL" );
	if ( buffer ) {
	    zone->default_offset = atoi ( buffer );
	    zone->default_dst = 1;
	} else {
	    zone->default_offset = zone->default_dst = 0;
	}
    }
    return zone;
}

/****************************************************************************/
/* pthread_once routine to create the global local_zone structure.
 */
static void init_local_zone ( ) 
{
    FILE *zic;				/* compiled zone info file */
    struct tz_header {			/* fixed header portion of zic file */
	char reserved[24];
	unsigned char trans_type_count[4];
	unsigned char leapsec_count[4];
	unsigned char transition_count[4];
	unsigned char local_type_count[4];
	unsigned char abbreviation_chars[4];
    } hdr;
    int type_count, leapsec_count, trans_count, local_count, abbrev_count;
    int i, zf_good, size, private_size, t_ndx, sys$bintim();
    unsigned char *buffer;
    char *abbreviations, *fname;
    time_t *transition_time, last_time;		/* ([trans_count]) */
    struct tdef *trans_def;
    struct zone_def *zone;
    unsigned char *transition_flag;
    struct ldef { time_t when; int offset; } *leap_def;
    $DESCRIPTOR(unix_zero, EPOCH_START);

    sys$bintim ( &unix_zero, zero_time );
    ticks_per_second = 10000000;
    INITIALIZE_MUTEX ( &cache_lock );
    SET_MUTEX_NAME(&cache_lock,"OSU FUTC cache_lock")
    /*
     * First open zone definition file and read fixed header portion.
     */
    fname = timezone_file;
    local_zone = (struct zone_def *) 0;
    LOCK_C_RTL
    zic = fopen ( fname, "rb", "ctx=stm" );
    UNLOCK_C_RTL
    switch ( (zic != (FILE *) 0) ) {
      case 1:
	zf_good = 0;		/* set true when ALL ok */
	LOCK_C_RTL
	i = fread ( &hdr, sizeof(hdr), 1, zic );
	UNLOCK_C_RTL
	if ( i != 1 ) break;
	/*
	 * Convert header fields in file to local header in machines byte order.
	 */
	type_count = ntoh_l ( hdr.trans_type_count );
	leapsec_count = ntoh_l ( hdr.leapsec_count );
	trans_count = ntoh_l ( hdr.transition_count );
	local_count = ntoh_l ( hdr.local_type_count );
	abbrev_count = ntoh_l ( hdr.abbreviation_chars );
	/*
	 * Do sanity checks on header fields.
	 */
        if ( type_count < 1 || type_count > 256 || trans_count < 0 ) break;
	/*
	 * Allocate buffer to hold all variable data and read it.
	 */
	size = (5 * trans_count) + (6*type_count) +
	    abbrev_count + (8*leapsec_count) + type_count;
	local_zone = zone = alloc_zone ( type_count, size );
	if ( !zone ) { fclose(zic); return; }
	buffer = (unsigned char *) zone->transition_time;
	LOCK_C_RTL
	i = fread ( buffer, size, 1, zic );
	UNLOCK_C_RTL
	if ( i == i ) zf_good = 1;
	break;
      default:
	zf_good = 0;
    }
    if ( zic ) {
	LOCK_C_RTL
	fclose ( zic );
	UNLOCK_C_RTL
    }
    if ( !zf_good ) { 
	if ( !local_zone ) local_zone = alloc_zone ( 0, 0 );
	else local_zone->type_count = 0;
	return; 
    }
    /*
     * Convert transition time array to local byte order and do
     * sanity checks: times must ascend and flags must be in range.
     */
    zone->trans_count = trans_count;
    transition_time = zone->transition_time;
    zone->trans_flag = transition_flag = &buffer[trans_count*4];
    last_time = 0;
    for ( i = 0; i < trans_count; i++ ) {
	transition_time[i] = ntoh_ul ( &buffer[i*4] );
	if ( (transition_time[i] < last_time) || 
	     (transition_flag[i] >= type_count) ) {
	   local_zone->type_count = 0;
	   return;
	}
	last_time = transition_time[i];
    }
    buffer = &buffer[trans_count*5];
    /*
     * Convert the offset definitions to a local structure.
     */
    trans_def = zone->trans_def; 
    for ( i = 0; i < type_count; i++ ) {
	trans_def[i].offset = ntoh_l ( &buffer[0] );
	trans_def[i].is_dst = buffer[4];
	trans_def[i].abbrev_off = buffer[5];
	if (buffer[5] >= abbrev_count) { local_zone = alloc_zone(0,0); return; }
	trans_def[i].trans_is_std = 0;
	buffer += 6;
    }
    for ( i = 0; i < type_count; i++ ) {
	trans_def[i].abbrev =
		(char *) &buffer[trans_def[i].abbrev_off];
    }
    buffer[abbrev_count-1] = '\0';	/* prevent overflows */
    buffer += abbrev_count;
    /*
     * Convert leap second info to local machine byte order.
     */
    leap_def = (struct ldef *) buffer;
    for ( i = 0; i < leapsec_count; i++ ) {
	leap_def[i].when = ntoh_ul ( (unsigned char *) &leap_def[i].when );
	leap_def[i].offset = ntoh_ul ( (unsigned char *) &leap_def[i].offset );
    }
    buffer += 8*leapsec_count;
    /*
     * Load final field in transition type list.
     */
    for ( i=0; i < type_count; i++ ) trans_def[i].trans_is_std = buffer[i];

    return;
}
/**************************************************************************/
/* Test unix binary date to see it it falls in 'standard' daylight savings
 * time part of year.
 */
static int test_us_dst ( time_t cand )
{
    unsigned long offset, conv, quad_time[2], final_time[2];
    int status, i, wdy, mon;
    unsigned short num_time[7];
    /*
     * Convert Unix time (seconds since 1970) to VMS binary time for that
     * same day (independant of timezone).
     */
    offset = 0;
    conv = 10000000;		/* 100 nanosecond ticks to seconds */
    lib$emul ( &cand, &conv, &offset, quad_time );
    lib$addx ( quad_time, zero_time, final_time );
    status = sys$numtim ( num_time, final_time );
    /*
     * The only months of interest are april to october.
     */
    if ( num_time[1] < 4 || num_time[1] > 10 ) return 0;
    if ( num_time[1] == 4 ) {		/* month of april */
	if ( num_time[2] > 7 ) return 1;
	wdy = cand / 86400;		/* day-number since 1970 */
	wdy = wdy % 7;			/* 0 is thursday, sun is 3 */
	for ( i = num_time[2]; i > 0; --i ) {
	    if ( wdy == 3 ) {		/* first sunday */
		if ( i != num_time[2] ) return 1;
		if ( num_time[3] < 2 ) return 0;
		else return 1;
	    }
	    if ( wdy <= 0 ) wdy = 6; else wdy = wdy-1;
	}
	return 0;	/* before first sunday */
    } else if ( num_time[1] == 10 ) {	/* month of octover */
	if ( num_time[2] < 25 ) return 1;
	wdy = cand / 86400;		/* day-number since 1970 */
	wdy = wdy % 7;			/* 0 is thursday, sun is 3 */
	for ( i = num_time[2]; i < 32; i++ ) {
	    if ( wdy == 3 ) {		/* last sunday */
		if ( i != num_time[2] ) return 1;
		if ( num_time[3] < 2 ) return 1;
		else return 0;
	    }
	    if ( wdy >= 6 ) wdy = 0; else wdy = wdy+1;
	}
	return 0;	/* must be after last sunday */
    }
    return 1;
}
/**************************************************************************/
/* Initialize routine loads the timezone info.
 *
 *  Return values:
 *     -1	Memory allocation error;
 *      0	sys$localtime not defined or could not be loaded, default
 *		timezone differential will be applied.
 *      n	Number of timezone offests loaded, usually 1 if no daylight
 *		savings adjustments and 2 otherwise.
 */
int futc_initialize ( int default_dst )
{
    pthread_once ( &futc_setup, init_local_zone );

    if ( !local_zone ) return -1;
    local_zone->default_dst = default_dst;
    return local_zone->type_count;
}
/****************************************************************************/
/* Return current time as seconds GMT since 1970.  For VMS 7.x, this should
 * be the same value as returned by time().
 */
time_t futc_current_time(time_t *result) 
{
   long now[2], offset[2], unix_now, remainder;

    sys$gettim(now);
    lib$subx(now,zero_time,offset);
    lib$ediv ( &ticks_per_second, offset, &unix_now, &remainder );
    if ( result ) {
	*result = futc_local_to_gmt( (time_t) unix_now );
	return *result;
    }
    else return futc_local_to_gmt ( (time_t) unix_now );
}

time_t futc_vms_to_gmt(long vms_time[2], time_t *result) 
{
   long offset[2], unix_now, remainder;

    lib$subx(vms_time,zero_time,offset);
    lib$ediv ( &ticks_per_second, offset, &unix_now, &remainder );
    if ( result ) {
	*result = futc_local_to_gmt( (time_t) unix_now );
	return *result;
    }
    else return futc_local_to_gmt ( (time_t) unix_now );
}
int futc_vms_time ( time_t unix_time, long vms_time[2] )
{
    long ticks[2], zero;
    zero = 0;
    lib$emul ( &unix_time, &ticks_per_second, &zero, ticks );
    lib$addx ( ticks, zero_time, vms_time );
    return 1;
}
/****************************************************************************/
/* Use sys$bintim to convert VMS time specification (DD-MMM-YYYY HH:MM:SS) to
 * seconds since 1970.  No correction for timezone is performed.  Return
 * value is VMS status of $bintim call.
 */
int futc_decode_vms_date ( int length, char *date_spec, time_t *result )
{
    struct { long l; char *s; } dx;		/* VMS descriptor */
    long vms_time[2], offset[2], remainder;
    int status;

    dx.l = length; dx.s = date_spec;
    status = sys$bintim ( &dx, vms_time );
    if ( (status&1) == 1 ) {
	lib$subx(vms_time,zero_time,offset);
	lib$ediv ( &ticks_per_second, offset, (long *) result, &remainder );
    }
    return status;
}
/****************************************************************************/
/* Convert local time offset to GMT.  Return value is seconds GMT.
 */
time_t futc_local_to_gmt ( time_t local_time )
{
    int status, high, low, mid, offset;
    time_t gmt_time, last_time, *transition_time;
    struct tdef *trans_def;
    struct zone_def *zone;
    unsigned char *transition_flag;
    /*
     * validate zone info.
     */
    if ( !local_zone ) return local_time;
    /*
     * do binary search of array.
     */
    low = 0;
    high = local_zone->trans_count-1;
    trans_def = local_zone->trans_def;
    transition_time = local_zone->transition_time;
    transition_flag = local_zone->trans_flag;

    while ( (low+1) < high ) {
	struct tdef *td;
	time_t mark;

	mid = (low+high)/2;
	td = &trans_def[transition_flag[mid]];
	mark = transition_time[mid] + td->offset;
	if ( mid > 0 ) {
	        /* Adjust if previous transition was to daylight savings */
		if (trans_def[transition_flag[mid-1]].is_dst) mark += 3600;
	}
	if ( local_time < mark ) high = mid; else low = mid;
    }

    if ( high >= 0 ) {
	/*
	 * local_zone has table.
	 */
	offset = trans_def[transition_flag[low]].offset;
    } else if ( local_zone->type_count > 0 ) {
	/*
	 * transitions defined but no transition times.
	 * Scan offsets and do daylight savings test if 1 found.
	 */
	int i, dst_ndx, nodst_ndx;
	dst_ndx = -1;		/* none */
	offset = trans_def[0].offset;
	for ( nodst_ndx = i = 0; i < local_zone->type_count; i++ ) {
	    if ( trans_def[i].is_dst ) dst_ndx = i;
	     else nodst_ndx = i;
	}
	if ( dst_ndx >= 0 ) {
	    if ( test_us_dst(local_time) ) offset = trans_def[dst_ndx].offset;
	    else offset = trans_def[nodst_ndx].offset;
	} else offset = trans_def[nodst_ndx].offset;
    } else {
	/*
	 * fallback to default zone.
	 */
	offset = local_zone->default_offset;
	if ( local_zone->default_dst ) {
	    if ( test_us_dst ( local_time ) ) offset = offset + 3600;
	}
    }

    gmt_time = local_time - offset;
    return gmt_time;
}
/*****************************************************************************/
/* Caching functions.
 */
int futc_test_cache ( time_t cand_time, 
	char *buffer, int bufsize, futc_tcache *cache )
{
    int status;
    pthread_mutex_lock ( &cache_lock );
    if ( cache->value && (cand_time == cache->time) ) {
	status = 1;
	tu_strnzcpy ( buffer, cache->value, bufsize-1 );
    }
    else status = 0;
    pthread_mutex_unlock ( &cache_lock );
    return status;
}
int futc_update_cache ( time_t new_time, char *buffer, int bufsize,
	futc_tcache *cache )
{
    int status;
    pthread_mutex_lock ( &cache_lock );
    /* Ensure buffer space adequate. */
    if ( !cache->value ) {
	LOCK_C_RTL
	cache->value = malloc ( bufsize+1 );
	cache->vlen = bufsize+1;
	UNLOCK_C_RTL
    } else if ( bufsize > cache->vlen ) {
	LOCK_C_RTL
	free ( cache->value );
	cache->value = malloc ( bufsize+1 );
	cache->vlen = bufsize+1;
	UNLOCK_C_RTL
    }
    if ( cache->value ) {
	tu_strnzcpy ( cache->value, buffer, bufsize-1 );
	cache->time = new_time;
	status = 1;
    } else status = 0;
    pthread_mutex_unlock ( &cache_lock );
    return status;
}

