/* All Rights reserved */

#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>
#import "PreferencesController.h"
#import "AppController.h"

static NSUserDefaults *ud = nil;
static NSNotificationCenter *nc = nil;

/*
 * NSScanner extensions category
 */
 
@interface NSScanner (UnitTestsExtensions)
- (BOOL) checkStrings: (NSArray *)strings stopForNewLine: (BOOL)stop;
- (BOOL) checkString: (NSString *)string stopForNewLine: (BOOL)stop;
@end

@implementation NSScanner (UnitTestsExtensions)

- (BOOL) checkStrings: (NSArray *)strings stopForNewLine: (BOOL)stop
{
	NSEnumerator *e = [strings objectEnumerator];
	NSString *str;
	
	while ((str = [e nextObject]) != nil)
	{
		if ([self checkString: str stopForNewLine: stop])
			return YES;
	}
	
	return NO;
}

- (BOOL) checkString: (NSString *)string stopForNewLine: (BOOL)stop
{
	int location = [self scanLocation];
	BOOL isFound = NO;
	NSCharacterSet *cset;
	
	if (stop)
	{
		cset = [[self charactersToBeSkipped] retain];
		[self setCharactersToBeSkipped: [NSCharacterSet whitespaceCharacterSet]];
	}
	
	NSString *checked = nil;
	if ([self isAtEnd] == NO && [self scanUpToString: string intoString: &checked])
		isFound = YES;
	
	//NSDebugLog(@"checkString:ForNewLine: %d checked %@", stop, checked);

	[self setScanLocation: location];
		
	if (stop)
	{
		[self setCharactersToBeSkipped: cset];
		[cset release];
	}
	
	return isFound;
}

@end

/*
 * AppController implementation
 */

@implementation AppController

- (void) awakeFromNib
{
	NSString *defaultSet;
	
	NSLog(@"AppController awakeFromNib");
	
	// FIXME: We aren't able to read NSRegistration domain set in
	// +[PreferencesController initialize], may be it is a GNUstep bug
	//ud = [[NSUserDefaults standardUserDefaults] retain];
	ud = [preferencesController userDefaults];
	nc = [[NSNotificationCenter defaultCenter] retain];
	
	[nc addObserver: self selector: @selector(testsSetsChanged:) 
		name: TestsSetsChangedNotification object: nil];

	[self testsSetsChanged: nil]; // Init popup tests sets menu
	
	[self showPreferencesPanel: nil];
}

- (void) showPreferencesPanel: (id)sender
{
	[preferencesPanel makeKeyAndOrderFront: self];
}

- (void) popupTestsSets: (id)sender
{
	NSString *key;
	
	if ([sender indexOfSelectedItem] == 0)
	{
		 key = @"Default";
	}
	else
	{
		key = [[sender selectedItem] title];
	}
	
	NSDebugLog(@"popupTestsSet with key %@", key);
	
	if ([key isEqualToString: [ud stringForKey: ActiveTestsSetDefault]] == NO)
	{
		NSLog(@"Sync default for popup");
		[ud setObject: key forKey: ActiveTestsSetDefault];
		[ud synchronize];
		[self noLight];
	}
}

- (void) testsSetsChanged: (NSNotification *)not
{	
	NSDictionary *variousTests = [ud dictionaryForKey: TestsSetsDefault];
	NSArray *testsSetsNames = [variousTests allKeys];
	NSEnumerator *e = [testsSetsNames reverseObjectEnumerator];
	NSString *testsSetName;
	NSString *defaultSetName = [ud stringForKey: ActiveTestsSetDefault];
	
	NSDebugLog(@"testsSetsChanged: %@", testsSetsNames);
	
	while ([popupTestsSets numberOfItems] != 1)
		[popupTestsSets removeItemAtIndex: [popupTestsSets numberOfItems] - 1]; 
	
	while ((testsSetName = [e nextObject]) != nil)
	{
		if ([testsSetName isEqualToString: @"Default"] == NO)
			[popupTestsSets addItemWithTitle: testsSetName];
	}
	
	if ([defaultSetName isEqualToString: @"Default"])
	{
		[popupTestsSets selectItemAtIndex: 0];
	}
	else
	{
		[popupTestsSets selectItemWithTitle: defaultSetName];
	}
}

#define UNKNOWN 0
#define PASSED  2
#define FAILED  4
#define WARNING 6

- (NSArray*) scanOutput: (NSString*) str
{	
	NSLog (@"SCAN : <%@>", str);
	
	NSString* Failed   = @".fail";
	NSString* Passed   = @".pass";
	NSString* warning  = @": warning:";
	NSString* postWarning  = @":: warning:";
	NSString* Result   = @"Result";
	NSString* ukrun    = @"ukrun";
	NSString* looking  = @"looking";
	NSString* classes  = @"classes,";
	NSString* methods  = @"methods,";
	NSString* tests    = @"tests,";
	NSString* failed   = @"failed";
	NSString* fileName = nil;
	NSString* resultString = nil;	
	NSMutableArray* results = [[NSMutableArray alloc] init];

	int testLine   = 0;
	int nbClasses  = 0;
	int nbMethods  = 0;
	int nbTests    = 0;
	int nbFailures = 0;
	int location   = 0;
	
	BOOL ukrunFound = NO;
	BOOL lookingFound = NO;
	
	NSArray *manyFailed = [NSArray arrayWithObjects: Failed, @"msgUKFail", nil];
	NSArray *manyPassed = [NSArray arrayWithObjects: Passed, @"msgUKPass", nil];
	unsigned int result = UNKNOWN;

	NSScanner* theScanner = [NSScanner scannerWithString: str];
	
	while ([theScanner isAtEnd] == NO)
	{
		result = UNKNOWN;
		fileName = nil;
		int stamp;
		
		// And any NSLog ...
		location = [theScanner scanLocation];
		if([theScanner scanInt: &stamp])
		{
			[theScanner scanUpToString: @"\n" intoString: NULL];
		}
		else [theScanner setScanLocation: location];
		
		// We skip the ukrun first line... 
		location = [theScanner scanLocation];
		if ([theScanner scanString: ukrun intoString: NULL])
		{
			ukrunFound = YES;
			[theScanner scanUpToString: @"\n" intoString: NULL];
		}
		else [theScanner setScanLocation: location];

		// And the looking into bundle line...
		location = [theScanner scanLocation];
		if ([theScanner scanString: looking intoString: NULL])
		{
			lookingFound = YES;
			[theScanner scanUpToString: @"\n" intoString: NULL];
		}
		else [theScanner setScanLocation: location];
	
		// And any NSLog ...
		location = [theScanner scanLocation];
		while ([theScanner scanInt: &stamp])
		{
			[theScanner scanUpToString: @"\n" intoString: NULL];
			location = [theScanner scanLocation];
		}
		
		if (ukrunFound && lookingFound)
		{
			[theScanner scanUpToString: @":" intoString: &fileName];
			[theScanner setScanLocation: [theScanner scanLocation] + 1];
		}
		
		NSDebugLog(@"fileName %@", fileName);
		
		if ([fileName isEqualToString: Result])
		{
			// It could be the result line:
			// Result: 1 classes, 1 methods, 2 tests, 1 failed

			[theScanner scanInt: &nbClasses];
			[theScanner scanString: classes intoString: NULL];
			[theScanner scanInt: &nbMethods];
			[theScanner scanString: methods intoString: NULL];
			[theScanner scanInt: &nbTests];
			[theScanner scanString: tests intoString: NULL];
			[theScanner scanInt: &nbFailures];
			[theScanner scanString: failed intoString: NULL];
			[theScanner scanUpToString: @"\n" intoString: NULL];

			NSMutableDictionary* test = [[NSMutableDictionary alloc] init];
			
			[test setObject: [NSString stringWithString: fileName] 
				 forKey: @"file"];
			[test setObject: [NSNumber numberWithInt: nbClasses] 
				 forKey: @"nbclasses"];
			[test setObject: [NSNumber numberWithInt: nbMethods] 
				 forKey: @"nbmethods"];
			[test setObject: [NSNumber numberWithInt: nbTests] 
				 forKey: @"nbtests"];
			[test setObject: [NSNumber numberWithInt: nbFailures] 
				 forKey: @"nbfailures"];
			[results addObject: test];
			[test release];
		}
		else if (fileName != nil)
		{
			// Or it could be a test line:
			// main.m:21: warning: Failed, expected 41, got 42
			// main.m:22 Passed, expected 42, got 42

			[theScanner scanInt: &testLine];
			location = [theScanner scanLocation];

			// We check eventual pre warning now...
			if ([theScanner scanString: warning intoString: NULL]) 
			{
				result |= WARNING;	
			}
			else
			{
				// FIXME: there is may be a bug in GNUstep here because we get
				// an out of range exception when location has the scanning 
				// process end value.
				// We avoid it with [theScanner isAtEnd] workaround test case
				if ([theScanner isAtEnd] == NO)
					[theScanner setScanLocation: location];
			}

			if ([theScanner checkStrings: manyPassed stopForNewLine: YES]) 
			{
				result |= PASSED;
			}
			else if ([theScanner checkStrings: manyFailed stopForNewLine: YES]) 
			{	
				result |= FAILED;
			}

			// FIXME: there is may be a bug in GNUstep here because we get
			// an out of range exception when location has the scanning 
			// process end value.
			// We avoid it with [theScanner isAtEnd] workaround test case
			if ([theScanner isAtEnd] == NO)
			{
				[theScanner setScanLocation: location];
				[theScanner scanUpToString: @"\n" intoString: &resultString];
				
				location = [theScanner scanLocation];
			}
			else
			{
				resultString = @"";
			}
			
			// We check eventual post warning now (located on next line)...
			if ([theScanner scanString: postWarning intoString: NULL]) 
			{
				NSString *postErr = @"";
				
				result |= WARNING;	
				[theScanner scanUpToString: @"\n" intoString: &postErr];
				postErr = [@" " stringByAppendingString: postErr];
				resultString = [resultString stringByAppendingString: postErr];
			}
			else if ([theScanner isAtEnd] == NO) // FIXME: Not really pretty.
			{
				[theScanner setScanLocation: location];	
			}	
			
			NSDebugLog (@"\nfile : %@", fileName);
			NSDebugLog (@"line : %d", testLine);
			NSDebugLog (@"res  : %@", resultString);
			NSDebugLog (@"result : %x\n", result);
			
			NSMutableDictionary* test = [[NSMutableDictionary alloc] init];

			[test setObject: [NSString stringWithString: fileName] 
				 forKey: @"file"];
			[test setObject: [NSNumber numberWithInt: testLine] 
				 forKey: @"line"];
			[test setObject: [NSString stringWithString: resultString]
				 forKey: @"result"];

			if (result & FAILED)
			{
				[test setObject: @"Failed" forKey: @"status"];
			}	
			else if (result & PASSED) 
			{
				[test setObject: @"Passed" forKey: @"status"];
			}

			[results addObject: test];
			[test release];
		}

	}

	return results;
	
}

- (void) redLight
{
	[status setColor: [NSColor redColor]];
}

- (void) greenLight
{
	[status setColor: [NSColor greenColor]];
}

- (void) noLight
{
	[status setColor: [NSColor blackColor]];
}

- (void) runTests: (id)sender
{
	NSTask *ukrun;
	NSArray *variousTests = [[ud objectForKey: TestsSetsDefault] 
		objectForKey: [ud objectForKey: ActiveTestsSetDefault]];
	NSArray *args = [variousTests valueForKey: @"path"];
	
	NSLog(@"Tests sets %@", [ud objectForKey: TestsSetsDefault]);
	NSLog(@"Active tests set %@", [ud objectForKey: ActiveTestsSetDefault]);
	NSLog(@"Run tests with bundles %@", [[ud objectForKey: TestsSetsDefault] objectForKey: ActiveTestsSetDefault]);
	
	NSString *path = [ud stringForKey: ToolPathDefault];
	NSLog (@"ukrun default path : %@", path);
	
	BOOL isDir;
	if (path == nil 
		|| [[NSFileManager defaultManager] fileExistsAtPath: path isDirectory: &isDir] == NO)
	{
		NSLog(@"Try system provided ukrun path");
		
		path = [[NSWorkspace sharedWorkspace] fullPathForApplication: @"ukrun"];
	}
	
	NSLog (@"ukrun path : %@", path);
	if (path == nil) {
         [self showPreferencesPanel: self];
         
         NSRunAlertPanel(
             @"ukrun not found",
             @"The needed ukrun tool to run UnitKit tests has not been found.\n"
             @"Please make sure it is installed and adjust its location in the preferences.",
             @"Okay", nil, nil
         );
         
         return;
    }
    
    ukrun = [NSTask new];
	[ukrun setLaunchPath: path];
	[ukrun setArguments: args];
	
	NSData* inData;
	NSFileHandle* readHandle;
	NSPipe* pipe;

	pipe = [NSPipe pipe];
	readHandle = [pipe fileHandleForReading];
	
	[ukrun setStandardOutput: pipe];	
	[ukrun setStandardError: pipe];	
    
    NS_DURING
    {
    	[ukrun launch];
    
	    while ((inData = [readHandle availableData]) && [inData length])
	    {
		    NSString* str = [[NSString alloc] initWithData: inData encoding: NSISOLatin1StringEncoding];
		    
		    resultsTests = [self scanOutput: str];
		    
		    [list reloadData];
		    
		    int i;
		    for (i=0; i< [resultsTests count]; i++)
		    {
			    NSDictionary* test = [resultsTests objectAtIndex: i];
			    
			    if ([[test objectForKey: @"file"] isEqualToString: @"Result"])
			    {
				    int nbfailures = [[test objectForKey: @"nbfailures"] 
					    		intValue];
							
				    nbfailures > 0 ? [self redLight] : [self greenLight];
				    
				    NSString* sum = [NSString stringWithFormat: 
					    @"%@ classes, %@ methods, %@ tests, %@ failed", 
					    [test objectForKey: @"nbclasses"],
					    [test objectForKey: @"nbmethods"],
					    [test objectForKey: @"nbtests"],
					    [test objectForKey: @"nbfailures"]
					    ];
				     
				    [summary setStringValue: sum];
			    }
		    }	
	    }
	}
    NS_HANDLER
    {
        [self showPreferencesPanel: self];
        
        NSRunAlertPanel(
            @"Running the tests failed.", //title
            [NSString stringWithFormat:
                        @"Running the ukrun tool from %@ failed:\n%@\n"
                        @"Please make sure the path is correct!",
                        path, [localException reason]],
            @"Okay", nil, nil
        );
    }
    NS_ENDHANDLER
    
    [ukrun release];
}

- (id) tableView: (NSTableView*) tv objectValueForTableColumn: (NSTableColumn*) tc
	row: (int) row
{
	if (row < [resultsTests count])
	{
		NSDictionary* test = [resultsTests objectAtIndex: row];

		if ([[tc identifier] isEqualToString: @"file"])
			return [test objectForKey: @"file"];

		if ([[tc identifier] isEqualToString: @"line"])
			return [test objectForKey: @"line"];
			
		if ([[tc identifier] isEqualToString: @"status"])
			return [test objectForKey: @"status"];
			
		if ([[tc identifier] isEqualToString: @"reason"])
			return [test objectForKey: @"result"];
	}
	
	return nil;
}

- (int) numberOfRowsInTableView: (NSTableView*) tv
{
	return [resultsTests count] - 1;
}

- (void) tableView: (NSTableView*) tv willDisplayCell: (id) cell 
	forTableColumn: (NSTableColumn*) tc row: (int) row
{
	if (row < [resultsTests count])
	{
		NSDictionary* test = [resultsTests objectAtIndex: row];
		
		[cell setDrawsBackground: YES];

		if ([[test objectForKey: @"status"] isEqualToString: @"Passed"])
		{
			[cell setBackgroundColor: [NSColor greenColor]];
		}
		else if ([[test objectForKey: @"status"] isEqualToString: @"Failed"])
		{
			[cell setBackgroundColor: [NSColor redColor]];
		}
	}
}

@end
