1/*
2  Copyright (C) 2004-2005 SKYRIX Software AG
3  Copyright (C) 2012 Inverse inc.
4
5  This file is part of SOPE.
6
7  SOPE is free software; you can redistribute it and/or modify it under
8  the terms of the GNU Lesser General Public License as published by the
9  Free Software Foundation; either version 2, or (at your option) any
10  later version.
11
12  SOPE is distributed in the hope that it will be useful, but WITHOUT ANY
13  WARRANTY; without even the implied warranty of MERCHANTABILITY or
14  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
15  License for more details.
16
17  You should have received a copy of the GNU Lesser General Public
18  License along with SOPE; see the file COPYING.  If not, write to the
19  Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
20  02111-1307, USA.
21*/
22
23#import <Foundation/NSArray.h>
24#import <Foundation/NSCalendarDate.h>
25#import <Foundation/NSEnumerator.h>
26#import <Foundation/NSString.h>
27#import <Foundation/NSTimeZone.h>
28
29#import <NGExtensions/NGCalendarDateRange.h>
30
31#import "NSCalendarDate+NGCards.h"
32#import "NSString+NGCards.h"
33#import "iCalDateTime.h"
34#import "iCalEvent.h"
35#import "iCalTimeZone.h"
36#import "iCalTimeZonePeriod.h"
37#import "iCalRecurrenceRule.h"
38#import "iCalRecurrenceCalculator.h"
39#import "iCalRepeatableEntityObject.h"
40
41@implementation iCalRepeatableEntityObject
42
43- (Class) classForTag: (NSString *) classTag
44{
45  Class tagClass;
46
47  if ([classTag isEqualToString: @"RRULE"])
48    tagClass = [iCalRecurrenceRule class];
49  else if ([classTag isEqualToString: @"EXDATE"])
50    tagClass = [iCalDateTime class];
51  else
52    tagClass = [super classForTag: classTag];
53
54  return tagClass;
55}
56
57/* Accessors */
58
59- (void) removeAllRecurrenceRules
60{
61  [self removeChildren: [self recurrenceRules]];
62}
63
64- (void) addToRecurrenceRules: (id) _rrule
65{
66  [self addChild: _rrule];
67}
68
69- (void) setRecurrenceRules: (NSArray *) _rrules
70{
71  [children removeObjectsInArray: [self childrenWithTag: @"rrule"]];
72  [self addChildren: _rrules];
73}
74
75- (BOOL) hasRecurrenceRules
76{
77  return ([[self childrenWithTag: @"rrule"] count] > 0);
78}
79
80- (NSArray *) recurrenceRules
81{
82  return [self childrenWithTag: @"rrule"];
83}
84
85- (NSArray *) recurrenceRulesWithTimeZone: (id) timezone
86{
87  NSArray *rules;
88
89  rules = [self recurrenceRules];
90  return [self rules: rules withTimeZone: timezone];
91}
92
93- (void) removeAllExceptionRules
94{
95  [self removeChildren: [self exceptionRules]];
96}
97
98- (void) addToExceptionRules: (id) _rrule
99{
100  [self addChild: _rrule];
101}
102
103- (void) setExceptionRules: (NSArray *) _rrules
104{
105  [children removeObjectsInArray: [self childrenWithTag: @"exrule"]];
106  [self addChildren: _rrules];
107}
108
109- (BOOL) hasExceptionRules
110{
111  return ([[self childrenWithTag: @"exrule"] count] > 0);
112}
113
114- (NSArray *) exceptionRules
115{
116  return [self childrenWithTag: @"exrule"];
117}
118
119- (NSArray *) exceptionRulesWithTimeZone: (id) timezone
120{
121  NSArray *rules;
122
123  rules = [self exceptionRules];
124  return [self rules: rules withTimeZone: timezone];
125}
126
127/**
128 * Returns a new set of rules, but with "until dates" adjusted to the
129 * specified timezone.
130 * Used when calculating a recurrence/exception rule.
131 * @param theRules the iCalRecurrenceRule instances
132 * @param theTimeZone the timezone of the entity.
133 * @see recurrenceRulesWithTimeZone:
134 * @see exceptionRulesWithTimeZone:
135 * @return a new array of iCalRecurrenceRule instances, adjusted for the timezone.
136 */
137- (NSArray *) rules: (NSArray *) theRules withTimeZone: (id) theTimeZone
138{
139  NSArray *rules;
140  NSCalendarDate *untilDate;
141  NSMutableArray *fixedRules;
142  iCalRecurrenceRule *currentRule;
143  int offset;
144  unsigned int max, count;
145
146  rules = theRules;
147  if (theTimeZone)
148    {
149      max = [rules count];
150      if (max)
151	{
152	  fixedRules = [NSMutableArray arrayWithCapacity: max];
153	  for (count = 0; count < max; count++)
154	    {
155	      currentRule = [rules objectAtIndex: count];
156	      untilDate = [currentRule untilDate];
157	      if (untilDate)
158		{
159                  if ([theTimeZone isKindOfClass: [iCalTimeZone class]])
160                    untilDate = [(iCalTimeZone *) theTimeZone computedDateForDate: untilDate];
161                  else
162                    {
163                      offset = [(NSTimeZone *) theTimeZone secondsFromGMTForDate: untilDate];
164                      untilDate = (NSCalendarDate *) [untilDate dateByAddingYears:0 months:0 days:0 hours:0 minutes:0
165                                                                          seconds:-offset];
166                    }
167		  [currentRule setUntilDate: untilDate];
168		}
169	      [fixedRules addObject: currentRule];
170	    }
171	  rules = fixedRules;
172	}
173    }
174
175  return rules;
176}
177
178- (void) removeAllExceptionDates
179{
180  [self removeChildren: [self childrenWithTag: @"exdate"]];
181}
182
183- (void) addToExceptionDates: (NSCalendarDate *) _rdate
184{
185  iCalDateTime *dateTime;
186
187  dateTime = [iCalDateTime new];
188  [dateTime setTag: @"exdate"];
189  if ([self isKindOfClass: [iCalEvent class]] && [(iCalEvent *)self isAllDay])
190    [dateTime setDate: _rdate];
191  else
192    [dateTime setDateTime: _rdate];
193  [self addChild: dateTime];
194  [dateTime release];
195}
196
197//- (void) setExceptionDates: (NSArray *) _rdates
198//{
199//  [children removeObjectsInArray: [self childrenWithTag: @"exdate"]];
200//  [self addChildren: _rdates];
201//}
202
203- (BOOL) hasExceptionDates
204{
205  return ([[self childrenWithTag: @"exdate"] count] > 0);
206}
207
208/**
209 * Return the exception dates of the entity in GMT.
210 * @return an array of strings.
211 */
212- (NSArray *) exceptionDates
213{
214  NSArray *exDates;
215  NSMutableArray *dates;
216  NSEnumerator *dateList;
217  NSCalendarDate *exDate;
218  NSString *dateString;
219  unsigned i;
220
221  dates = [NSMutableArray array];
222  dateList = [[self childrenWithTag: @"exdate"] objectEnumerator];
223
224  while ((dateString = [dateList nextObject]))
225    {
226      exDates = [(iCalDateTime*) dateString dateTimes];
227      for (i = 0; i < [exDates count]; i++)
228	{
229	  exDate = [exDates objectAtIndex: i];
230	  dateString = [NSString stringWithFormat: @"%@Z",
231				 [exDate iCalFormattedDateTimeString]];
232	  [dates addObject: dateString];
233	}
234    }
235
236  return dates;
237}
238
239/**
240 * Returns the exception dates for the entity, but adjusted to the entity timezone.
241 * Used when calculating a recurrence rule.
242 * @param theTimeZone the timezone of the entity.
243 * @see [iCalTimeZone computedDatesForStrings:]
244 * @return the exception dates, adjusted to the timezone.
245 */
246- (NSArray *) exceptionDatesWithTimeZone: (id) theTimeZone
247{
248  NSArray *dates, *exDates;
249  NSEnumerator *dateList;
250  NSCalendarDate *exDate;
251  NSString *dateString;
252  int offset;
253  unsigned i;
254
255  if (theTimeZone)
256    {
257      dates = [NSMutableArray array];
258      dateList = [[self childrenWithTag: @"exdate"] objectEnumerator];
259
260      while ((dateString = [dateList nextObject]))
261	{
262          exDates = [(iCalDateTime*) dateString dateTimes];
263          for (i = 0; i < [exDates count]; i++)
264	    {
265	      exDate = [exDates objectAtIndex: i];
266
267              // Example: timezone is -0400, date is 2012-05-24 (00:00:00 +0000),
268              //                      and changes to 2012-05-24 04:00:00 +0000
269              if ([theTimeZone isKindOfClass: [iCalTimeZone class]])
270                {
271                    exDate = [(iCalTimeZone *) theTimeZone computedDateForDate: exDate];
272                }
273              else
274                {
275                  offset = [(NSTimeZone *) theTimeZone secondsFromGMTForDate: exDate];
276                  exDate = (NSCalendarDate *) [exDate dateByAddingYears:0 months:0 days:0 hours:0 minutes:0
277                                                               seconds:-offset];
278                }
279	      [(NSMutableArray *) dates addObject: exDate];
280   	    }
281	}
282    }
283  else
284    dates = [self exceptionDates];
285
286  return dates;
287}
288
289/* Convenience */
290
291- (BOOL) isRecurrent
292{
293  return [self hasRecurrenceRules];
294}
295
296/* Matching */
297
298- (BOOL) isWithinCalendarDateRange: (NGCalendarDateRange *) _range
299    firstInstanceCalendarDateRange: (NGCalendarDateRange *) _fir
300{
301  NSArray *ranges;
302
303  ranges = [self recurrenceRangesWithinCalendarDateRange:_range
304                 firstInstanceCalendarDateRange:_fir];
305  return [ranges count] > 0;
306}
307
308- (NSArray *) recurrenceRangesWithinCalendarDateRange: (NGCalendarDateRange *)_r
309                       firstInstanceCalendarDateRange: (NGCalendarDateRange *)_fir
310{
311  return [iCalRecurrenceCalculator recurrenceRangesWithinCalendarDateRange: _r
312                                   firstInstanceCalendarDateRange: _fir
313                                   recurrenceRules: [self recurrenceRules]
314                                   exceptionRules: [self exceptionRules]
315                                   exceptionDates: [self exceptionDates]];
316}
317
318
319/* this is the outmost bound possible, not necessarily the real last date */
320-    (NSCalendarDate *)
321lastPossibleRecurrenceStartDateUsingFirstInstanceCalendarDateRange: (NGCalendarDateRange *)_r
322{
323  NSCalendarDate *date;
324  NSEnumerator *rRules;
325  iCalRecurrenceRule *rule;
326  iCalRecurrenceCalculator *calc;
327  NSCalendarDate *rdate;
328
329  date  = nil;
330
331  rRules = [[self recurrenceRules] objectEnumerator];
332  rule = [rRules nextObject];
333  while (rule && ![rule isInfinite] && !date)
334    {
335      calc = [iCalRecurrenceCalculator
336               recurrenceCalculatorForRecurrenceRule: rule
337                  withFirstInstanceCalendarDateRange: _r];
338      rdate = [[calc lastInstanceCalendarDateRange] startDate];
339      if (!rdate)
340        date = [_r startDate];
341      else if (!date || ([date compare: rdate] == NSOrderedAscending))
342        date = rdate;
343      else
344        rule = [rRules nextObject];
345    }
346
347  return date;
348}
349
350- (NSCalendarDate *) firstRecurrenceStartDateWithEndDate: (NSCalendarDate *) endDate
351{
352  NSCalendarDate *startDate, *firstOccurrenceStartDate, *endOfFirstRange;
353  NGCalendarDateRange *range, *firstInstanceRange;
354  iCalRecurrenceFrequency frequency;
355  iCalRecurrenceRule *rule;
356  NSArray *rules, *recurrences;
357  uint32_t units;
358
359  firstOccurrenceStartDate = nil;
360
361  rules = [self recurrenceRules];
362  if ([rules count] > 0)
363    {
364      rule = [rules objectAtIndex: 0];
365      frequency = [rule frequency];
366      units = [rule repeatInterval];
367
368      startDate = [self startDate];
369      switch (frequency)
370        {
371          /* second-based units */
372        case iCalRecurrenceFrequenceWeekly:
373          units *= 7;
374        case iCalRecurrenceFrequenceDaily:
375          units *= 24;
376        case iCalRecurrenceFrequenceHourly:
377          units *= 60;
378        case iCalRecurrenceFrequenceMinutely:
379          units *= 60;
380        case iCalRecurrenceFrequenceSecondly:
381          endOfFirstRange = [startDate dateByAddingYears: 0 months: 0 days: 0
382                                                   hours: 0 minutes: 0
383                                                 seconds: units];
384          break;
385
386          /* month-based units */
387        case iCalRecurrenceFrequenceYearly:
388          units *= 12;
389        case iCalRecurrenceFrequenceMonthly:
390          endOfFirstRange = [startDate dateByAddingYears: 0 months: (units + 1)
391                                                    days: 0
392                                                   hours: 0 minutes: 0
393                                                 seconds: 0];
394          break;
395
396        default:
397          endOfFirstRange = nil;
398        }
399
400      if (endOfFirstRange)
401        {
402          range = [NGCalendarDateRange calendarDateRangeWithStartDate: startDate
403                                                              endDate: endOfFirstRange];
404          firstInstanceRange = [NGCalendarDateRange calendarDateRangeWithStartDate: startDate
405                                                                           endDate: endDate];
406          recurrences = [iCalRecurrenceCalculator recurrenceRangesWithinCalendarDateRange: range
407                                                           firstInstanceCalendarDateRange: firstInstanceRange
408                                                                          recurrenceRules: rules
409                                                                           exceptionRules: nil
410                                                                           exceptionDates: nil];
411          if ([recurrences count] > 0)
412            firstOccurrenceStartDate = [[recurrences objectAtIndex: 0]
413                                         startDate];
414        }
415    }
416
417  return firstOccurrenceStartDate;
418}
419
420- (NSCalendarDate *) lastPossibleRecurrenceStartDate
421{
422  [self subclassResponsibility: _cmd];
423
424  return nil;
425}
426
427@end
428