/*
  Copyright (C) 2004-2005 SKYRIX Software AG
  
  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 <NGExtensions/NSCalendarDate+misc.h>
#import <NGExtensions/NGCalendarDateRange.h>
#import <NGExtensions/NSNull+misc.h>
#import <NGExtensions/NSObject+Logs.h>

#import "iCalRecurrenceRule.h"
#import "NSString+NGCards.h"

#import "iCalRecurrenceCalculator.h"

/* class cluster */


/* Private */

@interface iCalRecurrenceCalculator (PrivateAPI)
- (NSCalendarDate *)lastInstanceStartDate;

- (unsigned)offsetFromSundayForJulianNumber:(long)_jn;
- (unsigned)offsetFromSundayForWeekDay:(iCalWeekDay)_weekDay;
- (unsigned)offsetFromSundayForCurrentWeekStart;
  
- (iCalWeekDay)weekDayForJulianNumber:(long)_jn;
@end

@implementation iCalRecurrenceCalculator

static Class NSCalendarDateClass     = Nil;
static Class NSStringClass     = Nil;
static Class iCalRecurrenceRuleClass = Nil;
static Class dailyCalcClass   = Nil;
static Class weeklyCalcClass  = Nil;
static Class monthlyCalcClass = Nil;
static Class yearlyCalcClass  = Nil;

+ (void) initialize
{
  static BOOL didInit = NO;
  
  if (didInit) return;
  didInit = YES;

  NSCalendarDateClass     = [NSCalendarDate class];
  NSStringClass = [NSString class];
  iCalRecurrenceRuleClass = [iCalRecurrenceRule class];

  dailyCalcClass   = NSClassFromString(@"iCalDailyRecurrenceCalculator");
  weeklyCalcClass  = NSClassFromString(@"iCalWeeklyRecurrenceCalculator");
  monthlyCalcClass = NSClassFromString(@"iCalMonthlyRecurrenceCalculator");
  yearlyCalcClass  = NSClassFromString(@"iCalYearlyRecurrenceCalculator");
}

/* factory */

+ (id)  recurrenceCalculatorForRecurrenceRule: (iCalRecurrenceRule *) _rrule
	   withFirstInstanceCalendarDateRange: (NGCalendarDateRange *) _range
{
  iCalRecurrenceFrequency freq;
  Class calcClass;
  id calc;

  freq = [_rrule frequency];
  if (freq == iCalRecurrenceFrequenceDaily)
    calcClass = dailyCalcClass;
  else if (freq == iCalRecurrenceFrequenceWeekly)
    calcClass = weeklyCalcClass;
  else if (freq == iCalRecurrenceFrequenceMonthly)
    calcClass = monthlyCalcClass;
  else if (freq == iCalRecurrenceFrequenceYearly)
    calcClass = yearlyCalcClass;
  else
    calcClass = Nil;

  if (calcClass)
    {
      calc = [[calcClass alloc] initWithRecurrenceRule:_rrule
				firstInstanceCalendarDateRange: _range];
      [calc autorelease];
    }
  else
    {
      [self errorWithFormat: @"unsupported rrule frequency: %@", _rrule];
      calc = nil;
    }

  return calc;
}

/* complex calculation convenience */

+ (void)    _fillRanges: (NSMutableArray *) ranges
              fromRules: (NSArray *) rrules
	    withinRange: (NGCalendarDateRange *) limits
       startingWithDate: (NGCalendarDateRange *) first
{
  NSEnumerator *rules;
  iCalRecurrenceRule *currentRule;
  iCalRecurrenceCalculator *calc;

  rules = [rrules objectEnumerator];
  while ((currentRule = [rules nextObject]))
    {
      if ([currentRule isKindOfClass: NSStringClass])
	currentRule =
	  [iCalRecurrenceRule
	    recurrenceRuleWithICalRepresentation: (NSString *) currentRule];

      calc = [self recurrenceCalculatorForRecurrenceRule: currentRule
		   withFirstInstanceCalendarDateRange: first];
      [ranges addObjectsFromArray:
		[calc recurrenceRangesWithinCalendarDateRange: limits]];
    }
}

+ (void)    _fillRanges: (NSMutableArray *) ranges
              fromDates: (NSArray *) rdates
	    withinRange: (NGCalendarDateRange *) limits
       startingWithDate: (NGCalendarDateRange *) first
{
  NSArray *dates;

  dates = [self _ranges: rdates withinRange: limits  startingWithDate: first];
  [ranges addObjectsFromArray: dates];
}

+ (void) _removeExceptionsFromRanges: (NSMutableArray *) ranges
			   withRules: (NSArray *) exrules
			 withinRange: (NGCalendarDateRange *) limits
		    startingWithDate: (NGCalendarDateRange *) first
{
  NSEnumerator *rules;
  iCalRecurrenceRule *currentRule;
  iCalRecurrenceCalculator *calc;

  rules = [exrules objectEnumerator];
  while ((currentRule = [rules nextObject]))
    {
      if ([currentRule isKindOfClass: NSStringClass])
	currentRule =
	  [iCalRecurrenceRule
	    recurrenceRuleWithICalRepresentation: (NSString *) currentRule];
	  
      calc = [self recurrenceCalculatorForRecurrenceRule: currentRule
		   withFirstInstanceCalendarDateRange: first];
      [ranges removeObjectsInArray:
		[calc recurrenceRangesWithinCalendarDateRange: limits]];
    }
}

+ (NSArray *) _dates: (NSArray *) dateList
         withinRange: (NGCalendarDateRange *) limits
    startingWithDate: (NGCalendarDateRange *) first
{
  return [self _dates: dateList
          withinRange: limits
      startingWithDate: first
               ranges: NO];
}


+ (NSArray *) _ranges: (NSArray *) dateList
          withinRange: (NGCalendarDateRange *) limits
     startingWithDate: (NGCalendarDateRange *) first
{
  return [self _dates: dateList
          withinRange: limits
      startingWithDate: first
               ranges: YES];
}

+ (NSArray *) _dates: (NSArray *) dateList
         withinRange: (NGCalendarDateRange *) limits
    startingWithDate: (NGCalendarDateRange *) first
              ranges: (BOOL) returnRanges
{
  NSMutableArray *newDates;
  NSEnumerator *dates;
  NSCalendarDate *currentDate;
  NGCalendarDateRange *currentRange;

  newDates = [NSMutableArray array];

  dates = [dateList objectEnumerator];
  while ((currentDate = [dates nextObject]))
    {
      if ([currentDate isKindOfClass: NSStringClass])
	currentDate = [(NSString *) currentDate asCalendarDate];

      currentRange = [NGCalendarDateRange calendarDateRangeWithStartDate: currentDate
								 endDate: [currentDate dateByAddingYears: 0 months: 0 days: 0 hours: 0 minutes: 0 seconds: [first duration]]];
      if ([limits doesIntersectWithDateRange: currentRange])
        {
          if (returnRanges)
            [newDates addObject: currentRange];
          else
            [newDates addObject: currentDate];
        }
    }

  return newDates;
}

+ (void) _removeExceptionDatesFromRanges: (NSMutableArray *) ranges
			       withDates: (NSArray *) exdates
			     withinRange: (NGCalendarDateRange *) limits
			startingWithDate: (NGCalendarDateRange *) first
{
  NSEnumerator *dates;
  NSCalendarDate *currentDate;
  NGCalendarDateRange *currentRange;
  unsigned int count, maxRanges;
  NSComparisonResult compare;

  dates = [[self _dates: exdates withinRange: limits  startingWithDate: first] objectEnumerator];
  while ((currentDate = [dates nextObject]))
    {
      maxRanges = [ranges count];
      for (count = maxRanges; count > 0; count--)
	{
	  currentRange = [ranges objectAtIndex: count - 1];
          compare = [[currentRange startDate] compare: currentDate];
          if ((compare == NSOrderedAscending || compare == NSOrderedSame) &&
              [[currentRange endDate] compare: currentDate] == NSOrderedDescending)
            {
              [ranges removeObjectAtIndex: count - 1];
            }
	}
    }
}

+ (NSArray *)
 recurrenceRangesWithinCalendarDateRange: (NGCalendarDateRange *) _r
	  firstInstanceCalendarDateRange: (NGCalendarDateRange *) _fir
			 recurrenceRules: (NSArray *) _rRules
			  exceptionRules: (NSArray *) _exRules
                         recurrenceDates: (NSArray *) _rDates
			  exceptionDates: (NSArray *) _exDates
{
  NSMutableArray *ranges;

  ranges = [NSMutableArray arrayWithCapacity: 64];

  if ([_rRules count] > 0 || [_rDates count] > 0)
    {
      [self _fillRanges: ranges fromRules: _rRules
	    withinRange: _r startingWithDate: _fir];
      [self _fillRanges: ranges fromDates: _rDates
	    withinRange: _r startingWithDate: _fir];
      [self _removeExceptionsFromRanges: ranges withRules: _exRules
	    withinRange: _r startingWithDate: _fir];
      [self _removeExceptionDatesFromRanges: ranges withDates: _exDates
	    withinRange: _r startingWithDate: _fir];
    }

  return ranges;
}


/* init */

- (id)    initWithRecurrenceRule: (iCalRecurrenceRule *) _rrule
  firstInstanceCalendarDateRange: (NGCalendarDateRange *) _range
{
  if ((self = [super init]))
    {
      rrule = [_rrule retain];
      firstRange = [_range retain];
    }

  return self;  
}

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

/* helpers */

- (unsigned) offsetFromSundayForJulianNumber: (long) _jn
{
  return (unsigned)((int) (_jn + 1.5)) % 7;
}

- (unsigned) offsetFromSundayForWeekDay: (iCalWeekDay) _weekDay
{
  unsigned offset;
  
  switch (_weekDay)
    {
    case iCalWeekDaySunday:
      offset = 0; break;
    case iCalWeekDayMonday:
      offset = 1; break;
    case iCalWeekDayTuesday:
      offset = 2; break;
    case iCalWeekDayWednesday:
      offset = 3; break;
    case iCalWeekDayThursday:
      offset = 4; break;
    case iCalWeekDayFriday:
      offset = 5; break;
    case iCalWeekDaySaturday:
      offset = 6; break;
    default:
      offset = 0; break;
    }

  return offset;
}

- (unsigned) offsetFromSundayForCurrentWeekStart
{
  return [self offsetFromSundayForWeekDay: [rrule weekStart]];
}

- (iCalWeekDay) weekDayForJulianNumber: (long)_jn
{
  iCalWeekDay weekDays[] = {iCalWeekDaySunday, iCalWeekDayMonday,
			    iCalWeekDayTuesday, iCalWeekDayWednesday,
			    iCalWeekDayThursday, iCalWeekDayFriday,
			    iCalWeekDaySaturday};

  return weekDays[[self offsetFromSundayForJulianNumber: _jn]];
}

/* calculation */

- (NSArray *)
 recurrenceRangesWithinCalendarDateRange: (NGCalendarDateRange *) _r
{
  return nil; /* subclass responsibility */
}

- (BOOL) doesRecurrWithinCalendarDateRange: (NGCalendarDateRange *) _range
{
  return ([[self recurrenceRangesWithinCalendarDateRange: _range] count]
	  > 0);
}

- (NGCalendarDateRange *) firstInstanceCalendarDateRange
{
  return firstRange;
}

- (NGCalendarDateRange *) lastInstanceCalendarDateRange
{
  NSCalendarDate *start, *end;
  NGCalendarDateRange *range;

  range = nil;

  start = [self lastInstanceStartDate];
  if (start)
    {
      end = [start addTimeInterval: [firstRange duration]];
      range = [NGCalendarDateRange calendarDateRangeWithStartDate: start
				   endDate: end];
    }

  return range;
}

- (NSCalendarDate *) lastInstanceStartDate
{
  /* 
     NOTE: this is horribly inaccurate and doesn't even consider the use
           of repeatCount. It MUST be implemented by subclasses properly!
  */
  return [rrule untilDate];
}

@end /* iCalRecurrenceCalculator */
