// MORegexTextCell.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/MORegexTextCell.h"
#import "MOKit/MOString.h"
#import "regexp.cinc"
#import "regsub.cinc"
#import <objc/objc-runtime.h>

#define CLASS_VERSION		0
#define CLASS_NAME			"MORegexTextCell"

#define BUNDLE_TYPE							"bundle"

#define MOSTRING_CLASS_NAME					"MOString"

@interface MORegexTextCell(Private)

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

@end

@implementation MORegexTextCell

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;
}

+ (BOOL)isValidRegex:(const char *)str
{
	regexp *re;
	re = regcomp((char *)str);
	if (re)  {
		free(re);
		return YES;
	}  else  {
		return NO;
	}
}

- initTextCell:(const char *)aString
// Super's DI.  Overriden to call our DI.
{
	return [self initTextCell:aString withRegex:NULL];
}

- initTextCell:(const char *)aString withRegex:(const char *)re
// Our designated initializer.
{
	int i;
	
	[super initTextCell:aString];
	pieceCount = -1;
	for (i=0;i<NSUBEXP;i++)  {
		pieces[i]=NULL;
	}
	regexStrs = [[List allocFromZone:[self zone]] init]; 
	matchedIndex = -1;
	allowEmptyString = YES;
	delegate = nil;
	if ((re) && (*re))  {
		if (![self addRegexStr:re])  {
#ifdef DEBUG
			NXLogError("%s debug: initialized with "
						"incompatible string and pattern.\n", 
						[[self class] name]);
#endif /* DEBUG */
		}
	}
	return self;
}

- MO_copyRegexStrs:list zone:(NXZone *)zone
{
	int i, c;
	id string;
	
	regexStrs = [[List allocFromZone:zone] init];
	c = [list count];
	for (i=0; i<c; i++)  {
		string = [[MOStringClass allocFromZone:zone] 
						initStringValue:[[list objectAt:i] stringValue]];
		[regexStrs addObject:string];
	}
	return self;
}

- MO_resetPieces
{
	int i;
	for (i=0; i<NSUBEXP; i++)  {
		pieces[i]=NULL;
	}
	[self isEntryAcceptable:[self stringValue]];
	return self;
}

- copyFromZone:(NXZone *)zone
{
	id obj = [super copyFromZone:zone];
	[obj MO_copyRegexStrs:regexStrs zone:zone];
	[obj MO_resetPieces];
	
	return obj;
}

- free
{
	int i;
	for (i=0;i<NSUBEXP;i++)  {
		if (pieces[i]) NX_FREE(pieces[i]);
	}
	[regexStrs freeObjects];
	[regexStrs free];
	return [super free];
}

- (BOOL)isEntryAcceptable:(const char *)aString
// Returns YES if the given string matches at least one of the cell's pattern
// strings.  If a match is found, then the pieces array is filled with the
// substrings of aString which matched parenthesized parts of the regular
// expression that was matched if any.  The pieces methods will only return
// valid values if the last call to this method returned YES.
{
	int i, j, c = [regexStrs count];
	regexp *re = NULL;
	
	matchedIndex = -1;
	pieceCount = -1;

	if ((allowEmptyString) && ((aString==NULL) || (*aString=='\0')))  {
		return YES;
	}
	
	// If we don't have any pattern strings we match anything.
	if (c==0)  {
		if ((delegate) && 
				([delegate respondsTo:@selector(finishValidating: for:)]))  {
			return [delegate finishValidating:aString for:self];
		}
		return YES;
	}
	
	// Cycle through the patterns for this cell.  Check one by one
	// starting from the first stopping when one is found which matches
	// the string.  If one is found, populate the pieces array with all
	// the subexpression matches for the matched pattern.
	for (i=0; i<c; i++)  {
		// Compile the expression.  A bad expression causes immediate
		// failure.  No further patterns are checked.
		re = regcomp((char *)[[regexStrs objectAt:i] stringValue]);
		if (!re)  {
			NXBeep();
#ifdef DEBUG
			NXLogError("%s debug: Regular expression "
						"pattern #%d: '%s' failed to compile.\n", 
						[[self class] name], i, 
						[[regexStrs objectAt:i] stringValue]);
#endif /* DEBUG */
			return NO;
		}
		// The expression compiled, now check it against the string.
		if (regexec(re, (char *)aString)==0)  {
			// Failure, check the next one.
			NX_FREE(re);
			continue;
		}
		
		// The expression checked, now fill in the pieces array.
		
		// First empty it.
		for (j=0; j<NSUBEXP; j++)  {
			if (pieces[j])  NX_FREE(pieces[j]);
			pieces[j]=NULL;
		}
		// Now fill the array and count how many sub-expressions there were.
		pieceCount=0;
		for (j = 0; j < NSUBEXP; j++)
 {
			if (re->startp[j] != NULL && re->endp[j] != NULL)  {
				char subStr[5];
				NX_MALLOC(pieces[pieceCount], char, strlen(aString)+1);
				sprintf(subStr, "\\%d", j);
				regsub(re, subStr, pieces[pieceCount]);
				pieceCount++;
			}
		}
		NX_FREE(re);
		matchedIndex = i;
		if ((delegate) && 
				([delegate respondsTo:@selector(finishValidating: for:)]))  {
			return [delegate finishValidating:aString for:self];
		}
		return YES;
	}
	return NO;
}

- (const char *)regexStr
// returns the first regex string.  This method is a convenient shorthand
// for regexStrAt:0.  If your cell has more than 1 string it is advisable
// to use the long form for clarity.
{
	return [self regexStrAt:0];
}

- setRegexStr:(const char *)re
// For cells that will only have one string, this method will set it.
// It frees all the strings that might be there, then adds one for the
// given expression.
{
	if ((re) && (*re))  {
		[regexStrs freeObjects];
		[self addRegexStr:re];
		return self;
	}  else  {
		return nil;
	}
}

- (const char *)regexStrAt:(int)index
// Returns regex string number index for this cell.  Returns NULL if index
// is out of range.
{
	if ((index>=0) && ([regexStrs count]>index))  {
		return [[regexStrs objectAt:index] stringValue];
	}  else  {
		return NULL;
	}
}

- addRegexStr:(const char *)re
// If re is a valid non-empty char pointer a string object is allocated for it
// and it is added to the list of patterns for this cell.
{
	id newStr;
	
	if ((re) && (*re))  {
		newStr = [[MOStringClass allocFromZone:[self zone]] 
					initStringValue:re];
		[regexStrs addObject:newStr];
		if ([self isEntryAcceptable:[self stringValue]])  {
			return self;
		}  else  {
			return nil;
		}
	}  else  {
		return nil;
	}
}

- removeRegexStrAt:(int)index
// removes the string at index from the patterns for this cell.
{
	[[regexStrs removeObjectAt:index] free];
	return self;
}

- (int)regexStrCount
// Returns the number of pattern strings for this cell.
{
	return [regexStrs count];
}

- regexStrList
// returns the list object used to store the pattern strings for this cell.
{
	return regexStrs;
}

// These routines return the number of () sets in the last regular expression
// matched or the actual pattern matched for a given set of () in the last
// expression that was matched.
- (int)pieceCount
{
	return pieceCount;
}

- (const char *)pieceAt:(int)n
{
	if ((n >= 0) && (n < pieceCount))  {
		return pieces[n];
	}
	return NULL;
}

- (int)matchedPatternIndex
// Returns the index into regexStrs of the pattern that was matched with the
// last call to isEntryAcceptable or -1 if no match was found.
// Note that matchedIndex does not test for whether the string was valid
// since it will be -1 if empty strings are allowed and the cell is empty.
{
	return matchedIndex;
}

- (BOOL)doesAllowEmptyString
// Returns YES if this cell will allow empty strings regardless of whether
// one of its patterns matches the empty string.  NO otherwise.
{
	return allowEmptyString;
}

- setAllowEmptyString:(BOOL)flag;
// If flag is YES this cell will allow empty strings regardless of whether
// one of its patterns matches the empty string.  If its NO it won't.
{
	allowEmptyString=flag;
	return self;
}

- delegate
{
	return delegate;
}

- setDelegate:obj
{
	delegate = obj;
	return self;
}


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

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

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

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

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

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


- awake
// Validate the string so the pieces will be right
{
	pieceCount = -1;
	matchedIndex = -1;
	if (![self isEntryAcceptable:[self stringValue]])  {
#ifdef DEBUG
		NXLogError("MORegexFormCell debug: awoke with incompatible string and "
				"pattern.\n");
#endif /* DEBUG */
	}
	return self;
}

- read:(NXTypedStream *)strm
// Read our regular expression pattern string.
// int NXTypedStreamClassVersion(NXTypedStream *typedStream, 
// 				const char *className) 
// can be used to handle multi version reading and writing.
{
	int classVersion;

	[super read:strm];
	
	classVersion = NXTypedStreamClassVersion(strm, CLASS_NAME);
	
	switch (classVersion)  {
		case 0:		// First version.
			regexStrs = NXReadObject(strm);
			NXReadType(strm, "c", &allowEmptyString);
			delegate = NXReadObject(strm);
			break;
		default:
			NXLogError("[%s read:] class version %d cannot read "
						"instances archived with version %d", 
						CLASS_NAME, CLASS_VERSION, classVersion);
			regexStrs = [[List allocFromZone:[self zone]] init]; 
			allowEmptyString = YES;
			delegate = nil;
			break;
	}
	return self;
}

- write:(NXTypedStream *)strm
// Write our regular expression pattern string.
{
	[super write:strm];
	NXWriteObject(strm, regexStrs);
	NXWriteType(strm, "c", &allowEmptyString);
	NXWriteObjectReference(strm, delegate);
	return self;
}

- (const char *)getInspectorClassName
// Return the class name of our inspector.
{
	return "MORegexTextCellInspector";
}

@end
