<commit_before>/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */
/*
 * git
 * Copyright (C)  2011 <>
 * 
 */


#include "eas-cal-info-translator.h"

#include <libical/icalparser.h>
#include <libical/icalcomponent.h>
#include <libical/icaltypes.h>
#include <libical/icalduration.h>

#include <libwbxml-1.0/wbxml/wbxml.h>

// Values for converting icaldurationtype into a number of minutes
const gint SECONDS_PER_MINUTE          = 60;
const gint MINUTES_PER_HOUR            = 60;
const gint MINUTES_PER_DAY             = 60 * 24;
const gint SECONDS_PER_DAY             = 60 * 60 * 24;
const gint MINUTES_PER_WEEK            = 60 * 24 * 7;
const gint DAYS_PER_WEEK               = 7;
const gint EPOCH_START_YEAR            = 1970;

// Constants for <Calendar> parsing
const guint DAY_OF_WEEK_SUNDAY         = 0x00000001;
const guint DAY_OF_WEEK_MONDAY         = 0x00000002;
const guint DAY_OF_WEEK_TUESDAY        = 0x00000004;
const guint DAY_OF_WEEK_WEDNESDAY      = 0x00000008;
const guint DAY_OF_WEEK_THURSDAY       = 0x00000010;
const guint DAY_OF_WEEK_FRIDAY         = 0x00000020;
const guint DAY_OF_WEEK_SATURDAY       = 0x00000040;
const guint DAY_OF_WEEK_LAST_OF_MONTH  = 0x0000007F; // 127



const gchar* VCAL_EXTENSION_PROPERTY_PREFIX = "X-MEEGO-ACTIVESYNCD-";


#define compile_time_assert(cond, msg) \
	char msg[(cond)?1:-1]

typedef  struct {
	guint16 Year;
	guint16 Month;
	guint16 DayOfWeek;
	guint16 Day;
	guint16 Hour;
	guint16 Minute;
	guint16 Second;
	guint16 Millisecond;
} __attribute__((packed)) EasSystemTime;

compile_time_assert((sizeof(EasSystemTime) == 16), EasSystemTime_not_expected_size);

/* From ActiveSync Protocol Doc MS-ASDTYPE
 * The required values are Bias, which is the offset from UTC, in minutes, and 
 * the StandardDate and DaylightDate, which are needed when the biases take 
 * effect. For example, the bias for Pacific Time is 480.
 */
typedef struct {
	gint32 Bias;						// 4
	guint16 StandardName[32];			// 64
	EasSystemTime StandardDate;			// 16
	gint32 StandardBias;				// 4
	guint16 DaylightName[32];			// 64
	EasSystemTime DaylightDate;			// 16
	gint32 DaylightBias;				// 4
} __attribute__((packed)) EasTimeZone;	// 172

compile_time_assert((sizeof(EasTimeZone) == 172), EasTimeZone_not_expected_size);


/**
 * \brief The by_day member of icalrecurrencetype is a composite value, encoding
 *        both the day to repeat on and (in the case of monthly recurrence) the position
 *        of the day to repeat on (e.g. 2nd Monday in each month). libical doesn't provide
 *        an API to encode this, so we've had to write our own by reverse engineering
 *        the implementation of icalrecurrencetype_day_day_of_week() and
 *        icalrecurrencetype_day_position() in icalrecur.c.
 *
 * \param byDayDayOfWeek  The day of the week to repeat on
 * \param byDayPos        The position of this day in the month on which to repeat
 *                        (or 0 if not applicable)
 */
static short _xml2ical_make_rrule_by_day_value(icalrecurrencetype_weekday byDayDayOfWeek, short byDayPos)
{
	// Now calculate the composite value as follows:
	//  - Multiply byDayPos by 8
	//  - Add byDayDayOfWeek if byDayPos is positive, or subtract if negative
	short dowShort = (short)byDayDayOfWeek;
	return
		(byDayPos * 8) +
		((byDayPos >= 0) ? dowShort : (-1 * dowShort));
}


/**
 * \brief Private function to decode an EasSystemTime struct containing details of a recurring
 *        timezone change pattern, modify it to a format iCalendar expects and create the
 *        required RRULE property value.
 *
 * \param date        Pointer to the EasSystemTime struct. This will be modified in this
 *                    function to contain the first instance of the recurring sequence.
 * \param rrule       Pointer to an icalrecurrencetype struct to store the recurrence rule in.
 */
static void _xml2ical_convert_relative_timezone_date(EasSystemTime* date, struct icalrecurrencetype* rrule)
{
	// Microsoft's SYSTEMTIME (implemented here as EasSystemTime) stores a recurring date pattern as follows:
	//   - Year       set to 0
	//   - DayOfWeek  set to the weekday to repeat on (0 = Sunday,...)
	//   - Day        set to the nth weekday to repeat on (1 = first DayOfWeek of the month ... 5 = last of the month)
	//
	// For iCalendar, we have to convert this into the following:
	//   - A DSTART property identifying the first date of the recurring sequence in a year
	//     guaranteed to fall before any events using this timezone pattern. (We're using
	//     1970 as the start of the Unix epoch)
	//   - An RRULE property defining the recurring sequence, in the following format:
	//
	//         FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
	//
	// In the RRULE, the number in the BYDAY value denotes the nth occurrence in the month,
	// with a negatve value meaning the nth *last* occurrence in the month.
	//
	// So, we have all the information we need in the SYSTEMTIME but there's quite a bit of
	// conversin to do to get to the iCalendar properties.

	EasSystemTime              modifiedDate;
	GDate*                     recurrenceStartDate;
	GDateWeekday               weekday;
	gint                       occurrence;
	short                      byDayPos;
	icalrecurrencetype_weekday byDayDayOfWeek;

	// We're going to modify the value of date to reflect the iCal values. (e.g. the Year
	// will change from 0 to 1970 and the Day will change from a 1..5 "nth occurrence in the
	// month" value to an actual date of the required month in 1970. we'll use a copy to do
	// themodifications on then assign it back before we return.
	memcpy(&modifiedDate, date, sizeof(EasSystemTime));

	modifiedDate.Year = EPOCH_START_YEAR;

	// Now adjust the DayOfWeek.
	// SYSTEMTIME's wDayOfWeek has 0 = Sunday, 1 = Monday,...     http://msdn.microsoft.com/en-us/library/ms724950(v=vs.85).aspx
	// GDateWeekDay has 0 = G_DATE_BAD_WEEKDAY, 1 = Monday,...    http://developer.gnome.org/glib/stable/glib-Date-and-Time-Functions.html#GDateWeekday
	if (modifiedDate.DayOfWeek == 0)
	{
		modifiedDate.DayOfWeek = (guint16)G_DATE_SUNDAY;
		// Now our DayOfWeek matches GDateWeekDay (see below)
	}

	// date->DayOfWeek will give us the day to repeat on
	// date->Day will give us the nth occurrence in the month of this day
	// (where 5 == last, even if there are only 4)

	// We're going to build a GDate object to calculate the actual date in 1970 that represents
	// the first instance of this recurrence pattern.	
	// Start by initialising to the 1st of of the month
	recurrenceStartDate = g_date_new_dmy(1, modifiedDate.Month, modifiedDate.Year);

	// Now seek for the first occurrence of the day the sequence repeats on.
	weekday = (GDateWeekday)modifiedDate.DayOfWeek;
	while (g_date_get_weekday(recurrenceStartDate) != weekday)
	{
		g_date_add_days(recurrenceStartDate, 1);
	}

	// Now we've got the FIRST occurence of the correct weekday in the correct month in 1970.
	// Finally we need to seek for the nth occurrence (where 5th = last, even if there are only 4)
	occurrence = 1;
	while (occurrence++ < date->Day)
	{
		g_date_add_days(recurrenceStartDate, DAYS_PER_WEEK);

		// Check we havn't overrun the end of the month
		// (If we have, just roll back and we're done: we're on the last occurrence)
		if (g_date_get_month(recurrenceStartDate) != modifiedDate.Month)
		{
			g_date_subtract_days(recurrenceStartDate, DAYS_PER_WEEK);
			break;
		}
	}

	modifiedDate.Day = g_date_get_day(recurrenceStartDate);

	// Now populate the rrule value before modifying date->Day
	rrule->freq = ICAL_YEARLY_RECURRENCE;
	rrule->by_month[0] = modifiedDate.Month;
	
	// The by_day value in icalrecurrencetype is a short containing two
	// fields: the day, and the position in the month. Weirdly there's no
	// API to encode this (only to decode it) so we'll have to do it by hand.
	// Unfortunately the comment in icalrecur.c ("The day's position in the
	// period (Nth-ness) and the numerical value of the day are encoded
	// together as: pos*7 + dow") is wrong. :-\ From the code we can see it's
	// actually as implemented below.
	byDayPos = (short)date->Day;
	if (byDayPos == 5)
	{
		byDayPos = -1;
	}
	// Here the day is represented by the enum icalrecurrencetype_weekday, which has the
	// range 0 = ICAL_NO_WEEKDAY, 1 = ICAL_SUNDAY_WEEKDAY, 2 = ICAL_MONDAY_WEEKDAY...
	// so we can just add 1 to the EasSystemTime value (see above).
	byDayDayOfWeek = (icalrecurrencetype_weekday)(date->DayOfWeek + 1);

	// Now calculate the composite value as follows:
	//  - Multiply byDayPos by 8
	//  - Add byDayDayOfWeek if byDayPos is positive, or subtract if negative
	rrule->by_day[0] = _xml2ical_make_rrule_by_day_value(byDayDayOfWeek, byDayPos);

	// Finally, copy the modified version back into the date that was passed in
	memcpy(date, &modifiedDate, sizeof(EasSystemTime));
}


/**
 * \brief Process the <Attendees> element during parsing of an EAS XML document
 *
 * \param n       An XML node pointing at the <Attendees> element
 * \param vevent  The VEVENT iCal component to add the parsed attendees to
 */
static void _xml2ical_process_attendees(xmlNodePtr n, icalcomponent* vevent)
{
	gchar*         value = NULL;
	icalproperty*  prop  = NULL;
	icalparameter* param = NULL;

	xmlNode* attendeeNode = NULL;

	g_debug("Attendees element found in EAS XML");

	for (attendeeNode = n->children; attendeeNode; attendeeNode = attendeeNode->next)
	{
		// Variables for attendee properties and parameters
		gchar*                   email      = NULL;
		gchar*                   name       = NULL;
		icalparameter_partstat   partstat   = ICAL_PARTSTAT_NONE;	// Participatant Status parameter
		icalparameter_role       role       = ICAL_ROLE_NONE;
		xmlNodePtr               subNode    = NULL;

		g_debug("Attendee element found in EAS XML");

		for (subNode = attendeeNode->children; subNode; subNode = subNode->next)
		{
			if (subNode->type == XML_ELEMENT_NODE && g_strcmp0((gchar*)subNode->name, "Attendee_Email") == 0)
			{
				email = (gchar*)xmlNodeGetContent(subNode);
			}								
			else if (subNode->type == XML_ELEMENT_NODE && g_strcmp0((gchar*)subNode->name, "Attendee_Name") == 0)
			{
				name = (gchar*)xmlNodeGetContent(subNode);								
			}
			else if (subNode->type == XML_ELEMENT_NODE && g_strcmp0((gchar*)subNode->name, "Attendee_Status") == 0)
			{
				int status = 0;
				value = (gchar*)xmlNodeGetContent(subNode);
				status = atoi(value);
				switch(status)
				{
					case 0: // Response unknown
					case 5: // Not responded
						partstat = ICAL_PARTSTAT_NEEDSACTION;
						break;
					case 2: // Tentative
						partstat = ICAL_PARTSTAT_TENTATIVE;
						break;									
					case 3: // Accept
						partstat = ICAL_PARTSTAT_ACCEPTED;
						break;	
					case 4: // Decline
						partstat = ICAL_PARTSTAT_DECLINED;
						break;									
					default:
						partstat = ICAL_PARTSTAT_NONE;
						g_warning("unrecognised attendee status received");
						break;
				}// end switch status
				
				g_free(value); value = NULL;
			}
			else if (subNode->type == XML_ELEMENT_NODE && !g_strcmp0((gchar*)subNode->name, "Attendee_Type"))
			{
				int roleValue = 0;
				value = (gchar*)xmlNodeGetContent(subNode);
				roleValue = atoi(value);
				switch (roleValue)
				{
					// TODO create an enum for these values
					case 1: //Required
						role = ICAL_ROLE_REQPARTICIPANT;
						break;
					case 2: //Optional
						role = ICAL_ROLE_OPTPARTICIPANT;
						break;
					case 3: //Resource
						role = ICAL_ROLE_NONPARTICIPANT;
						break;
					default:
						role = ICAL_ROLE_NONE;
						g_warning("unrecognised attendee type received");
						break;
				}// end switch type
				
				g_free(value); value = NULL;
			}		
			
		}// end for subNodes

		// Now finally build and add the property, assuming we have at least an e-mail address
		if (email && strlen(email))
		{
			prop = icalproperty_new_attendee(email);

			if (email && strlen(name))
			{
				param = icalparameter_new_cn(name);
				icalproperty_add_parameter(prop, param);
			}
			if (partstat != ICAL_PARTSTAT_NONE)
			{
				param = icalparameter_new_partstat(partstat);
				icalproperty_add_parameter(prop, param);
			}
			if (role != ICAL_ROLE_NONE)
			{
				param = icalparameter_new_role(role);
				icalproperty_add_parameter(prop, param);
			}
			
			icalcomponent_add_property(vevent, prop);
		}
	}
}


/**
 * \brief Process the <Timezone> element during parsing of an EAS XML document
 *
 * \param n          An XML node pointing to the <Recurrence> element
 * \param vtimezone  The iCalendar VTIMEZONE component to add the parsed timezone properties to
 * \param tzid       Pointer to a buffer to initialise with the parsed TZID (timezone ID) string
 */
static void _xml2ical_process_timezone(xmlNodePtr n, icalcomponent* vtimezone, gchar** tzid)
{
	gchar*         value = NULL;
	icalproperty*  prop = NULL;
	xmlChar*       timeZoneBase64Buffer = xmlNodeGetContent(n);
	gsize          timeZoneRawBytesSize = 0;
	guchar*        timeZoneRawBytes = g_base64_decode((const gchar*)timeZoneBase64Buffer, &timeZoneRawBytesSize);
	EasTimeZone    timeZoneStruct;
	
	xmlFree(timeZoneBase64Buffer);

	// TODO Check decode of timezone for endianess problems

	if (timeZoneRawBytesSize == sizeof(EasTimeZone))
	{
		memcpy(&timeZoneStruct, timeZoneRawBytes, timeZoneRawBytesSize);
		g_free(timeZoneRawBytes);
		timeZoneRawBytes = NULL;

		{
			// Calculate the timezone offsets. See _ical2xml_process_xstandard_xdaylight()
			// comments for a full explanation of how EAS Bias relates to iCal UTC offsets
			const gint32                standardUtcOffsetMins = -1 * timeZoneStruct.Bias;
			const gint32                daylightUtcOffsetMins = -1 * (timeZoneStruct.Bias + timeZoneStruct.DaylightBias);
			icalcomponent*              xstandard             = NULL;
			icalcomponent*              xdaylight             = NULL;
			struct icalrecurrencetype   rrule;
			struct icaltimetype         time;
			
		
			// Using StandardName as the TZID
			// (Doesn't matter if it's not an exact description: this field is only used internally
			// during iCalendar encoding/decoding)
			// Note: using tzid here rather than value, as we need it elsewhere in this function
			*tzid = g_utf16_to_utf8((const gunichar2*)timeZoneStruct.StandardName,
				                   (sizeof(timeZoneStruct.StandardName)/sizeof(guint16)), NULL, NULL, NULL);
			// If no StandardName was supplied, we can just use a temporary name instead.
			// No need to support more than one: the EAS calendar will only have one Timezone
			// element. And no need to localise, as it's only used internally.
			if (*tzid == NULL || strlen(*tzid) == 0)
			{
				*tzid = g_strdup("Standard Timezone");
			}
			prop = icalproperty_new_tzid(*tzid);
			icalcomponent_add_property(vtimezone, prop);

			
			//
			// STANDARD component
			//
			
			xstandard = icalcomponent_new(ICAL_XSTANDARD_COMPONENT);

			icalrecurrencetype_clear(&rrule);

			// If timeZoneStruct.StandardDate.Year == 0 we need to convert it into
			// the start date of a recurring sequence, and add an RRULE.
			if (timeZoneStruct.StandardDate.Year == 0)
			{
				_xml2ical_convert_relative_timezone_date(&timeZoneStruct.StandardDate, &rrule);
			}

			// Add the DTSTART property
			time = icaltime_null_time();
			time.year = timeZoneStruct.StandardDate.Year;
			time.month = timeZoneStruct.StandardDate.Month;
			time.day = timeZoneStruct.StandardDate.Day;
			time.hour = timeZoneStruct.StandardDate.Hour;
			time.minute = timeZoneStruct.StandardDate.Minute;
			time.second = timeZoneStruct.StandardDate.Second;
			prop = icalproperty_new_dtstart(time);
			icalcomponent_add_property(xstandard, prop);

			// Add the RRULE (if required)
			if (rrule.freq != ICAL_NO_RECURRENCE)
			{
				prop = icalproperty_new_rrule(rrule);
				icalcomponent_add_property(xstandard, prop);
			}

			// Add TZOFFSETFROM and TZOFFSETTO
			// Note that libical expects these properties in seconds.
			prop = icalproperty_new_tzoffsetfrom(daylightUtcOffsetMins * SECONDS_PER_MINUTE);
			icalcomponent_add_property(xstandard, prop);
			prop = icalproperty_new_tzoffsetto(standardUtcOffsetMins * SECONDS_PER_MINUTE);
			icalcomponent_add_property(xstandard, prop);

			value = g_utf16_to_utf8((const gunichar2*)timeZoneStruct.StandardName, (sizeof(timeZoneStruct.StandardName)/sizeof(guint16)), NULL, NULL, NULL);
			if (value)
			{
				if (strlen(value))
				{
					prop = icalproperty_new_tzname(value);
					icalcomponent_add_property(xstandard, prop);
				}
				g_free(value); value = NULL;
			}

			// And now add the STANDARD component to the VTIMEZONE
			icalcomponent_add_component(vtimezone, xstandard);

			
			//
			// DAYLIGHT component
			//
		
			xdaylight = icalcomponent_new(ICAL_XDAYLIGHT_COMPONENT);

			// Reset the RRULE
			icalrecurrencetype_clear(&rrule);

			// If timeZoneStruct.DaylightDate.Year == 0 we need to convert it into
			// the start date of a recurring sequence, and add an RRULE.
			if (timeZoneStruct.DaylightDate.Year == 0)
			{
				_xml2ical_convert_relative_timezone_date(&timeZoneStruct.DaylightDate, &rrule);
			}

			// Add the DTSTART property
			time = icaltime_null_time();
			time.year = timeZoneStruct.DaylightDate.Year;
			time.month = timeZoneStruct.DaylightDate.Month;
			time.day = timeZoneStruct.DaylightDate.Day;
			time.hour = timeZoneStruct.DaylightDate.Hour;
			time.minute = timeZoneStruct.DaylightDate.Minute;
			time.second = timeZoneStruct.DaylightDate.Second;
			prop = icalproperty_new_dtstart(time);
			icalcomponent_add_property(xdaylight, prop);

			// Add the RRULE (if required)
			if (rrule.freq != ICAL_NO_RECURRENCE)
			{
				prop = icalproperty_new_rrule(rrule);
				icalcomponent_add_property(xdaylight, prop);
			}

			// Add TZOFFSETFROM and TZOFFSETTO
			// Note that libical expects these properties in seconds.
			prop = icalproperty_new_tzoffsetfrom(standardUtcOffsetMins * SECONDS_PER_MINUTE);
			icalcomponent_add_property(xdaylight, prop);
			prop = icalproperty_new_tzoffsetto(daylightUtcOffsetMins * SECONDS_PER_MINUTE);
			icalcomponent_add_property(xdaylight, prop);

			value = g_utf16_to_utf8((const gunichar2*)timeZoneStruct.DaylightName, (sizeof(timeZoneStruct.DaylightName)/sizeof(guint16)), NULL, NULL, NULL);
			if (value)
			{
				if (strlen(value))
				{
					prop = icalproperty_new_tzname(value);
					icalcomponent_add_property(xdaylight, prop);
				}
				g_free(value); value = NULL;
			}

			// And now add the DAYLIGHT component to the VTIMEZONE
			icalcomponent_add_component(vtimezone, xdaylight);
		}
	} // timeZoneRawBytesSize == sizeof(timeZoneStruct)
	else
	{
		g_critical("TimeZone BLOB did not match sizeof(EasTimeZone)");
	}
}


/**
 * \brief Process the <Recurrence> element during parsing of an EAS XML document
 *
 * \param n       An XML node pointing to the <Recurrence> element
 * \param vevent  The iCalendar VEVENT component to add the parsed RRULE property to
 */
static void _xml2ical_process_recurrence(xmlNodePtr n, icalcomponent* vevent)
{
	gchar*         value = NULL;
	icalproperty*  prop = NULL;

	xmlNode*       subNode = NULL;
	struct icalrecurrencetype recur;

	// Ensure the icalrecurrencetype is null
	icalrecurrencetype_clear(&recur);

	g_debug("Recurrence element found in EAS XML");

	for (subNode = n->children; subNode; subNode = subNode->next)
	{
		const gchar* elemName = (const gchar*)subNode->name;
		value = (gchar*)xmlNodeGetContent(subNode);

		// Type
		if (g_strcmp0(elemName, "Type") == 0)
		{
			int typeInt = atoi(value);
			switch (typeInt)
			{
				case 0: // Recurs daily
					recur.freq = ICAL_DAILY_RECURRENCE;
					break;
				case 1: // Recurs weekly
					recur.freq = ICAL_WEEKLY_RECURRENCE;
					break;
				case 2: // Recurs monthly
					recur.freq = ICAL_MONTHLY_RECURRENCE;
					break;
				case 3: // Recurs monthly on the nth day
					recur.freq = ICAL_MONTHLY_RECURRENCE;
					break;
				case 5: // Recurs yearly
					recur.freq = ICAL_YEARLY_RECURRENCE;
					break;
				case 6: // Recurs yearly on the nth day
					recur.freq = ICAL_YEARLY_RECURRENCE;
					break;
			}
		}
		
		// Occurrences
		else if (g_strcmp0(elemName, "Occurrences") == 0)
		{
			// From [MS-ASCAL]:
			// The Occurrences element and the Until element (section 2.2.2.18.7) are mutually exclusive. It is
			// recommended that only one of these elements be included in a Recurrence element (section
			// 2.2.2.18) in a Sync command request. If both elements are included, then the server MUST respect
			// the value of the Occurrences element and ignore the Until element.
			recur.count = atoi((char*)xmlNodeGetContent(subNode));
			recur.until = icaltime_null_time();
		}
		
		// Interval
		else if (g_strcmp0(elemName, "Interval") == 0)
		{
			recur.interval = (short)atoi((char*)xmlNodeGetContent(subNode));
		}

		// WeekOfMonth
		else if (g_strcmp0(elemName, "WeekOfMonth") == 0)
		{
			g_warning("DATA LOSS: Cannot handle <Calendar><Recurrence><WeekOfMonth> element (value=%d)", atoi((char*)xmlNodeGetContent(subNode)));
		}

		// DayOfWeek
		else if (g_strcmp0(elemName, "DayOfWeek") == 0)
		{
			// A recurrence rule can target any combination of the 7 week days.
			// EAS encodes these as a bit set in a single int value.
			// libical encodes them as an array (max size 7) of icalrecurrencetype_weekday
			// enum values.
			// This block of code converts the former into the latter.

			int dayOfWeek = atoi((char*)xmlNodeGetContent(subNode));
			int index = 0;

			// Note: must use IF for subsequent blocks, not ELSE IF
			if (dayOfWeek & DAY_OF_WEEK_MONDAY)
			{
				recur.by_day[index++] = _xml2ical_make_rrule_by_day_value(ICAL_MONDAY_WEEKDAY, 0);
			}
			if (dayOfWeek & DAY_OF_WEEK_TUESDAY)
			{
				recur.by_day[index++] = _xml2ical_make_rrule_by_day_value(ICAL_TUESDAY_WEEKDAY, 0);
			}
			if (dayOfWeek & DAY_OF_WEEK_WEDNESDAY)
			{
				recur.by_day[index++] = _xml2ical_make_rrule_by_day_value(ICAL_WEDNESDAY_WEEKDAY, 0);
			}
			if (dayOfWeek & DAY_OF_WEEK_THURSDAY)
			{
				recur.by_day[index++] = _xml2ical_make_rrule_by_day_value(ICAL_THURSDAY_WEEKDAY, 0);
			}
			if (dayOfWeek & DAY_OF_WEEK_FRIDAY)
			{
				recur.by_day[index++] = _xml2ical_make_rrule_by_day_value(ICAL_FRIDAY_WEEKDAY, 0);
			}
			if (dayOfWeek & DAY_OF_WEEK_SATURDAY)
			{
				recur.by_day[index++] = _xml2ical_make_rrule_by_day_value(ICAL_SATURDAY_WEEKDAY, 0);
			}
			if (dayOfWeek & DAY_OF_WEEK_SUNDAY)
			{
				recur.by_day[index++] = _xml2ical_make_rrule_by_day_value(ICAL_SUNDAY_WEEKDAY, 0);
			}
		}

		// MonthOfYear
		else if (g_strcmp0(elemName, "MonthOfYear") == 0)
		{
			recur.by_month[0] = (short)atoi((char*)xmlNodeGetContent(subNode));
		}

		// Until
		else if (g_strcmp0(elemName, "Until") == 0)
		{
			// From [MS-ASCAL]:
			// The Occurrences element and the Until element (section 2.2.2.18.7) are mutually exclusive. It is
			// recommended that only one of these elements be included in a Recurrence element (section
			// 2.2.2.18) in a Sync command request. If both elements are included, then the server MUST respect
			// the value of the Occurrences element and ignore the Until element.
			if (recur.count == 0)
			{
				recur.until = icaltime_from_string((gchar*)xmlNodeGetContent(subNode));
			}
		}

		// DayOfMonth
		else if (g_strcmp0(elemName, "DayOfMonth") == 0)
		{
			recur.by_month_day[0] = (short)atoi((char*)xmlNodeGetContent(subNode));
		}

		// CalendarType
		else if (g_strcmp0(elemName, "CalendarType") == 0)
		{
			const int calType = atoi((char*)xmlNodeGetContent(subNode));
			if ((calType != 0)    // Default
			    &&
			    (calType != 1))   // Gregorian
			{
				// No way of handling in iCal
				g_warning("DATA LOSS: Encountered a calendar type we can't handle in the <Calendar><Recurrence><CalendarType> element: %d", calType);
			}
		}

		// IsLeapMonth
		else if (g_strcmp0(elemName, "IsLeapMonth") == 0)
		{
			if (atoi((char*)xmlNodeGetContent(subNode)) == 1)
			{
				// This has nothing to do with Gregorian calendar leap years (ie. 29 days in Feb).
				// It only applies to non-Gregorian calendars so can't be handled in iCal.
				g_warning("DATA LOSS: Cannot handle <Calendar><Recurrence><IsLeapMonth> element");
			}
		}

		// FirstDayOfWeek
		else if (g_strcmp0(elemName, "FirstDayOfWeek") == 0)
		{
			int firstDayOfWeek = atoi((char*)xmlNodeGetContent(subNode));

			// EAS value is in range 0=Sunday..6=Saturday
			// iCal value is in range 0=NoWeekday, 1=Sunday..7=Saturday
			recur.week_start = (icalrecurrencetype_weekday)firstDayOfWeek + 1;
		}

		// Other fields...
		else
		{
			g_warning("DATA LOSS: Unknown element encountered in <Recurrence> element: %s", elemName);
		}
	}

	prop = icalproperty_new_rrule(recur);
	icalcomponent_add_property(vevent, prop);
}


/**
 * \brief Process the <Exceptions> element during parsing of an EAS XML document
 *
 * \param n       An XML node pointing to the <Exceptions> element
 * \param vevent  The iCalendar VEVENT component to add the parsed items property to
 */
static GSList* _xml2ical_process_exceptions(xmlNodePtr n, icalcomponent* vevent)
{
	// This is a bit tricky.
	//
	//  - If the EAS <Exception> element ONLY contains <ExceptionStartTime> and
	//    <Deleted> (with the latter's value set to 1), it's a simple exception
	//    that just deletes one of the occurrences of the recurrence (ie. an
	//    EXDATE in iCal);
	//
	//  - If it just contains an <ExceptionStartTime> (and optionally a <Deleted>
	//    element with the value 0), it represents an *additional* occurrence of
	//    the event, with all other attributes identical to those of the recurring
	//    event (ie. an RDATE in iCal);
	//
	//  - However, if it contains additional elements, it represents an exception
	//    where properties have been changed (eg. where the start time or subject
	//    have been changed for one occurrence only). These aren't supported in
	//    iCal so we have to create a whole new event. We can't do that here (as
	//    we're still part-way through parsing the event) so we return a list of
	//    exception event details so they can be processed later. Each exception
	//    event we return is stored as a hash table of property names/values.
	//    (So this function returns a list of hash tables.)

	
	gchar*         value = NULL;
	xmlNode*       exceptionNode = NULL;
	xmlNode*       subNode = NULL;
	GSList*        listOfNewEventEvents = NULL;

	g_debug("Exceptions element found in EAS XML");

	// Iterate through the <Exception> elements
	for (exceptionNode = n->children; exceptionNode; exceptionNode = exceptionNode->next)
	{
		GHashTable* newEventValues = NULL;
		
		// Iterate through each Exception's properties
		gchar* exceptionStartTime = NULL;
		gboolean deleted = FALSE;

		for (subNode = exceptionNode->children; subNode; subNode = subNode->next)
		{
			const gchar* name = (const gchar*)subNode->name;
			value = (gchar*)xmlNodeGetContent(subNode);

			if (g_strcmp0(name, "Deleted") == 0)
			{
				deleted = (g_strcmp0(value, "1") == 0);
			}
			else if (g_strcmp0(name, "ExceptionStartTime") == 0)
			{
				exceptionStartTime = g_strdup(value);
			}
			else if (strlen(value) > 0)
			{
				// We've got a non-trivial exception that will
				// require adding a new event: build a hash of its values
				// and add to the list
				if (newEventValues == NULL)
				{
					newEventValues = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
				}
				g_hash_table_insert(newEventValues, g_strdup(name), g_strdup(value));
			}

			g_free(value);
		}

		// ExceptionStartTime is mandatory so check we've got one
		if (exceptionStartTime == NULL)
		{
			g_warning("DATA LOSS: <Exception> element found with no ExceptionStartTime.");
			break;
		}

		// If the <Deleted> value is set to 1, it's dead easy: it maps straight onto an
		// EXDATE an we can ignore the other elements.
		if (deleted)
		{
			// Add an EXDATE property

			// I'm ASSUMING here that we add multiple single-value EXDATE properties and
			// libical takes care of merging them into one (just as it splits them when
			// we're reading an iCal). TODO: check this during testing...
			icalproperty* exdate = icalproperty_new_exdate(icaltime_from_string(exceptionStartTime));
			icalcomponent_add_property(vevent, exdate); // vevent takes ownership of exdate
		}
		// If it's not deleted, but the only other element present is ExceptionStartTime,
		// then it's an RDATE (i.e. just a one-off recurrence of the same event but not
		// included in the regular recurrence sequence)
		else if (newEventValues == NULL)
		{
			icalproperty* rdate = NULL;
			
			// Same assumption as for EXDATE: that we just add multiple properties
			// and libical takes care of merging them. TODO: check this during testing...
			struct icaldatetimeperiodtype dtper;
			dtper.period = icalperiodtype_null_period();
			dtper.time = icaltime_from_string(exceptionStartTime);
			rdate = icalproperty_new_rdate(dtper);
			icalcomponent_add_property(vevent, rdate); // vevent takes ownership of rdate
		}
		// Otherwise it's neither an EXDATE or an RDATE: it's a new instance of the
		// event with more substantial changes (e.g. start time/end time/subject/etc.
		// has changed)
		else
		{
			// Add ExceptionStartTime to the hash now too
			g_hash_table_insert(newEventValues, g_strdup("ExceptionStartTime"), g_strdup(exceptionStartTime));

			// And add the hash table to the list to be returned
			// (Using prepend rather than append for efficiency)
			// (No need to new the list first, it's allocated during prepend:
			// http://developer.gnome.org/glib/stable/glib-Singly-Linked-Lists.html#g-slist-alloc)
			listOfNewEventEvents = g_slist_prepend(listOfNewEventEvents, newEventValues);
		}
		
		g_free(exceptionStartTime);
	}// end of for loop

	return listOfNewEventEvents;
}


/**
 * TODO...
 */
static void _xml2ical_add_exception_events(icalcomponent* vcalendar, icalcomponent* vevent, GSList* exceptionEvents)
{
	if (vcalendar && vevent && exceptionEvents)
	{
		const guint newEventCount = g_slist_length(exceptionEvents);
		guint index = 0;

		// Iterate through the list adding each exception event in turn
		for (index = 0; index < newEventCount; index++)
		{
			icalcomponent* newEvent = icalcomponent_new_clone(vevent);
			GHashTable* properties = (GHashTable*)g_slist_nth_data(exceptionEvents, index);

			if (properties == NULL)
			{
				g_warning("_xml2ical_add_exception_events(): NULL hash table found in exceptionEvents.");
				break;
			}

			// TODO: amend the UID




			// Finally, destroy the hash and replace the list entry with NULL
			g_hash_table_destroy(properties);
			g_slist_nth(exceptionEvents, index)->data = NULL;
		}
	}
}


/**
 * \brief Parse an XML-formatted calendar object received from ActiveSync and return
 *        it as a serialised iCalendar object.
 *
 * \param node       ActiveSync XML <ApplicationData> object containing a calendar.
 * \param server_id  The ActiveSync server ID from the response
 */
gchar* eas_cal_info_translator_parse_response(xmlNodePtr node, const gchar* server_id)
{
	// Variable for the return value
	gchar* result = NULL;

	// Variable for property values as they're read from the XML
	gchar* value  = NULL;

	// Variable to store the TZID value when decoding a <calendar:Timezone> element
	// so we can use it in the rest of the iCal's date/time fields.
	gchar* tzid   = NULL;

	// iCalendar objects
	struct icaltimetype dateTime;
	struct icaltriggertype trigger;

	if (node && (node->type == XML_ELEMENT_NODE) && (!g_strcmp0((char*)(node->name), "ApplicationData")))
	{
		xmlNodePtr n = node;

		EasCalInfo* calInfo = NULL;

		// iCalendar objects
		icalcomponent* vcalendar = icalcomponent_new(ICAL_VCALENDAR_COMPONENT);
		icalcomponent* vevent = icalcomponent_new(ICAL_VEVENT_COMPONENT);
		icalcomponent* valarm = icalcomponent_new(ICAL_VALARM_COMPONENT);
		icalcomponent* vtimezone = icalcomponent_new(ICAL_VTIMEZONE_COMPONENT);
		icalproperty* prop = NULL;
		icalparameter* param = NULL;
		gboolean isAllDayEvent = FALSE;
		gchar* organizerName = NULL;
		gchar* organizerEmail = NULL;
		GSList* newExceptionEvents = NULL;

		// TODO: get all these strings into constants/#defines
		
		// TODO: make the PRODID configurable somehow
		prop = icalproperty_new_prodid("-//Meego//ActiveSyncD 1.0//EN");
		icalcomponent_add_property(vcalendar, prop);

		prop = icalproperty_new_version("2.0");
		icalcomponent_add_property(vcalendar, prop);

		prop = icalproperty_new_method(ICAL_METHOD_PUBLISH);
		icalcomponent_add_property(vcalendar, prop);

		for (n = n->children; n; n = n->next)
		{
			if (n->type == XML_ELEMENT_NODE)
			{
				const gchar* name = (const gchar*)(n->name);

				//
				// Subject
				//
				if (g_strcmp0(name, "Subject") == 0)
				{
					value = (gchar*)xmlNodeGetContent(n);
					prop = icalproperty_new_summary(value);
					icalcomponent_add_property(vevent, prop);
					g_free(value); value = NULL;
				}

				//
				// StartTime
				//
				else if (g_strcmp0(name, "StartTime") == 0)
				{
					value = (gchar*)xmlNodeGetContent(n);
					dateTime = icaltime_from_string(value);
					if (isAllDayEvent)
					{
						// Ensure time is set to 00:00:00 for all-day events
						dateTime.hour = dateTime.minute = dateTime.second = 0;
					}
					prop = icalproperty_new_dtstart(dateTime);
					if (tzid && strlen(tzid) && !dateTime.is_utc) // Note: TZID not specified if it's a UTC time
					{
						param = icalparameter_new_tzid(tzid);
						icalproperty_add_parameter(prop, param);
					}
					icalcomponent_add_property(vevent, prop);
					g_free(value); value = NULL;
				}

				//
				// EndTime
				//
				else if (g_strcmp0(name, "EndTime") == 0)
				{
					value = (gchar*)xmlNodeGetContent(n);
					dateTime = icaltime_from_string(value);
					if (isAllDayEvent)
					{
						// Ensure time is set to 00:00:00 for all-day events
						dateTime.hour = dateTime.minute = dateTime.second = 0;
					}
					prop = icalproperty_new_dtend(dateTime);
					if (tzid && strlen(tzid) && !dateTime.is_utc) // Note: TZID not specified if it's a UTC time
					{
						param = icalparameter_new_tzid(tzid);
						icalproperty_add_parameter(prop, param);
					}
					icalcomponent_add_property(vevent, prop);
					g_free(value); value = NULL;
				}

				//
				// DtStamp
				//
				else if (g_strcmp0(name, "DTStamp") == 0)
				{
					value = (gchar*)xmlNodeGetContent(n);
					dateTime = icaltime_from_string(value);
					prop = icalproperty_new_dtstamp(dateTime);
					if (tzid && strlen(tzid) && !dateTime.is_utc) // Note: TZID not specified if it's a UTC time
					{
						param = icalparameter_new_tzid(tzid);
						icalproperty_add_parameter(prop, param);
					}
					icalcomponent_add_property(vevent, prop);
					g_free(value); value = NULL;
				}
				
				//
				// UID
				//
				else if (g_strcmp0(name, "UID") == 0)
				{
					value = (gchar*)xmlNodeGetContent(n);
					prop = icalproperty_new_uid(value);
					icalcomponent_add_property(vevent, prop);
					g_free(value); value = NULL;
				}

				//
				// Location
				//
				else if (g_strcmp0(name, "Location") == 0)
				{
					value = (gchar*)xmlNodeGetContent(n);
					prop = icalproperty_new_location(value);
					icalcomponent_add_property(vevent, prop);
					g_free(value); value = NULL;
				}

				//
				// Body
				//
				else if (g_strcmp0(name, "Body") == 0)
				{
					xmlNodePtr subNode = NULL;
					for (subNode = n->children; subNode; subNode = subNode->next)
					{
						if (subNode->type == XML_ELEMENT_NODE && !g_strcmp0((gchar*)subNode->name, "Data"))
						{
							value = (gchar*)xmlNodeGetContent(subNode);
							prop = icalproperty_new_description(value);
							icalcomponent_add_property(vevent, prop);
							g_free(value); value = NULL;
							break;
						}
					}
				}

				//
				// Sensitivity
				//
				else if (g_strcmp0(name, "Sensitivity") == 0)
				{
					icalproperty_class classValue = ICAL_CLASS_PUBLIC;
					value = (gchar*)xmlNodeGetContent(n);
					if (g_strcmp0(value, "3") == 0)      // Confidential
					{
						classValue = ICAL_CLASS_CONFIDENTIAL;
					}
					else if (g_strcmp0(value, "2") == 0) // Private
					{
						classValue = ICAL_CLASS_PRIVATE;
					}
					else // Personal or Normal (iCal doesn't distinguish between them)
					{
						classValue = ICAL_CLASS_PUBLIC;
					}
					prop = icalproperty_new_class(classValue);
					icalcomponent_add_property(vevent, prop);
					g_free(value); value = NULL;
				}

				//
				// BusyStatus
				//
				else if (g_strcmp0(name, "BusyStatus") == 0)
				{
					icalproperty_transp transpValue = ICAL_TRANSP_OPAQUE;
					value = (gchar*)xmlNodeGetContent(n);
					if (g_strcmp0(value, "0") == 0) // Free
					{
						transpValue = ICAL_TRANSP_TRANSPARENT;
					}
					else // Tentative, Busy or Out of Office
					{
						transpValue = ICAL_TRANSP_OPAQUE;
					}
					prop = icalproperty_new_transp(transpValue);<commit_msg>More code to handle non-trivial recurrence exceptions in EAS XML<commit_after>/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */
/*
 * git
 * Copyright (C)  2011 <>
 * 
 */


#include "eas-cal-info-translator.h"

#include <libical/icalparser.h>
#include <libical/icalcomponent.h>
#include <libical/icaltypes.h>
#include <libical/icalduration.h>

#include <libwbxml-1.0/wbxml/wbxml.h>

// Values for converting icaldurationtype into a number of minutes
const gint SECONDS_PER_MINUTE          = 60;
const gint MINUTES_PER_HOUR            = 60;
const gint MINUTES_PER_DAY             = 60 * 24;
const gint SECONDS_PER_DAY             = 60 * 60 * 24;
const gint MINUTES_PER_WEEK            = 60 * 24 * 7;
const gint DAYS_PER_WEEK               = 7;
const gint EPOCH_START_YEAR            = 1970;

// Constants for <Calendar> parsing
const guint DAY_OF_WEEK_SUNDAY         = 0x00000001;
const guint DAY_OF_WEEK_MONDAY         = 0x00000002;
const guint DAY_OF_WEEK_TUESDAY        = 0x00000004;
const guint DAY_OF_WEEK_WEDNESDAY      = 0x00000008;
const guint DAY_OF_WEEK_THURSDAY       = 0x00000010;
const guint DAY_OF_WEEK_FRIDAY         = 0x00000020;
const guint DAY_OF_WEEK_SATURDAY       = 0x00000040;
const guint DAY_OF_WEEK_LAST_OF_MONTH  = 0x0000007F; // 127



const gchar* VCAL_EXTENSION_PROPERTY_PREFIX = "X-MEEGO-ACTIVESYNCD-";


#define compile_time_assert(cond, msg) \
	char msg[(cond)?1:-1]

typedef  struct {
	guint16 Year;
	guint16 Month;
	guint16 DayOfWeek;
	guint16 Day;
	guint16 Hour;
	guint16 Minute;
	guint16 Second;
	guint16 Millisecond;
} __attribute__((packed)) EasSystemTime;

compile_time_assert((sizeof(EasSystemTime) == 16), EasSystemTime_not_expected_size);

/* From ActiveSync Protocol Doc MS-ASDTYPE
 * The required values are Bias, which is the offset from UTC, in minutes, and 
 * the StandardDate and DaylightDate, which are needed when the biases take 
 * effect. For example, the bias for Pacific Time is 480.
 */
typedef struct {
	gint32 Bias;						// 4
	guint16 StandardName[32];			// 64
	EasSystemTime StandardDate;			// 16
	gint32 StandardBias;				// 4
	guint16 DaylightName[32];			// 64
	EasSystemTime DaylightDate;			// 16
	gint32 DaylightBias;				// 4
} __attribute__((packed)) EasTimeZone;	// 172

compile_time_assert((sizeof(EasTimeZone) == 172), EasTimeZone_not_expected_size);


/**
 * \brief Convert a <Sensitivity> value from EAS XML into an iCal CLASS property value
 */
static icalproperty_class _xml2ical_convert_sensitivity_to_class(const gchar* sensitivityValue)
{
	if (g_strcmp0(sensitivityValue, "3") == 0)      // Confidential
	{
		return ICAL_CLASS_CONFIDENTIAL;
	}
	else if (g_strcmp0(sensitivityValue, "2") == 0) // Private
	{
		return ICAL_CLASS_PRIVATE;
	}
	else // Personal or Normal (iCal doesn't distinguish between them)
	{
		return ICAL_CLASS_PUBLIC;
	}
}


/**
 * \brief Convert a <BusyStatus> value from EAS XML into an iCal TRANSP property value
 */
static icalproperty_transp _xml2ical_convert_busystatus_to_transp(const gchar* busystatusValue)
{
	if (g_strcmp0(busystatusValue, "0") == 0) // Free
	{
		return ICAL_TRANSP_TRANSPARENT;
	}
	else // Tentative, Busy or Out of Office
	{
		return ICAL_TRANSP_OPAQUE;
	}
}


/**
 * \brief The by_day member of icalrecurrencetype is a composite value, encoding
 *        both the day to repeat on and (in the case of monthly recurrence) the position
 *        of the day to repeat on (e.g. 2nd Monday in each month). libical doesn't provide
 *        an API to encode this, so we've had to write our own by reverse engineering
 *        the implementation of icalrecurrencetype_day_day_of_week() and
 *        icalrecurrencetype_day_position() in icalrecur.c.
 *
 * \param byDayDayOfWeek  The day of the week to repeat on
 * \param byDayPos        The position of this day in the month on which to repeat
 *                        (or 0 if not applicable)
 */
static short _xml2ical_make_rrule_by_day_value(icalrecurrencetype_weekday byDayDayOfWeek, short byDayPos)
{
	// Now calculate the composite value as follows:
	//  - Multiply byDayPos by 8
	//  - Add byDayDayOfWeek if byDayPos is positive, or subtract if negative
	short dowShort = (short)byDayDayOfWeek;
	return
		(byDayPos * 8) +
		((byDayPos >= 0) ? dowShort : (-1 * dowShort));
}


/**
 * \brief Private function to decode an EasSystemTime struct containing details of a recurring
 *        timezone change pattern, modify it to a format iCalendar expects and create the
 *        required RRULE property value.
 *
 * \param date        Pointer to the EasSystemTime struct. This will be modified in this
 *                    function to contain the first instance of the recurring sequence.
 * \param rrule       Pointer to an icalrecurrencetype struct to store the recurrence rule in.
 */
static void _xml2ical_convert_relative_timezone_date(EasSystemTime* date, struct icalrecurrencetype* rrule)
{
	// Microsoft's SYSTEMTIME (implemented here as EasSystemTime) stores a recurring date pattern as follows:
	//   - Year       set to 0
	//   - DayOfWeek  set to the weekday to repeat on (0 = Sunday,...)
	//   - Day        set to the nth weekday to repeat on (1 = first DayOfWeek of the month ... 5 = last of the month)
	//
	// For iCalendar, we have to convert this into the following:
	//   - A DSTART property identifying the first date of the recurring sequence in a year
	//     guaranteed to fall before any events using this timezone pattern. (We're using
	//     1970 as the start of the Unix epoch)
	//   - An RRULE property defining the recurring sequence, in the following format:
	//
	//         FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
	//
	// In the RRULE, the number in the BYDAY value denotes the nth occurrence in the month,
	// with a negatve value meaning the nth *last* occurrence in the month.
	//
	// So, we have all the information we need in the SYSTEMTIME but there's quite a bit of
	// conversin to do to get to the iCalendar properties.

	EasSystemTime              modifiedDate;
	GDate*                     recurrenceStartDate;
	GDateWeekday               weekday;
	gint                       occurrence;
	short                      byDayPos;
	icalrecurrencetype_weekday byDayDayOfWeek;

	// We're going to modify the value of date to reflect the iCal values. (e.g. the Year
	// will change from 0 to 1970 and the Day will change from a 1..5 "nth occurrence in the
	// month" value to an actual date of the required month in 1970. we'll use a copy to do
	// themodifications on then assign it back before we return.
	memcpy(&modifiedDate, date, sizeof(EasSystemTime));

	modifiedDate.Year = EPOCH_START_YEAR;

	// Now adjust the DayOfWeek.
	// SYSTEMTIME's wDayOfWeek has 0 = Sunday, 1 = Monday,...     http://msdn.microsoft.com/en-us/library/ms724950(v=vs.85).aspx
	// GDateWeekDay has 0 = G_DATE_BAD_WEEKDAY, 1 = Monday,...    http://developer.gnome.org/glib/stable/glib-Date-and-Time-Functions.html#GDateWeekday
	if (modifiedDate.DayOfWeek == 0)
	{
		modifiedDate.DayOfWeek = (guint16)G_DATE_SUNDAY;
		// Now our DayOfWeek matches GDateWeekDay (see below)
	}

	// date->DayOfWeek will give us the day to repeat on
	// date->Day will give us the nth occurrence in the month of this day
	// (where 5 == last, even if there are only 4)

	// We're going to build a GDate object to calculate the actual date in 1970 that represents
	// the first instance of this recurrence pattern.	
	// Start by initialising to the 1st of of the month
	recurrenceStartDate = g_date_new_dmy(1, modifiedDate.Month, modifiedDate.Year);

	// Now seek for the first occurrence of the day the sequence repeats on.
	weekday = (GDateWeekday)modifiedDate.DayOfWeek;
	while (g_date_get_weekday(recurrenceStartDate) != weekday)
	{
		g_date_add_days(recurrenceStartDate, 1);
	}

	// Now we've got the FIRST occurence of the correct weekday in the correct month in 1970.
	// Finally we need to seek for the nth occurrence (where 5th = last, even if there are only 4)
	occurrence = 1;
	while (occurrence++ < date->Day)
	{
		g_date_add_days(recurrenceStartDate, DAYS_PER_WEEK);

		// Check we havn't overrun the end of the month
		// (If we have, just roll back and we're done: we're on the last occurrence)
		if (g_date_get_month(recurrenceStartDate) != modifiedDate.Month)
		{
			g_date_subtract_days(recurrenceStartDate, DAYS_PER_WEEK);
			break;
		}
	}

	modifiedDate.Day = g_date_get_day(recurrenceStartDate);

	// Now populate the rrule value before modifying date->Day
	rrule->freq = ICAL_YEARLY_RECURRENCE;
	rrule->by_month[0] = modifiedDate.Month;
	
	// The by_day value in icalrecurrencetype is a short containing two
	// fields: the day, and the position in the month. Weirdly there's no
	// API to encode this (only to decode it) so we'll have to do it by hand.
	// Unfortunately the comment in icalrecur.c ("The day's position in the
	// period (Nth-ness) and the numerical value of the day are encoded
	// together as: pos*7 + dow") is wrong. :-\ From the code we can see it's
	// actually as implemented below.
	byDayPos = (short)date->Day;
	if (byDayPos == 5)
	{
		byDayPos = -1;
	}
	// Here the day is represented by the enum icalrecurrencetype_weekday, which has the
	// range 0 = ICAL_NO_WEEKDAY, 1 = ICAL_SUNDAY_WEEKDAY, 2 = ICAL_MONDAY_WEEKDAY...
	// so we can just add 1 to the EasSystemTime value (see above).
	byDayDayOfWeek = (icalrecurrencetype_weekday)(date->DayOfWeek + 1);

	// Now calculate the composite value as follows:
	//  - Multiply byDayPos by 8
	//  - Add byDayDayOfWeek if byDayPos is positive, or subtract if negative
	rrule->by_day[0] = _xml2ical_make_rrule_by_day_value(byDayDayOfWeek, byDayPos);

	// Finally, copy the modified version back into the date that was passed in
	memcpy(date, &modifiedDate, sizeof(EasSystemTime));
}


/**
 * \brief Process the <Attendees> element during parsing of an EAS XML document
 *
 * \param n       An XML node pointing at the <Attendees> element
 * \param vevent  The VEVENT iCal component to add the parsed attendees to
 */
static void _xml2ical_process_attendees(xmlNodePtr n, icalcomponent* vevent)
{
	gchar*         value = NULL;
	icalproperty*  prop  = NULL;
	icalparameter* param = NULL;

	xmlNode* attendeeNode = NULL;

	g_debug("Attendees element found in EAS XML");

	for (attendeeNode = n->children; attendeeNode; attendeeNode = attendeeNode->next)
	{
		// Variables for attendee properties and parameters
		gchar*                   email      = NULL;
		gchar*                   name       = NULL;
		icalparameter_partstat   partstat   = ICAL_PARTSTAT_NONE;	// Participatant Status parameter
		icalparameter_role       role       = ICAL_ROLE_NONE;
		xmlNodePtr               subNode    = NULL;

		g_debug("Attendee element found in EAS XML");

		for (subNode = attendeeNode->children; subNode; subNode = subNode->next)
		{
			if (subNode->type == XML_ELEMENT_NODE && g_strcmp0((gchar*)subNode->name, "Attendee_Email") == 0)
			{
				email = (gchar*)xmlNodeGetContent(subNode);
			}								
			else if (subNode->type == XML_ELEMENT_NODE && g_strcmp0((gchar*)subNode->name, "Attendee_Name") == 0)
			{
				name = (gchar*)xmlNodeGetContent(subNode);								
			}
			else if (subNode->type == XML_ELEMENT_NODE && g_strcmp0((gchar*)subNode->name, "Attendee_Status") == 0)
			{
				int status = 0;
				value = (gchar*)xmlNodeGetContent(subNode);
				status = atoi(value);
				switch(status)
				{
					case 0: // Response unknown
					case 5: // Not responded
						partstat = ICAL_PARTSTAT_NEEDSACTION;
						break;
					case 2: // Tentative
						partstat = ICAL_PARTSTAT_TENTATIVE;
						break;									
					case 3: // Accept
						partstat = ICAL_PARTSTAT_ACCEPTED;
						break;	
					case 4: // Decline
						partstat = ICAL_PARTSTAT_DECLINED;
						break;									
					default:
						partstat = ICAL_PARTSTAT_NONE;
						g_warning("unrecognised attendee status received");
						break;
				}// end switch status
				
				g_free(value); value = NULL;
			}
			else if (subNode->type == XML_ELEMENT_NODE && !g_strcmp0((gchar*)subNode->name, "Attendee_Type"))
			{
				int roleValue = 0;
				value = (gchar*)xmlNodeGetContent(subNode);
				roleValue = atoi(value);
				switch (roleValue)
				{
					// TODO create an enum for these values
					case 1: //Required
						role = ICAL_ROLE_REQPARTICIPANT;
						break;
					case 2: //Optional
						role = ICAL_ROLE_OPTPARTICIPANT;
						break;
					case 3: //Resource
						role = ICAL_ROLE_NONPARTICIPANT;
						break;
					default:
						role = ICAL_ROLE_NONE;
						g_warning("unrecognised attendee type received");
						break;
				}// end switch type
				
				g_free(value); value = NULL;
			}		
			
		}// end for subNodes

		// Now finally build and add the property, assuming we have at least an e-mail address
		if (email && strlen(email))
		{
			prop = icalproperty_new_attendee(email);

			if (email && strlen(name))
			{
				param = icalparameter_new_cn(name);
				icalproperty_add_parameter(prop, param);
			}
			if (partstat != ICAL_PARTSTAT_NONE)
			{
				param = icalparameter_new_partstat(partstat);
				icalproperty_add_parameter(prop, param);
			}
			if (role != ICAL_ROLE_NONE)
			{
				param = icalparameter_new_role(role);
				icalproperty_add_parameter(prop, param);
			}
			
			icalcomponent_add_property(vevent, prop);
		}
	}
}


/**
 * \brief Process the <Timezone> element during parsing of an EAS XML document
 *
 * \param n          An XML node pointing to the <Recurrence> element
 * \param vtimezone  The iCalendar VTIMEZONE component to add the parsed timezone properties to
 * \param tzid       Pointer to a buffer to initialise with the parsed TZID (timezone ID) string
 */
static void _xml2ical_process_timezone(xmlNodePtr n, icalcomponent* vtimezone, gchar** tzid)
{
	gchar*         value = NULL;
	icalproperty*  prop = NULL;
	xmlChar*       timeZoneBase64Buffer = xmlNodeGetContent(n);
	gsize          timeZoneRawBytesSize = 0;
	guchar*        timeZoneRawBytes = g_base64_decode((const gchar*)timeZoneBase64Buffer, &timeZoneRawBytesSize);
	EasTimeZone    timeZoneStruct;
	
	xmlFree(timeZoneBase64Buffer);

	// TODO Check decode of timezone for endianess problems

	if (timeZoneRawBytesSize == sizeof(EasTimeZone))
	{
		memcpy(&timeZoneStruct, timeZoneRawBytes, timeZoneRawBytesSize);
		g_free(timeZoneRawBytes);
		timeZoneRawBytes = NULL;

		{
			// Calculate the timezone offsets. See _ical2xml_process_xstandard_xdaylight()
			// comments for a full explanation of how EAS Bias relates to iCal UTC offsets
			const gint32                standardUtcOffsetMins = -1 * timeZoneStruct.Bias;
			const gint32                daylightUtcOffsetMins = -1 * (timeZoneStruct.Bias + timeZoneStruct.DaylightBias);
			icalcomponent*              xstandard             = NULL;
			icalcomponent*              xdaylight             = NULL;
			struct icalrecurrencetype   rrule;
			struct icaltimetype         time;
			
		
			// Using StandardName as the TZID
			// (Doesn't matter if it's not an exact description: this field is only used internally
			// during iCalendar encoding/decoding)
			// Note: using tzid here rather than value, as we need it elsewhere in this function
			*tzid = g_utf16_to_utf8((const gunichar2*)timeZoneStruct.StandardName,
				                   (sizeof(timeZoneStruct.StandardName)/sizeof(guint16)), NULL, NULL, NULL);
			// If no StandardName was supplied, we can just use a temporary name instead.
			// No need to support more than one: the EAS calendar will only have one Timezone
			// element. And no need to localise, as it's only used internally.
			if (*tzid == NULL || strlen(*tzid) == 0)
			{
				*tzid = g_strdup("Standard Timezone");
			}
			prop = icalproperty_new_tzid(*tzid);
			icalcomponent_add_property(vtimezone, prop);

			
			//
			// STANDARD component
			//
			
			xstandard = icalcomponent_new(ICAL_XSTANDARD_COMPONENT);

			icalrecurrencetype_clear(&rrule);

			// If timeZoneStruct.StandardDate.Year == 0 we need to convert it into
			// the start date of a recurring sequence, and add an RRULE.
			if (timeZoneStruct.StandardDate.Year == 0)
			{
				_xml2ical_convert_relative_timezone_date(&timeZoneStruct.StandardDate, &rrule);
			}

			// Add the DTSTART property
			time = icaltime_null_time();
			time.year = timeZoneStruct.StandardDate.Year;
			time.month = timeZoneStruct.StandardDate.Month;
			time.day = timeZoneStruct.StandardDate.Day;
			time.hour = timeZoneStruct.StandardDate.Hour;
			time.minute = timeZoneStruct.StandardDate.Minute;
			time.second = timeZoneStruct.StandardDate.Second;
			prop = icalproperty_new_dtstart(time);
			icalcomponent_add_property(xstandard, prop);

			// Add the RRULE (if required)
			if (rrule.freq != ICAL_NO_RECURRENCE)
			{
				prop = icalproperty_new_rrule(rrule);
				icalcomponent_add_property(xstandard, prop);
			}

			// Add TZOFFSETFROM and TZOFFSETTO
			// Note that libical expects these properties in seconds.
			prop = icalproperty_new_tzoffsetfrom(daylightUtcOffsetMins * SECONDS_PER_MINUTE);
			icalcomponent_add_property(xstandard, prop);
			prop = icalproperty_new_tzoffsetto(standardUtcOffsetMins * SECONDS_PER_MINUTE);
			icalcomponent_add_property(xstandard, prop);

			value = g_utf16_to_utf8((const gunichar2*)timeZoneStruct.StandardName, (sizeof(timeZoneStruct.StandardName)/sizeof(guint16)), NULL, NULL, NULL);
			if (value)
			{
				if (strlen(value))
				{
					prop = icalproperty_new_tzname(value);
					icalcomponent_add_property(xstandard, prop);
				}
				g_free(value); value = NULL;
			}

			// And now add the STANDARD component to the VTIMEZONE
			icalcomponent_add_component(vtimezone, xstandard);

			
			//
			// DAYLIGHT component
			//
		
			xdaylight = icalcomponent_new(ICAL_XDAYLIGHT_COMPONENT);

			// Reset the RRULE
			icalrecurrencetype_clear(&rrule);

			// If timeZoneStruct.DaylightDate.Year == 0 we need to convert it into
			// the start date of a recurring sequence, and add an RRULE.
			if (timeZoneStruct.DaylightDate.Year == 0)
			{
				_xml2ical_convert_relative_timezone_date(&timeZoneStruct.DaylightDate, &rrule);
			}

			// Add the DTSTART property
			time = icaltime_null_time();
			time.year = timeZoneStruct.DaylightDate.Year;
			time.month = timeZoneStruct.DaylightDate.Month;
			time.day = timeZoneStruct.DaylightDate.Day;
			time.hour = timeZoneStruct.DaylightDate.Hour;
			time.minute = timeZoneStruct.DaylightDate.Minute;
			time.second = timeZoneStruct.DaylightDate.Second;
			prop = icalproperty_new_dtstart(time);
			icalcomponent_add_property(xdaylight, prop);

			// Add the RRULE (if required)
			if (rrule.freq != ICAL_NO_RECURRENCE)
			{
				prop = icalproperty_new_rrule(rrule);
				icalcomponent_add_property(xdaylight, prop);
			}

			// Add TZOFFSETFROM and TZOFFSETTO
			// Note that libical expects these properties in seconds.
			prop = icalproperty_new_tzoffsetfrom(standardUtcOffsetMins * SECONDS_PER_MINUTE);
			icalcomponent_add_property(xdaylight, prop);
			prop = icalproperty_new_tzoffsetto(daylightUtcOffsetMins * SECONDS_PER_MINUTE);
			icalcomponent_add_property(xdaylight, prop);

			value = g_utf16_to_utf8((const gunichar2*)timeZoneStruct.DaylightName, (sizeof(timeZoneStruct.DaylightName)/sizeof(guint16)), NULL, NULL, NULL);
			if (value)
			{
				if (strlen(value))
				{
					prop = icalproperty_new_tzname(value);
					icalcomponent_add_property(xdaylight, prop);
				}
				g_free(value); value = NULL;
			}

			// And now add the DAYLIGHT component to the VTIMEZONE
			icalcomponent_add_component(vtimezone, xdaylight);
		}
	} // timeZoneRawBytesSize == sizeof(timeZoneStruct)
	else
	{
		g_critical("TimeZone BLOB did not match sizeof(EasTimeZone)");
	}
}


/**
 * \brief Process the <Recurrence> element during parsing of an EAS XML document
 *
 * \param n       An XML node pointing to the <Recurrence> element
 * \param vevent  The iCalendar VEVENT component to add the parsed RRULE property to
 */
static void _xml2ical_process_recurrence(xmlNodePtr n, icalcomponent* vevent)
{
	gchar*         value = NULL;
	icalproperty*  prop = NULL;

	xmlNode*       subNode = NULL;
	struct icalrecurrencetype recur;

	// Ensure the icalrecurrencetype is null
	icalrecurrencetype_clear(&recur);

	g_debug("Recurrence element found in EAS XML");

	for (subNode = n->children; subNode; subNode = subNode->next)
	{
		const gchar* elemName = (const gchar*)subNode->name;
		value = (gchar*)xmlNodeGetContent(subNode);

		// Type
		if (g_strcmp0(elemName, "Type") == 0)
		{
			int typeInt = atoi(value);
			switch (typeInt)
			{
				case 0: // Recurs daily
					recur.freq = ICAL_DAILY_RECURRENCE;
					break;
				case 1: // Recurs weekly
					recur.freq = ICAL_WEEKLY_RECURRENCE;
					break;
				case 2: // Recurs monthly
					recur.freq = ICAL_MONTHLY_RECURRENCE;
					break;
				case 3: // Recurs monthly on the nth day
					recur.freq = ICAL_MONTHLY_RECURRENCE;
					break;
				case 5: // Recurs yearly
					recur.freq = ICAL_YEARLY_RECURRENCE;
					break;
				case 6: // Recurs yearly on the nth day
					recur.freq = ICAL_YEARLY_RECURRENCE;
					break;
			}
		}
		
		// Occurrences
		else if (g_strcmp0(elemName, "Occurrences") == 0)
		{
			// From [MS-ASCAL]:
			// The Occurrences element and the Until element (section 2.2.2.18.7) are mutually exclusive. It is
			// recommended that only one of these elements be included in a Recurrence element (section
			// 2.2.2.18) in a Sync command request. If both elements are included, then the server MUST respect
			// the value of the Occurrences element and ignore the Until element.
			recur.count = atoi((char*)xmlNodeGetContent(subNode));
			recur.until = icaltime_null_time();
		}
		
		// Interval
		else if (g_strcmp0(elemName, "Interval") == 0)
		{
			recur.interval = (short)atoi((char*)xmlNodeGetContent(subNode));
		}

		// WeekOfMonth
		else if (g_strcmp0(elemName, "WeekOfMonth") == 0)
		{
			g_warning("DATA LOSS: Cannot handle <Calendar><Recurrence><WeekOfMonth> element (value=%d)", atoi((char*)xmlNodeGetContent(subNode)));
		}

		// DayOfWeek
		else if (g_strcmp0(elemName, "DayOfWeek") == 0)
		{
			// A recurrence rule can target any combination of the 7 week days.
			// EAS encodes these as a bit set in a single int value.
			// libical encodes them as an array (max size 7) of icalrecurrencetype_weekday
			// enum values.
			// This block of code converts the former into the latter.

			int dayOfWeek = atoi((char*)xmlNodeGetContent(subNode));
			int index = 0;

			// Note: must use IF for subsequent blocks, not ELSE IF
			if (dayOfWeek & DAY_OF_WEEK_MONDAY)
			{
				recur.by_day[index++] = _xml2ical_make_rrule_by_day_value(ICAL_MONDAY_WEEKDAY, 0);
			}
			if (dayOfWeek & DAY_OF_WEEK_TUESDAY)
			{
				recur.by_day[index++] = _xml2ical_make_rrule_by_day_value(ICAL_TUESDAY_WEEKDAY, 0);
			}
			if (dayOfWeek & DAY_OF_WEEK_WEDNESDAY)
			{
				recur.by_day[index++] = _xml2ical_make_rrule_by_day_value(ICAL_WEDNESDAY_WEEKDAY, 0);
			}
			if (dayOfWeek & DAY_OF_WEEK_THURSDAY)
			{
				recur.by_day[index++] = _xml2ical_make_rrule_by_day_value(ICAL_THURSDAY_WEEKDAY, 0);
			}
			if (dayOfWeek & DAY_OF_WEEK_FRIDAY)
			{
				recur.by_day[index++] = _xml2ical_make_rrule_by_day_value(ICAL_FRIDAY_WEEKDAY, 0);
			}
			if (dayOfWeek & DAY_OF_WEEK_SATURDAY)
			{
				recur.by_day[index++] = _xml2ical_make_rrule_by_day_value(ICAL_SATURDAY_WEEKDAY, 0);
			}
			if (dayOfWeek & DAY_OF_WEEK_SUNDAY)
			{
				recur.by_day[index++] = _xml2ical_make_rrule_by_day_value(ICAL_SUNDAY_WEEKDAY, 0);
			}
		}

		// MonthOfYear
		else if (g_strcmp0(elemName, "MonthOfYear") == 0)
		{
			recur.by_month[0] = (short)atoi((char*)xmlNodeGetContent(subNode));
		}

		// Until
		else if (g_strcmp0(elemName, "Until") == 0)
		{
			// From [MS-ASCAL]:
			// The Occurrences element and the Until element (section 2.2.2.18.7) are mutually exclusive. It is
			// recommended that only one of these elements be included in a Recurrence element (section
			// 2.2.2.18) in a Sync command request. If both elements are included, then the server MUST respect
			// the value of the Occurrences element and ignore the Until element.
			if (recur.count == 0)
			{
				recur.until = icaltime_from_string((gchar*)xmlNodeGetContent(subNode));
			}
		}

		// DayOfMonth
		else if (g_strcmp0(elemName, "DayOfMonth") == 0)
		{
			recur.by_month_day[0] = (short)atoi((char*)xmlNodeGetContent(subNode));
		}

		// CalendarType
		else if (g_strcmp0(elemName, "CalendarType") == 0)
		{
			const int calType = atoi((char*)xmlNodeGetContent(subNode));
			if ((calType != 0)    // Default
			    &&
			    (calType != 1))   // Gregorian
			{
				// No way of handling in iCal
				g_warning("DATA LOSS: Encountered a calendar type we can't handle in the <Calendar><Recurrence><CalendarType> element: %d", calType);
			}
		}

		// IsLeapMonth
		else if (g_strcmp0(elemName, "IsLeapMonth") == 0)
		{
			if (atoi((char*)xmlNodeGetContent(subNode)) == 1)
			{
				// This has nothing to do with Gregorian calendar leap years (ie. 29 days in Feb).
				// It only applies to non-Gregorian calendars so can't be handled in iCal.
				g_warning("DATA LOSS: Cannot handle <Calendar><Recurrence><IsLeapMonth> element");
			}
		}

		// FirstDayOfWeek
		else if (g_strcmp0(elemName, "FirstDayOfWeek") == 0)
		{
			int firstDayOfWeek = atoi((char*)xmlNodeGetContent(subNode));

			// EAS value is in range 0=Sunday..6=Saturday
			// iCal value is in range 0=NoWeekday, 1=Sunday..7=Saturday
			recur.week_start = (icalrecurrencetype_weekday)firstDayOfWeek + 1;
		}

		// Other fields...
		else
		{
			g_warning("DATA LOSS: Unknown element encountered in <Recurrence> element: %s", elemName);
		}
	}

	prop = icalproperty_new_rrule(recur);
	icalcomponent_add_property(vevent, prop);
}


/**
 * \brief Process the <Exceptions> element during parsing of an EAS XML document
 *
 * \param n       An XML node pointing to the <Exceptions> element
 * \param vevent  The iCalendar VEVENT component to add the parsed items property to
 */
static GSList* _xml2ical_process_exceptions(xmlNodePtr n, icalcomponent* vevent)
{
	// This is a bit tricky.
	//
	//  - If the EAS <Exception> element ONLY contains <ExceptionStartTime> and
	//    <Deleted> (with the latter's value set to 1), it's a simple exception
	//    that just deletes one of the occurrences of the recurrence (ie. an
	//    EXDATE in iCal);
	//
	//  - If it just contains an <ExceptionStartTime> (and optionally a <Deleted>
	//    element with the value 0), it represents an *additional* occurrence of
	//    the event, with all other attributes identical to those of the recurring
	//    event (ie. an RDATE in iCal);
	//
	//  - However, if it contains additional elements, it represents an exception
	//    where properties have been changed (eg. where the start time or subject
	//    have been changed for one occurrence only). These aren't supported in
	//    iCal so we have to create a whole new event. We can't do that here (as
	//    we're still part-way through parsing the event) so we return a list of
	//    exception event details so they can be processed later. Each exception
	//    event we return is stored as a hash table of property names/values.
	//    (So this function returns a list of hash tables.)

	
	gchar*         value = NULL;
	xmlNode*       exceptionNode = NULL;
	xmlNode*       subNode = NULL;
	GSList*        listOfNewEventEvents = NULL;

	g_debug("Exceptions element found in EAS XML");

	// Iterate through the <Exception> elements
	for (exceptionNode = n->children; exceptionNode; exceptionNode = exceptionNode->next)
	{
		GHashTable* newEventValues = NULL;
		
		// Iterate through each Exception's properties
		gchar* exceptionStartTime = NULL;
		gboolean deleted = FALSE;
		for (subNode = exceptionNode->children; subNode; subNode = subNode->next)
		{
			const gchar* name = (const gchar*)subNode->name;
			value = (gchar*)xmlNodeGetContent(subNode);

			if (g_strcmp0(name, "Deleted") == 0)
			{
				deleted = (g_strcmp0(value, "1") == 0);
			}
			else if (g_strcmp0(name, "ExceptionStartTime") == 0)
			{
				exceptionStartTime = g_strdup(value);
			}
			else if (strlen(value) > 0)
			{
				// We've got a non-trivial exception that will
				// require adding a new event: build a hash of its values
				// and add to the list
				if (newEventValues == NULL)
				{
					newEventValues = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
				}
				g_hash_table_insert(newEventValues, g_strdup(name), g_strdup(value));
			}

			g_free(value);
		}

		// ExceptionStartTime is mandatory so check we've got one
		if (exceptionStartTime == NULL)
		{
			g_warning("DATA LOSS: <Exception> element found with no ExceptionStartTime.");
			break;
		}

		// If the <Deleted> value is set to 1, it's dead easy: it maps straight onto an
		// EXDATE an we can ignore the other elements.
		if (deleted)
		{
			// Add an EXDATE property

			// I'm ASSUMING here that we add multiple single-value EXDATE properties and
			// libical takes care of merging them into one (just as it splits them when
			// we're reading an iCal). TODO: check this during testing...
			icalproperty* exdate = icalproperty_new_exdate(icaltime_from_string(exceptionStartTime));
			icalcomponent_add_property(vevent, exdate); // vevent takes ownership of exdate
		}
		// If it's not deleted, but the only other element present is ExceptionStartTime,
		// then it's an RDATE (i.e. just a one-off recurrence of the same event but not
		// included in the regular recurrence sequence)
		else if (newEventValues == NULL)
		{
			icalproperty* rdate = NULL;
			
			// Same assumption as for EXDATE: that we just add multiple properties
			// and libical takes care of merging them. TODO: check this during testing...
			struct icaldatetimeperiodtype dtper;
			dtper.period = icalperiodtype_null_period();
			dtper.time = icaltime_from_string(exceptionStartTime);
			rdate = icalproperty_new_rdate(dtper);
			icalcomponent_add_property(vevent, rdate); // vevent takes ownership of rdate
		}
		// Otherwise it's neither an EXDATE or an RDATE: it's a new instance of the
		// event with more substantial changes (e.g. start time/end time/subject/etc.
		// has changed)
		else
		{
			// Add ExceptionStartTime to the hash now too
			g_hash_table_insert(newEventValues, g_strdup("ExceptionStartTime"), g_strdup(exceptionStartTime));

			// And add the hash table to the list to be returned
			// (Using prepend rather than append for efficiency)
			// (No need to new the list first, it's allocated during prepend:
			// http://developer.gnome.org/glib/stable/glib-Singly-Linked-Lists.html#g-slist-alloc)
			listOfNewEventEvents = g_slist_prepend(listOfNewEventEvents, newEventValues);
		}
		
		g_free(exceptionStartTime);
	}// end of for loop

	return listOfNewEventEvents;
}


/**
 * TODO...
 */
static void _xml2ical_add_exception_events(icalcomponent* vcalendar, icalcomponent* vevent, GSList* exceptionEvents)
{
	if (vcalendar && vevent && exceptionEvents)
	{
		const guint newEventCount = g_slist_length(exceptionEvents);
		guint index = 0;

		// Iterate through the list adding each exception event in turn
		for (index = 0; index < newEventCount; index++)
		{
			icalcomponent* newEvent = icalcomponent_new_clone(vevent);
			GHashTable* exceptionProperties = (GHashTable*)g_slist_nth_data(exceptionEvents, index);
			icalproperty* prop = NULL;
			gchar* value = NULL;

			if (exceptionProperties == NULL)
			{
				g_warning("_xml2ical_add_exception_events(): NULL hash table found in exceptionEvents.");
				break;
			}

			// Remove any recurrence (RRULE, RDATE and EXDATE) properies from the new event
			while ((prop = icalcomponent_get_first_property(newEvent, ICAL_RRULE_PROPERTY)) != NULL)
			{
				icalcomponent_remove_property(newEvent, prop);
				icalproperty_free(prop); prop = NULL;
			}
			while ((prop = icalcomponent_get_first_property(newEvent, ICAL_RDATE_PROPERTY)) != NULL)
			{
				icalcomponent_remove_property(newEvent, prop);
				icalproperty_free(prop); prop = NULL;
			}
			while ((prop = icalcomponent_get_first_property(newEvent, ICAL_EXDATE_PROPERTY)) != NULL)
			{
				icalcomponent_remove_property(newEvent, prop);
				icalproperty_free(prop); prop = NULL;
			}

			// Form a new UID for the new event as follows:
			// {Original event UID}_{Exception start time}
			prop = icalcomponent_get_first_property(newEvent, ICAL_UID_PROPERTY); // Retains ownership of the pointer
			value = g_strconcat((const gchar*)icalproperty_get_uid(prop), "_", (const gchar*)g_hash_table_lookup(exceptionProperties, "ExceptionStartTime"), NULL);
			icalproperty_set_uid(prop, value);
			prop = NULL;

			// TODO: as we're parsing these from the <Exceptions> element, I think we need to
			// add an EXDATE for this ExceptionStartTime. I think ExceptionStartTime identifies
			// the recurrence this is *replacing*: StartTime specifies this exception's own
			// start time.

			
			// Add the other properties from the hash

			// StartTime
			if ((value = (gchar*)g_hash_table_lookup(exceptionProperties, "StartTime")) != NULL)
			{
				// If this exception has a new start time, use that. Otherwise we default
				// to the start time of the original event (as per [MS-ASCAL]).
				// TODO: this needs testing: [MS-ASCAL] is a bit ambiguous around
				// <StartTime> vs. <ExceptionStartTime>
				if ((prop = icalcomponent_get_first_property(newEvent, ICAL_DTSTART_PROPERTY)) != NULL)
				{
					icalcomponent_remove_property(newEvent, prop); // Now we have ownership of prop
					icalproperty_free(prop); prop = NULL;
				}
				prop = icalproperty_new_dtstart(icaltime_from_string(value));
				icalcomponent_add_property(newEvent, prop); // vevent takes ownership
			}

			// EndTime
			if ((value = (gchar*)g_hash_table_lookup(exceptionProperties, "EndTime")) != NULL)
			{
				if ((prop = icalcomponent_get_first_property(newEvent, ICAL_DTEND_PROPERTY)) != NULL)
				{
					icalcomponent_remove_property(newEvent, prop); // Now we have ownership of prop
					icalproperty_free(prop); prop = NULL;
				}
				prop = icalproperty_new_dtend(icaltime_from_string(value));
				icalcomponent_add_property(newEvent, prop); // vevent takes ownership
			}

			// Subject
			if ((value = (gchar*)g_hash_table_lookup(exceptionProperties, "Subject")) != NULL)
			{
				if ((prop = icalcomponent_get_first_property(newEvent, ICAL_SUMMARY_PROPERTY)) != NULL)
				{
					icalcomponent_remove_property(newEvent, prop); // Now we have ownership of prop
					icalproperty_free(prop); prop = NULL;
				}
				prop = icalproperty_new_summary(value);
				icalcomponent_add_property(newEvent, prop); // vevent takes ownership
			}

			// Location
			if ((value = (gchar*)g_hash_table_lookup(exceptionProperties, "Location")) != NULL)
			{
				if ((prop = icalcomponent_get_first_property(newEvent, ICAL_LOCATION_PROPERTY)) != NULL)
				{
					icalcomponent_remove_property(newEvent, prop); // Now we have ownership of prop
					icalproperty_free(prop); prop = NULL;
				}
				prop = icalproperty_new_location(value);
				icalcomponent_add_property(newEvent, prop); // vevent takes ownership
			}

			// Categories
			if ((value = (gchar*)g_hash_table_lookup(exceptionProperties, "Categories")) != NULL)
			{
				// TODO: sort out <Categories> - it's a collection element! :-(
			}

			// Sensitivity
			if ((value = (gchar*)g_hash_table_lookup(exceptionProperties, "Sensitivity")) != NULL)
			{
				// Clear out any existing property
				if ((prop = icalcomponent_get_first_property(newEvent, ICAL_CLASS_PROPERTY)) != NULL)
				{
					icalcomponent_remove_property(newEvent, prop); // Now we have ownership of prop
					icalproperty_free(prop); prop = NULL;
				}
				prop = icalproperty_new_class(_xml2ical_convert_sensitivity_to_class(value));
				icalcomponent_add_property(newEvent, prop);
			}

			// BusyStatus
			if ((value = (gchar*)g_hash_table_lookup(exceptionProperties, "BusyStatus")) != NULL)
			{
				// Clear out any existing property
				if ((prop = icalcomponent_get_first_property(newEvent, ICAL_TRANSP_PROPERTY)) != NULL)
				{
					icalcomponent_remove_property(newEvent, prop); // Now we have ownership of prop
					icalproperty_free(prop); prop = NULL;
				}
				prop = icalproperty_new_transp(_xml2ical_convert_busystatus_to_transp(value));
				icalcomponent_add_property(newEvent, prop);
			}

			// AllDayEvent
			if ((value = (gchar*)g_hash_table_lookup(exceptionProperties, "AllDayEvent")) != NULL)
			{
				// TODO
			}

			// Reminder
			if ((value = (gchar*)g_hash_table_lookup(exceptionProperties, "Reminder")) != NULL)
			{
				icalcomponent* valarm = NULL;
				struct icaltriggertype trigger;
				
				// Remove any existing VALARM
				if ((valarm = icalcomponent_get_first_component(newEvent, ICAL_VALARM_COMPONENT)) != NULL)
				{
					icalcomponent_remove_component(newEvent, valarm); // We now have ownership of valarm
					icalcomponent_free(valarm);
					valarm = NULL;
				}

				valarm = icalcomponent_new(ICAL_VALARM_COMPONENT);
				
				// TODO: find a way of merging this with the other VALARM creation
				// code to avoid the duplication
			
				// Build an icaltriggertype structure
				trigger = icaltriggertype_from_int(0); // Null the fields first
				trigger.duration.is_neg = 1;
				trigger.duration.minutes = (unsigned int)atoi(value);

				prop = icalproperty_new_action(ICAL_ACTION_DISPLAY);
				icalcomponent_add_property(valarm, prop);

				prop = icalproperty_new_description("Reminder"); // TODO: make this configurable
				icalcomponent_add_property(valarm, prop);

				prop = icalproperty_new_trigger(trigger);
				icalcomponent_add_property(valarm, prop);

				icalcomponent_add_component(newEvent, valarm);
			}

			// DtStamp
			if ((value = (gchar*)g_hash_table_lookup(exceptionProperties, "DtStamp")) != NULL)
			{
				if ((prop = icalcomponent_get_first_property(newEvent, ICAL_DTSTAMP_PROPERTY)) != NULL)
				{
					icalcomponent_remove_property(newEvent, prop); // Now we have ownership of prop
					icalproperty_free(prop); prop = NULL;
				}
				prop = icalproperty_new_dtstamp(icaltime_from_string(value));
				icalcomponent_add_property(newEvent, prop); // vevent takes ownership
			}

			// MeetingStatus
			if ((value = (gchar*)g_hash_table_lookup(exceptionProperties, "MeetingStatus")) != NULL)
			{
				// TODO
			}

			// AppointmentReplyTime
			if ((value = (gchar*)g_hash_table_lookup(exceptionProperties, "AppointmentReplyTime")) != NULL)
			{
				// TODO
			}

			// ResponseType
			if ((value = (gchar*)g_hash_table_lookup(exceptionProperties, "ResponseType")) != NULL)
			{
				// TODO
			}

			// Body
			if ((value = (gchar*)g_hash_table_lookup(exceptionProperties, "Body")) != NULL)
			{
				if ((prop = icalcomponent_get_first_property(newEvent, ICAL_DESCRIPTION_PROPERTY)) != NULL)
				{
					icalcomponent_remove_property(newEvent, prop); // Now we have ownership of prop
					icalproperty_free(prop); prop = NULL;
				}
				prop = icalproperty_new_description(value);
				icalcomponent_add_property(newEvent, prop); // vevent takes ownership
			}


			// Finally, destroy the hash and replace the list entry with NULL
			g_hash_table_destroy(exceptionProperties);
			g_slist_nth(exceptionEvents, index)->data = NULL;

			// Add the new event to the parent VCALENDAR
			icalcomponent_add_component(vcalendar, newEvent);

			// Debug output
			g_debug("Added new exception VEVENT to the VCALENDAR:n%s", icalcomponent_as_ical_string(newEvent));
		}
	}
}


/**
 * \brief Parse an XML-formatted calendar object received from ActiveSync and return
 *        it as a serialised iCalendar object.
 *
 * \param node       ActiveSync XML <ApplicationData> object containing a calendar.
 * \param server_id  The ActiveSync server ID from the response
 */
gchar* eas_cal_info_translator_parse_response(xmlNodePtr node, const gchar* server_id)
{
	// Variable for the return value
	gchar* result = NULL;

	// Variable for property values as they're read from the XML
	gchar* value  = NULL;

	// Variable to store the TZID value when decoding a <calendar:Timezone> element
	// so we can use it in the rest of the iCal's date/time fields.
	gchar* tzid   = NULL;

	// iCalendar objects
	struct icaltimetype dateTime;
	struct icaltriggertype trigger;

	if (node && (node->type == XML_ELEMENT_NODE) && (!g_strcmp0((char*)(node->name), "ApplicationData")))
	{
		xmlNodePtr n = node;

		EasCalInfo* calInfo = NULL;

		// iCalendar objects
		icalcomponent* vcalendar = icalcomponent_new(ICAL_VCALENDAR_COMPONENT);
		icalcomponent* vevent = icalcomponent_new(ICAL_VEVENT_COMPONENT);
		icalcomponent* valarm = icalcomponent_new(ICAL_VALARM_COMPONENT);
		icalcomponent* vtimezone = icalcomponent_new(ICAL_VTIMEZONE_COMPONENT);
		icalproperty* prop = NULL;
		icalparameter* param = NULL;
		gboolean isAllDayEvent = FALSE;
		gchar* organizerName = NULL;
		gchar* organizerEmail = NULL;
		GSList* newExceptionEvents = NULL;

		// TODO: get all these strings into constants/#defines
		
		// TODO: make the PRODID configurable somehow
		prop = icalproperty_new_prodid("-//Meego//ActiveSyncD 1.0//EN");
		icalcomponent_add_property(vcalendar, prop);

		prop = icalproperty_new_version("2.0");
		icalcomponent_add_property(vcalendar, prop);

		prop = icalproperty_new_method(ICAL_METHOD_PUBLISH);
		icalcomponent_add_property(vcalendar, prop);

		for (n = n->children; n; n = n->next)
		{
			if (n->type == XML_ELEMENT_NODE)
			{
				const gchar* name = (const gchar*)(n->name);

				//
				// Subject
				//
				if (g_strcmp0(name, "Subject") == 0)
				{
					value = (gchar*)xmlNodeGetContent(n);
					prop = icalproperty_new_summary(value);
					icalcomponent_add_property(vevent, prop);
					g_free(value); value = NULL;
				}

				//
				// StartTime
				//
				else if (g_strcmp0(name, "StartTime") == 0)
				{
					value = (gchar*)xmlNodeGetContent(n);
					dateTime = icaltime_from_string(value);
					if (isAllDayEvent)
					{
						// Ensure time is set to 00:00:00 for all-day events
						dateTime.hour = dateTime.minute = dateTime.second = 0;
					}
					prop = icalproperty_new_dtstart(dateTime);
					if (tzid && strlen(tzid) && !dateTime.is_utc) // Note: TZID not specified if it's a UTC time
					{
						param = icalparameter_new_tzid(tzid);
						icalproperty_add_parameter(prop, param);
					}
					icalcomponent_add_property(vevent, prop);
					g_free(value); value = NULL;
				}

				//
				// EndTime
				//
				else if (g_strcmp0(name, "EndTime") == 0)
				{
					value = (gchar*)xmlNodeGetContent(n);
					dateTime = icaltime_from_string(value);
					if (isAllDayEvent)
					{
						// Ensure time is set to 00:00:00 for all-day events
						dateTime.hour = dateTime.minute = dateTime.second = 0;
					}
					prop = icalproperty_new_dtend(dateTime);
					if (tzid && strlen(tzid) && !dateTime.is_utc) // Note: TZID not specified if it's a UTC time
					{
						param = icalparameter_new_tzid(tzid);
						icalproperty_add_parameter(prop, param);
					}
					icalcomponent_add_property(vevent, prop);
					g_free(value); value = NULL;
				}

				//
				// DtStamp
				//
				else if (g_strcmp0(name, "DTStamp") == 0)
				{
					value = (gchar*)xmlNodeGetContent(n);
					dateTime = icaltime_from_string(value);
					prop = icalproperty_new_dtstamp(dateTime);
					if (tzid && strlen(tzid) && !dateTime.is_utc) // Note: TZID not specified if it's a UTC time
					{
						param = icalparameter_new_tzid(tzid);
						icalproperty_add_parameter(prop, param);
					}
					icalcomponent_add_property(vevent, prop);
					g_free(value); value = NULL;
				}
				
				//
				// UID
				//
				else if (g_strcmp0(name, "UID") == 0)
				{
					value = (gchar*)xmlNodeGetContent(n);
					prop = icalproperty_new_uid(value);
					icalcomponent_add_property(vevent, prop);
					g_free(value); value = NULL;
				}

				//
				// Location
				//
				else if (g_strcmp0(name, "Location") == 0)
				{
					value = (gchar*)xmlNodeGetContent(n);
					prop = icalproperty_new_location(value);
					icalcomponent_add_property(vevent, prop);
					g_free(value); value = NULL;
				}

				//
				// Body
				//
				else if (g_strcmp0(name, "Body") == 0)
				{
					xmlNodePtr subNode = NULL;
					for (subNode = n->children; subNode; subNode = subNode->next)
					{
						if (subNode->type == XML_ELEMENT_NODE && !g_strcmp0((gchar*)subNode->name, "Data"))
						{
							value = (gchar*)xmlNodeGetContent(subNode);
							prop = icalproperty_new_description(value);
							icalcomponent_add_property(vevent, prop);
							g_free(value); value = NULL;
							break;
						}
					}
				}

				//
				// Sensitivity
				//
				else if (g_strcmp0(name, "Sensitivity") == 0)
				{
					value = (gchar*)xmlNodeGetContent(n);
					prop = icalproperty_new_class(_xml2ical_convert_sensitivity_to_class(value));
					icalcomponent_add_property(vevent, prop);
					g_free(value); value = NULL;
				}

				//
				// BusyStatus
				//
				else if (g_strcmp0(name, "BusyStatus") == 0)
				{
					value = (gchar*)xmlNodeGetContent(n);
					prop = icalproperty_new_transp(_xml2ical_convert_busystatus_to_transp(value));
{'fraction_non_alphanumeric': 0.0990236928603722, 'fraction_numerical': 0.010801696012903383, 'mean_word_length': 3.334516180287061, 'pattern_counts': {'":': 0, '<': 83, '<?xml version=': 0, '>': 163, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 130}, 'pii_count': 8, 'substrings_counts': 8, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1679696', 'n_tokens_mistral': 31754, 'n_tokens_neox': 26717, 'n_words': 7394}