/**
 * Date calculater and calendar ultility for ical calendar
 *
 * @author: Quoc Duong (duongdq@gmail.com)
 * @company: Vietintelligences Jsc. (http://vietintelligences.com)
 * @created: 2008-10-06
 * @last-modified: 2008-10-11
 * @last-modified: 2008-10-23
 */

// Cal class
var Cal = Cal || {};

Cal.MC_IN_DAY = 24 * 60 * 60 * 1000; // num microtime in a day
Cal.MC_IN_WEEK = 7 * Cal.MC_IN_DAY; // num microtime in a week

//
Cal.isSeries = true; // num microtime in a week

/**
 * get date from date string
 * ex: "2008-10-20", "2008-10-20 15:26:58", "March 11, 1985 09:25:00"
 *
 * @params	string date string input
 * @return		Date date object
 */
Cal.strToDate = function (str)
{
	str = new String(str);
	var match = str.match(/^(19\d\d|20\d\d)[\-\.]{1}(0[1-9]|1[012]|[1-9])[\-\.]{1}(0[1-9]|[12][0-9]|3[01]|[1-9])[\s]{1}([01][0-9]|2[0-3]|[0-9])[:]{1}([012345][0-9]|[0-9])[:]{1}([012345][0-9]|[0-9])/);
	if (match !== null) {
		return new Date(match[1], match[2]-1, match[3], match[4], match[5], match[6]);
	}

	match = str.match(/^(19\d\d|20\d\d)[\-]{1}(0[1-9]|1[012]|[1-9])[\-]{1}(0[1-9]|[12][0-9]|3[01]|[1-9])/);
	if (match !== null) {
		return new Date(match[1], match[2]-1, match[3], 0, 0, 0);
	}
	
	match = str.match(/^(19\d\d|20\d\d)(0[1-9]|1[012]|[1-9])(0[1-9]|[12][0-9]|3[01]|[1-9])/);
	if (match !== null) {
		return new Date(match[1], match[2]-1, match[3], 0, 0, 0);
	}
	
	try {
		var dt = new Date(str);
	} catch (e) {
		dt = new Date();
	}

	return dt;
}

/**
 * get date from timestamp
 * ex: -25200000, output 1970-01-01 00:00:00
 *
 * @params	int timestamp input
 * @return		Date date object
 */
Cal.timestampToDate = function (timestamp)
{
	var dt = new Date();
	dt.setTime(timestamp);
	return dt;
}

/**
 * get day of week by day string
 *
 * @params	string week day
 * @params	string week start (fixed by "SU": this version doesn't for week start parameter)
 * @return		int (0-6)
 */
Cal.weekDayToNum = function (wkday, wkst)
{
	wkday = wkday.toUpperCase();
	wkday = wkday.substr(0, 2);

	switch (wkday) {
		case 'SU':		return 0;
		case 'MO':	return 1;
		case 'TU':		return 2;
		case 'WE':	return 3;
		case 'TH':		return 4;
		case 'FR':		return 5;
		case 'SA':		return 6;
	}
	return null;
}

/**
 * get date by the format input, same as PHP date function
 *
 * @params	string	date format output
 * @params	mixed	date input (default is current time now)
 * @return		mixed	date output that is formated
 */
Cal.date = function(arg, dt)
{
	if (typeof(dt) == 'undefined') {
		// default is current time now
		dt = new Date();
	} else if (typeof(dt) == 'string') {
		// set by date string, ex "March 11, 1985 09:25:00"
		dt = new Date(dt);
	} else if (typeof(dt) == 'number') {
		if (arguments.length == 2) {
			// set by timestamp
			var xdt = Cal.timestampToDate(dt);
		} else {			
			// set by detail of time "year, month, day, hours, minutes, seconds, milliseconds"
			// ex alert(Cal.date("Y-m-d H:i:s", 1985, 10, 20, 11, 30, 29)) // output "1985-10-20 11:30:29"
			var xdt = new Date();
			for (var i = 1; i < arguments.length; i++) {
				switch(i) {
					case 1:
						if (arguments.substr(i, 1) >= 100) {
							xdt.setFullYear(arguments.substr(i, 1) );
						} else {
							xdt.setYear(arguments.substr(i, 1) )
						};
						break;
					case 2:	xdt.setMonth(arguments.substr(i, 1) -1);break;
					case 3:	xdt.setDate(arguments.substr(i, 1) );break;
					case 4:	xdt.setHours(arguments.substr(i, 1) );break;
					case 5:	xdt.setMinutes(arguments.substr(i, 1) );break;
					case 6:	xdt.setSeconds(arguments.substr(i, 1) );break;
					case 7:	xdt.setMilliseconds(arguments.substr(i, 1) );break;
				}
			}
		}
		dt = xdt;
	}

	// single character
	if (1 == (new String(arg)).length) {	
		switch(arg) {
			case 'Y':	return dt.getFullYear();
			case 'y':	return dt.getYear();
			case 'm':	return ((dt.getMonth()+1) < 10) ? ("0" + (dt.getMonth()+1)) : (dt.getMonth()+1);
			case 'd':	return (dt.getDate() < 10 ) ? ("0" + dt.getDate()) : dt.getDate();
			case 'H':	return (dt.getHours() < 10 ) ? ("0" + dt.getHours()) : dt.getHours();
			case 'h':	return (dt.getHours() > 12) ? dt.getHours() - 12 : dt.getHours();
			case 'a':	return (dt.getHours() >= 12) ? "pm" : "am";
			case 'A':	return (dt.getHours() >= 12) ? "PM" : "AM";
			case 'i':	return (dt.getMinutes() < 10 ) ? ("0" + dt.getMinutes()) : dt.getMinutes();
			case 's':	return (dt.getSeconds() < 10 ) ? ("0" + dt.getSeconds()) : dt.getSeconds();
			case 'U':	return dt.getTime();
			case 'B':	return dt.getMilliseconds();
			default:	return arg;
		}
	}

	// process for special character
	// ex Cal.date("\d\ate Y-m-d H");// output "date 2008-10-20"
	var strs = new Array();

	for (var i = 0; i < arg.length; i++) {
		var i_arg = arg.substr(i, 1);
		if (i_arg == "%" && i < arg.length-1) {
			strs.push(arg.substr(i++, 1) + i_arg);
		} else {
			strs.push(Cal.date(i_arg, dt));
		}
	}
	return strs.join("");
}

/**
 * Copy date from Date instance
 *
 * @params	Date target date object
 * @return		Date destination date object
 */
Cal.copyDate = function (dt)
{
	var copy = new Date();
	copy.setTime(dt.getTime());
	
	return copy;
}

/**
 * Change Date (day, month, year)
 *
 * @params	Date date target object
 * @params	int	new day
 * @params	int	new month
 * @params	int	new year
 * @return		Date destination date object
 */
Cal.changeDate = function (dt, day, month, year)
{
	var copy = Cal.copyDate(dt);

	copy.setDate(1);
	
	copy.setFullYear(year);
	copy.setMonth(month-1);
	copy.setDate(day);
	
	return copy;
}

/**
 * Check for the leap year
 *
 * @params	int year input
 * @return		boolean
 */
Cal.isLeapYear = function (year)
{
	if (typeof(year) == 'undefined') {
		year = Cal.date("Y");
	}
	
	if (year < 1000) {
		return false;
	}
	
	if (year < 1582) {
		// pre Gregorio XIII - 1582
		return (year % 4 == 0);
	} else {
		// post Gregorio XIII - 1582
		return ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0);
	}
}

/**
 * Get next leap year
 *
 * @params	int year input
 * @return		int next leap year
 */
Cal.nextLeapYear = function (year)
{
	if(Cal.isLeapYear(year)) {
		year++;
	}	
	while (!Cal.isLeapYear(++year));
	return year;
}

/**
 * Calculate the sum of day in month
 *
 * @params	int month input
 * @params	int year input
 * @return		int	days in month
 */
Cal.daysInMonth = function (month, year)
{
	if (typeof(month) == 'undefined') {
		month = Cal.date("m");
	}
	if (typeof(year) == 'undefined') {
		year = Cal.date("Y");
	}
	
	if (year == 1582 && month == 10) {
		return 21;  // October 1582 only had 1st-4th and 15th-31st
	}
	if (month == 2) {
		// leap
		if (Cal.isLeapYear(year)) {
			return 29;
		} else {
			return 28;
		}
	} else if (month == 4 || month == 6 || month == 9 || month == 11) {
		return 30;
	} else {
		return 31;
	}
}

/**
 * Calculate the day of week
 *
 * @params	int day input
 * @params	int month input
 * @params	int year input
 * @return		int	0-6 (SUN, MON, TUE, WED, THU, FRI, SAT)
 */
Cal.dayOfWeek = function(day, month, year)
{
	if (typeof(day) == 'undefined') {
		day = Cal.date("d");
	}
	if (typeof(month) == 'undefined') {
		month = Cal.date("m");
	}
	if (typeof(year) == 'undefined') {
		year = Cal.date("Y");
	}
	
	if (month > 2) {
		month -= 2;
	} else {
		month += 10;
		year--;
	}

	// Calculater
	day = (Math.floor((13 * month - 1) / 5) +
				day + (year % 100) +
				Math.floor((year % 100) / 4) +
				Math.floor((year / 100) / 4) - 2 *
				Math.floor(year / 100) + 77);

        var dayOfWeek = day - 7 * Math.floor(day / 7);
        
        return dayOfWeek;
}

/**
 * get previous month
 *
 * @params	int month input
 * @params	int year input
 * @return		int	previous month
 */
Cal.prevMonth = function(month, year) {
	
	if (typeof(month) == 'undefined') {
		month = Cal.date("m");
	}
	if (typeof(year) == 'undefined') {
		year = Cal.date("Y");
	}
	
	return (month == 1) ? 12: month-1;
}

/**
 * get next month
 *
 * @params	int month input
 * @params	int year input
 * @return		int	next month
 */
Cal.nextMonth = function(month, year) {

	if (typeof(month) == 'undefined') {
		month = Cal.date("m");
	}
	if (typeof(year) == 'undefined') {
		year = Cal.date("Y");
	}
	
	return (month == 12) ? 1 : month+1;
}

/**
 * get previous year
 *
 * @params	int month input
 * @params	int year input
 * @return		int	previous year
 */
Cal.prevYear = function(month, year) {

	if (typeof(month) == 'undefined') {
		month = Cal.date("m");
	}
	if (typeof(year) == 'undefined') {
		year = Cal.date("Y");
	}
	
	return (month == 1) ? year - 1 : year;
}

/**
 * get next year
 *
 * @params	int month input
 * @params	int year input
 * @return		int	next year
 */
Cal.nextYear = function(month, year) {

	if (typeof(month) == 'undefined') {
		month = Cal.date("m");
	}
	if (typeof(year) == 'undefined') {
		year = Cal.date("Y");
	}
	
	return (month == 12) ? year +1 : year;
}

/**
 * get month calendar
 *
 * @params	int month input
 * @params	int year input
 * @return		array (week => array (day in week))
 */
Cal.getMonthCal = function(month, year) 
{
	if (typeof(year) == 'undefined') {
		year = Cal.date("Y");
	}
	if (typeof(month) == 'undefined') {
		month = Cal.date("m");
	}
	
	var prev_month = Cal.prevMonth(month, year);
	var next_month = Cal.nextMonth(month, year);
	
	var prev_year = Cal.prevYear(month, year);
	var next_year = Cal.nextYear(month, year);
	
	var first_day = Cal.dayOfWeek(1, month, year); // first day of first week (first day of month)
	var days = Cal.daysInMonth(month, year); // sum of days in month
	var prev_days = Cal.daysInMonth(prev_month, prev_year); // sum of days in previous month
	
	// month calendar array (week => array(day of week))
	var month_days = new Array();	
	var day = 1;
	do {
		var week_days = new Array();	
		for(var i = 0; i < 7; i++){
			if(i >= first_day && day <= days){
				// days of current month
				week_days.push(day++);
			}else{
				if(i < first_day){
					// days of previous month
					week_days.push(prev_days - first_day + i +1);
				} else if(day > days){
					// days of next month
					week_days.push(first_day++ +1);
				}
			}
		}
		// from second week, reset first day to 0
		first_day = 0;
		month_days.push(week_days);
	} while (day <= days);
	
	return month_days;
}

/**
 * get components form text content
 *
 * @params	string	ics content input
 * @params	array	options for ICalendar file (ics) format, default is empty
 * @return		array	the list components form text content input
 */
Cal.getComponentsFromText = function (icsText, options)
{
	var ical = new ICalendar(); // Construction of the reader object. 
	ical.prepareData(icsText); // Prepare and set the data for the parser.
	ical.parse(); // Parse the data.
	//ical.sort(); // Sort the data.
	
	var events = ical.getCalendar().getEvents();

	return events;
}

/**
 * get date (1-31) from the rule BYDAY that is defined in the Monthly rule and Yearly rule
 *
 * @params	string	ics content input
 * @params	array	options for ICalendar file (ics) format, default is empty
 * @return		array	the list components form text content input
 */
Cal.getDateFromByDay = function(byDay, weekNo, first_day, days_in_month)
{
	var day = 0;
	if (weekNo > 0) { // set by week no
		if (byDay >= first_day) {
			day =  (byDay - first_day) + 1 + (weekNo-1) * 7;
		} else {
			day = (7 - (first_day-byDay)) + 1 + (weekNo-1) * 7;
		}
	} else if (weekNo == -1) { // set by last week					
		if (byDay >= first_day) {
			if ((byDay - first_day) +1 + 28 <= days_in_month) {
				day = (byDay - first_day) + 1 + 28;
			} else {
				day = (byDay - first_day) + 1 + 21;
			}
		} else {
			if (((byDay+7) - first_day) + 1 + 28 <= days_in_month) {
				day = ((byDay+7) - first_day) + 1 + 28;
			} else {
				day = ((byDay+7) - first_day) + 1 + 21;
			}
		}
	}

	return day;
}

/**
 * get minimum of start date
 *
 * @params	array	the list of components
 * @return		null|Date	null if components is empty
 */
Cal.getMinOfStartDate = function (components)
{
	var minStart = null;
	components.each(function(component){
		var rules = component.getRuleProperties();
		var dt = component.getProperty("DTSTART");
		var dtStart = Cal.strToDate(dt.get("DTSTART"));
		
		if (minStart == null || dtStart.getTime() < minStart.getTime()) {
			minStart = dtStart;
		}
	});
	
	return minStart;
}

/**
 * get components form components list by month input
 *
 * @params	array	components target
 * @params	int		month input
 * @params	int		year input
 * @params	bool	select only events of month or all
 *						false - only events of month
 *						true (default) - all events
 * @params	bool	(default is false)select components for series (when long time event - more than one day)
 * @return		array	the list components that open in the month input
 */
Cal.selectComponentsByMonth = function(components, month, year, bl_event, is_series)
{
	// set default month
	if (typeof(month) == 'undefined') {
		month = Cal.date("m");
	}
	
	// set default year
	if (typeof(year) == 'undefined') {
		year = Cal.date("Y");
	}
	
	// set default bl event
	if (typeof(bl_event) == 'undefined') {
		bl_event = true;
	}
	
	// set default series
	if (typeof(is_series) == 'undefined') {
		is_series = false;
	}
	
	var day_in_month = Cal.daysInMonth(month, year);
	var start = new Date(year, month-1, 1);
	var end = new Date(year, month-1, day_in_month);
	
	if (bl_event) {
		var first_day =  Cal.dayOfWeek(1, month, year);
		var last_day =  Cal.dayOfWeek(day_in_month, month, year);
		
		start = Cal.timestampToDate(start.getTime() - first_day * Cal.MC_IN_DAY);
		end =  Cal.timestampToDate(end.getTime() + (6 - last_day) * Cal.MC_IN_DAY);
	}

	return Cal.selectComponents(components, start, end, is_series);
}

/**
 * get components form components list
 * by the range of date input
 *
 * @params	array	components target
 * @params	mixed	start date that components output start
 * @params	mixed	end date that components output end
 * @params	bool	(default is false)select components for series (when long time event - more than one day)
 * @return		array	the list components that open between start date and end date
 */
Cal.selectComponents = function(components, start, end, is_series)
{
	// set default start date
	if (typeof(start) == 'undefined') {
		var start_date = Cal.strToDate(Cal.date("Y-m-d 00:00:00"));
	} else if (typeof(start) == 'string') {
		var start_date = Cal.strToDate(start);
	} else {
		var start_date = Cal.strToDate(Cal.date("Y-m-d 00:00:00", start));
	}
	
	// set default end date
	if (typeof(end) == 'undefined') {
		var end_date = Cal.strToDate(Cal.date("Y-m-d 23:59:59"));
	} else if (typeof(end) == 'string') {
		var end_date = Cal.strToDate(end);
	} else {		
		var end_date = Cal.strToDate(Cal.date("Y-m-d 23:59:59", end));
	}
	
	// set default series
	if (typeof(is_series) == 'undefined') {
		is_series = false;
	}

	var result = new Array();

	components.each(function(component){
		var rules = component.getRuleProperties();
		var start = component.getProperty("DTSTART");
		//var dtStart = Cal.strToDate(start.get("DTSTART"));
		var dtStart = start.get("JSDATE");
		
		var end = component.getProperty("DTEND");
		//var dtEnd = Cal.strToDate(end.get("DTEND"));
		var dtEnd = end.get("JSDATE");

		var dtime = dtEnd.getTime() - dtStart.getTime() - 1;
		if (is_series && dtime > Cal.MC_IN_DAY) {
			var real_start_date = Cal.timestampToDate(start_date.getTime() - Math.floor(dtime/Cal.MC_IN_DAY)*Cal.MC_IN_DAY);
		} else {
			var real_start_date = Cal.copyDate(start_date);
		}

		var recurComponents = Cal.rruleTodate(rules.properties, dtStart, real_start_date, end_date);

		for (var i = 0; i < recurComponents.length; i++) {
			//alert(Cal.date("Y-m-d H:i:s",dtStart)+"\n"+Cal.date("Y-m-d H:i:s",dtEnd)+"\n"+dtime+"\n"+Cal.MC_IN_DAY+"\n"+Math.floor(dtime/Cal.MC_IN_DAY)); // test
			if (is_series && dtime > Cal.MC_IN_DAY) {				
				var days = Math.floor(dtime/Cal.MC_IN_DAY);
				for (var day = 0; day <= days; day++) {
					if (recurComponents[i]+day*Cal.MC_IN_DAY >= start_date.getTime()) {
						var dateKey = Cal.date("Ymd", recurComponents[i]+day*Cal.MC_IN_DAY);
						if (typeof(result[dateKey]) == 'undefined') {
							result[dateKey] = [];
						}
						
						// set is belong to series (for event long time - more than one day)
						component.isInSeries = true;
						
						// check for the first of event series
						if (day == 0) {
							component.isFirstSeries = true;
						} else {
							component.isFirstSeries = false;
						}

						// check for the last of event series
						if (day == days) {
							component.isLastSeries = true;
						} else {
							component.isLastSeries = false;
						}
						result[dateKey].push(component);
					}
				}
			} else {
				var dateKey = Cal.date("Ymd", recurComponents[i]);
				if (typeof(result[dateKey]) == 'undefined') {
					result[dateKey] = [];
				}
				
				// set is belong to series (for event sort time - less than one day)
				component.isInSeries = false;
				result[dateKey].push(component);
			}
		}
	});

	return result;
}

/**
 *  get components by recurent rule
 *
 * @params	Array rrule
 * @params	Date	event start date (when date)
 * @params	Date	start date
 * @params	Date	end date
 * @return		Array	list of component between start date and end date, filter by rrule rule
 */
Cal.rruleTodate = function (rrule, wdate, sdate, edate) {
	if (typeof(rrule) == 'object') {
		// check date given is not avaiable
		if (isNaN(wdate.getTime()) || isNaN(sdate.getTime()) || isNaN(edate.getTime())) {
			return [];
		}

		// fix recurence rule
		if (typeof(rrule['FREQ']) == 'undefined') {
			rrule['FREQ'] = null;
		}
	
		if (typeof(rrule['INTERVAL']) == 'undefined') {
			rrule['INTERVAL'] = 1;
		} else {
			rrule['INTERVAL'] = (parseInt(rrule['INTERVAL']) > 0) ? parseInt(rrule['INTERVAL']) : 1;
		}
		
		if (typeof(rrule['WKST']) == 'undefined') {
			rrule['WKST'] = 'SU';
		} else {
			rrule['WKST'] = (rrule['WKST'] == 'SU') ? 'SU' : 'MO';
		}
		
		if (typeof(rrule['UNTIL']) == 'undefined') {
			rrule['UNTIL'] = null;
		} else if (typeof(rrule['UNTIL']) == 'string') {
			rrule['UNTIL'] = Cal.strToDate(rrule['UNTIL']);
		}

		switch (rrule['FREQ']) {
			case 'DAILY':		return Cal.freqDailyToDate(rrule, wdate, sdate, edate);
			case 'WEEKLY':	return Cal.freqWeeklyToDate(rrule, wdate, sdate, edate);
			case 'MONTHLY':return Cal.freqMonthlyToDate(rrule, wdate, sdate, edate);
			case 'YEARLY':	return Cal.freqYearlyToDate(rrule, wdate, sdate, edate);
			default: return Cal.noFreqToDate(wdate, sdate, edate);
		}
	}
	return [];
}

/**
 *  get components by daily rule
 *
 * @params	Array rrule
 * @params	Date	event start date (when date)
 * @params	Date	start date
 * @params	Date	end date
 * @return		Array	list of component between start date and end date, filter by rrule rule
 */
Cal.freqDailyToDate = function (rrule, wdate, sdate, edate)
{
	var result = new Array();
	var mc = wdate.getTime();
	var mcSdate = sdate.getTime();
	var mcEdate = edate.getTime();
	var step = rrule['INTERVAL'] * Cal.MC_IN_DAY;
	var until = rrule['UNTIL'];
	do {
		if (mc >= mcSdate) {
			result.push(mc);
		}
		// loop step
		mc += step;
		
		// check past until
		if (until && mc > until.getTime()) {
			break;
		}
	} while(mc <= mcEdate);

	return result;
}

/**
 *  get components by weekly rule
 *
 * @params	Array rrule
 * @params	Date	event start date (when date)
 * @params	Date	start date
 * @params	Date	end date
 * @return		Array	list of component between start date and end date, filter by rrule rule
 */
Cal.freqWeeklyToDate = function (rrule, wdate, sdate, edate)
{
	var mcWdate = wdate.getTime();
	var mcEdate = edate.getTime();
	var mcSdate = sdate.getTime();
	var result = new Array();
	var until = rrule['UNTIL'];
	var byDay = (typeof(rrule['BYDAY']) != 'undefined') ? rrule['BYDAY'] : null;
	var step = rrule['INTERVAL'] * Cal.MC_IN_WEEK;
	
	if (byDay == null || byDay == "") {
		byDay = [];
		byDay[0] = Cal.dayOfWeek(wdate.getDate(), wdate.getMonth()+1, wdate.getFullYear());
	} else {
		byDay = byDay.split(",");
		for (var i = 0; i < byDay.length; i++) {
			byDay[i] = Cal.weekDayToNum(byDay[i]);
		}
	}

	var wkday = Cal.dayOfWeek(wdate.getDate(), wdate.getMonth()+1, wdate.getFullYear());
	var mcWKST = wdate.getTime() - (wkday * Cal.MC_IN_DAY);
	
	// looking for first result
	var first_result = null;
	if (mcWdate >= mcSdate && mcWdate <= mcEdate) {
		if (until == null || (until != null && mcWdate <= until.getTime())) {
			first_result = mcWdate;
		}
	}
	
	//
	if (first_result != null) {
		result.push(first_result);
	}
	
	if (byDay.length == 0) {
		return [];
	}

	while (true) {
		// start calculator from start date
		if (mcWKST + (Cal.MC_IN_WEEK) >= mcSdate) {
			for (var i = 0; i < byDay.length; i++) {
				var mcDay = mcWKST + byDay[i] * Cal.MC_IN_DAY;
				if (mcDay >= mcWdate && mcDay >= mcSdate && mcDay <= mcEdate) {
					if (until == null || (until != null && mcDay <= until.getTime())) {
						if (first_result == null || (first_result != null && first_result != mcDay)) {
							result.push(mcDay);
						}
					}
				}
			}
		}
		
		// loop step
		mcWKST += step;
		
		// check loop end
		if (mcWKST> mcEdate) {
			break;
		}
	}

	return result;
}

/**
 *  get components by monthly rule
 *
 * @params	Array rrule
 * @params	Date	event start date (when date)
 * @params	Date	start date
 * @params	Date	end date
 * @return		Array	list of component between start date and end date, filter by rrule rule
 */
Cal.freqMonthlyToDate = function (rrule, wdate, sdate, edate)
{
	var result = new Array();
	var until = rrule['UNTIL'];
	var byMonthDay = (typeof(rrule['BYMONTHDAY']) ) ? parseInt(rrule['BYMONTHDAY']) : 0;	
	var byDay = (typeof(rrule['BYDAY']) != 'undefined') ? rrule['BYDAY'] : null;
	var weekNo = 0;
	byMonthDay = (byMonthDay >= 1 && byMonthDay <= 31) ? byMonthDay : 0;
	if (byDay !== null) {
		// ex "-1FR", "4MO", ...;
		var match = byDay.match(/^([\-]{0,1}[\d])([A-Z]{2})/);
		if (match) {
			weekNo = parseInt(match[1]);
			byDay = Cal.weekDayToNum(match[2]);
		} else {
			byDay = null;
		}
	}
	
	// return empty when input is incorrect
	if (byMonthDay == 0 && byDay < 0) {
		return [];
	}

	var cnt = 0;
	for (var year = wdate.getFullYear(); year <= edate.getFullYear(); year++) {
		for (var month = 1; month <= 12; month++) {		
			// only start when event ready start
			if (cnt == 0 && (month-1) < wdate.getMonth()) {
				continue;
			}
			
			// start count from when event ready start
			cnt++;
			
			// only get result when past start date
			if (year < sdate.getFullYear() || (year == sdate.getFullYear() && (month-1) < sdate.getMonth())) {
				continue;
			}

			// end when month is past end date
			if (year == edate.getFullYear() && (month-1) > edate.getMonth()) {
				break;
			}

			// check for step
			if ((cnt-1) % rrule['INTERVAL'] != 0) {
				continue;
			}
			
			if (byMonthDay > 0) { // monthly by month day				
				// only get result when by month day less than the sum day in month
				if (byMonthDay <= Cal.daysInMonth(month, year)) {
					// check for result is past end date
					if (year != edate.getFullYear() || (month-1) != edate.getMonth() || byMonthDay <= edate.getDate()) { // must be less than end date
						if (year != wdate.getFullYear() || (month-1) != wdate.getMonth() || byMonthDay >= wdate.getDate()) { // must be great than when date
							if (year != sdate.getFullYear() || (month-1) != sdate.getMonth() || byMonthDay >= sdate.getDate()) { // must be great than start date
								var copy = Cal.changeDate(wdate, byMonthDay, month, year);
								if (until == null || (until != null && copy.getTime() <= until.getTime())) {
									result.push(copy.getTime());
								}
							}
						}
					}
				}
			} else if (byDay != null && weekNo != 0) { // monthly by week day
				var days_in_month = Cal.daysInMonth(month, year); // sum of day in month
				var first_day =  Cal.dayOfWeek(1, month, year); // the week day (0-6) of the first day of month				
				var day = Cal.getDateFromByDay(byDay, weekNo, first_day, days_in_month);

				if (day > 0 && day <= days_in_month) {
					if (year != edate.getFullYear() || (month-1) != edate.getMonth() || day <= edate.getDate()) { // must be less than end date
						if (year != wdate.getFullYear() || (month-1) != wdate.getMonth() || day >= wdate.getDate()) { // must be great than when date
							if (year != sdate.getFullYear() || (month-1) != sdate.getMonth() || day >= sdate.getDate()) { // must be great than start date
								var copy = Cal.changeDate(wdate, day, month, year);
								if (until == null || (until != null && copy.getTime() <= until.getTime())) {
									result.push(copy.getTime());
								}
							}
						}
					}					
				}
			}
		}
	}

	return result;
}

/**
 *  get components by yearly rule
 *
 * @params	Array rrule
 * @params	Date	event start date (when date)
 * @params	Date	start date
 * @params	Date	end date
 * @return		Array	list of component between start date and end date, filter by rrule rule
 */
Cal.freqYearlyToDate = function (rrule, wdate, sdate, edate)
{
	var result = new Array();
	var until = rrule['UNTIL'];
	var step = rrule['INTERVAL'];

	var bymonth = new Array();
	if (typeof(rrule['BYMONTH']) == 'string') {
		var months = rrule['BYMONTH'].split(",");
		for (var i = 0; i < months.length; i++) {
			if (parseInt(months[i]) > 0 && parseInt(months[i]) <= 12) {
				bymonth.push(parseInt(months[i]));
			}
		}
	} else if (typeof(rrule['BYMONTH']) == 'object') {
		bymonth = rrule['BYMONTH'];
	}
	if (bymonth.length == 0) {
		bymonth.push(wdate.getMonth()+1);
	}
	
	var byDay = (typeof(rrule['BYDAY']) != 'undefined') ? rrule['BYDAY'] : null;
	var weekNo = 0;
	if (byDay !== null) {
		// ex "-1FR", "4MO", ...;
		var match = byDay.match(/^([\-]{0,1}[\d])([A-Z]{2})/);
		if (match) {
			weekNo = parseInt(match[1]);
			byDay = Cal.weekDayToNum(match[2]);
		} else {
			byDay = null;
		}
	}

	var cnt = 0;
	for (var year = wdate.getFullYear(); year <= edate.getFullYear(); year++) {
		// begin add for fix bug 2008-11-27
		// check for step
		if ((cnt++) % step != 0) {
			continue;
		}
		// end
		for (var month = 1; month <= 12; month++) {		
			// only start when event ready start
			//if (cnt == 0 && (month-1) < wdate.getMonth()) { // comment for fix bug 2008-11-27
			if ((month-1) < wdate.getMonth()) {
				continue;
			}
			
			/* // comment for fix bug 2008-1127
			// start count from when event ready start
			cnt++;
			*/
			
			// only get result when past start date
			if (year < sdate.getFullYear() || (year == sdate.getFullYear() && (month-1) < sdate.getMonth())) {
				continue;
			}

			// end when month is past end date
			if (year == edate.getFullYear() && (month-1) > edate.getMonth()) {
				break;
			}

			/* // comment for fix bug 2008-1127
			// check for step
			if ((cnt-1) % rrule['INTERVAL'] != 0) {
				continue;
			}
			*/
			
			if (bymonth.indexOf(month) == -1) {
				continue;
			}
			
			if (wdate.getDate() <= Cal.daysInMonth(month, year)) {
				if (byDay == null || weekNo == 0) { // monthly by week day
					if (year != edate.getFullYear() || (month-1) != edate.getMonth() || wdate.getDate() <= edate.getDate()) { // must be less than end date
						if (year != wdate.getFullYear() || (month-1) != wdate.getMonth() || wdate.getDate() >= wdate.getDate()) { // must be great than when date
							if (year != sdate.getFullYear() || (month-1) != sdate.getMonth() || wdate.getDate() >= sdate.getDate()) { // must be great than start date
								var copy = Cal.changeDate(wdate, wdate.getDate(), month, year);
								if (until == null || (until != null && copy.getTime() <= until.getTime())) {
									result.push(copy.getTime());
								}
							}
						}
					}
				} else {
					var days_in_month = Cal.daysInMonth(month, year); // sum of day in month
					var first_day =  Cal.dayOfWeek(1, month, year); // the week day (0-6) of the first day of month				
					var day = Cal.getDateFromByDay(byDay, weekNo, first_day, days_in_month);

					if (day > 0 && day <= days_in_month) {
						if (year != edate.getFullYear() || (month-1) != edate.getMonth() || day <= edate.getDate()) { // must be less than end date
							if (year != wdate.getFullYear() || (month-1) != wdate.getMonth() || day >= wdate.getDate()) { // must be great than when date
								if (year != sdate.getFullYear() || (month-1) != sdate.getMonth() || day >= sdate.getDate()) { // must be great than start date
									var copy = Cal.changeDate(wdate, day, month, year);
									if (until == null || (until != null && copy.getTime() <= until.getTime())) {
										result.push(copy.getTime());
									}
								}
							}
						}					
					}
				}
			}
		}
	}
	return result;
}

/**
 *  get components by no rule (doesn't rule defination)
 *
 * @params	Array rrule
 * @params	Date	event start date (when date)
 * @params	Date	start date
 * @params	Date	end date
 * @return		Array	list of component between start date and end date, filter by rrule rule
 */
Cal.noFreqToDate = function(wdate, sdate, edate)
{
	var result = [];
	if (wdate.getTime() >= sdate.getTime() && wdate.getTime() <= edate.getTime()) {
		result.push(wdate.getTime());
	}
	return result;
}

/**
 * retrieve components from the url of ICalendar file (.ics)
 *
 * @params	string		url input
 * @params	function	callback function (one parameter is comonents that is retrieved from the url given)
 */
Cal.retrieveComponents = function(url, callback) {

	var myAjax = new Ajax.Request(
		url, 
		{
			method: 'get', 
			onComplete: function(originalRequest) {
				var options = Cal.ics_options;
				var components = Cal.getComponentsFromText(originalRequest.responseText);
				/*if (components != false) {
					callback(components);
				}*/
				callback(components); // 2009-01-08 (fixbug) Modify to always handle callback function
			},
			onCreate: function(){},
			onLoaded: function(){},
//			onException: function(){alert("exception")}, // test
			onException: function(){callback([]);}, // return empty array when there are exceptions
//			onFailure: function(){alert("Failure")} // test
			onFailure: function(){callback([]);} // return empty array when can not load components data
		});
}

// end

