// MODateTextCell.m
//
// by Mike Ferris
// Part of MOKit
// Copyright 1993, all rights reserved.

// ABOUT MOKit
// by Mike Ferris (mike@lorax.com)
//
// MOKit is a collection of useful and general objects.  Permission is 
// granted by the author to use MOKit in your own programs in any way 
// you see fit.  All other rights pertaining to the kit are reserved by the 
// author including the right to sell these objects as objects,  as part 
// of a LIBRARY, or as SOURCE CODE.  In plain English, I wish to retain 
// rights to these objects as objects, but allow the use of the objects 
// as pieces in a fully functional program.  Permission is also granted to 
// redistribute the source code of MOKit for FREE as long as this copyright 
// notice is left intact and unchanged.  NO WARRANTY is expressed or implied.  
// The author will under no circumstances be held responsible for ANY 
// consequences from the use of these objects.  Since you don't have to pay 
// for them, and full source is provided, I think this is perfectly fair.

#import "MOKit/MODateTextCell.h"
#import "MOKit/MOString.h"
#import <objc/objc-runtime.h>

#define CLASS_VERSION		0
#define CLASS_NAME			"MODateTextCell"

#define BUNDLE_TYPE							"bundle"

#define MOSTRING_CLASS_NAME					"MOString"

#define AM_DATE_REGEX_1		"^[ ]*(([0][1-9]|[1][0-2])([0][1-9]|[1-2][0-9]|[3][0-1])([0-9]?[0-9]?[0-9][0-9]))[ ]*$"

#define AM_DATE_REGEX_2		"^[ ]*(([0]?[1-9]|[1][0-2])[-/]([0]?[1-9]|[1-2][0-9]|[3][0-1])[-/]([0-9]?[0-9]?[0-9][0-9]))[ ]*$"

#define AM_DATE_REGEX_3		"^[ ]*(([jJ][aA][nN]?[uU]?[aA]?[rR]?[yY]?|[fF][eE]?[bB]?[rR]?[uU]?[aA]?[rR]?[yY]?|[mM][aA][rR][cC]?[hH]?|[aA][pP][rR]?[iI]?[lL]?|[mM][aA][yY]|[jJ][uU][nN][eE]?|[jJ][uU][lL][yY]?|[aA][uU][gG]?[uU]?[sS]?[tT]?|[sS][eE]?[pP]?[tT]?[eE]?[mM]?[bB]?[eE]?[rR]?|[oO][cC]?[tT]?[oO]?[bB]?[eE]?[rR]?|[nN][oO]?[vV]?[eE]?[mM]?[bB]?[eE]?[rR]?|[dD][eE]?[cC]?[eE]?[mM]?[bB]?[eE]?[rR]?)[ ]*([0]?[1-9]|[1-2][0-9]|[3][0-1])[ ]*[,]?[ ]*([0-9]?[0-9]?[0-9][0-9]))[ ]*$"

#define EU_DATE_REGEX_1		"^[ ]*(([0][1-9]|[1-2][0-9]|[3][0-1])([0][1-9]|[1][0-2])([0-9]?[0-9]?[0-9][0-9]))[ ]*$"

#define EU_DATE_REGEX_2		"^[ ]*(([0]?[1-9]|[1-2][0-9]|[3][0-1])[-/]([0]?[1-9]|[1][0-2])[-/]([0-9]?[0-9]?[0-9][0-9]))[ ]*$"

#define EU_DATE_REGEX_3		"^[ ]*(([0]?[1-9]|[1-2][0-9]|[3][0-1])[ ]*[,]?[ ]*([jJ][aA][nN]?[uU]?[aA]?[rR]?[yY]?|[fF][eE]?[bB]?[rR]?[uU]?[aA]?[rR]?[yY]?|[mM][aA][rR][cC]?[hH]?|[aA][pP][rR]?[iI]?[lL]?|[mM][aA][yY]|[jJ][uU][nN][eE]?|[jJ][uU][lL][yY]?|[aA][uU][gG]?[uU]?[sS]?[tT]?|[sS][eE]?[pP]?[tT]?[eE]?[mM]?[bB]?[eE]?[rR]?|[oO][cC]?[tT]?[oO]?[bB]?[eE]?[rR]?|[nN][oO]?[vV]?[eE]?[mM]?[bB]?[eE]?[rR]?|[dD][eE]?[cC]?[eE]?[mM]?[bB]?[eE]?[rR]?)[ ]*[,]?[ ]*([0-9]?[0-9]?[0-9][0-9]))[ ]*$"

@interface MODateTextCell(Private)

+ (Class)MO_loadClassBundle:(const char *)className;

@end

@implementation MODateTextCell

static id MOStringClass;

+ (Class)MO_loadClassBundle:(const char *)className
// Finds the bundle of the same name as the class, grabs it and loads the
// class from it and returns the named class.
{
	char pathBuff[MAXPATHLEN+1];
	id classBundle = nil;
	Class class = nil;
	
	// Load the bundle
	if ((class = objc_lookUpClass(className)) == nil)  {
		// class is not already loaded... load it.
		
		// Look for the bundle in the main bundle first, 
		// else try in this class's bundle.
		if (![[NXBundle mainBundle] getPath:pathBuff forResource:className 
					ofType:BUNDLE_TYPE])  {
			if (![[NXBundle bundleForClass:[self class]] getPath:pathBuff 
						forResource:className ofType:BUNDLE_TYPE])  {
				NXLogError("[%s loadClassBundle] failed to "
						"find %s class bundle.", [self name], className);
				return nil;
			}
		}
		classBundle = [[NXBundle allocFromZone:[self zone]] 
					initForDirectory:pathBuff];
		if (!classBundle)  {
			NXLogError("[%s loadClassBundle] failed to "
						"create bundle for class %s.", [self name], className);
			return nil;
		}
		if ((class = [classBundle classNamed:className]) == nil)  {
			NXLogError("[%s loadClassBundle] failed to "
						"load %s class from bundle.", [self name], className);
			return nil;
		}
	}
	
	return class;
}


+ initialize
// Set the version.
{
	if (self == objc_lookUpClass(CLASS_NAME))  {
		[self setVersion:CLASS_VERSION];

		// Load the string class if necessary
		MOStringClass = [self MO_loadClassBundle:MOSTRING_CLASS_NAME];
	}
	return self;
}

- initTextCell:(const char *)aString withRegex:(const char *)re
// We'll ignore any regex expression passed to us, cause we manage our own
// expressions.
{
	[super initTextCell:aString withRegex:AM_DATE_REGEX_1];
	
	isEuropeanStyle = NO;
	doesFormat = NO;
	formatString = nil;
	[self addRegexStr:AM_DATE_REGEX_2];
	[self addRegexStr:AM_DATE_REGEX_3];
	
	formatString = [[MOStringClass allocFromZone:[self zone]] init];
	
	return self;
}

- MO_copyFormatString:str zone:(NXZone *)zone
{
	formatString = [[MOStringClass allocFromZone:zone] 
				initStringValue:[str stringValue]];
	return self;
}

- copyFromZone:(NXZone *)zone
{
	id obj = [super copyFromZone:zone];
	[obj MO_copyFormatString:formatString zone:zone];
	
	return obj;
}

- free
{
	[formatString free];
	return [super free];
}

- (BOOL)isEuropeanStyle
{
	return isEuropeanStyle;
}

- setEuropeanStyle:(BOOL)flag
{
	if (flag != isEuropeanStyle)  {
		isEuropeanStyle = flag;
		if (isEuropeanStyle)  {
			[self setRegexStr:EU_DATE_REGEX_1];
			[self addRegexStr:EU_DATE_REGEX_2];
			[self addRegexStr:EU_DATE_REGEX_3];
		}  else  {
			[self setRegexStr:AM_DATE_REGEX_1];
			[self addRegexStr:AM_DATE_REGEX_2];
			[self addRegexStr:AM_DATE_REGEX_3];
		}
	}
	// reset the pieces and stuff.
	[self isEntryAcceptable:[self stringValue]];
	
	return self;
}

- (BOOL)doesFormatDates
{
	return doesFormat;
}

- setFormatDates:(BOOL)flag
{
	doesFormat = flag;
	[self formatDate];
	return self;
}

- (const char *)formatString
{
	return [formatString stringValue];
}

- setFormatString:(const char *)str
{
	[formatString setStringValue:str];
	[self formatDate];
	return self;
}

- (BOOL)MO_finishCheckingDate
// Make sure that the days in the month is valid.  We can assume that if we're
// here, the regular expression parts have checked out.
{
	int monthNum= [self month], yearNum= [self year], dayNum= [self day];
	
	if ((monthNum<0) || (yearNum<0) || (dayNum<0))  {
		return NO;
	}
	
	switch (monthNum)  {
		// "thirty days has ..."
		case 9:		// "September, ..."
		case 4:		// "April, ..."
		case 6:		// "June, and ..."
		case 11:	// "November ..."
			if (dayNum>30) return NO;
			break;
		
		// "All the rest have thirty one ..."
		// (but the regular expression made sure it was at least 31 or 
		// less.)
		// "Except for February which has 28 ..."
		// "(or 29 if its a leap year)."
		case 2:		// February
			if (dayNum > 29) return NO;
			
			if (dayNum == 29)  {
				BOOL isLeapYear=NO;
								
				// figure out if it's a leap year.
				if (yearNum%4 == 0)  {
					if (yearNum%100 == 0)  {
						if (yearNum%1000 == 0)  {
							isLeapYear=YES;
						}
					}  else  {
						isLeapYear=YES;
					}
				}
				if (!isLeapYear)  return NO;
			}
			break;
		default:
			break;
	}
	
	return YES;
}

- (BOOL)isEntryAcceptable:(const char *)aString
{
	if ([super isEntryAcceptable:aString])  {
		if ((aString) && (*aString))  {
			return [self MO_finishCheckingDate];
		}  else  {
			return YES;
		}
	}  else  {
		return NO;
	}
}

- (int)day
{
	const char *day;
	int dayNum = 0;

	if (isEuropeanStyle)  {
		day = [self pieceAt:2];
	}  else  {
		day = [self pieceAt:3];
	}
	if (day)  {
		dayNum = atoi(day);
	}  else  {
		dayNum = -1;
	}
	return dayNum;
}

- (int)month
{
	const char *month;
	int monthNum = 0;
	
	if (isEuropeanStyle)  {
		month = [self pieceAt:3];
	}  else  {
		month = [self pieceAt:2];
	}
	
	if (!month)  {
		return -1;
	}
	// Is month numeric or name?
	if (NXIsDigit(*month))  {
		// it's number
		monthNum = atoi(month);
	}  else  {
		// it's name
		if (NXOrderStrings(month, "ja", NO, 2, NULL) == 0)  {
			monthNum = 1;
		}  else if (NXOrderStrings(month, "f", NO, 1, NULL) == 0)  {
			monthNum = 2;
		}  else if (NXOrderStrings(month, "mar", NO, 3, NULL) == 0)  {
			monthNum = 3;
		}  else if (NXOrderStrings(month, "ap", NO, 2, NULL) == 0)  {
			monthNum = 4;
		}  else if (NXOrderStrings(month, "may", NO, 3, NULL) == 0)  {
			monthNum = 5;
		}  else if (NXOrderStrings(month, "jun", NO, 3, NULL) == 0)  {
			monthNum = 6;
		}  else if (NXOrderStrings(month, "jul", NO, 3, NULL) == 0)  {
			monthNum = 7;
		}  else if (NXOrderStrings(month, "au", NO, 2, NULL) == 0)  {
			monthNum = 8;
		}  else if (NXOrderStrings(month, "s", NO, 1, NULL) == 0)  {
			monthNum = 9;
		}  else if (NXOrderStrings(month, "o", NO, 1, NULL) == 0)  {
			monthNum = 10;
		}  else if (NXOrderStrings(month, "n", NO, 1, NULL) == 0)  {
			monthNum = 11;
		}  else if (NXOrderStrings(month, "d", NO, 1, NULL) == 0)  {
			monthNum = 12;
		}  else  {
			// this shouldn't happen since the regular expression
			// has been checked to make sure it was a valid month.
			return -1;
		}
	}
	return monthNum;
}

- (int)year
{
	const char *year;
	int yearNum = 0;
	
	if (isEuropeanStyle)  {
		year = [self pieceAt:4];
	}  else  {
		year = [self pieceAt:4];
	}
	
	if (!year)  {
		return -1;
	}
	yearNum=atoi(year);
	if (yearNum < 100)  {
		// adjust for current century
		struct timeval tv;
		struct timezone tz;
		struct tm *tmp = NULL;
		int currentCentury;
		
		gettimeofday(&tv, &tz);
		tmp = localtime(&tv.tv_sec);
		currentCentury = 1900 + (tmp->tm_year / 100);
		yearNum += currentCentury;
	}
	return yearNum;
}

- endEditing:textObject
{
	[super endEditing:textObject];
	
	[self formatDate];
	
	return self;
}

- (const char *)MO_monthNameForNum:(int)monthNum short:(BOOL)flag
{
	static char buf[20];
	switch(monthNum)  {
		case 1:
			if (flag)  {
				strcpy(buf, "Jan");
			}  else  {
				strcpy(buf, "January");
			}
			break;
		case 2:
			if (flag)  {
				strcpy(buf, "Feb");
			}  else  {
				strcpy(buf, "February");
			}
			break;
		case 3:
			if (flag)  {
				strcpy(buf, "Mar");
			}  else  {
				strcpy(buf, "March");
			}
			break;
		case 4:
			if (flag)  {
				strcpy(buf, "Apr");
			}  else  {
				strcpy(buf, "April");
			}
			break;
		case 5:
			strcpy(buf, "May");
			break;
		case 6:
			if (flag)  {
				strcpy(buf, "Jun");
			}  else  {
				strcpy(buf, "June");
			}
			break;
		case 7:
			if (flag)  {
				strcpy(buf, "Jul");
			}  else  {
				strcpy(buf, "July");
			}
			break;
		case 8:
			if (flag)  {
				strcpy(buf, "Aug");
			}  else  {
				strcpy(buf, "August");
			}
			break;
		case 9:
			if (flag)  {
				strcpy(buf, "Sep");
			}  else  {
				strcpy(buf, "September");
			}
			break;
		case 10:
			if (flag)  {
				strcpy(buf, "Oct");
			}  else  {
				strcpy(buf, "October");
			}
			break;
		case 11:
			if (flag)  {
				strcpy(buf, "Nov");
			}  else  {
				strcpy(buf, "November");
			}
			break;
		case 12:
			if (flag)  {
				strcpy(buf, "Dec");
			}  else  {
				strcpy(buf, "December");
			}
			break;
		default:
			strcpy(buf, "");;
	}
	return buf;
}

- formatDate
{
	static char buf[1024];
	if (doesFormat)  {
		const char *format = [formatString stringValue];
		const char *fPtr = NULL;
		char *bPtr = NULL;
		const char *month = NULL;
		
		int dayNum = [self day], monthNum = [self month];
		int yearNum = [self year];
		
		if ((!format) || (!*format) || (dayNum < 0) || 
					(monthNum < 0) || (yearNum < 0))  {
			return nil;
		}
		
		bPtr = buf;
		fPtr = format;
		while (*fPtr)  {
			if (*fPtr == '%')  {
				// Its a format code...
				fPtr++;
				switch (*fPtr)  {
					case 'd':
						sprintf(bPtr, "%d", dayNum);
						break;
					case 'D':
						sprintf(bPtr, "%02d", dayNum);
						break;
					case 'm':
						sprintf(bPtr, "%d", monthNum);
						break;
					case 'M':
						sprintf(bPtr, "%02d", monthNum);
						break;
					case 'n':
						month = [self MO_monthNameForNum:monthNum short:YES];
						sprintf(bPtr, "%s", month);
						break;
					case 'N':
						month = [self MO_monthNameForNum:monthNum short:NO];
						sprintf(bPtr, "%s", month);
						break;
					case 'y':
						sprintf(bPtr, "%d", yearNum%100);
						break;
					case 'Y':
						sprintf(bPtr, "%d", yearNum);
						break;
					case '%':
						strcpy(bPtr, "%");
						break;
					default:
						NXLogError("%s: Invalid date "
									"formatting code.", [[self class] name]);
						break;
				}
				bPtr = buf + strlen(buf);
				fPtr++;
			}  else  {
				// Just copy the character...
				*bPtr = *fPtr;
				bPtr++; fPtr++;
			}
		}
		*bPtr = '\0';
		
		// call super to avoid an infinite loop...
		// (our setStringValue calls formatDate)
		[super setStringValue:buf];
	}
	return self;
}

- setDoubleValue:(double)aDouble
{
	id ret = [super setDoubleValue:aDouble];
	[self formatDate];
	return ret;
}

- setFloatValue:(float)aFloat
{
	id ret = [super setFloatValue:aFloat];
	[self formatDate];
	return ret;
}

- setIntValue:(int)anInt
{
	id ret = [super setIntValue:anInt];
	[self formatDate];
	return ret;
}

- setStringValue:(const char *)aString
{
	id ret = [super setStringValue:aString];
	[self formatDate];
	return ret;
}

- setStringValueNoCopy:(const char *)aString
{
	id ret = [super setStringValueNoCopy:aString];
	[self formatDate];
	return ret;
}

- setStringValueNoCopy:(char *)aString shouldFree:(BOOL)flag
{
	id ret = [super setStringValueNoCopy:aString shouldFree:flag];
	[self formatDate];
	return ret;
}

- read:(NXTypedStream *)strm
{
	int classVersion;

	[super read:strm];
	
	classVersion = NXTypedStreamClassVersion(strm, CLASS_NAME);
	
	switch (classVersion)  {
		case 0:		// First version.
			NXReadType(strm, "C", &isEuropeanStyle);
			NXReadType(strm, "C", &doesFormat);
			formatString = NXReadObject(strm);
			break;
		default:
			NXLogError("[%s read:] class version %d cannot read "
						"instances archived with version %d", 
						CLASS_NAME, CLASS_VERSION, classVersion);
			isEuropeanStyle = NO;
			doesFormat = NO;
			formatString = [[MOStringClass allocFromZone:[self zone]] init];
			break;
	}
	return self;
}

- write:(NXTypedStream *)strm;
{
	[super write:strm];
	NXWriteType(strm, "C", &isEuropeanStyle);
	NXWriteType(strm, "C", &doesFormat);
	NXWriteObject(strm, formatString);
	return self;
}

- (const char *)getInspectorClassName
{
	return "MODateTextCellInspector";
}


@end
