/* 
 * Copyright (C) 2005 Andrew Beekhof <andrew@beekhof.net>
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

/*
 * Primary reference:
 *	http://en.wikipedia.org/wiki/ISO_8601 (as at 2005-08-01)
 *
 * Secondary references:
 *	http://hydracen.com/dx/iso8601.htm
 *	http://www.personal.ecu.edu/mccartyr/ISOwdALG.txt
 *	http://www.personal.ecu.edu/mccartyr/isowdcal.html
 *	http://www.phys.uu.nl/~vgent/calendar/isocalendar.htm
 *
 */

#include <crm_internal.h>
#include <crm/crm.h>
#include <time.h>
#include <ctype.h>
#include <crm/common/iso8601.h>

gboolean gregorian_to_ordinal(ha_time_t *a_date);
gboolean ordinal_to_gregorian(ha_time_t *a_date);
gboolean ordinal_to_weekdays(ha_time_t *a_date);
void normalize_time(ha_time_t *a_time);

/*
 * Andrew's code was originally written for OSes whose "struct tm" contains:
 *	long tm_gmtoff;		:: Seconds east of UTC
 *	const char *tm_zone;	:: Timezone abbreviation
 * Some OSes lack these, instead having:
 *	time_t (or long) timezone;
		:: "difference between UTC and local standard time"
 *	char *tzname[2] = { "...", "..." };
 * I (David Lee) confess to not understanding the details.  So my attempted
 * generalisations for where their use is necessary may be flawed.
 *
 * 1. Does "difference between ..." subtract the same or opposite way?
 * 2. Should it use "altzone" instead of "timezone"?
 * 3. Should it use tzname[0] or tzname[1]?  Interaction with timezone/altzone?
 */
#if defined(HAVE_STRUCT_TM_TM_GMTOFF)
#define GMTOFF(tm) ((tm)->tm_gmtoff)
#else
/* Note: extern variable; macro argument not actually used.  */
#define GMTOFF(tm) (timezone)
#endif

char *
date_to_string(ha_time_t *date_time, int flags) 
{
	char *date_s = NULL;
	char *time_s = NULL;
	char *offset_s = NULL;
	char *result_s = NULL;
	ha_time_t *dt = NULL;
	
	if(flags & ha_log_local) {
		crm_debug_6("Local version");
		dt = date_time;
	} else {
		dt = date_time->normalized;
	}

	CRM_CHECK(dt != NULL, return NULL);
	
	if(flags & ha_log_date) {
		crm_malloc0(date_s, 32);
		if(date_s == NULL) {
			return NULL;
			
		} else if(flags & ha_date_weeks) {
			snprintf(date_s, 31, "%d-W%.2d-%d",
				 dt->weekyears, dt->weeks, dt->weekdays);

		} else if(flags & ha_date_ordinal) {
			snprintf(date_s, 31, "%d-%.3d",dt->years, dt->yeardays);

		} else {
			snprintf(date_s, 31, "%.4d-%.2d-%.2d",
				 dt->years, dt->months, dt->days);
		}
	}
	if(flags & ha_log_time) {
		int offset = 0;
		crm_malloc0(time_s, 32);
		if(time_s == NULL) {
		    goto cleanup;
		} 

		snprintf(time_s, 31, "%.2d:%.2d:%.2d",
			 dt->hours, dt->minutes, dt->seconds);
		
		if(dt->offset != NULL) {
			offset =(dt->offset->hours * 100) + dt->offset->minutes;
		}
		
		crm_malloc0(offset_s, 32);
		if((flags & ha_log_local) == 0 || offset == 0) {
			snprintf(offset_s, 31, "Z");

		} else {
			int hr = dt->offset->hours;
			int mins = dt->offset->minutes;
			if(hr < 0) {
				hr = 0 - hr;
			}
			if(mins < 0) {
				mins = 0 - mins;
			}
			snprintf(offset_s, 31, " %s%.2d:%.2d",
				 offset>0?"+":"-", hr, mins);
		}
	}

	crm_malloc0(result_s, 100);

	snprintf(result_s, 100, "%s%s%s%s",
		 date_s?date_s:"", (date_s!=NULL&&time_s!=NULL)?" ":"",
		 time_s?time_s:"", offset_s?offset_s:"");

  cleanup:
	crm_free(date_s);
	crm_free(time_s);
	crm_free(offset_s);

	return result_s;
}

void
log_date(int log_level, const char *prefix, ha_time_t *date_time, int flags) 
{
	char *date_s = date_to_string(date_time, flags);
	do_crm_log(log_level, "%s%s%s",
		      prefix?prefix:"", prefix?": ":"",
		      date_s?date_s:"__invalid_date__");

	crm_free(date_s);
}

void
log_time_period(int log_level, ha_time_period_t *dtp, int flags) 
{
	log_date(log_level, "Period start:", dtp->start, flags);
	log_date(log_level, "Period end:", dtp->end, flags);
}

ha_time_t*
parse_time_offset(char **offset_str)
{
	ha_time_t *new_time = NULL;
	crm_malloc0(new_time, sizeof(ha_time_t));
	crm_malloc0(new_time->has, sizeof(ha_has_time_t));

	if((*offset_str)[0] == 'Z') {
		
	} else if((*offset_str)[0] == '+'
		  || (*offset_str)[0] == '-'
		  || isdigit((int) (*offset_str)[0])) {
		gboolean negate = FALSE;
		if((*offset_str)[0] == '-') {
			negate = TRUE;
			(*offset_str)++;
		}
		parse_time(offset_str, new_time, FALSE);
		if(negate) {
			new_time->hours = 0 - new_time->hours;
			new_time->minutes = 0 - new_time->minutes;
			new_time->seconds = 0 - new_time->seconds;
		}
		
	} else {
#if defined(HAVE_STRUCT_TM_TM_GMTOFF)
		time_t now = time(NULL);
		struct tm *now_tm = localtime(&now);
#endif
		int h_offset = GMTOFF(now_tm) / (3600); 
		int m_offset = (GMTOFF(now_tm) - (3600 * h_offset)) / (60);
		if(h_offset < 0 && m_offset < 0) {
			m_offset = 0 - m_offset;
		}
		new_time->hours = h_offset;
		new_time->minutes = m_offset;
		new_time->has->hours = TRUE;
		new_time->has->minutes = TRUE;
	}
	return new_time;
}

ha_time_t*
parse_time(char **time_str, ha_time_t *a_time, gboolean with_offset)
{
	ha_time_t *new_time = a_time;
	tzset();
	if(a_time == NULL) {
		new_time = new_ha_date(FALSE);
	}

	CRM_CHECK(new_time != NULL, return NULL);
	CRM_CHECK(new_time->has != NULL, free_ha_date(new_time); return NULL);

	/* reset the time fields */
	new_time->hours = 0;
	new_time->minutes = 0;
	new_time->seconds = 0;
	
	crm_debug_4("Get hours...");
	new_time->has->hours = FALSE;
	if(parse_int(time_str, 2, 24, &new_time->hours)) {
		new_time->has->hours = TRUE;
	}

	crm_debug_4("Get minutes...");
	new_time->has->minutes = FALSE;
	if(parse_int(time_str, 2, 60, &new_time->minutes)) {
		new_time->has->minutes = TRUE;
	}

	crm_debug_4("Get seconds...");	
	new_time->has->seconds = FALSE;
	if(parse_int(time_str, 2, 60, &new_time->seconds)){
		new_time->has->seconds = TRUE;
	}
	   
	if(with_offset) {
		crm_debug_4("Get offset...");
		while(isspace((int) (*time_str)[0])) {
			(*time_str)++;
		}

		new_time->offset = parse_time_offset(time_str);
		normalize_time(new_time);
	}
	return new_time;
}

void
normalize_time(ha_time_t *a_time)
{
	CRM_CHECK(a_time != NULL, return);
	CRM_CHECK(a_time->has != NULL, return);

	if(a_time->normalized == NULL) {
		crm_malloc0(a_time->normalized, sizeof(ha_time_t));
	}
	if(a_time->normalized->has == NULL) {
		crm_malloc0(a_time->normalized->has, sizeof(ha_has_time_t));
	}

	ha_set_time(a_time->normalized, a_time, FALSE);
	if(a_time->offset != NULL) {
		if(a_time->offset->has->hours) {
			sub_hours(a_time->normalized, a_time->offset->hours);
		}
		if(a_time->offset->has->minutes) {
			sub_minutes(a_time->normalized,a_time->offset->minutes);
		}
		if(a_time->offset->has->seconds) {
			sub_seconds(a_time->normalized,a_time->offset->seconds);
		}
	}
	CRM_CHECK(is_date_sane(a_time), return);
}



ha_time_t *
parse_date(char **date_str)
{
	gboolean is_done = FALSE;
	gboolean converted = FALSE;
	ha_time_t *new_time = NULL;

	CRM_CHECK(date_str != NULL, return NULL);
	CRM_CHECK(strlen(*date_str) > 0, return NULL);
	
	if((*date_str)[0] == 'T' || (*date_str)[2] == ':') {
	    /* Just a time supplied - Infer current date */
	    new_time = new_ha_date(TRUE);

	    parse_time(date_str, new_time, TRUE);
	    normalize_time(new_time);
	    is_done = TRUE;

	} else {
	    crm_malloc0(new_time, sizeof(ha_time_t));
	    crm_malloc0(new_time->has, sizeof(ha_has_time_t));
	}

	while(is_done == FALSE) {
		char ch = (*date_str)[0];
		crm_debug_5("Switching on ch=%c (len=%d)",
			    ch, (int)strlen(*date_str));
		
		if(ch == 0) {
			/* all done */
			is_done = TRUE;
			break;

		} else if(ch == '/') {
			/* all done - interval marker */
			is_done = TRUE;
			break;
			
		} else if(ch == 'W') {
			CRM_CHECK(new_time->has->weeks == FALSE, ;);
			(*date_str)++;
			if(parse_int(date_str, 2, 53, &new_time->weeks)){
				new_time->has->weeks = TRUE;
				new_time->weekyears  = new_time->years;
				new_time->has->weekyears = new_time->has->years;
			}
			if((*date_str)[0] == '-') {
				(*date_str)++;
				if(parse_int(date_str, 1, 7, &new_time->weekdays)) {
					new_time->has->weekdays = TRUE;
				}
			}
			
			if(new_time->weekdays == 0
			   || new_time->has->weekdays == FALSE) {
				new_time->weekdays = 1;
				new_time->has->weekdays = TRUE;
			}
			
		} else if(ch == '-') {
			(*date_str)++;
			if(check_for_ordinal(*date_str)) {
				if(parse_int(date_str, 3, 366, &new_time->yeardays)) {
					new_time->has->yeardays = TRUE;
				}
			}
			
		} else if(ch == 'O') {
			/* ordinal date */
			(*date_str)++;
			if(parse_int(date_str, 3, 366, &new_time->yeardays)){
				new_time->has->yeardays = TRUE;
			}

		} else if(ch == 'T' || ch == ' ') {
			if(new_time->has->yeardays) {
				converted = convert_from_ordinal(new_time);
				
			} else if(new_time->has->weekdays) {
				converted = convert_from_weekdays(new_time);
				
			} else {
				converted = convert_from_gregorian(new_time);
			}
			(*date_str)++;
			parse_time(date_str, new_time, TRUE);
			is_done = TRUE;

		} else if(isdigit((int) ch)) {
			if(new_time->has->years == FALSE
			   && parse_int(date_str, 4, 9999, &new_time->years)) {
				new_time->has->years = TRUE;
				
			} else if(check_for_ordinal(*date_str) && parse_int(
					  date_str, 3,
					  is_leap_year(new_time->years)?366:365,
					  &new_time->yeardays)) {
				new_time->has->yeardays = TRUE;
				
			} else if(new_time->has->months == FALSE
				  && parse_int(date_str, 2, 12, &new_time->months)) {
					new_time->has->months = TRUE;
				
			} else if(new_time->has->days == FALSE) {
				if(parse_int(date_str, 2,
					     days_per_month(new_time->months, new_time->years),
					     &new_time->days)) {
					new_time->has->days = TRUE;
				}
			}

		} else {
			crm_err("Unexpected characters at: %s", *date_str);
			is_done = TRUE;
			break;
		}
	}

	if(converted) {
		
	} else if(new_time->has->yeardays) {
		convert_from_ordinal(new_time);

	} else if(new_time->has->weekdays) {
		convert_from_weekdays(new_time);

	} else {
 		convert_from_gregorian(new_time);
	}

	normalize_time(new_time);
	
	log_date(LOG_DEBUG_3, "Unpacked", new_time, ha_log_date|ha_log_time);

	CRM_CHECK(is_date_sane(new_time), return NULL);
	
	return new_time;
}

ha_time_t*
parse_time_duration(char **interval_str)
{
	gboolean is_time = FALSE;
	ha_time_t *diff = NULL;

	CRM_CHECK(interval_str != NULL, goto bail);
	CRM_CHECK(strlen(*interval_str) > 0, goto bail);
	CRM_CHECK((*interval_str)[0] == 'P', goto bail);
	(*interval_str)++;

	crm_malloc0(diff, sizeof(ha_time_t));
	crm_malloc0(diff->has, sizeof(ha_has_time_t));
	
	while(isspace((int) (*interval_str)[0]) == FALSE) {
		int an_int = 0;
		char ch = 0;

		if((*interval_str)[0] == 'T') {
			is_time = TRUE;
			(*interval_str)++;
		}
		
		if(parse_int(interval_str, 10, 0, &an_int) == FALSE) {
			break;
		}
		ch = (*interval_str)[0];
		(*interval_str)++;

		crm_debug_4("%c=%d", ch, an_int);
		
		switch(ch) {
			case 0:
				return diff;
				break;
			case 'Y':
				diff->years = an_int;
				diff->has->years = TRUE;
				break;
			case 'M':
				if(is_time) {
					diff->minutes = an_int;
					diff->has->minutes = TRUE;
				} else {
					diff->months = an_int;
					diff->has->months = TRUE;
				}
				break;
			case 'W':
				diff->weeks = an_int;
				diff->has->weeks = TRUE;
				break;
			case 'D':
				diff->days = an_int;
				diff->has->days = TRUE;
				diff->yeardays = an_int;
				diff->has->yeardays = TRUE;
				break;
			case 'H':
				diff->hours = an_int;
				diff->has->hours = TRUE;
				break;
			case 'S':
				diff->seconds = an_int;
				diff->has->seconds = TRUE;
				break;
			default:
				break;
		}
	}
	return diff;

  bail:
	if(diff) {
		crm_free(diff->has);
	}
	crm_free(diff);
	return NULL;
}

ha_time_period_t*
parse_time_period(char **period_str)
{
	gboolean invalid = FALSE;
	const char *original = *period_str;
	ha_time_period_t *period = NULL;

	CRM_CHECK(period_str != NULL, return NULL);
	CRM_CHECK(strlen(*period_str) > 0, return NULL);

	tzset();
	crm_malloc0(period, sizeof(ha_time_period_t));
	
	if((*period_str)[0] == 'P') {
		period->diff = parse_time_duration(period_str);
	} else {
		period->start = parse_date(period_str);
	}

	if((*period_str)[0] != 0) {
		CRM_CHECK((*period_str)[0] == '/', invalid = TRUE; goto bail);
		(*period_str)++;
		
		if((*period_str)[0] == 'P') {
			period->diff = parse_time_duration(period_str);
		} else {
			period->end = parse_date(period_str);
		}
		
	} else if(period->diff != NULL) {
		/* just aduration starting from now */
		time_t now = time(NULL);
		crm_malloc0(period->start, sizeof(ha_time_t));
		crm_malloc0(period->start->has, sizeof(ha_has_time_t));
		crm_malloc0(period->start->offset, sizeof(ha_time_t));
		crm_malloc0(period->start->offset->has, sizeof(ha_has_time_t));
		
		ha_set_timet_time(period->start, &now);
		normalize_time(period->start);

	} else {
		invalid = TRUE;
		CRM_CHECK((*period_str)[0] == '/', goto bail);
		goto bail;
	}
	
	
	/* sanity checks */
	if(period->start == NULL && period->end == NULL) {
		crm_err("Invalid time period: %s", original);
		invalid = TRUE;
		
	} else if(period->start == NULL && period->diff == NULL) {
		crm_err("Invalid time period: %s", original);
		invalid = TRUE;

	} else if(period->end == NULL && period->diff == NULL) {
		crm_err("Invalid time period: %s", original);
		invalid = TRUE;
	}

  bail:
	if(invalid) {
		crm_free(period->start);
		crm_free(period->end);
		crm_free(period->diff);
		crm_free(period);
		return NULL;
	}
	if(period->end == NULL && period->diff == NULL) {
	}
	
	if(period->start == NULL) {
		period->start = subtract_duration(period->end, period->diff);
		normalize_time(period->start);
		
	} else if(period->end == NULL) {
		period->end = add_time(period->start, period->diff);
		normalize_time(period->end);
	}

	is_date_sane(period->start);
	is_date_sane(period->end);
	
	return period;
}

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

/* http://www.personal.ecu.edu/mccartyr/ISOwdALG.txt */
int
january1(int year) 
{
	int YY = (year - 1) % 100;
	int C = (year - 1) - YY;
	int G = YY + YY/4;
	int jan1 = 1 + (((((C / 100) % 4) * 5) + G) % 7);
	crm_debug_6("YY=%d, C=%d, G=%d", YY, C, G);
	crm_debug_5("January 1 %.4d: %d", year, jan1);
	return jan1;
}

int
weeks_in_year(int year) 
{
	int weeks = 52;
	int jan1 = january1(year);
	/* if jan1 == thursday */
	if(jan1 == 4) {
		weeks++;
	} else {
		jan1 = january1(year+1);
		/* if dec31 == thursday aka. jan1 of next year is a friday */
		if(jan1 == 5) {
			weeks++;
		}
		
	}
	return weeks;
}

gboolean
convert_from_gregorian(ha_time_t *a_date)
{
	CRM_CHECK(gregorian_to_ordinal(a_date), return FALSE);
	CRM_CHECK(ordinal_to_weekdays(a_date), return FALSE);
	return TRUE;
}

gboolean
gregorian_to_ordinal(ha_time_t *a_date)
{
	CRM_CHECK(a_date->has->years, return FALSE);
	CRM_CHECK(a_date->has->months, return FALSE);
	CRM_CHECK(a_date->has->days, return FALSE);

	CRM_CHECK(a_date->months > 0, return FALSE);
	CRM_CHECK(a_date->days > 0, return FALSE);

	a_date->yeardays = month2days[a_date->months-1];
	a_date->yeardays += a_date->days;
	a_date->has->yeardays = TRUE;
	
	if(is_leap_year(a_date->years) && a_date->months > 2) {
		(a_date->yeardays)++;
	}
	crm_debug_4("Converted %.4d-%.2d-%.2d to %.4d-%.3d",
		    a_date->years, a_date->months, a_date->days,
		    a_date->years, a_date->yeardays);
	
	return TRUE;
}

gboolean
convert_from_ordinal(ha_time_t *a_date)
{
	CRM_CHECK(ordinal_to_gregorian(a_date), return FALSE);
	CRM_CHECK(ordinal_to_weekdays(a_date), return FALSE);
	return TRUE;
}


gboolean ordinal_to_gregorian(ha_time_t *a_date) 
{
	/* Day of the year this month ends on */
	int m_end = 0;

	CRM_CHECK(a_date->has->years, return FALSE);
	CRM_CHECK(a_date->has->yeardays, return FALSE);

	CRM_CHECK(a_date->yeardays > 0, return FALSE);
	
	if(is_leap_year(a_date->years) && a_date->yeardays > 366) {
		crm_err("Year %.4d only has 366 days (supplied %.3d)",
			a_date->years, a_date->yeardays);
		a_date->yeardays = 366;
		
	} else if(!is_leap_year(a_date->years) && a_date->yeardays > 365) {
		crm_err("Year %.4d only has 365 days (supplied %.3d)",
			a_date->years, a_date->yeardays);
		a_date->yeardays = 365;
	}
	
	a_date->days = a_date->yeardays;
	a_date->months = 0;
	do {
		a_date->months++;
		m_end += days_per_month(a_date->months, a_date->years);
		a_date->days -= days_per_month(a_date->months-1, a_date->years);
	
		crm_debug_6("month %d: %d vs. %d - current day: %d",
			    a_date->months, a_date->yeardays,
			    m_end, a_date->days);
	} while (a_date->months < 12 && m_end < a_date->yeardays);

	CRM_CHECK(a_date->months > 0, return FALSE);
	CRM_CHECK(a_date->days <= days_per_month(a_date->months, a_date->years), 
			return FALSE);

	a_date->has->days = TRUE;
	a_date->has->months = TRUE;
	a_date->has->years = TRUE;

	crm_debug_4("Converted %.4d-%.3d to %.4d-%.2d-%.2d",
		    a_date->years, a_date->yeardays,
		    a_date->years, a_date->months, a_date->days);
	
	return TRUE;
}


gboolean
ordinal_to_weekdays(ha_time_t *a_date)
{
	int year_num = 0;
	int jan1 = january1(a_date->years);
	int h = -1;

	CRM_CHECK(a_date->has->years, return FALSE);
	CRM_CHECK(a_date->has->yeardays, return FALSE);
	CRM_CHECK(a_date->yeardays > 0, return FALSE);
	
	h = a_date->yeardays + jan1 - 1;
 	a_date->weekdays = 1 + ((h-1) % 7);
	a_date->has->weekdays = TRUE;

	if(a_date->yeardays <= (8-jan1) && jan1 > 4) {
		year_num = a_date->years - 1;
		a_date->weeks = weeks_in_year(year_num);
		a_date->has->weeks = TRUE;
		
	} else {
		year_num = a_date->years;
	}

	if(year_num == a_date->years) {
		int i = 365;
		if(is_leap_year(year_num)) {
			i = 366;
		}
		if( (i - a_date->yeardays) < (4 - a_date->weekdays) ) {
			year_num = a_date->years + 1;
			a_date->weeks = 1;
			a_date->has->weeks = TRUE;
		}
	}

	if(year_num == a_date->years) {
		int j = a_date->yeardays + (7-a_date->weekdays) + (jan1 - 1);
		a_date->weeks = j / 7;
		a_date->has->weeks = TRUE;
		if(jan1 > 4) {
			a_date->weeks -= 1;
		}
	}

	a_date->weekyears = year_num;
	a_date->has->weekyears = TRUE;
	crm_debug_4("Converted %.4d-%.3d to %.4dW%.2d-%d",
		    a_date->years, a_date->yeardays,
		    a_date->weekyears, a_date->weeks, a_date->weekdays);
	return TRUE;
}

gboolean
convert_from_weekdays(ha_time_t *a_date)
{
	gboolean conversion = FALSE;
	int jan1 = january1(a_date->weekyears);

	CRM_CHECK(a_date->has->weekyears, return FALSE);
	CRM_CHECK(a_date->has->weeks, return FALSE);
	CRM_CHECK(a_date->has->weekdays, return FALSE);

	CRM_CHECK(a_date->weeks > 0, return FALSE);
	CRM_CHECK(a_date->weekdays > 0, return FALSE);
	CRM_CHECK(a_date->weekdays < 8, return FALSE);
	
	a_date->has->years = TRUE;
	a_date->years = a_date->weekyears;

	a_date->has->yeardays = TRUE;
	a_date->yeardays = (7 * (a_date->weeks-1));

	/* break up the addition to make sure overflows are correctly handled */
	if(a_date->yeardays == 0) {
		a_date->yeardays = a_date->weekdays;
	} else {
		add_yeardays(a_date, a_date->weekdays);
	}
	
	crm_debug_5("Pre-conversion: %dW%d-%d to %.4d-%.3d",
		    a_date->weekyears, a_date->weeks, a_date->weekdays,
		    a_date->years, a_date->yeardays);

	conversion = ordinal_to_gregorian(a_date);

	if(conversion) {
		if(jan1 < 4) {
			sub_days(a_date, jan1-1);
		} else if(jan1 > 4) {
			add_days(a_date, jan1-4);
		}
	}
	return conversion; 
}

void
ha_set_time(ha_time_t *lhs, ha_time_t *rhs, gboolean offset)
{
	crm_debug_6("lhs=%p, rhs=%p, offset=%d", lhs, rhs, offset);

	CRM_CHECK(lhs != NULL && rhs != NULL, return);
	CRM_CHECK(lhs->has != NULL && rhs->has != NULL, return);
	
	lhs->years = rhs->years;
	lhs->has->years = rhs->has->years;

	lhs->weekyears = rhs->weekyears;
	lhs->has->weekyears = rhs->has->weekyears;
	
	lhs->months = rhs->months;
	lhs->has->months = rhs->has->months;

	lhs->weeks = rhs->weeks;
	lhs->has->weeks = rhs->has->weeks;

	lhs->days = rhs->days;
	lhs->has->days = rhs->has->days;

	lhs->weekdays = rhs->weekdays;
	lhs->has->weekdays = rhs->has->weekdays;
	
	lhs->yeardays = rhs->yeardays;
	lhs->has->yeardays = rhs->has->yeardays;

	lhs->hours = rhs->hours;
	lhs->has->hours = rhs->has->hours;

	lhs->minutes = rhs->minutes;
	lhs->has->minutes = rhs->has->minutes;

	lhs->seconds = rhs->seconds;
	lhs->has->seconds = rhs->has->seconds;

	if(lhs->offset) {
		reset_time(lhs->offset);
	}
	if(offset && rhs->offset) {
		ha_set_time(lhs->offset, rhs->offset, FALSE);
	}
	
}



void
ha_set_tm_time(ha_time_t *lhs, struct tm *rhs)
{
	int wday = rhs->tm_wday;
	int h_offset = 0;
	int m_offset = 0;
	
	if(rhs->tm_year > 0) {
		/* years since 1900 */
		lhs->years = 1900 + rhs->tm_year;
		lhs->has->years = TRUE;
	}

	if(rhs->tm_yday >= 0) {
		/* days since January 1 [0-365] */
		lhs->yeardays = 1 + rhs->tm_yday;
		lhs->has->yeardays =TRUE;
	}

	if(rhs->tm_hour >= 0) {
		lhs->hours = rhs->tm_hour;
		lhs->has->hours =TRUE;
	}
	if(rhs->tm_min >= 0) {
		lhs->minutes = rhs->tm_min;
		lhs->has->minutes =TRUE;
	}
	if(rhs->tm_sec >= 0) {
		lhs->seconds = rhs->tm_sec;
		lhs->has->seconds =TRUE;
	}

	convert_from_ordinal(lhs);

	/* months since January [0-11] */
	CRM_CHECK(rhs->tm_mon < 0  || lhs->months == (1 + rhs->tm_mon), return);

	/* day of the month [1-31] */
	CRM_CHECK(rhs->tm_mday < 0 || lhs->days == rhs->tm_mday, return);

	/* days since Sunday [0-6] */
	if(wday == 0) {
		wday= 7;
	}
	
	CRM_CHECK(rhs->tm_wday < 0 || lhs->weekdays == wday, return);
	CRM_CHECK(lhs->offset != NULL, return);
	CRM_CHECK(lhs->offset->has != NULL, return);

	/* tm_gmtoff == offset from UTC in seconds */
	h_offset = GMTOFF(rhs) / (3600); 
	m_offset = (GMTOFF(rhs) - (3600 * h_offset)) / (60);
	crm_debug_6("Offset (s): %ld, offset (hh:mm): %.2d:%.2d",
		    GMTOFF(rhs), h_offset, m_offset);
	
	lhs->offset->hours = h_offset;
	lhs->offset->has->hours = TRUE;

	lhs->offset->minutes = m_offset;
	lhs->offset->has->minutes = TRUE;

	normalize_time(lhs);
}


void
ha_set_timet_time(ha_time_t *lhs, time_t *rhs)
{
	ha_set_tm_time(lhs, localtime(rhs));
}

ha_time_t *
add_time(ha_time_t *lhs, ha_time_t *rhs)
{
	ha_time_t *answer = NULL;
	CRM_CHECK(lhs != NULL && rhs != NULL, return NULL);
	
	answer = new_ha_date(FALSE);
	ha_set_time(answer, lhs, TRUE);	

	normalize_time(lhs);
	normalize_time(answer);

	if(rhs->has->years) {
		add_years(answer, rhs->years);
	}
	if(rhs->has->months) {
		add_months(answer, rhs->months);
	}
	if(rhs->has->weeks) {
		add_weeks(answer, rhs->weeks);
	}
	if(rhs->has->days) {
		add_days(answer, rhs->days);
	}

	add_hours(answer, rhs->hours);
	add_minutes(answer, rhs->minutes);
	add_seconds(answer, rhs->seconds);

	normalize_time(answer);
	
	return answer;
}

ha_time_t *
subtract_time(ha_time_t *lhs, ha_time_t *rhs)
{
	ha_time_t *answer = NULL;
	CRM_CHECK(lhs != NULL && rhs != NULL, return NULL);

	answer = new_ha_date(FALSE);
	ha_set_time(answer, lhs, TRUE);	

	normalize_time(lhs);
	normalize_time(rhs);
	normalize_time(answer);

	sub_seconds(answer, rhs->seconds);
	sub_minutes(answer, rhs->minutes);
	sub_hours(answer, rhs->hours);

	answer->yeardays -= rhs->yeardays;
	while(answer->yeardays < 0) {
	    answer->yeardays += is_leap_year(answer->years)?356:355;
	    answer->years--;
	}
	
	answer->days -= rhs->days;
	while(answer->days < 0) {
	    answer->days += days_per_month(answer->months, answer->years);
	    answer->months--;
	}
	
	answer->months -= rhs->months;
	while(answer->months < 0) {
	    answer->months += 12;
	    /* answer->years--; : done in the yeardays section */
	}
	
	answer->years -= rhs->years;

	return answer;
}

ha_time_t *
subtract_duration(ha_time_t *lhs, ha_time_t *rhs)
{
	ha_time_t *answer = NULL;
	CRM_CHECK(lhs != NULL && rhs != NULL, return NULL);

	answer = new_ha_date(FALSE);
	ha_set_time(answer, lhs, TRUE);	

	normalize_time(lhs);
	normalize_time(rhs);
	normalize_time(answer);

	sub_seconds(answer, rhs->seconds);
	sub_minutes(answer, rhs->minutes);
	sub_hours(answer, rhs->hours);

	sub_days(answer, rhs->days);
	sub_weeks(answer, rhs->weeks);
	sub_months(answer, rhs->months);
	sub_years(answer, rhs->years);
	
	normalize_time(answer);
	
	return answer;
}

/* ha_time_interval_t* */
/* parse_time_interval(char **interval_str) */
/* { */
/* 	return NULL; */
/* } */

int month_days[14] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 29 };
int
days_per_month(int month, int year)
{
	if(month == 2 && is_leap_year(year)) {
		month = 13;
	}
	return month_days[month];
}

gboolean
is_leap_year(int year) 
{
	gboolean is_leap = FALSE;
	if(year % 4 == 0) {
		is_leap = TRUE;
	}
	if(year % 100 == 0 && year % 400 != 0 ) {
		is_leap = FALSE;
	}
	return is_leap;
}


gboolean
parse_int(char **str, int field_width, int uppper_bound, int *result)
{
	int lpc = 0;
	int intermediate = 0;
	gboolean fraction = FALSE;
	gboolean negate = FALSE;

	CRM_CHECK(str != NULL, return FALSE);
	CRM_CHECK(*str != NULL, return FALSE);
	CRM_CHECK(result != NULL, return FALSE);

	*result = 0;

	if(strlen(*str) <= 0) {
		return FALSE;
	}
	
	if((*str)[0] == 'T') {
	    (*str)++;
	}

	if((*str)[0] == '.' || (*str)[0] == ',') {
		fraction = TRUE;
		field_width = -1;
		(*str)++;
	} else if((*str)[0] == '-') {
		negate = TRUE;
		(*str)++;
	} else if((*str)[0] == '+'
		|| (*str)[0] == ':') {
		(*str)++;
	}

	for(; (fraction || lpc < field_width) && isdigit((int) (*str)[0]); lpc++) {
		if(fraction) {
			intermediate = ((*str)[0] - '0')/(10^lpc);
		} else {
			*result *= 10;
			intermediate = (*str)[0] - '0';
		}
		*result += intermediate;
		(*str)++;
	}
	if(fraction) {
		*result = (int)(*result * uppper_bound);
		
	} else if(uppper_bound > 0 && *result > uppper_bound) {
		*result = uppper_bound;
	}
	if(negate) {
		*result = 0 - *result;
	}
	if(lpc > 0) {
		crm_debug_5("Found int: %d.  Stopped at str[%d]='%c'", *result, lpc, (*str)[lpc]);
		return TRUE;
	}
	return FALSE;
}

gboolean
check_for_ordinal(const char *str)
{
	if(isdigit((int) str[2]) == FALSE) {
		crm_debug_6("char 3 == %c", str[2]);
		return FALSE;		
	}
	if(isspace((int) str[3])) {
		return TRUE;
	} else if(str[3] == 0) {
		return TRUE;
	} else if(str[3] == 'T') {
		return TRUE;
	} else if(str[3] == '/') {
		return TRUE;
	}
	crm_debug_6("char 4 == %c", str[3]);
	return FALSE;
}

int str_lookup(const char *str, enum date_fields field)
{
	return 0;
}

void
reset_time(ha_time_t *a_time)
{
	a_time->years = 0;
	a_time->has->years = FALSE;
		
	a_time->weekyears = 0;
	a_time->has->weekyears = FALSE;
		
	a_time->months = 0;
	a_time->has->months = FALSE;
		
	a_time->weeks = 0;
	a_time->has->weeks = FALSE;
		
	a_time->days = 0;
	a_time->has->days = FALSE;
		
	a_time->weekdays = 0;
	a_time->has->weekdays = FALSE;
		
	a_time->yeardays = 0;
	a_time->has->yeardays = FALSE;
		
	a_time->hours = 0;
	a_time->has->hours = FALSE;
		
	a_time->minutes = 0;
	a_time->has->minutes = FALSE;
		
	a_time->seconds = 0;
	a_time->has->seconds = FALSE;
}


void
reset_tm(struct tm *some_tm) 
{
	some_tm->tm_sec = -1;	 /* seconds after the minute [0-60] */
	some_tm->tm_min = -1;	 /* minutes after the hour [0-59] */
	some_tm->tm_hour = -1;	 /* hours since midnight [0-23] */
	some_tm->tm_mday = -1;	 /* day of the month [1-31] */
	some_tm->tm_mon = -1;	 /* months since January [0-11] */
	some_tm->tm_year = -1;	 /* years since 1900 */
	some_tm->tm_wday = -1;	 /* days since Sunday [0-6] */
	some_tm->tm_yday = -1;	 /* days since January 1 [0-365] */
	some_tm->tm_isdst = -1;	 /* Daylight Savings Time flag */
#if defined(HAVE_STRUCT_TM_TM_GMTOFF)
	some_tm->tm_gmtoff = -1; /* offset from CUT in seconds */
#endif
#if defined(HAVE_TM_ZONE)
	some_tm->tm_zone = NULL;/* timezone abbreviation */
#endif
}

gboolean
is_date_sane(ha_time_t *a_date)
{
	int ydays = 0;
	int mdays = 0;
	int weeks = 0;

	CRM_CHECK(a_date != NULL, return FALSE);
	
	ydays = is_leap_year(a_date->years)?366:365;
	mdays = days_per_month(a_date->months, a_date->years);
	weeks = weeks_in_year(a_date->weekyears);
	crm_debug_5("max ydays: %d, max mdays: %d, max weeks: %d",
		    ydays, mdays, weeks);
	
	CRM_CHECK(a_date->has->years, return FALSE);
	CRM_CHECK(a_date->has->weekyears, return FALSE);

	CRM_CHECK(a_date->has->months, return FALSE);
	CRM_CHECK(a_date->months > 0, return FALSE);
	CRM_CHECK(a_date->months <= 12, return FALSE);

	CRM_CHECK(a_date->has->weeks, return FALSE);
	CRM_CHECK(a_date->weeks > 0, return FALSE);
	CRM_CHECK(a_date->weeks <= weeks, return FALSE);

	CRM_CHECK(a_date->has->days, return FALSE);
	CRM_CHECK(a_date->days > 0, return FALSE);
	CRM_CHECK(a_date->days <= mdays, return FALSE);

	CRM_CHECK(a_date->has->weekdays, return FALSE);
	CRM_CHECK(a_date->weekdays > 0, return FALSE);
	CRM_CHECK(a_date->weekdays <= 7, return FALSE);

	CRM_CHECK(a_date->has->yeardays, return FALSE);
	CRM_CHECK(a_date->yeardays > 0, return FALSE);
	CRM_CHECK(a_date->yeardays <= ydays, return FALSE);

	CRM_CHECK(a_date->hours >= 0, return FALSE);
	CRM_CHECK(a_date->hours < 24, return FALSE);

	CRM_CHECK(a_date->minutes >= 0, return FALSE);
	CRM_CHECK(a_date->minutes < 60, return FALSE);
	
	CRM_CHECK(a_date->seconds >= 0, return FALSE);
	CRM_CHECK(a_date->seconds <= 60, return FALSE);
	
	return TRUE;
}

#define do_cmp_field(lhs, rhs, field)					\
	{								\
		if(lhs->field > rhs->field) {				\
			crm_debug_2("%s: %d > %d",			\
				    #field, lhs->field, rhs->field);	\
			return 1;					\
		} else if(lhs->field < rhs->field) {			\
			crm_debug_2("%s: %d < %d",			\
				    #field, lhs->field, rhs->field);	\
			return -1;					\
		}							\
	}

int
compare_date(ha_time_t *lhs, ha_time_t *rhs)
{
	if(lhs == NULL && rhs == NULL) {
		return 0;
	} else if(lhs == NULL) {
		return -1;
	} else if(rhs == NULL) {
		return 1;		
	}
	
	normalize_time(lhs);
	normalize_time(rhs);

	do_cmp_field(lhs->normalized, rhs->normalized, years);
	do_cmp_field(lhs->normalized, rhs->normalized, yeardays);
	do_cmp_field(lhs->normalized, rhs->normalized, hours);
	do_cmp_field(lhs->normalized, rhs->normalized, minutes);
	do_cmp_field(lhs->normalized, rhs->normalized, seconds);

	return 0;
}

ha_time_t *
new_ha_date(gboolean set_to_now) 
{
	time_t tm_now;
	ha_time_t *now = NULL;

	tzset();
	crm_malloc0(now, sizeof(ha_time_t));
	crm_malloc0(now->has, sizeof(ha_has_time_t));
	crm_malloc0(now->offset, sizeof(ha_time_t));
	crm_malloc0(now->offset->has, sizeof(ha_has_time_t));
	if(set_to_now) {
	    tm_now = time(NULL);
	    now->tm_now = tm_now;
	    ha_set_timet_time(now, &tm_now);
	}
	return now;
}

void
free_ha_date(ha_time_t *a_date) 
{
	if(a_date == NULL) {
		return;
	}
	free_ha_date(a_date->normalized);
	free_ha_date(a_date->offset);
	crm_free(a_date->has);
	crm_free(a_date);
}

void
log_tm_date(int log_level, struct tm *some_tm) 
{
	const char *tzn;

#if defined(HAVE_TM_ZONE)
	tzn = some_tm->tm_zone;
#elif defined(HAVE_TZNAME)
	tzn = tzname[0];
#else
	tzn = NULL;
#endif

	do_crm_log(log_level,
		      "%.2d/%.2d/%.4d %.2d:%.2d:%.2d %s"
		      " (wday=%d, yday=%d, dst=%d, offset=%ld)",
		      some_tm->tm_mday,
		      some_tm->tm_mon,
		      1900+some_tm->tm_year,
		      some_tm->tm_hour,
		      some_tm->tm_min,
		      some_tm->tm_sec,
		      tzn,
		      some_tm->tm_wday==0?7:some_tm->tm_wday,
		      1+some_tm->tm_yday,
		      some_tm->tm_isdst,
		      GMTOFF(some_tm));
}

ha_time_t *the_epoch = NULL;

#define update_seconds(date, field, multiplier) do {		\
	before = in_seconds;					\
	in_seconds += a_date->field;				\
	in_seconds *= multiplier;				\
	if(before > in_seconds) {				\
	    crm_crit("Date wrap detected: %s", #field);		\
	    return 0;						\
	}							\
    } while(0)
    
unsigned long long date_in_seconds(ha_time_t *a_date) 
{
    unsigned long long before = 0;
    unsigned long long in_seconds = 0;
    /* normalize_time(a_date); */    
    update_seconds(a_date, years,    365);
    update_seconds(a_date, yeardays, 24);
    update_seconds(a_date, hours,    60);
    update_seconds(a_date, minutes,  60);
    update_seconds(a_date, seconds,  1);
    return in_seconds;
}

unsigned long long date_in_seconds_since_epoch(ha_time_t *a_date) 
{
    ha_time_t *since_epoch = NULL;
    unsigned long long in_seconds = 0;
    normalize_time(a_date);

    if(the_epoch == NULL) {
	char *EPOCH = crm_strdup("1970-01-01");
	the_epoch = parse_date(&EPOCH);
	normalize_time(the_epoch);
	crm_free(EPOCH);
    }

    since_epoch = subtract_time(a_date, the_epoch);
    in_seconds = date_in_seconds(since_epoch);
    free_ha_date(since_epoch);
    return in_seconds;
}

