/** NSCalendar.m

   Copyright (C) 2010 Free Software Foundation, Inc.

   Written by: Stefan Bidigaray
   Date: December, 2010

   This library 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 of the License, or (at your option) any later version.

   This library 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 this library; see the file COPYING.LIB.
   If not, see <http://www.gnu.org/licenses/> or write to the
   Free Software Foundation, 31 Milk Street #960789 Boston, MA 02196 USA.
*/

#import "common.h"
#import "Foundation/NSCalendar.h"
#import "Foundation/NSCoder.h"
#import "Foundation/NSDate.h"
#import "Foundation/NSDictionary.h"
#import "Foundation/NSException.h"
#import "Foundation/NSLocale.h"
#import "Foundation/NSLock.h"
#import "Foundation/NSNotification.h"
#import "Foundation/NSString.h"
#import "Foundation/NSTimeZone.h"
#import "Foundation/NSUserDefaults.h"

#if defined(HAVE_UNICODE_UCAL_H)
#define id ucal_id
#include <unicode/ucal.h>
#include <unicode/uvernum.h>
#undef id
#elif defined(HAVE_ICU_H)
#include <icu.h>
#endif


#if GS_USE_ICU == 1
static UCalendarDateFields _NSCalendarUnitToDateField(NSCalendarUnit unit)
{
  if (unit & NSCalendarUnitEra)
    return UCAL_ERA;
  if (unit & NSCalendarUnitYear)
    return UCAL_YEAR;
  if (unit & NSCalendarUnitMonth)
    return UCAL_MONTH;
  if (unit & NSCalendarUnitDay)
    return UCAL_DAY_OF_MONTH;
  if (unit & NSCalendarUnitHour)
    return UCAL_HOUR_OF_DAY;
  if (unit & NSCalendarUnitMinute)
    return UCAL_MINUTE;
  if (unit & NSCalendarUnitSecond)
    return UCAL_SECOND;
  if (unit & NSCalendarUnitWeekOfYear)
    return UCAL_WEEK_OF_YEAR;
  if (unit & NSCalendarUnitWeekday)
    return UCAL_DAY_OF_WEEK;
  if (unit & NSCalendarUnitWeekdayOrdinal)
    return UCAL_DAY_OF_WEEK_IN_MONTH;
  return (UCalendarDateFields)-1;
}
#endif /* GS_USE_ICU */

typedef struct {
  NSString      *identifier;
  NSString      *localeID;
  NSTimeZone    *tz;
  void          *cal;
  NSInteger     firstWeekday;
  NSInteger     minimumDaysInFirstWeek;
} Calendar;
#define my ((Calendar*)_NSCalendarInternal)

@interface NSCalendar (PrivateMethods)
- (void *) _openCalendarFor: (NSTimeZone *)timeZone;
- (void) _resetCalendar;
- (void *) _UCalendar;
- (NSString *) _localeIDWithLocale: (NSLocale*)locale;
- (NSString *) _localeIdentifier;
- (void) _setLocaleIdentifier: (NSString*)identifier;
@end

static NSCalendar *autoupdatingCalendar = nil;
static NSRecursiveLock *classLock = nil;

#define TZ_NAME_LENGTH 1024
#define SECOND_TO_MILLI 1000.0
#define MILLI_TO_NANO 1000000

@implementation NSCalendar (PrivateMethods)

#if GS_USE_ICU == 1
- (void *) _openCalendarFor: (NSTimeZone *)timeZone
{
  NSString *tzName;
  NSUInteger tzLen;
  unichar cTzId[TZ_NAME_LENGTH];
  const char *cLocaleId;
  UErrorCode err = U_ZERO_ERROR;
  UCalendarType type;

  cLocaleId = [my->localeID UTF8String];
  tzName = [timeZone name];
  tzLen = [tzName length];
  if (tzLen > TZ_NAME_LENGTH)
    {
      tzLen = TZ_NAME_LENGTH;
    }
  [tzName getCharacters: cTzId range: NSMakeRange(0, tzLen)];

  if ([NSGregorianCalendar isEqualToString: my->identifier])
    {
      type = UCAL_GREGORIAN;
    }
  else
    {
#ifndef	UCAL_DEFAULT
/*
 * Older versions of ICU used UCAL_TRADITIONAL rather than UCAL_DEFAULT
 * so if one is not available we use the other.
 */
      type = UCAL_TRADITIONAL;
#else
      type = UCAL_DEFAULT;
#endif
      // We do not need to call uloc_setKeywordValue() here to set the calendar on the locale
      // as the calendar is already encoded in the locale id by _localeIDWithLocale:.
    }

  return ucal_open((const UChar *)cTzId, tzLen, cLocaleId, type, &err);
}
#endif

- (void) _resetCalendar
{
#if GS_USE_ICU == 1
  if (my->cal != NULL)
    {
      ucal_close(my->cal);
    }

  my->cal = [self _openCalendarFor: my->tz];

  if (NSNotFound == my->firstWeekday)
    {
      my->firstWeekday
        = ucal_getAttribute(my->cal, UCAL_FIRST_DAY_OF_WEEK);
    }
  else
    {
      ucal_setAttribute(my->cal, UCAL_FIRST_DAY_OF_WEEK,
        (int32_t)my->firstWeekday);
    }

  if (NSNotFound == my->minimumDaysInFirstWeek)
    {
      my->minimumDaysInFirstWeek
        = ucal_getAttribute(my->cal, UCAL_MINIMAL_DAYS_IN_FIRST_WEEK);
    }
  else
    {
      ucal_setAttribute(my->cal, UCAL_MINIMAL_DAYS_IN_FIRST_WEEK,
        (int32_t)my->minimumDaysInFirstWeek);
    }
#endif
}

- (void *) _UCalendar
{
  return my->cal;
}

- (NSString*) _localeIDWithLocale: (NSLocale *)locale
{
  NSString *result;
  NSString *localeId;
  NSMutableDictionary *tmpDict;

  localeId = [locale localeIdentifier];
  if (my->identifier)
    {
      tmpDict = [[NSLocale componentsFromLocaleIdentifier: localeId]
                  mutableCopyWithZone: NULL];
      [tmpDict removeObjectForKey: NSLocaleCalendar];
      [tmpDict setObject: my->identifier forKey: NSLocaleCalendarIdentifier];
      result = [NSLocale localeIdentifierFromComponents: tmpDict];
      RELEASE(tmpDict);
    }
  else
    {
      result = localeId;
    }

  return result;
}

- (NSString*) _localeIdentifier
{
  return my->localeID;
}

- (void) _setLocaleIdentifier: (NSString *) identifier
{
  if ([identifier isEqualToString: my->localeID])
    {
      return;
    }

  ASSIGN(my->localeID, identifier);
  [self _resetCalendar];
}

- (void) _defaultsDidChange: (NSNotification*)n
{
  NSUserDefaults *defs;
  NSString *locale;
  NSString *calendar;
  NSString *tz;

  defs = [NSUserDefaults standardUserDefaults];
  locale = [defs stringForKey: @"Locale"];
  calendar = [defs stringForKey: @"Calendar"];
  tz = [defs stringForKey: @"Local Time Zone"];

  [classLock lock];
  if ([locale isEqual: my->localeID] == NO
    || [calendar isEqual: my->identifier] == NO
    || [tz isEqual: [my->tz name]] == NO)
    {
#if GS_USE_ICU == 1
      ucal_close(my->cal);
#endif

      ASSIGN(my->localeID, locale);
      ASSIGN(my->identifier, calendar);
      RELEASE(my->tz);
      my->tz = [[NSTimeZone alloc] initWithName: tz];

      [self _resetCalendar];
    }
  [classLock unlock];
}
@end

@implementation NSCalendar

+ (void) initialize
{
  if (self == [NSCalendar class])
    {
      classLock = [NSRecursiveLock new];
    }
}

+ (id) currentCalendar
{
  NSCalendar *result;
  NSString *identifier;

  // This identifier may be nil 
  identifier = [[NSLocale currentLocale] objectForKey: NSLocaleCalendarIdentifier];
  result = [[NSCalendar alloc] initWithCalendarIdentifier: identifier];

  return AUTORELEASE(result);
}

+ (id) autoupdatingCurrentCalendar
{
  [classLock lock];
  if (nil == autoupdatingCalendar)
    {
      autoupdatingCalendar = [[self currentCalendar] copy];
      [[NSNotificationCenter defaultCenter]
        addObserver: autoupdatingCalendar
        selector: @selector(_defaultsDidChange:)
        name: NSUserDefaultsDidChangeNotification
        object: nil];
    }

  [classLock unlock];
  return autoupdatingCalendar;
}


+ (id) calendarWithIdentifier: (NSString *) identifier
{
  return AUTORELEASE([[self alloc] initWithCalendarIdentifier: identifier]);
}

- (id) init
{
  return [self initWithCalendarIdentifier: nil];
}

- (id) initWithCalendarIdentifier: (NSString *) identifier
{
  NSAssert(0 == _NSCalendarInternal, NSInvalidArgumentException);
  _NSCalendarInternal = NSZoneCalloc([self zone], sizeof(Calendar), 1);

  my->firstWeekday = NSNotFound;
  my->minimumDaysInFirstWeek = NSNotFound;
  ASSIGN(my->identifier, identifier);
  ASSIGN(my->tz, [NSTimeZone defaultTimeZone]);
  [self setLocale: [NSLocale currentLocale]];

  return self;
}

- (void) dealloc
{
  if (0 != _NSCalendarInternal)
    {
#if GS_USE_ICU == 1
      ucal_close (my->cal);
#endif
      RELEASE(my->identifier);
      RELEASE(my->localeID);
      RELEASE(my->tz);
      NSZoneFree([self zone], _NSCalendarInternal);
    }
  [super dealloc];
}

- (NSString *) calendarIdentifier
{
  return my->identifier;
}

- (NSInteger) component: (NSCalendarUnit)unit 
               fromDate: (NSDate *)date
{
  NSDateComponents *comps = [self components: unit
                                    fromDate: date];
  NSInteger val = 0;

  switch (unit)
    {
    case NSCalendarUnitEra:
      val = [comps era];
      break;
    case NSCalendarUnitYear:
      val = [comps year];
      break;
    case NSCalendarUnitMonth:
      val = [comps month];
      break;
    case NSCalendarUnitDay:
      val = [comps day];
      break;
    case NSCalendarUnitHour:
      val = [comps hour];
      break;
    case NSCalendarUnitMinute:
      val = [comps minute];
      break;
    case NSCalendarUnitSecond:
      val = [comps second];
      break;
    case NSCalendarUnitWeekday:
      val = [comps weekday];
      break;
    case NSCalendarUnitWeekdayOrdinal:
      val = [comps weekdayOrdinal];
      break;
    case NSCalendarUnitQuarter:
      val = [comps quarter];
      break;
    case NSCalendarUnitWeekOfMonth: 
      val = [comps weekOfMonth];
      break;
    case NSCalendarUnitWeekOfYear: 
      val = [comps weekOfYear];
      break;
    case NSCalendarUnitYearForWeekOfYear:
      val = [comps yearForWeekOfYear];
      break;
    case NSCalendarUnitNanosecond:
      val = [comps nanosecond];
      break;
    case NSCalendarUnitCalendar:
    case NSCalendarUnitTimeZone:
      // in these cases do nothing since they are undefined.
      break;
    }

  return val;
}

- (NSDateComponents *) components: (NSUInteger) unitFlags
                         fromDate: (NSDate *) date
{
#if GS_USE_ICU == 1
  NSDateComponents *comps;
  UErrorCode err = U_ZERO_ERROR;
  UDate udate;

  udate = (UDate)floor([date timeIntervalSince1970] * SECOND_TO_MILLI);
  ucal_setMillis(my->cal, udate, &err);
  if (U_FAILURE(err))
    {
      return nil;
    }

  comps = [[NSDateComponents alloc] init];
  if (unitFlags & NSCalendarUnitEra)
    {
      [comps setEra: ucal_get(my->cal, UCAL_ERA, &err)];
    }
  if (unitFlags & NSCalendarUnitYear)
    {
      [comps setYear: ucal_get(my->cal, UCAL_YEAR, &err)];
    }
  if (unitFlags & NSCalendarUnitMonth)
    {
      [comps setMonth: ucal_get(my->cal, UCAL_MONTH, &err) + 1];
    }
  if (unitFlags & NSCalendarUnitDay)
    {
      [comps setDay: ucal_get(my->cal, UCAL_DAY_OF_MONTH, &err)];
    }
  if (unitFlags & NSCalendarUnitHour)
    {
      [comps setHour: ucal_get(my->cal, UCAL_HOUR_OF_DAY, &err)];
    }
  if (unitFlags & NSCalendarUnitMinute)
    {
      [comps setMinute: ucal_get(my->cal, UCAL_MINUTE, &err)];
    }
  if (unitFlags & NSCalendarUnitSecond)
    {
      [comps setSecond: ucal_get(my->cal, UCAL_SECOND, &err)];
    }
  if (unitFlags & (NSWeekCalendarUnit | NSCalendarUnitWeekOfYear))
    {
      [comps setWeek: ucal_get(my->cal, UCAL_WEEK_OF_YEAR, &err)];
    }
  if (unitFlags & NSCalendarUnitWeekday)
    {
      [comps setWeekday: ucal_get(my->cal, UCAL_DAY_OF_WEEK, &err)];
    }
  if (unitFlags & NSCalendarUnitWeekdayOrdinal)
    {
      [comps setWeekdayOrdinal:
               ucal_get(my->cal, UCAL_DAY_OF_WEEK_IN_MONTH, &err)];
    }
  if (unitFlags & NSCalendarUnitQuarter)
    {
      [comps setQuarter: (ucal_get(my->cal, UCAL_MONTH, &err) + 3) / 3];
    }
  if (unitFlags & NSCalendarUnitWeekOfMonth)
    {
      [comps setWeekOfMonth: ucal_get(my->cal, UCAL_WEEK_OF_MONTH, &err)];
    }
  if (unitFlags & NSCalendarUnitYearForWeekOfYear)
    {
      [comps setYearForWeekOfYear: ucal_get(my->cal, UCAL_YEAR_WOY, &err)];
    }
  if (unitFlags & NSCalendarUnitNanosecond)
    {
      [comps setNanosecond: ucal_get(my->cal, UCAL_MILLISECOND, &err) * MILLI_TO_NANO];
    }

  return AUTORELEASE(comps);
#else
  return nil;
#endif
}

/*
 * Convenience macro for field extraction.
 * TODO: We need to implement NSWrapCalendarComponents,
 * but it is unclear how that actually works.
 */
#define COMPONENT_DIFF( \
  cal, units, components, toDate, nsunit, setSel, uunit, err) \
do \
  { \
    if (nsunit == (units & nsunit)) \
      { \
        int32_t uunit ## Diff \
          = ucal_getFieldDifference(cal, toDate, uunit, &err); \
        if (U_FAILURE(err)) \
          { \
            RELEASE(components); \
            return nil; \
          } \
        [components setSel uunit ## Diff]; \
      } \
  } while (0)


- (NSDateComponents *) components: (NSUInteger) unitFlags
                         fromDate: (NSDate *) startingDate
                           toDate: (NSDate *) resultDate
                          options: (NSUInteger) opts
{
#if GS_USE_ICU == 1 && (U_ICU_VERSION_MAJOR_NUM > 4 \
  || (U_ICU_VERSION_MAJOR_NUM == 4 && U_ICU_VERSION_MINOR_NUM >= 8) \
  || defined(HAVE_ICU_H))

  NSDateComponents *comps = nil;
  UErrorCode err = U_ZERO_ERROR;
  UDate udateFrom = (UDate)floor([startingDate timeIntervalSince1970] * SECOND_TO_MILLI);
  UDate udateTo = (UDate)floor([resultDate timeIntervalSince1970] * SECOND_TO_MILLI);

  ucal_setMillis(my->cal, udateFrom, &err);
  if (U_FAILURE(err))
    {
      return nil;
    }
  comps = [[NSDateComponents alloc] init];
  /*
   * Since the ICU field difference function automatically advances
   * the calendar as appropriate, we need to process the units from
   * the largest to the smallest.
   */
  COMPONENT_DIFF(my->cal, unitFlags, comps, udateTo,
    NSCalendarUnitEra, setEra:, UCAL_ERA, err);
  COMPONENT_DIFF(my->cal, unitFlags, comps, udateTo,
    NSCalendarUnitYear, setYear:, UCAL_YEAR, err);
  COMPONENT_DIFF(my->cal, unitFlags, comps, udateTo,
    NSCalendarUnitMonth, setMonth:, UCAL_MONTH, err);
  COMPONENT_DIFF(my->cal, unitFlags, comps, udateTo,
    NSCalendarUnitWeekOfYear, setWeek:, UCAL_WEEK_OF_YEAR, err);
  if (!(unitFlags & NSCalendarUnitWeekOfYear))
    {
      /* We must avoid setting the same unit twice (it would be zero because
       * of the automatic advancement.
       */
      COMPONENT_DIFF(my->cal, unitFlags, comps, udateTo,
        NSWeekCalendarUnit, setWeek:, UCAL_WEEK_OF_YEAR, err);
    }
  COMPONENT_DIFF(my->cal, unitFlags, comps, udateTo,
    NSCalendarUnitWeekOfMonth, setWeekOfMonth:, UCAL_WEEK_OF_MONTH, err);
  COMPONENT_DIFF(my->cal, unitFlags, comps, udateTo,
    NSCalendarUnitDay, setDay:, UCAL_DAY_OF_MONTH, err);
  COMPONENT_DIFF(my->cal, unitFlags, comps, udateTo,
    NSCalendarUnitWeekdayOrdinal, setWeekdayOrdinal:,
    UCAL_DAY_OF_WEEK_IN_MONTH, err);
  COMPONENT_DIFF(my->cal, unitFlags, comps, udateTo,
    NSCalendarUnitWeekday, setWeekday:, UCAL_DAY_OF_WEEK, err);
  COMPONENT_DIFF(my->cal, unitFlags, comps, udateTo,
    NSCalendarUnitHour, setHour:, UCAL_HOUR_OF_DAY, err);
  COMPONENT_DIFF(my->cal, unitFlags, comps, udateTo,
    NSCalendarUnitMinute, setMinute:, UCAL_MINUTE, err);
  COMPONENT_DIFF(my->cal, unitFlags, comps, udateTo,
    NSCalendarUnitSecond, setSecond:, UCAL_SECOND, err);
  if (unitFlags & NSCalendarUnitNanosecond)
    {
      int32_t ms;

      ms = ucal_getFieldDifference(my->cal, udateTo, UCAL_MILLISECOND, &err);
      if (U_FAILURE(err))
        {
          RELEASE(comps);
          return nil;
        }
      [comps setNanosecond: ms * MILLI_TO_NANO];
    }

  return AUTORELEASE(comps);

#else
  return nil;
#endif
}

#undef COMPONENT_DIFF

#define _ADD_COMPONENT(c, n)           \
  if (opts & NSWrapCalendarComponents) \
    ucal_roll(my->cal, c, n, &err); \
  else \
    ucal_add(my->cal, c, n, &err); \
  if (U_FAILURE(err)) \
    { \
      return nil; \
    }

- (NSDate *) dateByAddingComponents: (NSDateComponents *) comps
                             toDate: (NSDate *) date
                            options: (NSUInteger) opts
{
#if GS_USE_ICU == 1
  NSInteger amount;
  UErrorCode err = U_ZERO_ERROR;
  UDate udate;

  [self _resetCalendar];
  udate = (UDate)([date timeIntervalSince1970] * SECOND_TO_MILLI);
  ucal_setMillis(my->cal, udate, &err);

  if ((amount = [comps era]) != NSDateComponentUndefined)
    {
      _ADD_COMPONENT(UCAL_ERA, (int32_t)amount);
    }
  if ((amount = [comps year]) != NSDateComponentUndefined)
    {
      _ADD_COMPONENT(UCAL_YEAR, (int32_t)amount);
    }
  if ((amount = [comps month]) != NSDateComponentUndefined)
    {
      _ADD_COMPONENT(UCAL_MONTH, (int32_t)amount);
    }
  if ((amount = [comps day]) != NSDateComponentUndefined)
    {
      _ADD_COMPONENT(UCAL_DAY_OF_MONTH, (int32_t)amount);
    }
  if ((amount = [comps hour]) != NSDateComponentUndefined)
    {
      _ADD_COMPONENT(UCAL_HOUR_OF_DAY, (int32_t)amount);
    }
  if ((amount = [comps minute]) != NSDateComponentUndefined)
    {
      _ADD_COMPONENT(UCAL_MINUTE, (int32_t)amount);
    }
  if ((amount = [comps second]) != NSDateComponentUndefined)
    {
      _ADD_COMPONENT(UCAL_SECOND, (int32_t)amount);
    }
  if ((amount = [comps week]) != NSDateComponentUndefined)
    {
      _ADD_COMPONENT(UCAL_WEEK_OF_YEAR, (int32_t)amount);
    }
  if ((amount = [comps weekday]) != NSDateComponentUndefined)
    {
      _ADD_COMPONENT(UCAL_DAY_OF_WEEK, (int32_t)amount);
    }
  if ((amount = [comps weekOfMonth]) != NSDateComponentUndefined)
    {
      _ADD_COMPONENT(UCAL_WEEK_OF_MONTH, (int32_t)amount);
    }
  if ((amount = [comps yearForWeekOfYear]) != NSDateComponentUndefined)
    {
      _ADD_COMPONENT(UCAL_YEAR_WOY, (int32_t)amount);
    }
  if ((amount = [comps nanosecond]) != NSDateComponentUndefined)
    {
      _ADD_COMPONENT(UCAL_MILLISECOND, (int32_t)(amount / MILLI_TO_NANO));
    }

  udate = ucal_getMillis(my->cal, &err);
  if (U_FAILURE(err))
    {
      return nil;
    }

  return [NSDate dateWithTimeIntervalSince1970: (udate / SECOND_TO_MILLI)];
#else
  return nil;
#endif
}

#undef _ADD_COMPONENT

- (NSDate *) dateFromComponents: (NSDateComponents *) comps
{
#if GS_USE_ICU == 1
  NSInteger amount;
  UDate udate;
  UErrorCode err = U_ZERO_ERROR;
  void *cal;
  NSTimeZone *timeZone;

  timeZone = [comps timeZone];
  if (timeZone == nil)
    {
      timeZone = [self timeZone];
    }
  cal = [self _openCalendarFor: timeZone];
  if (!cal)
    {
      return nil;
    }
  ucal_clear(cal);

  if ((amount = [comps era]) != NSDateComponentUndefined)
    {
      ucal_set(cal, UCAL_ERA, (int32_t)amount);
    }
  if ((amount = [comps year]) != NSDateComponentUndefined)
    {
      ucal_set (cal, UCAL_YEAR, (int32_t)amount);
    }
  if ((amount = [comps month]) != NSDateComponentUndefined)
    {
      ucal_set (cal, UCAL_MONTH, amount - 1);
    }
  if ((amount = [comps day]) != NSDateComponentUndefined)
    {
      ucal_set (cal, UCAL_DAY_OF_MONTH, (int32_t)amount);
    }
  if ((amount = [comps hour]) != NSDateComponentUndefined)
    {
      ucal_set (cal, UCAL_HOUR_OF_DAY, (int32_t)amount);
    }
  if ((amount = [comps minute]) != NSDateComponentUndefined)
    {
      ucal_set (cal, UCAL_MINUTE, (int32_t)amount);
    }
  if ((amount = [comps second]) != NSDateComponentUndefined)
    {
      ucal_set (cal, UCAL_SECOND, (int32_t)amount);
    }
  if ((amount = [comps week]) != NSDateComponentUndefined)
    {
      ucal_set (cal, UCAL_WEEK_OF_YEAR, (int32_t)amount);
    }
  if ((amount = [comps weekday]) != NSDateComponentUndefined)
    {
      ucal_set (cal, UCAL_DAY_OF_WEEK, (int32_t)amount);
    }
  if ((amount = [comps weekdayOrdinal]) != NSDateComponentUndefined)
    {
      ucal_set (cal, UCAL_DAY_OF_WEEK_IN_MONTH, (int32_t)amount);
    }
  if ((amount = [comps weekOfMonth]) != NSDateComponentUndefined)
    {
      ucal_set (cal, UCAL_WEEK_OF_MONTH, (int32_t)amount);
    }
  if ((amount = [comps yearForWeekOfYear]) != NSDateComponentUndefined)
    {
      ucal_set (cal, UCAL_YEAR_WOY, (int32_t)amount);
    }
  if ((amount = [comps nanosecond]) != NSDateComponentUndefined)
    {
      ucal_set (cal, UCAL_MILLISECOND, (int32_t)(amount / MILLI_TO_NANO));
    }

  udate = ucal_getMillis(cal, &err);
  ucal_close(cal);
  if (U_FAILURE(err))
    {
      return nil;
    }

  return [NSDate dateWithTimeIntervalSince1970: (udate / SECOND_TO_MILLI)];
#else
  return nil;
#endif
}

- (NSLocale *) locale
{
  return AUTORELEASE([[NSLocale alloc] initWithLocaleIdentifier: my->localeID]);
}

- (void) setLocale: (NSLocale *) locale
{
  // It's much easier to keep a copy of the NSLocale's string representation
  // than to have to build it everytime we have to open a UCalendar.
  [self _setLocaleIdentifier: [self _localeIDWithLocale: locale]];
}

- (NSUInteger) firstWeekday
{
  return my->firstWeekday;
}

- (void) setFirstWeekday: (NSUInteger)weekday
{
  my->firstWeekday = weekday;
#if GS_USE_ICU == 1
  ucal_setAttribute(my->cal, UCAL_FIRST_DAY_OF_WEEK, my->firstWeekday);
#endif
}

- (NSUInteger) minimumDaysInFirstWeek
{
  return my->minimumDaysInFirstWeek;
}

- (void) setMinimumDaysInFirstWeek: (NSUInteger)mdw
{
  my->minimumDaysInFirstWeek = (int32_t)mdw;
#if GS_USE_ICU == 1
  ucal_setAttribute(my->cal, UCAL_MINIMAL_DAYS_IN_FIRST_WEEK,
    my->minimumDaysInFirstWeek);
#endif
}

- (NSTimeZone *) timeZone
{
  return my->tz;
}

- (void) setTimeZone: (NSTimeZone *) tz
{
  if ([tz isEqual: my->tz])
    {
      return;
    }

  ASSIGN(my->tz, tz);
  [self _resetCalendar];
}

- (NSRange) maximumRangeOfUnit: (NSCalendarUnit) unit
{
  NSRange result = NSMakeRange (0, 0);
#if GS_USE_ICU == 1
  UCalendarDateFields dateField;
  UErrorCode err = U_ZERO_ERROR;

  [self _resetCalendar];
  dateField = _NSCalendarUnitToDateField(unit);
  if (dateField != (UCalendarDateFields)-1)
    {
      // We really don't care if there are any errors...
      result.location =
        (NSUInteger)ucal_getLimit(my->cal, dateField, UCAL_MINIMUM, &err);
      result.length =
        (NSUInteger)ucal_getLimit(my->cal, dateField, UCAL_MAXIMUM, &err)
        - result.location + 1;
      // ICU's month is 0-based, while NSCalendar is 1-based
      if (dateField == UCAL_MONTH)
        {
          result.location += 1;
        }
    }
#endif

  return result;
}

- (NSRange) minimumRangeofUnit: (NSCalendarUnit) unit
{
  NSRange result = NSMakeRange (0, 0);
#if GS_USE_ICU == 1
  UCalendarDateFields dateField;
  UErrorCode err = U_ZERO_ERROR;

  [self _resetCalendar];
  dateField = _NSCalendarUnitToDateField(unit);
  if (dateField != (UCalendarDateFields)-1)
    {
      // We really don't care if there are any errors...
      result.location =
        (NSUInteger)ucal_getLimit(my->cal, dateField, UCAL_GREATEST_MINIMUM, &err);
      result.length =
        (NSUInteger)ucal_getLimit(my->cal, dateField, UCAL_LEAST_MAXIMUM, &err)
        - result.location + 1;
      // ICU's month is 0-based, while NSCalendar is 1-based
      if (dateField == UCAL_MONTH)
        {
          result.location += 1;
        }
    }
#endif
  return result;
}

- (NSUInteger) ordinalityOfUnit: (NSCalendarUnit) smaller
                         inUnit: (NSCalendarUnit) larger
                        forDate: (NSDate *) date
{
  return 0;
}

- (NSRange) rangeOfUnit: (NSCalendarUnit) smaller
                 inUnit: (NSCalendarUnit) larger
                forDate: (NSDate *) date
{
  return NSMakeRange (0, 0);
}

- (BOOL) rangeOfUnit: (NSCalendarUnit) unit
           startDate: (NSDate **) datep
            interval: (NSTimeInterval *)tip
             forDate: (NSDate *)date
{
  return NO;
}

- (BOOL) isEqual: (id) obj
{
#if GS_USE_ICU == 1
  return (BOOL)ucal_equivalentTo(my->cal, [obj _UCalendar]);
#else
  if ([obj isKindOfClass: [self class]])
    {
      if (![my->identifier isEqual: [obj calendarIdentifier]])
        return NO;
      if (![my->localeID isEqual: [obj localeIdentifier]])
        return NO;
      if (![my->tz isEqual: [obj timeZone]])
        return NO;
      if (my->firstWeekday != [obj firstWeekday])
        return NO;
      if (my->minimumDaysInFirstWeek != [obj minimumDaysInFirstWeek])
        return NO;
      return YES;
    }

  return NO;
#endif
}


- (void) getEra: (NSInteger *)eraValuePointer
           year: (NSInteger *)yearValuePointer
          month: (NSInteger *)monthValuePointer
            day: (NSInteger *)dayValuePointer
       fromDate: (NSDate *)date
{
#if GS_USE_ICU == 1
  UErrorCode err = U_ZERO_ERROR;
  UDate udate;
  [self _resetCalendar];
  ucal_clear (my->cal);
  
  udate = (UDate)floor([date timeIntervalSince1970] * 1000.0);
  ucal_setMillis (my->cal, udate, &err);
  if (U_FAILURE(err))
    return;
  
  if (eraValuePointer != NULL)
    *eraValuePointer = ucal_get(my->cal, UCAL_ERA, &err);
  if (yearValuePointer != NULL)
    *yearValuePointer = ucal_get(my->cal, UCAL_YEAR, &err);
  if (monthValuePointer != NULL)
    *monthValuePointer = ucal_get(my->cal, UCAL_MONTH, &err) + 1;
  if (dayValuePointer != NULL)
    *dayValuePointer = ucal_get(my->cal, UCAL_DAY_OF_MONTH, &err);
#endif
}

- (void) getHour: (NSInteger *)hourValuePointer
          minute: (NSInteger *)minuteValuePointer
          second: (NSInteger *)secondValuePointer
      nanosecond: (NSInteger *)nanosecondValuePointer
        fromDate: (NSDate *)date
{ 
#if GS_USE_ICU == 1
  UErrorCode err = U_ZERO_ERROR;
  UDate udate;
  [self _resetCalendar];
  ucal_clear (my->cal);

  udate = (UDate)floor([date timeIntervalSince1970] * 1000.0);
  ucal_setMillis (my->cal, udate, &err);
  if (U_FAILURE(err))
    return;
	
  if (hourValuePointer != NULL)
    *hourValuePointer = ucal_get(my->cal, UCAL_HOUR_OF_DAY, &err);
  if (minuteValuePointer != NULL)
    *minuteValuePointer = ucal_get(my->cal, UCAL_MINUTE, &err);
  if (secondValuePointer != NULL)
    *secondValuePointer = ucal_get(my->cal, UCAL_SECOND, &err);
  if (nanosecondValuePointer != NULL)
    *nanosecondValuePointer = ucal_get(my->cal, UCAL_MILLISECOND, &err) * 1000;
#endif
}

- (void) getEra: (NSInteger *)eraValuePointer 
yearForWeekOfYear: (NSInteger *)yearValuePointer 
     weekOfYear: (NSInteger *)weekValuePointer 
        weekday: (NSInteger *)weekdayValuePointer 
       fromDate: (NSDate *)date
{
#if GS_USE_ICU == 1
  UErrorCode err = U_ZERO_ERROR;
  UDate udate;
  [self _resetCalendar];
  ucal_clear (my->cal);

  udate = (UDate)floor([date timeIntervalSince1970] * 1000.0);
  ucal_setMillis (my->cal, udate, &err);
  if (U_FAILURE(err))
    return;
	
  if (eraValuePointer != NULL)
    *eraValuePointer = ucal_get(my->cal, UCAL_ERA, &err);
  if (yearValuePointer != NULL)
    *yearValuePointer = ucal_get(my->cal, UCAL_YEAR_WOY, &err);
  if (weekValuePointer != NULL)
    *weekValuePointer = ucal_get(my->cal, UCAL_WEEK_OF_YEAR, &err);
  if (weekdayValuePointer != NULL)
    *weekdayValuePointer = ucal_get(my->cal, UCAL_DAY_OF_WEEK, &err);
#endif
}

- (void) encodeWithCoder: (NSCoder*)encoder
{
  [encoder encodeObject: my->identifier];
  [encoder encodeObject: my->localeID];
  [encoder encodeObject: my->tz];
}

- (id) initWithCoder: (NSCoder*)decoder
{
  NSString	*s = [decoder decodeObject];

  [self initWithCalendarIdentifier: s];
  [self _setLocaleIdentifier: [decoder decodeObject]];
  [self setTimeZone: [decoder decodeObject]];

  return self;
}

- (id) copyWithZone: (NSZone*)zone
{
  NSCalendar *result;

  if (NSShouldRetainWithZone(self, zone))
    {
      return RETAIN(self);
    }
  else
    {
      result = [[[self class] allocWithZone: zone]
        initWithCalendarIdentifier: my->identifier];
      [result _setLocaleIdentifier: my->localeID];
      [result setTimeZone: my->tz];
    }

  return result;
}

@end

#undef  my


@implementation NSDateComponents

typedef struct {
  NSInteger era;
  NSInteger year;
  NSInteger month;
  NSInteger day;
  NSInteger hour;
  NSInteger minute;
  NSInteger second;
  NSInteger week;
  NSInteger weekday;
  NSInteger weekdayOrdinal;
  NSInteger quarter;
  NSInteger weekOfMonth;
  NSInteger yearForWeekOfYear;
  BOOL leapMonth;
  NSInteger nanosecond;
  NSCalendar *cal;
  NSTimeZone *tz;
} DateComp;

#define my ((DateComp*)_NSDateComponentsInternal)

- (void) dealloc
{
  if (0 != _NSDateComponentsInternal)
    {
      RELEASE(my->cal);
      RELEASE(my->tz);
      NSZoneFree([self zone], _NSDateComponentsInternal);
    }
  [super dealloc];
}

- (id) init
{
  if (nil != (self = [super init]))
    {
      _NSDateComponentsInternal =
        NSZoneCalloc([self zone], sizeof(DateComp), 1);

      my->era = NSDateComponentUndefined;
      my->year = NSDateComponentUndefined;
      my->month = NSDateComponentUndefined;
      my->day = NSDateComponentUndefined;
      my->hour = NSDateComponentUndefined;
      my->minute = NSDateComponentUndefined;
      my->second = NSDateComponentUndefined;
      my->week = NSDateComponentUndefined;
      my->weekday = NSDateComponentUndefined;
      my->weekdayOrdinal = NSDateComponentUndefined;
      my->quarter = NSDateComponentUndefined;
      my->weekOfMonth = NSDateComponentUndefined;
      my->yearForWeekOfYear = NSDateComponentUndefined;
      my->leapMonth = NO;
      my->nanosecond = NSDateComponentUndefined;
      my->cal = NULL;
      my->tz = NULL;
     }
  return self;
}

- (NSInteger) day
{
  return my->day;
}

- (NSInteger) era
{
  return my->era;
}

- (NSInteger) hour
{
  return my->hour;
}

- (NSInteger) minute
{
  return my->minute;
}

- (NSInteger) month
{
  return my->month;
}

- (NSInteger) quarter
{
  return my->quarter;
}

- (NSInteger) second
{
  return my->second;
}

- (NSInteger) nanosecond
{
  return my->nanosecond;
}

- (NSInteger) week
{
  return my->week;
}

- (NSInteger) weekday
{
  return my->weekday;
}

- (NSInteger) weekdayOrdinal
{
  return my->weekdayOrdinal;
}

- (NSInteger) year
{
  return my->year;
}

- (NSInteger) weekOfMonth
{
  return my->weekOfMonth;
}

- (NSInteger) weekOfYear
{
  return my->week;
}

- (NSInteger) yearForWeekOfYear
{
  return my->yearForWeekOfYear;
}

- (BOOL) leapMonth
{
  return my->leapMonth;
}

- (NSCalendar *) calendar
{
  return my->cal;
}

- (NSTimeZone *) timeZone
{
  return my->tz;
}

- (NSDate *) date
{
  NSCalendar* cal = [self calendar];

  return [cal dateFromComponents: self];
}


- (void) setDay: (NSInteger) v
{
  my->day = v;
}

- (void) setEra: (NSInteger) v
{
  my->era = v;
}

- (void) setHour: (NSInteger) v
{
  my->hour = v;
}

- (void) setMinute: (NSInteger) v
{
  my->minute = v;
}

- (void) setMonth: (NSInteger) v
{
  my->month = v;
}

- (void) setQuarter: (NSInteger) v
{
  my->quarter = v;
}

- (void) setSecond: (NSInteger) v
{
  my->second = v;
}

- (void) setNanosecond: (NSInteger) v
{
  my->nanosecond = v;
}

- (void) setWeek: (NSInteger) v
{
  my->week = v;
}

- (void) setWeekday: (NSInteger) v
{
  my->weekday = v;
}

- (void) setWeekdayOrdinal: (NSInteger) v
{
  my->weekdayOrdinal = v;
}

- (void) setYear: (NSInteger) v
{
  my->year = v;
}

- (void) setWeekOfYear: (NSInteger) v
{
  my->week = v;
}

- (void) setWeekOfMonth: (NSInteger) v
{
  my->weekOfMonth = v;
}

- (void) setYearForWeekOfYear: (NSInteger) v
{
  my->yearForWeekOfYear = v;
}

- (void) setLeapMonth: (BOOL) v
{
  my->leapMonth = v;
}

- (void) setCalendar: (NSCalendar *) cal
{
  ASSIGN(my->cal, cal);
}

- (void) setTimeZone: (NSTimeZone *) tz
{
  ASSIGN(my->tz, tz);
}

- (BOOL) isValidDate
{
  if (my->cal == nil)
    {
      return NO;
    }
  return [self isValidDateInCalendar: my->cal];
}

- (BOOL) isValidDateInCalendar: (NSCalendar *) calendar
{
  return [calendar dateFromComponents: self] != nil;
}

- (NSInteger) valueForComponent: (NSCalendarUnit) unit
{
  switch (unit)
    {
      case NSCalendarUnitEra: return my->era;
      case NSCalendarUnitYear: return my->year;
      case NSCalendarUnitMonth: return my->month;
      case NSCalendarUnitDay: return my->day;
      case NSCalendarUnitHour: return my->hour;
      case NSCalendarUnitMinute: return my->minute;
      case NSCalendarUnitSecond: return my->second;
      case NSCalendarUnitWeekday: return my->weekday;
      case NSCalendarUnitWeekdayOrdinal: return my->weekdayOrdinal;
      case NSCalendarUnitQuarter: return my->quarter;
      case NSCalendarUnitWeekOfMonth: return my->weekOfMonth;
      case NSCalendarUnitWeekOfYear: return my->week;
      case NSWeekCalendarUnit: return my->week;
      case NSCalendarUnitYearForWeekOfYear: return my->yearForWeekOfYear;
      case NSCalendarUnitNanosecond: return my->nanosecond;
      default: return 0;
    }
}

- (void) setValue: (NSInteger) value
     forComponent: (NSCalendarUnit) unit
{
  switch (unit)
    {
      case NSCalendarUnitEra:
        my->era = value;
        break;
      case NSCalendarUnitYear:
        my->year = value;
        break;
      case NSCalendarUnitMonth:
        my->month = value;
        break;
      case NSCalendarUnitDay:
          my->day = value;
        break;
      case NSCalendarUnitHour:
        my->hour = value;
        break;
      case NSCalendarUnitMinute:
        my->minute = value;
        break;
      case NSCalendarUnitSecond:
        my->second = value;
        break;
      case NSCalendarUnitWeekday:
        my->weekday = value;
        break;
      case NSCalendarUnitWeekdayOrdinal:
        my->weekdayOrdinal = value;
        break;
      case NSCalendarUnitQuarter:
        my->quarter = value;
        break;
      case NSCalendarUnitWeekOfMonth:
        my->weekOfMonth = value;
        break;
      case NSCalendarUnitWeekOfYear:
        my->week = value;
        break;
      case NSWeekCalendarUnit:
        my->week = value;
        break;
      case NSCalendarUnitYearForWeekOfYear:
        my->yearForWeekOfYear = value;
        break;
      case NSCalendarUnitNanosecond:
        my->nanosecond = value;
        break;
      default:
        break;
    }
}

- (id) copyWithZone: (NSZone*)zone
{
  if (NSShouldRetainWithZone(self, zone))
    {
      return RETAIN(self);
    }
  else
    {
      NSDateComponents  *c = [[NSDateComponents allocWithZone: zone] init];

      memcpy(c->_NSDateComponentsInternal, _NSDateComponentsInternal,
        sizeof(DateComp));
      /* We gave objects to the copy, so we need to retain them too.
       */
      RETAIN(my->cal);
      RETAIN(my->tz);
      return c;
    }
}

@end
