/*
  Copyright (C) 2004-2005 SKYRIX Software AG
  Copyright (C) 2017 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.
*/

#import <Foundation/NSTimeZone.h>

#import <NGExtensions/NGCalendarDateRange.h>

#import "NSCalendarDate+NGCards.h"
#import "NSString+NGCards.h"
#import "iCalDateTime.h"
#import "iCalEvent.h"
#import "iCalTimeZone.h"
#import "iCalRecurrenceRule.h"
#import "iCalRecurrenceCalculator.h"

@implementation iCalRepeatableEntityObject

- (Class) classForTag: (NSString *) classTag
{
  Class tagClass;

  if ([classTag isEqualToString: @"RRULE"])
    tagClass = [iCalRecurrenceRule class];
  else if ([classTag isEqualToString: @"RDATE"])
    tagClass = [iCalDateTime class];
  else if ([classTag isEqualToString: @"EXDATE"])
    tagClass = [iCalDateTime class];
  else
    tagClass = [super classForTag: classTag];

  return tagClass;
}

/* Accessors */

- (void) removeAllRecurrenceRules
{
  [self removeChildren: [self recurrenceRules]];
}

- (void) addToRecurrenceRules: (id) _rrule
{
  [self addChild: _rrule];
}

- (void) setRecurrenceRules: (NSArray *) _rrules
{
  [children removeObjectsInArray: [self childrenWithTag: @"rrule"]];
  [self addChildren: _rrules];
}

- (BOOL) hasRecurrenceRules
{
  return ([[self childrenWithTag: @"rrule"] count] > 0);
}

- (NSArray *) recurrenceRules
{
  return [self childrenWithTag: @"rrule"];
}

- (NSArray *) recurrenceRulesWithTimeZone: (id) timezone
{
  NSArray *rules;

  rules = [self recurrenceRules];
  return [self rules: rules withTimeZone: timezone];
}

- (void) removeAllRecurrenceDates
{
  [self removeChildren: [self childrenWithTag: @"rdate"]];
}

- (void) addToRecurrenceDates: (NSCalendarDate *) _rdate
{
  iCalDateTime *dateTime;

  dateTime = [iCalDateTime new];
  [dateTime setTag: @"rdate"];
  if ([self isKindOfClass: [iCalEvent class]] && [(iCalEvent *)self isAllDay])
    [dateTime setDate: _rdate];
  else
    [dateTime setDateTime: _rdate];
  [self addChild: dateTime];
  [dateTime release];
}

- (BOOL) hasRecurrenceDates
{
  return ([[self childrenWithTag: @"rdate"] count] > 0);
}

/**
 * Returns the recurrence dates for the entity, but adjusted to the entity timezone.
 * @param theTimeZone the timezone of the entity.
 * @see [iCalTimeZone computedDatesForStrings:]
 * @return the exception dates, adjusted to the timezone.
 */
- (NSArray *) recurrenceDatesWithTimeZone: (id) theTimeZone
{
  NSArray *dates, *rDates;
  NSEnumerator *dateList;
  NSCalendarDate *rDate;
  NSString *dateString;
  int offset;
  unsigned i;

  if (theTimeZone)
    {
      dates = [NSMutableArray array];
      dateList = [[self childrenWithTag: @"rdate"] objectEnumerator];

      while ((dateString = [dateList nextObject]))
	{
          rDates = [(iCalDateTime*) dateString dateTimes];
          for (i = 0; i < [rDates count]; i++)
	    {
	      rDate = [rDates objectAtIndex: i];

              // Example: timezone is -0400, date is 2012-05-24 (00:00:00 +0000),
              //                      and changes to 2012-05-24 04:00:00 +0000
              if ([theTimeZone isKindOfClass: [iCalTimeZone class]])
                {
                    rDate = [(iCalTimeZone *) theTimeZone computedDateForDate: rDate];
                }
              else
                {
                  offset = [(NSTimeZone *) theTimeZone secondsFromGMTForDate: rDate];
                  rDate = (NSCalendarDate *) [rDate dateByAddingYears:0 months:0 days:0 hours:0 minutes:0
                                                               seconds:-offset];
                }
	      [(NSMutableArray *) dates addObject: rDate];
            }
	}
    }
  else
    dates = [self recurrenceDates];

  return dates;
}

/**
 * Return the recurrence dates of the entity in GMT.
 * @return an array of NSCalendarDate instances.
 */
- (NSArray *) recurrenceDates
{
  NSArray *rDates;
  NSMutableArray *dates;
  NSEnumerator *dateList;
  NSCalendarDate *rDate;
  NSString *dateString;
  unsigned i;

  dates = [NSMutableArray array];
  dateList = [[self childrenWithTag: @"rdate"] objectEnumerator];

  while ((dateString = [dateList nextObject]))
    {
      rDates = [(iCalDateTime*) dateString dateTimes];
      for (i = 0; i < [rDates count]; i++)
	{
	  rDate = [rDates objectAtIndex: i];
          [dates addObject: rDate];
	}
    }

  return dates;
}

- (void) removeAllExceptionRules
{
  [self removeChildren: [self exceptionRules]];
}

- (void) addToExceptionRules: (id) _rrule
{
  [self addChild: _rrule];
}

- (void) setExceptionRules: (NSArray *) _rrules
{
  [children removeObjectsInArray: [self childrenWithTag: @"exrule"]];
  [self addChildren: _rrules];
}

- (BOOL) hasExceptionRules
{
  return ([[self childrenWithTag: @"exrule"] count] > 0);
}

- (NSArray *) exceptionRules
{
  return [self childrenWithTag: @"exrule"];
}

- (NSArray *) exceptionRulesWithTimeZone: (id) timezone
{
  NSArray *rules;

  rules = [self exceptionRules];
  return [self rules: rules withTimeZone: timezone];
}

/**
 * Returns a new set of rules, but with "until dates" adjusted to the 
 * specified timezone.
 * Used when calculating a recurrence/exception rule.
 * @param theRules the iCalRecurrenceRule instances
 * @param theTimeZone the timezone of the entity.
 * @see recurrenceRulesWithTimeZone:
 * @see exceptionRulesWithTimeZone:
 * @return a new array of iCalRecurrenceRule instances, adjusted for the timezone.
 */
- (NSArray *) rules: (NSArray *) theRules withTimeZone: (id) theTimeZone
{
  NSArray *rules;
  NSCalendarDate *untilDate;
  NSMutableArray *fixedRules;
  iCalRecurrenceRule *currentRule;
  int offset;
  unsigned int max, count;

  rules = theRules;
  if (theTimeZone)
    {
      max = [rules count];
      if (max)
	{
	  fixedRules = [NSMutableArray arrayWithCapacity: max];
	  for (count = 0; count < max; count++)
	    {
	      currentRule = [rules objectAtIndex: count];
	      untilDate = [currentRule untilDate];
	      if (untilDate)
		{
                  if ([theTimeZone isKindOfClass: [iCalTimeZone class]])
                    untilDate = [(iCalTimeZone *) theTimeZone computedDateForDate: untilDate];
                  else
                    {
                      offset = [(NSTimeZone *) theTimeZone secondsFromGMTForDate: untilDate];
                      untilDate = (NSCalendarDate *) [untilDate dateByAddingYears:0 months:0 days:0 hours:0 minutes:0
                                                                          seconds:-offset];
                    }
		  [currentRule setUntilDate: untilDate];
		}
	      [fixedRules addObject: currentRule];
	    }
	  rules = fixedRules;
	}
    }

  return rules;
}

- (void) removeAllExceptionDates
{
  [self removeChildren: [self childrenWithTag: @"exdate"]];
}

- (void) addToExceptionDates: (NSCalendarDate *) _rdate
{
  iCalDateTime *dateTime;

  dateTime = [iCalDateTime new];
  [dateTime setTag: @"exdate"];
  if ([self isKindOfClass: [iCalEvent class]] && [(iCalEvent *)self isAllDay])
    [dateTime setDate: _rdate];
  else
    [dateTime setDateTime: _rdate];
  [self addChild: dateTime];
  [dateTime release];
}

//- (void) setExceptionDates: (NSArray *) _rdates
//{
//  [children removeObjectsInArray: [self childrenWithTag: @"exdate"]];
//  [self addChildren: _rdates];
//}

- (BOOL) hasExceptionDates
{
  return ([[self childrenWithTag: @"exdate"] count] > 0);
}

/**
 * Return the exception dates of the entity in GMT.
 * @return an array of strings.
 */
- (NSArray *) exceptionDates
{
  NSArray *exDates;
  NSMutableArray *dates;
  NSEnumerator *dateList;
  NSCalendarDate *exDate;
  NSString *dateString;
  unsigned i;

  dates = [NSMutableArray array];
  dateList = [[self childrenWithTag: @"exdate"] objectEnumerator];
  
  while ((dateString = [dateList nextObject]))
    {
      exDates = [(iCalDateTime*) dateString dateTimes];
      for (i = 0; i < [exDates count]; i++)
	{
	  exDate = [exDates objectAtIndex: i];
	  dateString = [NSString stringWithFormat: @"%@Z",
				 [exDate iCalFormattedDateTimeString]];
	  [dates addObject: dateString];
	}
    }

  return dates;
}

/**
 * Returns the exception dates for the entity, but adjusted to the entity timezone.
 * Used when calculating a recurrence rule.
 * @param theTimeZone the timezone of the entity.
 * @see [iCalTimeZone computedDatesForStrings:]
 * @return the exception dates, adjusted to the timezone.
 */
- (NSArray *) exceptionDatesWithTimeZone: (id) theTimeZone
{
  NSArray *dates, *exDates;
  NSEnumerator *dateList;
  NSCalendarDate *exDate;
  NSString *dateString;
  int offset;
  unsigned i;

  if (theTimeZone)
    {
      dates = [NSMutableArray array];
      dateList = [[self childrenWithTag: @"exdate"] objectEnumerator];
      
      while ((dateString = [dateList nextObject]))
	{
          exDates = [(iCalDateTime*) dateString dateTimes];
          for (i = 0; i < [exDates count]; i++)
	    {
	      exDate = [exDates objectAtIndex: i];

              // Example: timezone is -0400, date is 2012-05-24 (00:00:00 +0000),
              //                      and changes to 2012-05-24 04:00:00 +0000
              if ([theTimeZone isKindOfClass: [iCalTimeZone class]])
                {
                    exDate = [(iCalTimeZone *) theTimeZone computedDateForDate: exDate];
                }
              else
                {
                  offset = [(NSTimeZone *) theTimeZone secondsFromGMTForDate: exDate];
                  exDate = (NSCalendarDate *) [exDate dateByAddingYears:0 months:0 days:0 hours:0 minutes:0
                                                               seconds:-offset];
                }
	      [(NSMutableArray *) dates addObject: exDate];
   	    }
	}
    }
  else
    dates = [self exceptionDates];

  return dates;
}

/* Convenience */

- (BOOL) isRecurrent
{
  return [self hasRecurrenceRules] || [self hasRecurrenceDates];
}

/* Matching */

- (BOOL) isWithinCalendarDateRange: (NGCalendarDateRange *) _range
    firstInstanceCalendarDateRange: (NGCalendarDateRange *) _fir
{
  NSArray *ranges;
  
  ranges = [self recurrenceRangesWithinCalendarDateRange:_range
                 firstInstanceCalendarDateRange:_fir];
  return [ranges count] > 0;
}

- (NSArray *) recurrenceRangesWithinCalendarDateRange: (NGCalendarDateRange *)_r
                       firstInstanceCalendarDateRange: (NGCalendarDateRange *)_fir
{
  return [iCalRecurrenceCalculator recurrenceRangesWithinCalendarDateRange: _r
                                            firstInstanceCalendarDateRange: _fir
                                                           recurrenceRules: [self recurrenceRules]
                                                            exceptionRules: [self exceptionRules]
                                                           recurrenceDates: [self recurrenceDates]
                                                            exceptionDates: [self exceptionDates]];
}


/* this is the outmost bound possible, not necessarily the real last date */
-    (NSCalendarDate *)
lastPossibleRecurrenceStartDateUsingFirstInstanceCalendarDateRange: (NGCalendarDateRange *)_r
{
  NSCalendarDate *date;
  NSCalendarDate *rdate;

  date  = nil;

  if ([self hasRecurrenceRules])
    {
      NSEnumerator *rRules;
      iCalRecurrenceRule *rule;
      iCalRecurrenceCalculator *calc;

      rRules = [[self recurrenceRules] objectEnumerator];
      rule = [rRules nextObject];
      while (rule && ![rule isInfinite] && !date)
        {
          calc = [iCalRecurrenceCalculator
                   recurrenceCalculatorForRecurrenceRule: rule
                      withFirstInstanceCalendarDateRange: _r];
          rdate = [[calc lastInstanceCalendarDateRange] startDate];
          if (!rdate)
            date = [_r startDate];
          else if (!date || ([date compare: rdate] == NSOrderedAscending))
            date = rdate;
          else
            rule = [rRules nextObject];
        }
    }

  if ([self hasRecurrenceDates])
    {
      NSEnumerator *rDates;

      rDates = [[self recurrenceDates] objectEnumerator];
      while ((rdate = [rDates nextObject]))
        {
          if (!date || ([date compare: rdate] == NSOrderedAscending))
            date = rdate;
        }
    }

  return date;
}

- (NSCalendarDate *) firstRecurrenceStartDateWithEndDate: (NSCalendarDate *) endDate
{
  NSCalendarDate *startDate, *firstOccurrenceStartDate, *endOfFirstRange;
  NGCalendarDateRange *range, *firstInstanceRange;
  iCalRecurrenceFrequency frequency;
  iCalRecurrenceRule *rule;
  NSArray *rules, *recurrences;
  uint32_t units;

  firstOccurrenceStartDate = nil;

  rules = [self recurrenceRules];
  if ([rules count] > 0)
    {
      rule = [rules objectAtIndex: 0];
      frequency = [rule frequency];
      units = [rule repeatInterval];

      startDate = [self startDate];
      switch (frequency)
        {
          /* second-based units */
        case iCalRecurrenceFrequenceWeekly:
          units *= 7;
        case iCalRecurrenceFrequenceDaily:
          units *= 24;
        case iCalRecurrenceFrequenceHourly:
          units *= 60;
        case iCalRecurrenceFrequenceMinutely:
          units *= 60;
        case iCalRecurrenceFrequenceSecondly:
          endOfFirstRange = [startDate dateByAddingYears: 0 months: 0 days: 0
                                                   hours: 0 minutes: 0
                                                 seconds: units];
          break;

          /* month-based units */
        case iCalRecurrenceFrequenceYearly:
          units *= 12;
        case iCalRecurrenceFrequenceMonthly:
          endOfFirstRange = [startDate dateByAddingYears: 0 months: (units + 1)
                                                    days: 0
                                                   hours: 0 minutes: 0
                                                 seconds: 0];
          break;

        default:
          endOfFirstRange = nil;
        }

      if (endOfFirstRange)
        {
          range = [NGCalendarDateRange calendarDateRangeWithStartDate: startDate
                                                              endDate: endOfFirstRange];
          firstInstanceRange = [NGCalendarDateRange calendarDateRangeWithStartDate: startDate
                                                                           endDate: endDate];
          recurrences = [iCalRecurrenceCalculator recurrenceRangesWithinCalendarDateRange: range
                                                           firstInstanceCalendarDateRange: firstInstanceRange
                                                                          recurrenceRules: rules
                                                                           exceptionRules: nil
                                                                          recurrenceDates: nil
                                                                           exceptionDates: nil];
          if ([recurrences count] > 0)
            firstOccurrenceStartDate = [[recurrences objectAtIndex: 0]
                                         startDate];
        }
    }

  return firstOccurrenceStartDate;
}

- (NSCalendarDate *) lastPossibleRecurrenceStartDate
{
  [self subclassResponsibility: _cmd];

  return nil;
}

@end
