/*
  Copyright (C) 2004-2005 SKYRIX Software AG
  Copyright (C) 2006-2016 Inverse inc.

  This file is part of SOPE.

  SOPE is free software; you can redistribute it and/or modify it under
  the terms of the GNU Lesser General Public License as published by the
  Free Software Foundation; either version 2, or (at your option) any
  later version.

  SOPE is distributed in the hope that it will be useful, but WITHOUT ANY
  WARRANTY; without even the implied warranty of MERCHANTABILITY or
  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
  License for more details.

  You should have received a copy of the GNU Lesser General Public
  License along with SOPE; see the file COPYING.  If not, write to the
  Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
  02111-1307, USA.
*/

/*
  See http://tools.ietf.org/html/rfc2445#section-4.3.10

4.3.10 Recurrence Rule

   Value Name: RECUR

   Purpose: This value type is used to identify properties that contain
   a recurrence rule specification.

   Formal Definition: The value type is defined by the following
   notation:

     recur      = "FREQ"=freq *(

                ; either UNTIL or COUNT may appear in a 'recur',
                ; but UNTIL and COUNT MUST NOT occur in the same 'recur'

                ( ";" "UNTIL" "=" enddate ) /
                ( ";" "COUNT" "=" 1*DIGIT ) /

                ; the rest of these keywords are optional,
                ; but MUST NOT occur more than once

                ( ";" "INTERVAL" "=" 1*DIGIT )          /
                ( ";" "BYSECOND" "=" byseclist )        /
                ( ";" "BYMINUTE" "=" byminlist )        /
                ( ";" "BYHOUR" "=" byhrlist )           /
                ( ";" "BYDAY" "=" bywdaylist )          /
                ( ";" "BYMONTHDAY" "=" bymodaylist )    /
                ( ";" "BYYEARDAY" "=" byyrdaylist )     /
                ( ";" "BYWEEKNO" "=" bywknolist )       /
                ( ";" "BYMONTH" "=" bymolist )          /
                ( ";" "BYSETPOS" "=" bysplist )         /
                ( ";" "WKST" "=" weekday )              /
                ( ";" x-name "=" text )
                )

     freq       = "SECONDLY" / "MINUTELY" / "HOURLY" / "DAILY"
                / "WEEKLY" / "MONTHLY" / "YEARLY"

     enddate    = date
     enddate    =/ date-time            ;An UTC value

     byseclist  = seconds / ( seconds *("," seconds) )

     seconds    = 1DIGIT / 2DIGIT       ;0 to 59

     byminlist  = minutes / ( minutes *("," minutes) )

     minutes    = 1DIGIT / 2DIGIT       ;0 to 59

     byhrlist   = hour / ( hour *("," hour) )

     hour       = 1DIGIT / 2DIGIT       ;0 to 23

     bywdaylist = weekdaynum / ( weekdaynum *("," weekdaynum) )

     weekdaynum = [([plus] ordwk / minus ordwk)] weekday

     plus       = "+"

     minus      = "-"

     ordwk      = 1DIGIT / 2DIGIT       ;1 to 53

     weekday    = "SU" / "MO" / "TU" / "WE" / "TH" / "FR" / "SA"
     ;Corresponding to SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY,
     ;FRIDAY, SATURDAY and SUNDAY days of the week.

     bymodaylist = monthdaynum / ( monthdaynum *("," monthdaynum) )

     monthdaynum = ([plus] ordmoday) / (minus ordmoday)

     ordmoday   = 1DIGIT / 2DIGIT       ;1 to 31

     byyrdaylist = yeardaynum / ( yeardaynum *("," yeardaynum) )

     yeardaynum = ([plus] ordyrday) / (minus ordyrday)

     ordyrday   = 1DIGIT / 2DIGIT / 3DIGIT      ;1 to 366

     bywknolist = weeknum / ( weeknum *("," weeknum) )

     weeknum    = ([plus] ordwk) / (minus ordwk)

     bymolist   = monthnum / ( monthnum *("," monthnum) )

     monthnum   = 1DIGIT / 2DIGIT       ;1 to 12

     bysplist   = setposday / ( setposday *("," setposday) )

     setposday  = yeardaynum
*/

/*
  Examples :

   Every other week on Monday, Wednesday and Friday until December 24,
   1997, but starting on Tuesday, September 2, 1997:

     DTSTART;TZID=US-Eastern:19970902T090000
     RRULE:FREQ=WEEKLY;INTERVAL=2;UNTIL=19971224T000000Z;WKST=SU;
      BYDAY=MO,WE,FR
     ==> (1997 9:00 AM EDT)September 2,3,5,15,17,19,29;October
     1,3,13,15,17
         (1997 9:00 AM EST)October 27,29,31;November 10,12,14,24,26,28;
                           December 8,10,12,22

   Monthly on the 1st Friday for ten occurrences:

     DTSTART;TZID=US-Eastern:19970905T090000
     RRULE:FREQ=MONTHLY;COUNT=10;BYDAY=1FR

     ==> (1997 9:00 AM EDT)September 5;October 3
         (1997 9:00 AM EST)November 7;Dec 5
         (1998 9:00 AM EST)January 2;February 6;March 6;April 3
         (1998 9:00 AM EDT)May 1;June 5

   Every other month on the 1st and last Sunday of the month for 10
   occurrences:

     DTSTART;TZID=US-Eastern:19970907T090000
     RRULE:FREQ=MONTHLY;INTERVAL=2;COUNT=10;BYDAY=1SU,-1SU

     ==> (1997 9:00 AM EDT)September 7,28
         (1997 9:00 AM EST)November 2,30

   Monthly on the third to the last day of the month, forever:

     DTSTART;TZID=US-Eastern:19970928T090000
     RRULE:FREQ=MONTHLY;BYMONTHDAY=-3

     ==> (1997 9:00 AM EDT)September 28
         (1997 9:00 AM EST)October 29;November 28;December 29
         (1998 9:00 AM EST)January 29;February 26
     ...

   Every other year on January, February, and March for 10 occurrences:

     DTSTART;TZID=US-Eastern:19970310T090000
     RRULE:FREQ=YEARLY;INTERVAL=2;COUNT=10;BYMONTH=1,2,3

     ==> (1997 9:00 AM EST)March 10
         (1999 9:00 AM EST)January 10;February 10;March 10
         (2001 9:00 AM EST)January 10;February 10;March 10
         (2003 9:00 AM EST)January 10;February 10;March 10

   Everyday in January, for 3 years:

     DTSTART;TZID=US-Eastern:19980101T090000
     RRULE:FREQ=YEARLY;UNTIL=20000131T090000Z;
      BYMONTH=1;BYDAY=SU,MO,TU,WE,TH,FR,SA
     or
     RRULE:FREQ=DAILY;UNTIL=20000131T090000Z;BYMONTH=1

     ==> (1998 9:00 AM EDT)January 1-31
         (1999 9:00 AM EDT)January 1-31
         (2000 9:00 AM EDT)January 1-31

*/

#import <Foundation/NSArray.h>
#import <Foundation/NSDictionary.h>
#import <NGExtensions/NSString+Ext.h>
#import <NGExtensions/NSObject+Logs.h>


#import "NSCalendarDate+ICal.h"
#import "NSCalendarDate+NGCards.h"
#import "NSString+NGCards.h"

#import "CardGroup.h"
#import "iCalByDayMask.h"

NSString *iCalWeekDayString[] = { @"SU", @"MO", @"TU", @"WE", @"TH", @"FR",
				  @"SA" };

/*
  freq       = rrFreq;
  until      = rrUntil;
  count      = rrCount;
  interval   = rrInterval;
  bysecond   = rrBySecondList;
  byminute   = rrByMinuteList;
  byhour     = rrByHourList;
  byday      = rrByDayList;
  bymonthday = rrByMonthDayList;
  byyearday  = rrByYearDayList;
  byweekno   = rrByWeekNumberList;
  bymonth    = rrByMonthList;
  bysetpos   = rrBySetPosList;
  wkst       = rrWeekStart;
*/

// TODO: private API in the header file?!
@interface iCalRecurrenceRule (PrivateAPI)

- (iCalWeekDay) weekDayFromICalRepresentation: (NSString *) _day;
- (NSString *) freq;
- (NSString *) wkst;
- (NSString *) byDayList;

@end

@implementation iCalRecurrenceRule

+ (id) recurrenceRuleWithICalRepresentation: (NSString *) _iCalRep
{
  iCalRecurrenceRule *rule;

  rule = [self elementWithTag: @"rrule"];
  [rule setRrule: _iCalRep];

  return rule;
}

- (id) init
{
  if ((self = [super init]) != nil)
    {
      [self setTag: @"rrule"];
      dayMask = nil;
    }

  return self;
}

- (id) initWithString: (NSString *) _str
{
  if ((self = [self init]))
    {
      [self setRrule: _str];
    }

  return self;
}

- (void) dealloc
{
  [dayMask release];
  [super dealloc];
}

- (void) setRrule: (NSString *) _rrule
{
  CardGroup *mockParent;
  NSString *wrappedRule;
  CardElement *mockRule;

  if ([_rrule length] > 0)
    {
      wrappedRule = [NSString stringWithFormat:
                                @"BEGIN:MOCK\r\nRRULE:%@\r\nEND:MOCK",
                              _rrule];
      mockParent = [CardGroup parseSingleFromSource: wrappedRule];
      mockRule = [mockParent uniqueChildWithTag: @"rrule"];
      [values release];
      values = [[mockRule values] mutableCopy];
    }
}

- (iCalRecurrenceFrequency) valueForFrequency: (NSString *) value
{
  NSString *frequency;
  iCalRecurrenceFrequency freq;

  freq = 0;

  if ([value length] > 0)
    {
      frequency = [value uppercaseString];
      if ([frequency isEqualToString:@"WEEKLY"])
	freq = iCalRecurrenceFrequenceWeekly;
      else if ([frequency isEqualToString:@"MONTHLY"])
	freq = iCalRecurrenceFrequenceMonthly;
      else if ([frequency isEqualToString:@"DAILY"])
	freq = iCalRecurrenceFrequenceDaily;
      else if ([frequency isEqualToString:@"YEARLY"])
	freq = iCalRecurrenceFrequenceYearly;
      else if ([frequency isEqualToString:@"HOURLY"])
	freq = iCalRecurrenceFrequenceHourly;
      else if ([frequency isEqualToString:@"MINUTELY"])
	freq = iCalRecurrenceFrequenceMinutely;
      else if ([frequency isEqualToString:@"SECONDLY"])
	freq = iCalRecurrenceFrequenceSecondly;
    }

  return freq;
}

- (NSString *) frequencyForValue: (iCalRecurrenceFrequency) freq
{
  NSString *frequency;

  switch (freq)
    {
    case iCalRecurrenceFrequenceWeekly:
      frequency = @"WEEKLY";
      break;
    case iCalRecurrenceFrequenceMonthly:
      frequency = @"MONTHLY";
      break;
    case iCalRecurrenceFrequenceDaily:
      frequency = @"DAILY";
      break;
    case iCalRecurrenceFrequenceYearly:
      frequency = @"YEARLY";
      break;
    case iCalRecurrenceFrequenceHourly:
      frequency = @"HOURLY";
      break;
    case iCalRecurrenceFrequenceMinutely:
      frequency = @"MINUTELY";
      break;
    case iCalRecurrenceFrequenceSecondly:
      frequency = @"SECONDLY";
      break;
    default:
      frequency = nil;
    }

  return frequency;
}

/* accessors */

- (void) setFrequency: (iCalRecurrenceFrequency) _frequency
{
  [self setSingleValue: [self frequencyForValue: _frequency] forKey: @"freq"];
}

- (iCalRecurrenceFrequency) frequency
{
  return [self valueForFrequency: [self flattenedValuesForKey: @"freq"]];
}

- (void) setUntilDate: (NSCalendarDate *) _untilDate
{
  [self setSingleValue: [_untilDate icalString] forKey: @"until"];
}

- (NSCalendarDate *) untilDate
{
#warning handling of default timezone needs to be implemented
  return [[self flattenedValuesForKey: @"until"] asCalendarDate];
}

- (void) setInterval: (NSString *) _interval
{
  if ([_interval intValue] < 2)
    [self setSingleValue: nil forKey: @"interval"];
  else
    [self setSingleValue: _interval forKey: @"interval"];
}

- (void) setRepeatInterval: (int) _repeatInterval
{
  [self setInterval: [NSString stringWithFormat: @"%d", _repeatInterval]];
}

- (int) repeatInterval
{
  int interval;

  interval = [[self flattenedValuesForKey: @"interval"] intValue];
  if (interval < 1)
    interval = 1;

  return interval;
}

- (void) setRepeatCount: (int) _repeatCount
{
  [self setSingleValue: [NSString stringWithFormat: @"%d", _repeatCount]
                forKey: @"count"];
}

- (int) repeatCount
{
  return [[self flattenedValuesForKey: @"count"] intValue];
}

- (BOOL) hasRepeatCount
{
  return [[self flattenedValuesForKey: @"count"] length] > 0;
}

- (void) setCount: (NSString *) _count
{
  [self setSingleValue: _count forKey: @"count"];
}

- (void) setUntil: (NSString *) _until
{
  [self setSingleValue: _until forKey: @"until"];
}

- (void) setWkst: (NSString *) _weekStart
{
  [self setSingleValue: _weekStart forKey: @"wkst"];
}

#warning we also should handle the user weekstarts
- (NSString *) wkst
{
  NSString *start;

  start = [self flattenedValuesForKey: @"wkst"];
  if (![start length])
    start = @"MO";

  return start;
}

- (void) setWeekStart: (iCalWeekDay) _weekStart
{
  [self setWkst: [self iCalRepresentationForWeekDay: _weekStart]];
}

- (iCalWeekDay) weekStart
{
  return [self weekDayFromICalRepresentation: [self wkst]];
}

- (void) setByDay: (NSString *) newByDay
{
  NSMutableArray *byDays;

  byDays = [[newByDay componentsSeparatedByString: @","] mutableCopy];
  [self setValues: byDays atIndex: 0 forKey: @"byday"];
  [byDays release];
}

- (NSString *) byDay
{
  return [self flattenedValuesForKey: @"byday"];
}

- (void) setByDayMask: (iCalByDayMask *) newByDayMask
{
  [self setByDay: [newByDayMask asRuleString]];
}

- (iCalByDayMask *) byDayMask
{
  NSArray *setPos;
  NSString *day;

  if (dayMask == nil && [[self byDay] length])
    {
      day = [self byDay];
      setPos = [self bySetPos];
      if ([setPos count])
        day = [NSString stringWithFormat: @"%@%@", [setPos lastObject], day];
      dayMask = [iCalByDayMask byDayMaskWithRuleString: day];
      [dayMask retain];
    }
  
  return dayMask;
}

- (NSArray *) byMonthDay
{
  NSArray *byMonthDay;

  byMonthDay = [self valuesAtIndex: 0 forKey: @"bymonthday"];
  if (![byMonthDay count])
    byMonthDay = nil;

  return byMonthDay;
}

- (NSArray *) byMonth
{
  NSArray *byMonth;

  byMonth = [self valuesAtIndex: 0 forKey: @"bymonth"];
  if (![byMonth count])
    byMonth = nil;

  return byMonth;
}

- (BOOL) hasByMask
{
  /* There are more BYxxx rule parts but we don't support them yet :
   * - BYYEARDAY
   * - BYWEEKNO
   * - BYHOUR
   * - BYMINUTE
   * - BYSECOND
   * - BYSETPOS
   */
  return ([[self valuesAtIndex: 0 forKey: @"bymonthday"] count] || 
	  [[self valuesAtIndex: 0 forKey: @"byday"] count] ||
	  [[self valuesAtIndex: 0 forKey: @"bymonth"] count]);
}

- (BOOL) isInfinite
{
  return !(([self repeatCount] && [self repeatCount] > 0) || [self untilDate]);
}

/* private */

- (iCalWeekDay) weekDayFromICalRepresentation: (NSString *) _day
{
  /* be tolerant */
  iCalWeekDay foundDay;
  unichar chars[2];
  unsigned int dayLength;

  foundDay = 0;

  dayLength = [_day length];
  if (dayLength > 1)
    {
      // Ignore any prefix, only consider last two characters
      [[_day uppercaseString] getCharacters: chars
			      range: NSMakeRange (dayLength - 2, 2)];

      switch (chars[0])
	{
	case 'M': foundDay = iCalWeekDayMonday;
	  break;
	case 'W': foundDay = iCalWeekDayWednesday;
	  break;
	case 'F': foundDay = iCalWeekDayFriday;
	  break;
	case 'T':
	  if (chars[1] == 'U')
	    foundDay = iCalWeekDayTuesday;
	  else if (chars[1] == 'H')
	    foundDay = iCalWeekDayThursday;
	  break;
	case 'S':
	  if (chars[1] == 'A')
	    foundDay = iCalWeekDaySaturday;
	  else if (chars[1] == 'U')
	    foundDay = iCalWeekDaySunday;
	  break;
	}
    }

  if (!foundDay)
    [self errorWithFormat: @"wrong weekday representation: '%@'", _day];

  return foundDay;
//   // TODO: do not raise but rather return an error value?
//   [NSException raise:NSGenericException
// 	       format:@"Incorrect weekDay '%@' specified!", _day];
//   return iCalWeekDayMonday; /* keep compiler happy */
}

- (NSString *) iCalRepresentationForWeekDay: (iCalWeekDay) _weekDay
{
  switch (_weekDay)
    {
    case iCalWeekDayMonday: return @"MO";
    case iCalWeekDayTuesday: return @"TU";
    case iCalWeekDayWednesday: return @"WE";
    case iCalWeekDayThursday: return @"TH";
    case iCalWeekDayFriday: return @"FR";
    case iCalWeekDaySaturday: return @"SA";
    case iCalWeekDaySunday: return @"SU";
    default:  return @"MO"; // TODO: return error?
    }
}

- (NSArray *) bySetPos
{
  NSArray *lists, *bySetPos;

  lists = [self valuesForKey: @"bysetpos"];
  if ([lists count] > 0)
    bySetPos = [lists objectAtIndex: 0];
  else
    bySetPos = nil;

  return bySetPos;
}

// - (iCalWeekDay) weekDayForiCalRepre: (NSString *) _weekDay
// {
//   iCalWeekDay day;
//   NSString *weekDay;

//   weekDay = [_weekDay uppercaseString];
//   if ([weekDay isEqualToString: @"TU"])
//     day = iCalWeekDayTuesday;
//   else if ([weekDay isEqualToString: @"WE"])
//     day = iCalWeekDayWednesday;
//   else if ([weekDay isEqualToString: @"TH"])
//     day = iCalWeekDayThursday;
//   else if ([weekDay isEqualToString: @"FR"])
//     day = iCalWeekDayFriday;
//   else if ([weekDay isEqualToString: @"SA"])
//     day = iCalWeekDaySaturday;
//   else if ([weekDay isEqualToString: @"SU"])
//     day = iCalWeekDaySunday;
//   else
//     day = iCalWeekDayMonday;

//   return day;
// }

// - (NSString *) wkst
// {
//   return [self iCalRepresentationForWeekDay:byDay.weekStart];
// }

/*
  TODO:
  Each BYDAY value can also be preceded by a positive (+n) or negative
  (-n) integer. If present, this indicates the nth occurrence of the
  specific day within the MONTHLY or YEARLY RRULE. For example, within
  a MONTHLY rule, +1MO (or simply 1MO) represents the first Monday
  within the month, whereas -1MO represents the last Monday of the
  month. If an integer modifier is not present, it means all days of
  this type within the specified frequency. For example, within a
  MONTHLY rule, MO represents all Mondays within the month.
*/
// - (NSString *) byDayList
// {
//   NSMutableString *s;
//   unsigned        dow, mask, day;
//   BOOL            needsComma;
  
//   s          = [NSMutableString stringWithCapacity:20];
//   needsComma = NO;
//   mask       = byDay.mask;
//   day        = iCalWeekDayMonday;
  
//   for (dow = 0 /* Sun */; dow < 7; dow++) {
//     if (mask & day) {
//       if (needsComma)
//         [s appendString:@","];
      
//       if (byDay.useOccurence)
// 	// Note: we only support one occurrence for all currently
// 	[s appendFormat:@"%i", byDayOccurence1];
      
//       [s appendString:[self iCalRepresentationForWeekDay:day]];
//       needsComma = YES;
//     }
//     day = (day << 1);
//   }

//   return s;
// }

/* parsing rrule */

// - (void) _parseRuleString: (NSString *) _rrule
// {
//   // TODO: to be exact we would need a timezone to properly process the 'until'
//   //       date
//   unsigned i, count;
//   NSString *pFrequency = nil;
//   NSString *pUntil     = nil;
//   NSString *pCount     = nil;
//   NSString *pByday     = nil;
//   NSString *pBymday    = nil;
//   NSString *pBysetpos  = nil;
//   NSString *pInterval  = nil;
  
//   for (i = 0, count = [values count]; i < count; i++) {
//     NSString *prop, *key, *value;
//     NSRange  r;
//     NSString **vHolder = NULL;
    
//     prop = [values objectAtIndex:i];
//     r    = [prop rangeOfString:@"="];
//     if (r.length > 0) {
//       key   = [prop substringToIndex:r.location];
//       value = [prop substringFromIndex:NSMaxRange(r)];
//     }
//     else {
//       key   = prop;
//       value = nil;
//     }
    
//     key = [[key stringByTrimmingSpaces] lowercaseString];
//     if (![key isNotEmpty]) {
//       [self errorWithFormat:@"empty component in rrule: %@", _rrule];
//       continue;
//     }
    
//     vHolder = NULL;
//     switch ([key characterAtIndex:0]) {
//     case 'b':
//       if ([key isEqualToString:@"byday"])      { vHolder = &pByday;    break; }
//       if ([key isEqualToString:@"bymonthday"]) { vHolder = &pBymday;   break; }
//       if ([key isEqualToString:@"bysetpos"])   { vHolder = &pBysetpos; break; }
//       break;
//     case 'c':
//       if ([key isEqualToString:@"count"]) { vHolder = &pCount; break; }
//       break;
//     case 'f':
//       if ([key isEqualToString:@"freq"]) { vHolder = &pFrequency; break; }
//       break;
//     case 'i':
//       if ([key isEqualToString:@"interval"]) { vHolder = &pInterval; break; }
//       break;
//     case 'u':
//       if ([key isEqualToString:@"until"]) { vHolder = &pUntil; break; }
//       break;
//     default:
//       break;
//     }
    
//     if (vHolder != NULL) {
//       if ([*vHolder isNotEmpty])
//         [self errorWithFormat:@"more than one '%@' in: %@", key, _rrule];
//       else
//         *vHolder = [value copy];
//     }
//     else {
//       // TODO: we should just parse known keys and put remainders into a
//       //       separate dictionary
//       [self logWithFormat:@"TODO: add explicit support for key: %@", key];
//       [self takeValue:value forKey:key];
//     }
//   }
  
//   /* parse and fill individual values */
//   // TODO: this method should be a class method and create a new rrule object
  
//   if ([pFrequency isNotEmpty])
//     [self setNamedValue: @"FREQ" to: pFrequency];
//   else
//     [self errorWithFormat:@"rrule contains no frequency: '%@'", _rrule];
//   [pFrequency release]; pFrequency = nil;

//   if (pInterval != nil)
//     interval = [pInterval intValue];
//   [pInterval release]; pInterval = nil;
  
//   // TODO: we should parse byday in here
//   if (pByday != nil) [self setByday:pByday];
//   [pByday release]; pByday = nil;

//   if (pBymday != nil) {
//     NSArray *t;
    
//     t = [pBymday componentsSeparatedByString:@","];
//     ASSIGNCOPY(byMonthDay, t);
//   }
//   [pBymday release]; pBymday = nil;
  
//   if (pBysetpos != nil)
//     // TODO: implement
//     [self errorWithFormat:@"rrule contains bysetpos, unsupported: %@", _rrule];
//   [pBysetpos release]; pBysetpos = nil;
  
//   if (pUntil != nil) {
//     NSCalendarDate *pUntilDate;
    
//     if (pCount != nil) {
//       [self errorWithFormat:@"rrule contains 'count' AND 'until': %@", _rrule];
//       [pCount release];
//       pCount = nil;
//     }
    
//     /*
//       The spec says:
//         "If specified as a date-time value, then it MUST be specified in an
//          UTC time format."
//       TODO: we still need some object representing a 'timeless' date.
//     */
//     if (![pUntil hasSuffix:@"Z"] && [pUntil length] > 8) {
//       [self warnWithFormat:@"'until' date has no explicit UTC marker: '%@'",
//               _rrule];
//     }
    
//     pUntilDate = [NSCalendarDate calendarDateWithICalRepresentation:pUntil];
//     if (pUntilDate != nil)
//       [self setUntilDate:pUntilDate];
//     else {
//       [self errorWithFormat:@"could not parse 'until' in rrule: %@", 
//               _rrule];
//     }
//   }
//   [pUntil release]; pUntil = nil;
  
//   if (pCount != nil) 
//     [self setRepeatCount:[pCount intValue]];
//   [pCount release]; pCount = nil;
// }

/* properties */

// - (void) setByday: (NSString *) _byDayList
// {
//   // TODO: each day can have an associated occurence, eg:
//   //        +1MO,+2TU,-9WE
//   // TODO: this should be moved to the parser
//   NSArray  *days;
//   unsigned i, count;
//   NSString    *iCalDay;
//   iCalWeekDay day;
//   unsigned    len;
//   unichar     c0;
//   int         occurence;
//   int offset;

//   /* reset mask */
//   byDay.mask = 0;
//   byDay.useOccurence = 0;
//   byDayOccurence1 = 0;
  
//   days  = [_byDayList componentsSeparatedByString:@","];
//   for (i = 0, count = [days count]; i < count; i++)
//     {
//       iCalDay = [days objectAtIndex:i]; // eg: MO or TU
//       if ((len = [iCalDay length]) == 0)
//         {
//           [self errorWithFormat:@"found an empty day in byday list: '%@'", 
//                 _byDayList];
//           continue;
//         }
    
//       c0 = [iCalDay characterAtIndex:0];
//       if (((c0 == '+' || c0 == '-') && len > 2) || (isdigit(c0) && len > 1)) {
//         occurence = [iCalDay intValue];
      
//         offset = 1; /* skip occurence */
//         while (offset < len && isdigit([iCalDay characterAtIndex:offset]))
//           offset++;
        
//         iCalDay = [iCalDay substringFromIndex:offset];
        
//         if (byDay.useOccurence && (occurence != byDayOccurence1))
//           {
//             [self errorWithFormat:
//                     @"we only supported one occurence (occ=%i,day=%@): '%@'", 
//                   occurence, iCalDay, _byDayList];
//             continue;
//           }
        
//         byDay.useOccurence = 1;
//         byDayOccurence1 = occurence;
//       }
//     else if (byDay.useOccurence)
//       [self errorWithFormat:
// 	      @"a byday occurence was specified on one day, but not on others"
//             @" (unsupported): '%@'", _byDayList];
    
//       day = [self weekDayFromICalRepresentation:iCalDay];
//       byDay.mask |= day;
//     }
// }

/* versit key ordering */
- (NSArray *) orderOfValueKeys
{
  return [NSArray arrayWithObjects: @"freq", @"interval", @"count", @"until",
                  @"bymonth", @"byweekno", @"byyearday", @"bymonthday",
                  @"byday", @"byhour", @"byminute", @"bysecond", @"bysetpos",
                  nil];
}

/* key/value coding */

- (void) handleTakeValue: (id) _value
           forUnboundKey: (NSString *)_key
{
  [self warnWithFormat:@"Cannot handle unbound key: '%@'", _key];
}

- (BOOL) isEqual: (id) rrule
{
  BOOL isEqual = YES;

  if ([rrule isKindOfClass: [iCalRecurrenceRule class]])
    {
      /*
      NSLog(@"*** iCalRecurrenceRule comparison ***");      
      NSLog(@"Event 1 : repeat %i, interval %i, frequency %i, until %@",
	    [self repeatCount], [self repeatInterval], [self frequency], [self untilDate]);
      NSLog(@"Event 2 : repeat %i, interval %i, frequency %i, until %@",
	    [rrule repeatCount], [rrule repeatInterval], [rrule frequency], [rrule untilDate]);
      */

      if ([self untilDate] && [rrule untilDate])
	isEqual = [[self untilDate] isEqual: [rrule untilDate]];
      else if ([self untilDate] || [self untilDate])
	isEqual = NO;
      
      isEqual = isEqual &&
	[self repeatCount] == [rrule repeatCount] &&
	[self repeatInterval] == [rrule repeatInterval] &&
	[self frequency] == [rrule frequency];
    }
  else
    isEqual = NO;
  
  return isEqual;
}

- (NSString *) description
{
  NSMutableString *str;

  str = [NSMutableString stringWithString: [self flattenedValuesForKey: @"freq"]];

  if ([self repeatInterval] > 1)
    [str appendFormat: @" interval of %i", [self repeatInterval]];
  if ([self repeatCount])
    [str appendFormat: @" for %i times", [self repeatCount]];
  if ([self untilDate])
    [str appendFormat: @" until %@", [self untilDate]];
  if ([[self valuesAtIndex: 0 forKey: @"bymonthday"] count])
    [str appendFormat: @" BYMONTHDAY:%@", [self flattenedValuesForKey: @"bymonthday"]];
  if ([[self valuesAtIndex: 0 forKey: @"byday"] count])
    [str appendFormat: @" BYDAY:%@", [self flattenedValuesForKey: @"byday"]];
  if ([[self valuesAtIndex: 0 forKey: @"bymonth"] count])
    [str appendFormat: @" BYMONTH:%@", [self flattenedValuesForKey: @"bymonth"]];
  if ([[self valuesAtIndex: 0 forKey: @"bysetpos"] count])
    [str appendFormat: @" BYSETPOS:%@", [self flattenedValuesForKey: @"bysetpos"]];

  return str;
}

@end /* iCalRecurrenceRule */
