/* 
   NSConcreteTimeZone.m

   Copyright (C) 1995, 1996 Ovidiu Predescu and Mircea Oancea.
   All rights reserved.

   Author: Ovidiu Predescu <ovidiu@bx.logicnet.ro>

   This file is part of libFoundation.

   Permission to use, copy, modify, and distribute this software and its
   documentation for any purpose and without fee is hereby granted, provided
   that the above copyright notice appear in all copies and that both that
   copyright notice and this permission notice appear in supporting
   documentation.

   We disclaim all warranties with regard to this software, including all
   implied warranties of merchantability and fitness, in no event shall
   we be liable for any special, indirect or consequential damages or any
   damages whatsoever resulting from loss of use, data or profits, whether in
   an action of contract, negligence or other tortious action, arising out of
   or in connection with the use or performance of this software.
*/

#include <Foundation/NSString.h>
#include <Foundation/NSArray.h>
#include <Foundation/NSDictionary.h>
#include <Foundation/NSDate.h>
#include <Foundation/NSScanner.h>
#include <Foundation/NSAutoreleasePool.h>
#include <Foundation/NSCharacterSet.h>
#include <Foundation/NSException.h>
#include <Foundation/NSUtilities.h>

#include "NSCalendarDate.h"
#include "NSConcreteTimeZone.h"
#include "NSConcreteTimeZoneDetail.h"

static NSString* FORMAT = @"%a %b %d %H:%M:%S %Y";
static NSString* FORMAT2 = @"%a %b %d %H:%M:%S %Y GMT";
static BOOL warnings = NO;

@implementation NSConcreteTimeZone

+ (id)allocWithZone:(NSZone*)zone
{
    return NSAllocateObject(self, 0, zone);
}

+ timeZoneWithOffset:(int)time
{
    NSConcreteTimeZone* timezone = [[self new] autorelease];
    int hour, minute, second;
    BOOL isNegative = (time < 0);

    time = abs (time);
    hour = time / 3600;
    minute = (time - hour * 3600) / 60;
    second = time - hour * 3600 - minute * 60;
    timezone->name = [[NSString stringWithFormat:@"GMT%c%02d%02d%02d",
				    (isNegative ? '-' : '+'),
				    hour,
				    minute,
				    second]
			retain];
    timezone->timeZoneDetails
	= [[NSArray arrayWithObject:
		    [[[NSConcreteTimeZoneDetail alloc]
			initWithAbbreviation:nil secondsFromGMT:time
				isDaylightSaving:NO name:nil]
			autorelease]]
	    retain];

    return timezone;
}

- initWithName:(NSString*)_name
{
    [_name retain];
    [name release];
    name = _name;
    return self;
}

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

- (NSString*)timeZoneName		{ return name; }
- (NSArray*)timeZoneDetailArray		{ return timeZoneDetails; }

- (NSTimeZoneDetail*)timeZoneDetailForDate:(NSCalendarDate*)date
{
    return [timeZoneDetails objectAtIndex:0];
}

@end /* NSConcreteTimeZone */


@implementation NSConcreteTimeZoneFile

static NSTimeZone* gmtTimeZone = nil;

+ (void)initialize
{
    gmtTimeZone = [[NSConcreteTimeZone timeZoneWithOffset:0] retain];
}

- initFromFile:(NSString*)_filename withName:(NSString*)_name
{
    [self initWithName:_name];
    [_filename retain];
    [filename release];
    filename = _filename;
    return self;
}

- (void)_initFromFile
{
    id pool = [NSAutoreleasePool new];
    id propertyList = [[NSString stringWithContentsOfFile:filename]
			    propertyList];
    id detailsDict = [[[propertyList objectForKey:@"details"] mutableCopy]
			    autorelease];
    id rulesArray = [propertyList objectForKey:@"rules"];
    id transitionsArray = [propertyList objectForKey:@"transitions"];
    int i = 0, count = [detailsDict count];
    id details[count];
    NSEnumerator* enumerator = [detailsDict keyEnumerator];
    id detailName, detailDict;

    while ((detailName = [enumerator nextObject])) {
	detailDict = [detailsDict objectForKey:detailName];
	details[i++]
	    = [NSConcreteTimeZoneDetail detailFromPropertyList:detailDict
					name:detailName];
    }
    timeZoneDetails = [[NSArray alloc] initWithObjects:details count:i];

    transitions = [[NSMutableArray arrayWithCapacity:
			    [rulesArray count] + [transitionsArray count]]
			retain];
    for (i = 0, count = [rulesArray count]; i < count; i++)
	[transitions addObject:
	    [NSTimeZoneTransitionRule transitionRuleFromPropertyList:
					[rulesArray objectAtIndex:i]
				    timezone:self]];

    for (i = 0, count = [transitionsArray count]; i < count; i++)
	[transitions addObject:
	    [NSTimeZoneTransitionDate transitionDateFromPropertyList:
					[transitionsArray objectAtIndex:i]
				    timezone:self]];

    /* Now the transitions array contains NSTimeZoneTransitionRule and
       NSTimeZoneTransitionDate objects. We sort these instances in ascending
       order because the algorithm in timeZoneDetailForDate: is based on this
       fact. It is an error to have transition dates included in the time
       covered by a rule and also to have overlaping rules. */

    /* Turn on the warnings during sort */
    warnings = YES;
    [transitions sortUsingSelector:@selector(compare:)];
    warnings = NO;

    [pool release];
}

- detailWithName:(NSString*)detailName
{
    int i, count = [timeZoneDetails count];

    for (i = 0; i < count; i++) {
	id detail = [timeZoneDetails objectAtIndex:i];
	if ([detailName isEqual:[detail name]])
	    return detail;
    }

    return nil;
}

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

- (NSArray*)timeZoneDetailArray
{
    if (!timeZoneDetails)
	[self _initFromFile];
    return timeZoneDetails;
}

- (NSTimeZoneDetail*)timeZoneDetailForDate:(NSCalendarDate*)date
{
    int down, up, i = 0;
    /* Create a transition date instance to be the key for searching in the
       transitions array. */
    NSTimeZoneTransitionDate* tranDate;
    NSComparisonResult result;
    id detail;

    if (!date)
	return nil;

    if (!timeZoneDetails)
	[self _initFromFile];

    if (![transitions count])
	return [timeZoneDetails objectAtIndex:0];

    tranDate = [[[NSTimeZoneTransitionDate alloc] initWithDate:date detail:nil]
		    autorelease];
    down = 0;
    up = [transitions count] - 1;

    /* First check with the head and the end of the interval. */
    if ([tranDate compare:[transitions objectAtIndex:0]]
	    == NSOrderedAscending) {
	/* Find a detail that has no saving time in effect and return it. */
	int count = [timeZoneDetails count];

	for (i = 0; i < count; i++) {
	    detail = [timeZoneDetails objectAtIndex:i];
	    if (![detail isDaylightSavingTimeZone])
		return detail;
	}
	/* If we cannot find such a detail just return nil. */
	return nil;
    }

    if ([tranDate compare:[transitions objectAtIndex:up]]
	    == NSOrderedDescending)
	return [[transitions objectAtIndex:up] detailAfterLastDate];

    /* Use a binary search algorithm to find the position. */
    i = (down + up) / 2;
    while (down <= up) {
	result = [tranDate compare:[transitions objectAtIndex:i]];
	if (result == NSOrderedSame)
	    break;
	else if (result == NSOrderedAscending)
	    up = i - 1;
	else if (result == NSOrderedDescending)
	    down = i + 1;
	i = (down + up) / 2;
    }

    /* Send the -detailForDate: message to the transition object at index i */
    return [[transitions objectAtIndex:i] detailForDate:date];
}

- (NSString*)filename			{ return filename; }
- (NSArray*)transitions			{ return transitions; }

@end /* NSConcreteTimeZoneFile */


@implementation NSTimeZoneTransitionDate

+ (NSTimeZoneTransitionDate*)transitionDateFromPropertyList:(id)plist
  timezone:(id)tz
{
    NSTimeZoneTransitionDate* tranDate = [[self new] autorelease];
    NSString* dateString = [plist objectForKey:@"date"];
    NSString* detailName;

    tranDate->date
	= [[NSCalendarDate dateWithString:dateString calendarFormat:FORMAT]
		retain];
    if (!tranDate->date)
	NSLog (@"Unable to parse transition date from '%@'!", [tz filename]);

    detailName = [plist objectForKey:@"detail"];
    if (!detailName)
	NSLog (@"No detail is specified for transition with date '%@' in "
		@"'%@'!",  dateString, [tz filename]);
    else {
	tranDate->detail = [tz detailWithName:detailName];
	if (!tranDate->detail)
	    NSLog (@"No detail '%@' in '%@'!", detailName, [tz filename]);
    }

    return tranDate;
}

- initWithDate:(NSCalendarDate*)_date detail:_detail
{
    date = [_date retain];
    detail = [_detail retain];
    return self;
}

- detailForDate:(NSCalendarDate*)date
{
    return detail;
}

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

- (NSCalendarDate*)date			{ return date; }

- (NSComparisonResult)compare:tranDateOrTranRule
{
    NSComparisonResult result;

    /* Send the comparision to TransitionRule and return the negated result */
    if ([tranDateOrTranRule isKindOfClass:[NSTimeZoneTransitionRule class]])
	return -[tranDateOrTranRule compare:self];

    if ([tranDateOrTranRule isKindOfClass:isa]) {
	if (tranDateOrTranRule == self)
	    return NSOrderedSame;
	else {
	    result = [date compare:[tranDateOrTranRule date]];
	    if (warnings && result == NSOrderedSame)
		NSLog (@"Duplicate dates for transitions '%@' and '%@'",
			self, tranDateOrTranRule);
	    return result;
	}
    }

    NSAssert (0, @"Cannot compare NSTimeZoneTransitionDate with %@",
		[tranDateOrTranRule class]);
    return NSOrderedSame;
}

- (NSString*)description
{
    return [NSString stringWithFormat:@"{ date = \"%@\"; detail = %@; }",
			[date descriptionWithCalendarFormat:FORMAT2],
			[[detail timeZoneAbbreviation] stringRepresentation]];
}

- detailAfterLastDate
{
    return detail;
}

@end /* NSTimeZoneTransitionDate */


@implementation NSTimeZoneTransitionRule

+ (NSTimeZoneTransitionRule*)transitionRuleFromPropertyList:(id)plist
  timezone:(id)tz
{
    NSTimeZoneTransitionRule* tranRule = [[self new] autorelease];
    NSArray* transitionsArray = [plist objectForKey:@"transitions"];
    NSCalendarDate *date1, *date2;

    date1 = [NSCalendarDate dateWithString:[plist objectForKey:@"startDate"]
			  calendarFormat:FORMAT];
    if (!date1)
	NSLog (@"Unable to parse start date from '%@'!", [tz filename]);

    date2 = [NSCalendarDate dateWithString:[plist objectForKey:@"endDate"]
			  calendarFormat:FORMAT];
    if (!date2)
	NSLog (@"Unable to parse end date from '%@'!", [tz filename]);

    if (date1 && date2 && [date1 compare:date2] != NSOrderedAscending)
	NSLog (@"Start date '%@' should be less than end date '%@'",
		[plist objectForKey:@"startDate"],
		[plist objectForKey:@"endDate"]);

    tranRule->startRule
	= [[NSTimeZoneRule ruleFromPropertyList:
				[transitionsArray objectAtIndex:0]
			    timezone:tz]
		    retain];
    tranRule->endRule
	= [[NSTimeZoneRule ruleFromPropertyList:
				[transitionsArray objectAtIndex:1]
			    timezone:tz]
		    retain];

    tranRule->startDate = [date1 retain];
    tranRule->endDate = [date2 retain];

    return tranRule;
}

- detailForDate:(NSCalendarDate*)date
{
    int year = [date yearOfCommonEra];
    NSCalendarDate* startDateInYear = [startRule dateInYear:year];
    NSCalendarDate* endDateInYear = [endRule dateInYear:year];

    /* Check to see if date is outside the domain of this rule. Compare only
       with endDate, because the comparison with startDate was performed in
       timeZoneDetailForDate: method. */
    if ([endDate compare:date] == NSOrderedAscending) {
	/* Date is to the right of time interval of this rule. */
	return [endRule detail];
    }

    if ([date compare:startDateInYear] == NSOrderedAscending) {
	/* Date is before the first change in year, so is after the last change
	   in the preceding year. */
	return [endRule detail];
    }
    else if ([date compare:endDateInYear] == NSOrderedAscending) {
	/* Date is between the two dates of the year. */
	return [startRule detail];
    }
    else {
	/* Date is after the second change in the year */
	return [endRule detail];
    }
}

- (NSCalendarDate*)startDate		{ return startDate; }
- (NSCalendarDate*)endDate		{ return endDate; }

- (NSComparisonResult)relativePositionOfDate:(NSCalendarDate*)date
{
    NSComparisonResult result;

    result = [endDate compare:date];
    if (result == NSOrderedAscending)
	return NSOrderedAscending;

    /* Date is less than or equal with endDate. Test with startDate. */
    result = [startDate compare:date];
    if (result == NSOrderedSame || result == NSOrderedAscending) {
	/* Date is between startDate and endDate. */
	return NSOrderedSame;
    }
    else {
	/* Date is less than startDate */
	return NSOrderedDescending;
    }
}

- (NSComparisonResult)compare:tranDateOrTranRule
{
    /* If the object is a date return its position relative to start and end
       date. Note that we should return the value of comparison relative to
       self. */
    if ([tranDateOrTranRule isKindOfClass:[NSTimeZoneTransitionDate class]]) {
	NSCalendarDate* date = [tranDateOrTranRule date];
	NSComparisonResult result;

	result = [endDate compare:date];

	if (result == NSOrderedAscending || result == NSOrderedSame) {
	    /* Date is greater than or equal with endDate so we consider the
	       date greater than the interval time of self. */
	    return NSOrderedAscending;
	}
	else {
	    /* Date is less than endDate. Compare date with startDate. */
	    result = [startDate compare:date];
	    if (result == NSOrderedAscending || result == NSOrderedSame) {
		/* Date is between startDate and endDate. This is an error
		   since we require dates to not be included in time intervals
		   of rules. */
		if (warnings)
		    NSLog (@"Date '%@' is included in the time interval "
			    @"described by rule %@",
			    [date descriptionWithCalendarFormat:FORMAT2],
			    self);
		return NSOrderedSame;
	    }
	    else {
		/* Date is less than or equal with startDate so we consider the
		   interval greater than date. */
		return NSOrderedDescending;
	    }
	}
    }
    else if ([tranDateOrTranRule isKindOfClass:isa]) {
	/* Determine the relative position of the other's time interval
	   comparative with self. */
	NSComparisonResult result1;
	NSComparisonResult result2;

	result1 = [self relativePositionOfDate:[tranDateOrTranRule startDate]];
	result2 = [self relativePositionOfDate:[tranDateOrTranRule endDate]];

	if (result1 != result2) {
	    /* We have two overlaping intervals */
	    if (warnings)
		NSLog (@"Time intervals or rules '%@' and '%@' overlap!",
			self, tranDateOrTranRule);
	    return NSOrderedSame;
	}
	else {
	    /* We have result1 == result2. We should also check if they are
	       equal with NSOrderedSame, which means the second interval in
	       inside self. */
	    if (result1 == NSOrderedSame) {
		if (warnings)
		    NSLog (@"Time interval of '%@' is inside '%@'",
			    tranDateOrTranRule, self);
		return NSOrderedSame;
	    }
	    return result1;
	}
    }
    else
	NSAssert (0, @"Cannot compare NSTimeZoneTransitionRule with %@",
		    [tranDateOrTranRule class]);
    return NSOrderedSame;
}

- detailAfterLastDate
{
    return [endRule detail];
}

- (NSString*)descriptionWithIndent:(unsigned)indent
{
    id plist = [NSMutableDictionary dictionaryWithCapacity:3];
    id tranArray = [NSMutableArray arrayWithCapacity:2];

    [plist setObject:[startDate descriptionWithCalendarFormat:FORMAT2]
	   forKey:@"startDate"];
    [plist setObject:[endDate descriptionWithCalendarFormat:FORMAT2]
	   forKey:@"endDate"];
    [plist setObject:tranArray forKey:@"transitions"];

    [tranArray addObject:startRule];
    [tranArray addObject:endRule];

    return [plist descriptionWithIndent:indent];
}

- (NSString*)description
{
    return [self descriptionWithIndent:0];
}

@end /* NSTimeZoneTransitionRule */


@implementation NSTimeZoneRule

+ (NSTimeZoneRule*)ruleFromPropertyList:(id)plist
  timezone:(id)tz
{
    id months[] = {
	@"January", @"February", @"March", @"April", @"May", @"June", 
	@"July", @"August", @"September", @"October", @"November", @"December"
    };
    id days[] = {
	@"Sunday", @"Monday", @"Tuesday", @"Wednesday",
	@"Thusday", @"Friday", @"Saturday"
    };
    id weeks[] = {
	@"first", @"second", @"third", @"fourth", @"last"
    };
    NSTimeZoneRule* rule = [[self new] autorelease];
    NSString* dateString = [plist objectForKey:@"date"];
    NSCharacterSet* blanks = [NSCharacterSet whitespaceCharacterSet];
    NSScanner* scanner;
    NSString* monthName;
    NSString* weekName;
    NSString* dayName;
    NSString* detailName;
    int i;

    if (!dateString) {
	NSLog (@"No date rule specified in '%@'!", [tz filename]);
	return rule;
    }

    scanner = [NSScanner scannerWithString:dateString];
    if ([scanner scanUpToString:@"/" intoString:&monthName]
	&& [scanner scanString:@"/" intoString:NULL]
	&& [scanner scanUpToCharactersFromSet:blanks intoString:&weekName]
	&& [scanner scanUpToString:@"/" intoString:&dayName]
	&& [scanner scanString:@"/" intoString:NULL]
	&& [scanner scanInt:&rule->hours]
	&& [scanner scanString:@":" intoString:NULL]
	&& [scanner scanInt:&rule->minutes]
	&& [scanner scanString:@":" intoString:NULL]
	&& [scanner scanInt:&rule->seconds]) {

	for (i = 0; i < sizeof (months) / sizeof (id); i++)
	    if ([monthName isEqual:months[i]]) {
		rule->monthOfYear = i + 1;
		break;
	    }
	if (rule->monthOfYear == -1)
	    NSLog (@"Unknown month name '%@' in '%@'",
		    monthName, [tz filename]);

	for (i = 0; i < sizeof (weeks) / sizeof (id); i++)
	    if ([weekName isEqual:weeks[i]]) {
		rule->weekOfMonth = i;
		break;
	    }
	if (rule->weekOfMonth == -1)
	    NSLog (@"Unknown week name '%@' in '%@'", weekName, [tz filename]);

	for (i = 0; i < sizeof (days) / sizeof (id); i++)
	    if ([dayName isEqual:days[i]]) {
		rule->dayOfWeek = i;
		break;
	    }
	if (rule->dayOfWeek == -1)
	    NSLog (@"Unknown day name '%@' in '%@'", dayName, [tz filename]);

	if (rule->hours < 0 || rule->hours > 23)
	    NSLog (@"Hours should be between 0 and 23 in '%@'", [tz filename]);

	if (rule->minutes < 0 || rule->minutes > 59)
	    NSLog (@"Minutes should be between 0 and 59 in '%@'",
		    [tz filename]);

	if (rule->seconds < 0 || rule->seconds > 59)
	    NSLog (@"Seconds should be between 0 and 59 in '%@'",
		    [tz filename]);
    }
    else
	NSLog (@"Unable to parse date rule from '%@'!", [tz filename]);

    detailName = [plist objectForKey:@"detail"];
    if (!detailName)
	NSLog (@"No detail is specified for rule with date '%@' in '%@'!",
		    dateString, [tz filename]);
    else {
	rule->detail = [tz detailWithName:detailName];
	if (!rule->detail)
	    NSLog (@"No detail '%@' in '%@'!", detailName, [tz filename]);
    }

    return rule;
}

- init
{
    monthOfYear = -1;
    weekOfMonth = -1;
    dayOfWeek = -1;
    hours = -1;
    minutes = -1;
    seconds = -1;
    return self;
}

- (NSCalendarDate*)dateInYear:(int)year
{
    NSCalendarDate* firstOfMonth
	= [NSCalendarDate dateWithYear:year month:monthOfYear day:1 hour:hours
			  minute:minutes second:seconds timeZone:gmtTimeZone];
    int firstOfMonthInWeekDay = [firstOfMonth dayOfWeek];
    int additionalDays = 0;
    int nDays[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};

#define isLeap(y) \
	((y) % 4 == 0 && ((y) % 400 == 0 || (y) % 100 ))

    if (firstOfMonthInWeekDay != dayOfWeek) {
	/* Add the number needed to have the same day of week as rule says */
	if (dayOfWeek < firstOfMonthInWeekDay)
	    additionalDays = 7 - (firstOfMonthInWeekDay - dayOfWeek);
	else additionalDays = (dayOfWeek - firstOfMonthInWeekDay);
    }
    /* Add the number of days according to the number of weeks in month */
    additionalDays += 7 * weekOfMonth;
    if (isLeap (year))
	nDays[1]++;
    if (additionalDays > nDays[monthOfYear]) {
	if (weekOfMonth == 4)
	    additionalDays -= 7;
	else
	    NSAssert (0, @"additionalDays is greater than %d and week in "
			@"month is different than last", additionalDays);
    }

    /* Adjust additionalDays if it's greater than the number of days in month
       and weekOfMonth is last. */

    /* Return the date with the number of additional days added to it. */
    return [firstOfMonth dateByAddingYears:0 month:0 day:additionalDays
		    hour:0 minute:0 second:0];
#undef isLeap
}

- (NSString*)description
{
    id months[] = {
	@"January", @"February", @"March", @"April", @"May", @"June", 
	@"July", @"August", @"September", @"October", @"November", @"December"
    };
    id days[] = {
	@"Sunday", @"Monday", @"Tuesday", @"Wednesday",
	@"Thusday", @"Friday", @"Saturday"
    };
    id weeks[] = {
	@"first", @"second", @"third", @"fourth", @"last"
    };

    return [NSString stringWithFormat:@"{ date = \"%@/%@ %@/%d:%02d:%02d\"; "
			@"detail = %@; }",
			months[monthOfYear - 1],
			weeks[weekOfMonth],
			days[dayOfWeek],
			hours,
			minutes,
			seconds,
			[[detail timeZoneAbbreviation] stringRepresentation]];
}

- detail		{ return detail; }

@end /* NSTimeZoneRule */
