/* 
   NSCalendarDate.m

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

   Author: Mircea Oancea <mircea@jupiter.elcom.pub.ro>
	   Ovidiu Predescu <ovidiu@bx.logicnet.ro>
	   Florin Mihaila <phil@pathcom.com>

   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/NSDate.h>
#include <Foundation/NSArray.h>
#include <Foundation/NSDictionary.h>
#include <Foundation/NSString.h>
#include <Foundation/NSException.h>
#include <Foundation/NSAutoreleasePool.h>
#include <Foundation/NSCoder.h>

#include <extensions/PrintfFormatScanner.h>

#include "NSConcreteDate.h"
#include "NSCalendarDate.h"
#include "NSCalendarDateScannerHandler.h"
#include "NSCalendarDateScanf.h"

#include <math.h>

/*
 * Functions to deal with date conversions (phil & miki)
 * deal with dates in day:month:year reprezented in int form
 * as days since year 1st Jan year 1 ac
 */

static int  AdjustDay(int month, int day, int year);
static void DecDate(int *month, int *day, int *year);
static void IncDate(int *month, int *day, int *year);
static int  nr_nebisect(int a);
static int  day_in_year(int month, int day, int year);
static void Date2Long(int theMonth, int theDay, int theYear, long *theDate);
static void Long2Date(long theDate, int*theMonth, int*theDay, int*theYear);
static void SubFromDate(int *month, int *day, int *year, int dif);
static void AddToDate(int *month, int *day, int *year, int dif);
static void DecDate( int *month, int *day, int *year);
static void IncDate( int *month, int *day, int *year);

/*
 * Magic conversion offsets
 */

#define DATE_OFFSET 730486	/* Number of days from January 1, 1
				   to January 1, 2001 */
#define DAY_OFFSET 0

/*
 * NSCalendarDate implementation
 */

/* timeSinceRef is expressed in seconds relative to GMT from the reference day.
   The timeSinceRef is adjusted to represent this value when a date is created
   and a time zone is specified. Changing the time zone explicitly does not
   modify the timeSinceRef value. Only the methods that work with time
   components should take into consideration the time zone.
*/

@implementation NSCalendarDate

static NSString* DEFAULT_FORMAT = @"%Y-%m-%d %H:%M:%S %z";
static NSDictionary* defaultLocaleDictionary = nil;

+ (void)initialize
{
    id fullMonths[] = {
	@"January", @"February", @"March", @"April", @"May", @"June", 
	@"July", @"August", @"September", @"October", @"November", @"December"
    };
    id shortMonths[] = {
	@"Jan", @"Feb", @"Mar", @"Apr", @"May", @"Jun",
	@"Jul", @"Aug", @"Sep", @"Oct", @"Nov", @"Dec"
    };
    id fullDays[] = {
	@"Sunday", @"Monday", @"Tuesday", @"Wednesday",
	@"Thusday", @"Friday", @"Saturday"
    };
    id shortDays[] = {
	@"Sun", @"Mon", @"Tue", @"Wed", @"Thu", @"Fri", @"Sat"
    };
    defaultLocaleDictionary = [[NSDictionary dictionaryWithObjectsAndKeys:
	@"%a %b %d %H:%M:%S %z %Y", @"NSTimeDateFormatString",
	[NSArray arrayWithObjects:@"AM", @"PM", nil], @"NSAMPMDesignation",
	[[[NSArray alloc] initWithObjects:fullMonths
		 count:sizeof(fullMonths) / sizeof(id)]
		 autorelease], @"NSMonthNameArray",
	[[[NSArray alloc] initWithObjects:shortMonths
		 count:sizeof(shortMonths) / sizeof(id)]
		 autorelease],
		@"NSShortMonthNameArray",
	[[[NSArray alloc] initWithObjects:fullDays
		 count:sizeof(fullDays) / sizeof(id)]
		 autorelease], @"NSWeekDayNameArray",
	[[[NSArray alloc] initWithObjects:shortDays
		 count:sizeof(shortDays) / sizeof(id)]
		 autorelease],
		@"NSShortWeekDayNameArray",
		nil] retain];
}

/*
 * Inherited from NSDate cluster
 */

- (id)copyWithZone:(NSZone*)zone
{
    NSCalendarDate* date = [[self class] allocWithZone:zone];
    
    date->timeSinceRef = timeSinceRef;
    date->timeZoneDetail = [timeZoneDetail retain];
    date->formatString = [formatString retain];
    return date;
}

- (void)dealloc
{
    [timeZoneDetail release];
    [formatString release];
    [super dealloc];
}

- initWithTimeIntervalSinceReferenceDate:(NSTimeInterval)seconds
{
    [super init];
    timeSinceRef = seconds;
    timeZoneDetail = [[[NSTimeZone localTimeZone] timeZoneDetailForDate:self]
			retain];
    timeSinceRef -= [timeZoneDetail timeZoneSecondsFromGMT];
    formatString = [DEFAULT_FORMAT retain];
    return self;
}

- init
{
    [super init];
    timeSinceRef = [NSDate timeIntervalSinceReferenceDate];
    timeZoneDetail = [[[NSTimeZone localTimeZone] timeZoneDetailForDate:self]
			retain];
    timeSinceRef -= [timeZoneDetail timeZoneSecondsFromGMT];
    formatString = [DEFAULT_FORMAT retain];
    return self;
}

- (NSTimeInterval)timeIntervalSinceReferenceDate
{
    return timeSinceRef;
}

- (void)setTimeIntervalSinceReferenceDate:(NSTimeInterval)seconds
{
    timeSinceRef = seconds;
}

- (NSComparisonResult)compare:(NSDate*)other
{
    if ([other class] == [self class]) {
	NSTimeInterval diff
	    = timeSinceRef - [other timeIntervalSinceReferenceDate];

	return (diff < 0 ?
		  NSOrderedAscending
		: (diff == 0 ? NSOrderedSame : NSOrderedDescending));
    }

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

/*
* NSCalendarDate methods
*/

+ (id)calendarDate
{
    return [[[self alloc] init] autorelease];
}

+ (id)dateWithYear:(int)year month:(unsigned)month 
  day:(unsigned)day hour:(unsigned)hour minute:(unsigned)minute 
  second:(unsigned)second timeZone:(NSTimeZone*)aTimeZone
{
    return [[[self alloc]
	    initWithYear:year month:month day:day 
	    hour:hour minute:minute second:second 
	    timeZone:aTimeZone]
	    autorelease]; 
}

+ (id)dateWithString:(NSString*)string
{
    return [[[self alloc]
	    initWithString:string calendarFormat:nil locale:nil]
	    autorelease];
}

+ (id)dateWithString:(NSString*)string 
  calendarFormat:(NSString*)format
{
    return [[[self alloc]
	    initWithString:string calendarFormat:format locale:nil]
	    autorelease];
}

+ (id)dateWithString:(NSString*)string calendarFormat:(NSString*)format
  locale:(NSDictionary*)locale
{
    return [[[self alloc]
	    initWithString:string calendarFormat:format locale:locale]
	    autorelease];
}

- initWithYear:(int)year month:(unsigned)month day:(unsigned)day 
  hour:(unsigned)hour minute:(unsigned)minute second:(unsigned)second 
  timeZone:(NSTimeZone*)timeZone
{
    long date;

    Date2Long(month, day, year, &date);

    if (!timeZone)
	timeZone = [NSTimeZone localTimeZone];

    timeSinceRef = ((NSTimeInterval)date - DATE_OFFSET) * 86400 +
			hour * 3600 + minute * 60 + second;
    timeZoneDetail = [[timeZone timeZoneDetailForDate:self] retain];
    timeSinceRef -= [timeZoneDetail timeZoneSecondsFromGMT];
    formatString = [DEFAULT_FORMAT retain];
    return self;
}

- initWithString:(NSString*)description
{
    return [self initWithString:description
		 calendarFormat:DEFAULT_FORMAT
		 locale:defaultLocaleDictionary];
}

- initWithString:(NSString*)description calendarFormat:(NSString*)format
{
    return [self initWithString:description
		 calendarFormat:format
		 locale:defaultLocaleDictionary];
}

- initWithString:(NSString*)description
  calendarFormat:(NSString*)format
  locale:(NSDictionary*)locale
{
    id pool = [NSAutoreleasePool new];
    id formatScanner = [[NSCalendarDateScanf new] autorelease];

    if (!locale)
	locale = defaultLocaleDictionary;
    if (!format)
	format = DEFAULT_FORMAT;
    
    [formatScanner setString:description withLocale:locale];

    if ([formatScanner parseFormatString:format context:NULL])
	[self initWithYear:[formatScanner year]
	      month:[formatScanner month]
	      day:[formatScanner day]
	      hour:[formatScanner hour]
	      minute:[formatScanner minute]
	      second:[formatScanner second]
	      timeZone:[formatScanner timeZone]];
    else {
	[self autorelease];
	return nil;
    }

    [format retain];
    [formatString release];
    formatString = format;

    [pool release];

    return self;
}

- (NSTimeZoneDetail*)timeZoneDetail
{
    return timeZoneDetail;
}

- (void)setTimeZone:(NSTimeZone*)timeZone
{
    [timeZoneDetail release];
    timeZoneDetail = [[timeZone timeZoneDetailForDate:self] retain];
}
    
- (NSString*)calendarFormat
{
    return formatString;
}

- (void)setCalendarFormat:(NSString*)format
{
    [format retain];
    [formatString release];
    formatString = format;
}

- (int)yearOfCommonEra
{
    NSTimeInterval tm = timeSinceRef + [timeZoneDetail timeZoneSecondsFromGMT];
    long date = floor (tm / 86400) + DATE_OFFSET;
    int d, m, y;

    Long2Date(date, &m, &d, &y);
    return y;
}

- (int)monthOfYear
{
    NSTimeInterval tm = timeSinceRef + [timeZoneDetail timeZoneSecondsFromGMT];
    long date = floor (tm / 86400) + DATE_OFFSET;
    int d, m, y;

    Long2Date(date, &m, &d, &y);
    return m;
}

- (int)dayOfMonth
{
    NSTimeInterval tm = timeSinceRef + [timeZoneDetail timeZoneSecondsFromGMT];
    long date = floor (tm / 86400) + DATE_OFFSET;
    int d, m, y;

    Long2Date(date, &m, &d, &y);
    return d;
}

- (int)dayOfWeek
{
    NSTimeInterval tm = timeSinceRef + [timeZoneDetail timeZoneSecondsFromGMT];
    long noOfDays = floor (tm / 86400) + DATE_OFFSET;
    int dayOfWeek = abs (noOfDays) % 7 + DAY_OFFSET;

    return dayOfWeek;
}

- (int)dayOfYear
{
    NSTimeInterval tm = timeSinceRef + [timeZoneDetail timeZoneSecondsFromGMT];
    long date = floor(tm / 86400) + DATE_OFFSET;
    int d, m, y;

    Long2Date(date, &m, &d, &y);
    return day_in_year(m, d, y);
}

- (int)hourOfDay
{
    NSTimeInterval tm = timeSinceRef + [timeZoneDetail timeZoneSecondsFromGMT];
    NSTimeInterval tr = tm / 3600;
    NSTimeInterval ti = floor(tr/24);
    int ts = (tr - ti * 24);
    return ts < 0 ? 24 + ts : ts;
}

- (int)minuteOfHour
{
    NSTimeInterval tm = timeSinceRef + [timeZoneDetail timeZoneSecondsFromGMT];
    NSTimeInterval tr = tm / 60;
    NSTimeInterval ti = floor (tr / 60);
    int ts = (tr - ti * 60);

    return ts < 0 ? 60 + ts : ts;
}

- (int)secondOfMinute
{
    NSTimeInterval tm = timeSinceRef + [timeZoneDetail timeZoneSecondsFromGMT];
    NSTimeInterval ti = floor (tm / 60);
    int ts = (tm - ti * 60);

    return ts < 0 ? 60 + ts : ts;
}

- (NSCalendarDate*)addYear:(int)year month:(int)month day:(int)day
  hour:(int)hour minute:(int)minute second:(int)second
{
    return [self dateByAddingYears:year month:month day:day hour:hour
		    minute:minute second:second];
}

- (id)dateByAddingYears:(int)year month:(int)month day:(int)day 
  hour:(int)hour minute:(int)minute second:(int)second
{
    NSTimeInterval tm = timeSinceRef + [timeZoneDetail timeZoneSecondsFromGMT];
    NSCalendarDate* date;
    long monthDayYear, hourMinuteSecond;
    int selfMonth, selfDay, selfYear;
    NSTimeInterval newInterval;

    monthDayYear = floor (tm / 86400) + DATE_OFFSET;
    Long2Date(monthDayYear, &selfMonth, &selfDay, &selfYear);
    hourMinuteSecond
	= (long)(tm - 86400 * ((NSTimeInterval)monthDayYear - DATE_OFFSET));

    /* Add day */
    if (day >= 0)
	AddToDate (&selfMonth, &selfDay, &selfYear, day);
    else
	SubFromDate (&selfMonth, &selfDay, &selfYear, -day);

    /* Add month and year */
    selfYear += month / 12 + year;
    selfMonth += month % 12;
    if (selfMonth > 12) {
	selfYear++; selfMonth -= 12;
    }
    /* Adjust the day */
    selfDay = AdjustDay(selfMonth, selfDay, selfYear);

    /* Convert the (month, day, year) to long */
    Date2Long (selfMonth, selfDay, selfYear, &monthDayYear);

    /* Compute the new interval */
    newInterval = ((NSTimeInterval)monthDayYear - DATE_OFFSET) * 86400
		    + hourMinuteSecond
		    + hour * 3600 + minute * 60 + second;

    date = [[[self class] alloc] autorelease];
    date->timeSinceRef = newInterval - [timeZoneDetail timeZoneSecondsFromGMT];
    date->timeZoneDetail = [timeZoneDetail retain];
    date->formatString = [formatString retain];

    return date;
}

- (NSString*)description
{
    return [NSCalendarDate descriptionForCalendarDate:self
		withFormat:formatString
		timeZoneDetail:timeZoneDetail
		locale:nil];
}

- (NSString*)descriptionWithLocale:(NSDictionary*)locale
{
    return [NSCalendarDate descriptionForCalendarDate:self
		withFormat:formatString
		timeZoneDetail:timeZoneDetail
		locale:locale];
}

- (NSString*)descriptionWithCalendarFormat:(NSString*)format
{
    return [NSCalendarDate descriptionForCalendarDate:self
		withFormat:format
		timeZoneDetail:timeZoneDetail
		locale:nil];
}

- (NSString*)descriptionWithCalendarFormat:(NSString*)format 
  timeZone:(NSTimeZone*)timeZone
{
    return [NSCalendarDate descriptionForCalendarDate:self
		withFormat:format
		timeZoneDetail:[timeZone timeZoneDetailForDate:self]
		locale:nil];
}

- (NSString*)descriptionWithCalendarFormat:(NSString*)format
  locale:(NSDictionary*)locale
{
    return [NSCalendarDate descriptionForCalendarDate:self
		withFormat:format
		timeZoneDetail:timeZoneDetail
		locale:locale];
}

/* Encoding */
- (Class)classForCoder
{
    return [NSCalendarDate class];
}

- (void)encodeWithCoder:(NSCoder*)aCoder
{
    [aCoder encodeValueOfObjCType:@encode(NSTimeInterval) at:&timeSinceRef];
    [aCoder encodeObject:[timeZoneDetail timeZoneName]];
    [aCoder encodeObject:formatString];
}

- (id)initWithCoder:(NSCoder*)aDecoder
{
    NSTimeZone* timeZone;

    [aDecoder decodeValueOfObjCType:@encode(NSTimeInterval) at:&timeSinceRef];
    timeZone = [NSTimeZone timeZoneWithName:[aDecoder decodeObject]];
    timeZoneDetail = [[timeZone timeZoneDetailForDate:self] retain];
    formatString = [[aDecoder decodeObject] retain];
    return self;
}

@end

/*
 * NSCalendarDateImplementation
 */

@implementation NSCalendarDate (NSCalendarDateImplementation)

+ (NSString*)descriptionForCalendarDate:(NSCalendarDate*)date
  withFormat:(NSString*)format
  timeZoneDetail:(NSTimeZoneDetail*)detail
  locale:(NSDictionary*)locale
{
    id formatScanner = [PrintfFormatScanner new];
    id scannerHandler = [NSCalendarDateScannerHandler alloc];
    id string;

    [scannerHandler initForCalendarDate:date timeZoneDetail:detail];
    [formatScanner setFormatScannerHandler:scannerHandler];
    string = [formatScanner stringWithFormat:format arguments:NULL];
    [scannerHandler release];
    [formatScanner release];
    return string;
}

+ (NSString*)shortDayOfWeek:(int)day
{
    return [[defaultLocaleDictionary objectForKey:@"NSShortWeekDayNameArray"]
		objectAtIndex:day];
}

+ (NSString*)fullDayOfWeek:(int)day
{
    return [[defaultLocaleDictionary objectForKey:@"NSWeekDayNameArray"]
		objectAtIndex:day];
}

+ (NSString*)shortMonthOfYear:(int)month
{
    return [[defaultLocaleDictionary objectForKey:@"NSShortMonthNameArray"]
		objectAtIndex:month - 1];
}

+ (NSString*)fullMonthOfYear:(int)month
{
    return [[defaultLocaleDictionary objectForKey:@"NSMonthNameArray"]
		objectAtIndex:month - 1];
}

+ (int)decimalDayOfYear:(int)year month:(int)month day:(int)day
{
    return day_in_year(month, day, year);
}

@end

/*
* Functions to deal with date conversions (phil & miki)
* deal with dates in day:month:year reprezented in int form
* as days since year 1st Jan year 1 ac
*/

static int nDays[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};

#define bisect(a) 	((a) % 4 == 0 && ((a) % 400 == 0 || (a) % 100 ))
#define	nr_bisect(a) 	((a - 1) / 4 - nr_nebisect(a - 1))

static int  AdjustDay(int month, int day, int year)
{
    if(bisect(year) && month == 2 && day == 29)
	return day;
    if (day > nDays[month - 1])
	day = nDays[month - 1];
    return day;
}

static int nr_nebisect(int a)
{
    int i;
    int ret = 0;

    for(i = 100; i <= a; i += 100)
	ret += (i % 400 != 0);
    return ret;
}

static int day_in_year(int month, int day, int year)
{
    int ret = day + (month > 2 && bisect(year)) ? 1 : 0;
    while (month--)
	day += nDays[month];
    return ret;
}

static void Date2Long(int theMonth, int theDay, int theYear, long *theDate)
{
    long base, offset = 0;
    int i;

    base = ((long)theYear - 1) * 365 + nr_bisect(theYear);
    for (i = 1; i < theMonth; i++)
	offset += nDays[i - 1];
    offset += theDay;
    if (theMonth > 2 && bisect(theYear))
	offset++;
    *theDate = base + offset;
}

static void Long2Date(long theDate, int *theMonth, int *theDay, int *theYear)
{
    int month = 1, day, year, offset, i, days = 0, dif;
    long aproxDate;

    year = (theDate - 1) / 365 + 1;
    offset = (theDate - 1) % 365 + 1;
    for (i = 1; i <= 12; i++) {
	month = i;
	if (days + nDays[i - 1] >= offset)
	    break;
	days += nDays[i - 1];
    }

    day = offset - days;
    Assert (day <= nDays[month - 1] && day > 0);

    Date2Long(month, day, year, &aproxDate);
    dif = aproxDate - theDate;

    Assert(dif >= 0);

    SubFromDate(&month, &day, &year, dif);
    *theMonth = month;
    *theDay = day;
    *theYear = year;
}

static void SubFromDate(int *month, int *day, int *year, int dif)
{
    while (dif > 0) {
	if (*day > dif) {
	    *day -= dif;
	    break;
	}
	dif -= *day;
	*day = 1;
	DecDate(month, day, year);
    }
}

static void AddToDate(int *month, int *day, int *year, int dif)
{
    int rest, bi;

    while (dif > 0) {
	bi = bisect(*year);
	if ((*day + dif <= nDays[*month - 1])
		|| (*month == 2 && bi && *day + dif <= 29)) {
	    (*day) += dif;
	    break;
	}
	rest = nDays[*month - 1] - (*day) + (*month == 2 && bi) + 1;
	dif -= rest;
	*day = nDays[*month - 1] + (*month == 2 && bi);
	IncDate(month, day, year);
    }
}

static void DecDate( int *month, int *day, int *year)
{
    (*day)--;
    if (*day)
	return;
    (*month)--;
    if (*month) {
	if (*month == 2 && bisect(*year))
	    *day = 29;
	else
	    *day = nDays[*month-1];
	return;
    }
    *month = 12;
    *day = 31;
    (*year)--;

    Assert(*year != 0);
}

static void IncDate( int *month, int *day, int *year)
{
    (*day)++;
    if (*day <= nDays[*month - 1] || 
	    (*month == 2 && bisect(*year) && *day == 29))
	return;

    *day = 1;
    (*month)++;
    if (*month <= 12)
	    return;
    *month = 1;
    (*year)++;
    Assert(*year != 0);
}
