//
//  MPT_AUAppDelegate.m
//  AUCoderDemo
//
//  Created by Andrew Salamon on 5/31/06.
//  Copyright 2006 Machine Perception Laboratory, University of California San Diego. All rights reserved.
//

#import <Carbon/Carbon.h>

#import <QTKit/QTMovie.h>
#import "Scripting/Mail.h"

#import "MPT_AUAppDelegate.h"
#import "MPT_CERT.h"
#import "MPT_Pair.h"
#import "MPT_FaceView.h"
#import "MPT_Face.h"
#import "MPT_AUView.h"
#import "MPT_ResolveAlias.h"
#import "MPT_RimageAdaptor.h"

#import "MPT_Preferences.h"
#import "MPT_PluginController.h"
#import "MPT_PluginStepTransformer.h"
#import "MPT_PluginProtocolResults.h"

#import "MPT_DataRecorder.h"
#import "MPT_SplitViewConstrainer.h"
#import "MPT_DataLoader.h"

#import "TestController.h"

#import "MPT_Scheduler.h"
#import "MPT_SourceLoader.h"

#import "MPT_Demo.h"

#import "MPT_DocumentController.h"

#ifdef CERT_TIMING
#import "MPT_Timing.h"
#define STAMPTIME(ct_inputs,ct_id) [(ct_inputs) stampWithID:(ct_id)]
#else
#define STAMPTIME(...)
#endif

// PRODUCT_NAME should be defined as part of the build process. Here we convert it into a static NSString.
static NSString *appName = @PRODUCT_NAME;

// This is declared in NSImageToRimage, but we can't include that header because it uses Objective-C++ and this file doesn't, so far.
@interface NSImage()
- (NSBitmapImageRep *)bitmapImageRep;
@end

@interface MPT_AUAppDelegate(PrivateMethods)
- (void)checkLicense;

- (void)handleResults:(NSDictionary *)pluginResults;
- (BOOL)loading;
- (void)setLoading:(BOOL)value;

- (void)setIsProcessing:(BOOL)ignored;
- (void)prepareForProcessing;

- (BOOL)autostartWebCam;
@end

@implementation MPT_AUAppDelegate
+ (void)initialize
{
	// Register our plugin step name transformer
	MPT_PluginStepTransformer *transformer = [[[MPT_PluginStepTransformer alloc] init] autorelease];
	[NSValueTransformer setValueTransformer:transformer forName:@"MPT_PluginStepTransformer"];
	
	// Make sure the correct alignment method is used by default unless the user overrides it.
	NSUserDefaults *_prefs = [NSUserDefaults standardUserDefaults];
	NSDictionary *defPrefs = [NSDictionary dictionaryWithObjectsAndKeys:
							  [NSNumber numberWithBool:YES], @"findFaces", 
							  [NSNumber numberWithFloat:30.0], @"maxFramesPerSecond", nil];
	[_prefs registerDefaults:defPrefs];
}

- (id)init
{
	if( self = [super init] )
	{
		pluginController = [MPT_PluginController defaultController];
	}
	return self;
}

- (void)awakeFromNib
{	
	// Restrict the image view half of the splitter to at least 400 pixels wide,
	// and the AUView side to at least 216 pixels wide (one AUView wide).
	[splitViewDelegate setMinLeftPane:400];
	[splitViewDelegate setMinRightPane:216];
	
	[pluginController setTileView:tileView];
	[pluginController setPluginMenu:pluginMenu];
	[pluginController initPlugins];

	//cert = [[MPT_CERT alloc] init];
	scheduler = [[MPT_Scheduler alloc] initWithDelegate:self];

	frameNum = 1;
	nextFrame = 1;
	resultsBuffer = [[NSMutableArray alloc] init];

	videoController = [[TestController alloc] init];
	NSNumber *max = [[NSUserDefaults standardUserDefaults] objectForKey:@"maxFramesPerSecond"];
	if( max && ([max floatValue] > 0.0) )
		maxFramesPerSecond = [max floatValue];
	else
		[self setMaxFramesPerSecond:30.0];
	
	[imageView resetFaceAndEyes];
	[imageView setFaceType:[[NSUserDefaults standardUserDefaults] integerForKey:@"FaceDisplayType"]];
	[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(faceDisplayTypeChanged:) name:faceDisplayTypeNotification object:nil];
	
	[self setLoading:YES];
	[self setVideoWidth:[videoController videoWidth]];
	[self setVideoHeight:[videoController videoHeight]];
	
	rimageTypes = [[NSArray alloc] initWithObjects:@"rimage", @"rimage_bin", @"rimage_xml", nil];
	movieTypes = [[QTMovie movieFileTypes:QTIncludeCommonTypes] retain];
	imageTypes = [[NSImage imageFileTypes] retain];
	allTypes = [rimageTypes arrayByAddingObjectsFromArray:movieTypes];
	allTypes = [allTypes arrayByAddingObjectsFromArray:imageTypes];
	
	MPT_DocumentController *docController = [MPT_DocumentController sharedDocumentController];
	NSArray *mptDocTypes = [docController extensionsForReading];
	allTypes = [[allTypes arrayByAddingObjectsFromArray:mptDocTypes] retain];
	
	[allTypes retain];

	sourceLoader = [[MPT_SourceLoader alloc] initWithDelegate:self imageTypes:imageTypes movieTypes:movieTypes];
	
	id tmp = [pluginController pluginForPluginID:@"edu.ucsd.mplab.plugins.Distributed"];
	if( tmp && [tmp respondsToSelector:@selector(doesScheduleInput:)] )
	{
		doClient = [tmp retain];
		[doClient performSelector:@selector(setScheduler:) withObject:scheduler];
//		[doClient setScheduler:scheduler];
		[doClient setAppDelegate:self];
	}
	
	{
		// Simple Remote Input
		id sri = [pluginController pluginForPluginID:@"edu.ucsd.mplab.plugins.SRI"];
		if( sri && [sri respondsToSelector:@selector(setScheduler:)] )
		{
			[sri performSelector:@selector(setScheduler:) withObject:scheduler];
		}
	}
	[[NSUserDefaults standardUserDefaults] addObserver:self forKeyPath:@"findFaces" options:NSKeyValueObservingOptionNew context:NULL];
	[[NSUserDefaults standardUserDefaults] addObserver:self forKeyPath:@"maxFramesPerSecond" options:NSKeyValueObservingOptionNew context:NULL];

	versionReporter = [pluginController pluginForPluginID:@"com.mpt4u.plugins.VersionReporter"];

	[self checkLicense];
	
	[pluginArrayController rearrangeObjects];
}

- (void)dealloc
{
	[cert release];
	[videoController release];
	[rimageTypes release];
	[movieTypes release];
	[imageTypes release];
	[allTypes release];
	[scheduler release];
	[resultsBuffer release];
	[doClient release];
	
	[[NSNotificationCenter defaultCenter] removeObserver:self];
	[[NSUserDefaults standardUserDefaults] removeObserver:self forKeyPath:@"findFaces"];
	[[NSUserDefaults standardUserDefaults] removeObserver:self forKeyPath:@"maxFramesPerSecond"];
	[super dealloc];
}

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
	BOOL optKeyDown = ((GetCurrentKeyModifiers() & (optionKey |  rightOptionKey)) != 0) ? YES : NO;

	[[self window] makeKeyAndOrderFront:self];
	
	// Don't load the kernels if the option key is down.
	if( !optKeyDown )
	{
		MPT_DataLoader *loader = [[MPT_DataLoader alloc] initWithDelegate:self andScheduler:scheduler]; // deallocated in loaderFinished:.
		[dataLoading setMinValue:1.0];
		[dataLoading setDoubleValue:1.0];
		[dataLoading setIndeterminate:NO];
		[NSThread detachNewThreadSelector:@selector(loadAllWeights) toTarget:loader withObject:nil];
	}
	else
	{
		[self setLoading:NO];
	}
	
	//[saveDataWindowItem setEnabled:NO];  
	//[showDataWindowItem setEnabled:NO]; 
}

- (void)checkLicense
{
	MPT_Demo *demo = [MPT_Demo sharedDemo];
	
	if( lic_invalid == [demo type] )
	{
		[demo notifyUserInvalidLicense];
		[NSApp terminate:self];
	}
	else if( [demo isEvaluation] )
	{
		
		if( [demo isDemo] )
		{
			///////////////////////////
			// This code is problematic on Javier's machine for some reason.
			//if( [demo hasDemoEnded] )
			//{
			//	[demo notifyUserDemoEnded];
			//	[NSApp terminate:self];
			//}
			///////////////////////////
		}
		else
		{
			if( ![demo startDemo] )
			{
				[demo notifyUserDemoNotAllowed];
				[NSApp terminate:self];
			}
		}
		
		// Evaluation copies need to prevent the user from saving data, so remove the menu item that shows the data recorder window.
		//[[showDataWindowItem menu] removeItem:showDataWindowItem];
		//[[showDataWindowItem menu] removeItem:saveDataWindowItem]; 
		[saveDataWindowItem setEnabled:NO];  
		[showDataWindowItem setEnabled:NO]; 
		[saveDataWindowItem setTitle:[NSString stringWithFormat:@"%@ (DEMO)", [saveDataWindowItem title]]]; 
		[showDataWindowItem setTitle:[NSString stringWithFormat:@"%@ (DEMO)", [showDataWindowItem title]]]; 
		[window	 setTitle:[NSString stringWithFormat:@"%@ (Demo)", [window title]]]; 
		// For some reason if we put up an alert box during awakeFromNib we end up with two help menus, so remove one.
		//[[helpMenuItem menu] removeItem:helpMenuItem];

		// If we were already started and are still in the demo period,
		// or if we successfully started a demo period then notify the user how much time they have left.
		//[demo notifyUserDemoLeft];
	}
	else if( [demo isLicensed] )
	{
		if( [demo licenseHasEnded] )
		{
			[demo notifyUserLicenseEnded];
			[NSApp terminate:self];
		}
		else
		{
			// For some reason if we put up an alert box during awakeFromNib we end up with two help menus, so remove one.
			[[helpMenuItem menu] removeItem:helpMenuItem];
			[demo notifyUserLicenseInfo];
		}
	}
	
	// if the license type is lic_internal, then just continue.	
}

// Callbacks for handling progress and errors from the weights loader
- (void)setLoadingMax:(NSNumber *)max
{
	[dataLoading setMaxValue:[max floatValue]];
	[dataLoading setDoubleValue:0.0];
}
- (void)setLoadingMessage:(NSString *)msg
{
	[dataLoadingMessage setStringValue:NSLocalizedString( msg, msg )];
}
- (void)incrementWeightsProgressBy:(NSNumber *)incNum
{
	float inc = 1.0;
	if( incNum )
		inc = [incNum floatValue];
	[dataLoading incrementBy:inc];
	[dataLoading displayIfNeeded];
}
- (void)loaderFinished:(MPT_DataLoader *)loader
{
	BOOL hadErrors = NO;
	NSEnumerator *errorEnum = [[loader errors] objectEnumerator];
	id<MPT_PluginProtocol> plugin;
	while( plugin = [errorEnum nextObject] )
	{
		NSString *pluginName = [plugin localizedName];
		NSString *msg = [NSString stringWithFormat:@"Unable to load all '%@' weights files.", pluginName];
		
		[plugin setEnabled:NO];

		[[NSAlert alertWithMessageText:msg
						 defaultButton:@"OK"
					   alternateButton:nil
						   otherButton:nil
			 informativeTextWithFormat:@"The '%@' plugin will not work correctly and has been disabled.", pluginName] runModal];
		hadErrors = YES;
	}

	if( hadErrors )
		[tileView setNeedsDisplay:YES];
	
	[self setLoading:NO];
	
	[loader release];
	
	// Now that all of the weights are loaded, we can try starting the web cam, if the pref is set.
	if( [self autostartWebCam] )
		[self startVideo:self];	
}

- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender
{
	[self interrupt:self];
	if( [self videoTimer] )
	{
		[self stopVideo:self];
	}
	[scheduler cleanup];
	[scheduler release];
	scheduler = nil;
	//	NSTerminateLater - it may be ok to proceed with termination later.  The application must call -replyToApplicationShouldTerminate: with YES or NO once the answer is known
	//	this return value is for delegates who need to provide document modal alerts (sheets) in order to decide whether to quit.
	
	return NSTerminateNow;
}

- (void)applicationWillTerminate:(NSNotification *)notification
{
	[pluginController applicationWillTerminate:notification];
}

- (NSWindow *)window
{
	return window;
}

- (IBAction)interrupt:(id)sender
{
	interrupt = YES;
	[sourceLoader interrupt];
//	[pluginController performSelector:@selector(resetViews) withObject:nil afterDelay:0.1];
	[[MPT_DocumentController sharedDocumentController] cancelMovie];
//	[pluginController reportPluginState];
}

- (IBAction)chooseSource:(id)sender
{
	NSOpenPanel *panel = [NSOpenPanel openPanel];
	[panel setCanChooseDirectories:YES];
	[panel setAllowsMultipleSelection:YES];
	[panel setCanChooseFiles:YES];
	[panel setMessage:@"Choose files and/or directories to process"];
//	[panel setAccessoryView:openPanelAccessory];
	    
	[cert setInputIsFaces:NO];
	
	[panel beginSheetForDirectory:nil
							 file:nil
							types:allTypes
				   modalForWindow:[self window]
					modalDelegate:self
				   didEndSelector:@selector(openSourcePanelDidEnd:returnCode:contextInfo:)
					  contextInfo:nil];
}

/*
 If any of the returned files are mpt4up files, then we need to send the whole file list to a
 MPT_FilePlayback object and let it handle everything. It will probably need a pointer to this
 object so it can call back.
 */
- (void)openSourcePanelDidEnd:(NSOpenPanel *)panel returnCode:(int)returnCode  contextInfo:(void *)contextInfo
{
	if( returnCode == NSOKButton )
	{
		NSArray *filenames = [panel filenames];
		if( [filenames count] > 0 )
		{
			NSString *path = [filenames objectAtIndex:0];
			if( [path length] > 0 )
			{
				MPT_DocumentController *docController = [MPT_DocumentController sharedDocumentController];
				if( ![docController handlePlaybackFiles:filenames withDelegate:self] )
				{
					[self prepareForProcessing];
					batchStart = [[NSDate date] retain];
					[sourceLoader processSources:[panel filenames]];
				}
			}
		}
	}
}

- (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename
{
	// TODO: It would be nice if we could save the file for processing after we finish loading...
	// At the moment you can't launch CERT with an image or movie. Filetypes aren't setup properly
	if( [self loading] )
		return NO;
	
	[self prepareForProcessing];
	batchStart = [[NSDate date] retain];
	[sourceLoader processSources:[NSArray arrayWithObject:filename]];
	 // we should return NO if we failed to open the file, but since that determination happens on a separate thread, I don't see how to do it.
	return YES;
}

- (void)prepareForProcessing
{
	interrupt = NO;
	nextFrame = frameNum; // make sure we're in sync. I could also just reset to zero, but it might be useful to leave it counting.
	[self setIsProcessing:YES];
	[progress startAnimation:self];
	[frameRateField setStringValue:@""];
	sourceLoaderFinished = NO;
	scheduleIsEmpty = NO;
	[resultsBuffer removeAllObjects];
	[dataRecorder startRecording];
}

- (void)doProcessingFinished
{
	[progress stopAnimation:self];
	[self setIsProcessing:NO];
	interrupt = YES;
	[pluginController performSelector:@selector(resetViews) withObject:nil afterDelay:0.1];
	if( batchStart )
	{
		NSLog( @"Batch run time: %f", [[NSDate date] timeIntervalSinceDate:batchStart] );
		[batchStart release];
		batchStart = nil;
	}
}

- (void)sourceProcessingFinished
{
	sourceLoaderFinished = YES;
	if( [scheduler isEmpty] )
	{
		[self doProcessingFinished];
	}
}

- (void)scheduleIsEmpty
{
	scheduleIsEmpty = YES;
	if( sourceLoaderFinished )
	{
		[self doProcessingFinished];
	}
}

- (BOOL)displayGray
{
    return [[NSUserDefaults standardUserDefaults] boolForKey:@"displayGray"];
}

- (BOOL)findFaces
{
	return [[NSUserDefaults standardUserDefaults] boolForKey:@"findFaces"];
}

- (MPT_FaceView *)faceView
{
	return [[imageView retain] autorelease];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
	if( [keyPath isEqualToString:@"findFaces"] )
	{
		if( ![[change objectForKey:NSKeyValueChangeNewKey] boolValue] )
			[imageView resetFaceAndEyes];
	}
	else if( [keyPath isEqualToString:@"maxFramesPerSecond"] )
	{
		NSNumber *newNum = [change objectForKey:NSKeyValueChangeNewKey];
		if( newNum )
		{
			[self willChangeValueForKey:@"maxFramesPerSecond"];
			maxFramesPerSecond = [newNum floatValue];
			[self didChangeValueForKey:@"maxFramesPerSecond"];
		}
	}
	else
	{
#ifdef DEBUG
		NSLog( @"Received Unsupported key value change notification in MPT_AUAppDelegate: %@", keyPath );
#endif
	}
}

- (float)videoWidth {
    return [videoController videoWidth];
}

- (void)setVideoWidth:(float)value {
	[videoController setVideoWidth:value];
}

- (float)videoHeight {
	return [videoController videoHeight];
}

- (void)setVideoHeight:(float)value {
	[videoController setVideoHeight:value];
}

- (BOOL)loading
{
    return loading;
}

- (void)setLoading:(BOOL)value
{
    if (loading != value)
	{
        loading = value;
		[self setIsProcessing:value];
    }
}

- (BOOL)isProcessing;
{
//	if( loading ) return NO;
//	if( [self videoTimer] ) return NO;
    return isProcessing;
}
- (void)setIsProcessing:(BOOL)ignored
{
	isProcessing = ignored;
}

- (BOOL)videoTimer
{
    return videoTimer;
}

- (void)setVideoTimer:(BOOL)value
{
    videoTimer = value;
}

- (float)maxFramesPerSecond
{
    return maxFramesPerSecond;
}

- (void)setMaxFramesPerSecond:(float)value
{
    if( maxFramesPerSecond != value )
	{
        maxFramesPerSecond = value;
		[[NSUserDefaults standardUserDefaults] setFloat:value forKey:@"setMaxFramesPerSecond"];
    }
}

- (NSDictionary *)latestResults {
    return [[latestResults retain] autorelease];
}

- (void)setLatestResults:(NSDictionary *)value {
    if (latestResults != value) {
        [latestResults release];
        latestResults = [value retain];
    }
}

- (MPT_CERT *)cert
{
    return cert;
}

- (IBAction)imageChanged:(id)sender
{
	BOOL haveRImage = NO;
	NSImage *image = NULL;
	NSString *imagePath = NULL;
	NSMutableDictionary *inputs = nil;
	
	if( [sender isKindOfClass:[NSImage class]] )
	{
		image = (NSImage *)sender;
	}
	else if( [sender isKindOfClass:[MPT_Pair class]] )
	{
		MPT_Pair *pair = (MPT_Pair *)sender; 
		image = [pair first];
		imagePath = [pair second];
	}
	else if( [sender isKindOfClass:[MPT_FaceView class]] )
	{
		[imageView resetFaceAndEyes];
		image = [imageView image];
		imagePath = [sender lastDropPath];
	}
	else if( [sender isKindOfClass:[NSString class]] )
	{
		haveRImage = YES;
		image = NULL;
		imagePath = sender;
	}
	else if( [sender isKindOfClass:[NSMutableDictionary class]] )
	{
		inputs = sender;
		if( ![inputs objectForKey:@"CERT.frame"] )
			[inputs setObject:[NSNumber numberWithUnsignedInt:frameNum++] forKey:@"CERT.frame"];
		if( ![inputs objectForKey:@"CERT.date"] )
			[inputs setObject:[NSDate date] forKey:@"CERT.date"];
	}
	else
	{
		return;
	}
	
	if( !inputs )
	{
		inputs = [NSMutableDictionary dictionary];
		if( image )
			[inputs setObject:image forKey:@"CERT.image"];
		if( imagePath )
			[inputs setObject:imagePath forKey:@"CERT.path"];
		[inputs setObject:[NSNumber numberWithUnsignedInt:frameNum++] forKey:@"CERT.frame"];
		[inputs setObject:[NSDate date] forKey:@"CERT.date"];
	}
	
	if( ![doClient doesScheduleInput:inputs] )
		[scheduler scheduleInput:inputs];
}

- (void)handleUnsortedResults:(NSDictionary *)pluginResults
{
	unsigned int curFrame = [[pluginResults objectForKey:@"CERT.frame"] unsignedIntValue];
	
	if( curFrame == nextFrame )
	{
		[self handleResults:pluginResults];
		++nextFrame;
	}
	else if( 0 == [resultsBuffer count] )
	{
		[resultsBuffer addObject:pluginResults];
	}
	else
	{
		BOOL inserted = NO;
		for( int ind = 0; ind < [resultsBuffer count]; ++ind )
		{
			unsigned int frm = [[[resultsBuffer objectAtIndex:ind] objectForKey:@"CERT.frame"] unsignedIntValue];
			if( curFrame < frm )
			{
				inserted = YES;
				[resultsBuffer insertObject:pluginResults atIndex:ind]; // have to make sure it's added at the right place.
				break;
			}
		}
		if( !inserted )
		{
			[resultsBuffer addObject:pluginResults];
		}
	}
	
	for( int ind = 0; ind < [resultsBuffer count]; ++ind )
	{
		NSDictionary *res = [resultsBuffer objectAtIndex:ind];
		unsigned int frm = [[res objectForKey:@"CERT.frame"] unsignedIntValue];
		if( frm == nextFrame )
		{
			[self handleResults:res];
			[resultsBuffer removeObjectAtIndex:ind];
			++nextFrame;
			--ind; // because we removed an object
		}
		else if( frm < nextFrame )
		{
			// uh, oh, missed one. Silently drop it, for now
			NSLog( @"Dropping frame %d", frm );
			[resultsBuffer removeObjectAtIndex:ind];
			--ind; // because we removed an object
		}
		else if( frm > nextFrame )
		{
			break;
		}
	}
}

- (void)handleResults:(NSDictionary *)pluginResults
{
	// First let all post processing plugins run
	NSMutableDictionary *tmp = [[pluginResults mutableCopy] autorelease];
	NSArray *sortedResultsPlugins = [[pluginController enabledPluginsForStep:MPT_PluginStep_Results] sortedArrayUsingSelector:@selector(pluginCompare:)];
	NSEnumerator *postEnum = [sortedResultsPlugins objectEnumerator];
	id<MPT_PluginProtocolResults> postPlugin;
	while( postPlugin = [postEnum nextObject] )
	{
		NSMutableDictionary *postProc = nil;
		@try
		{
			postProc = [postPlugin processResults:tmp usingController:pluginController];
		}
		@catch( NSException *exception )
		{
			NSLog( @"Error running plugin %@ (ID %@). Error Message: %@", [postPlugin localizedName], [postPlugin pluginID], [exception reason] );
		}
		if( postProc && ([postProc count] > 0) )
			[tmp setObject:postProc forKey:[postPlugin pluginID]];
	}
	pluginResults = tmp;
	
	[self updateViewsWith:pluginResults];
	[self setLatestResults:pluginResults];
	
	[dataRecorder recordDataPoint:pluginResults];
	
	if( [self videoTimer] )
	{
		NSTimeInterval dur = [[NSDate date] timeIntervalSinceDate:batchStart];
		if( dur > 30.0 )
		{
			[batchStart release];
			batchStart = [[NSDate date] retain];
			int curFrame = [[pluginResults objectForKey:@"CERT.frame"] intValue];
			float frmPerSec = curFrame - videoBatchFrame; // number of frames
			frmPerSec /= dur;
			NSLog( @"Average frame rate over last 30 seconds: %f", frmPerSec );
			videoBatchFrame = curFrame;
		}
		[frameRateField setFloatValue:[videoController frameRate]];
	}

#ifdef CERT_TIMING
	NSLog( @"\n%@", [[pluginResults objectForKey:@"CERT.timing"] desc] );
#endif
}

- (void)updateViewsWith:(NSDictionary *)pluginResults
{	
	// Get results for all plugins and update their views.
	NSEnumerator *pluginEnum = [[pluginController plugins] objectEnumerator];
	id<MPT_PluginProtocol> plugin;
	while( plugin = [pluginEnum nextObject] )
	{
		NSEnumerator *labelEnum = [[plugin labels] objectEnumerator];
		NSString *label;
		while( label = [labelEnum nextObject] )
		{
			MPT_AUView *view = [plugin viewForLabel:label];
			if( view && [plugin enabled] )
			{
				NSNumber *resNum = [pluginResults objectForKey:[plugin pluginID]];
				if( !resNum )
				{
					[view addNoValue];
				}
				else if( [resNum isKindOfClass:[NSNumber class]] )
				{
					double res = [resNum doubleValue];
					[view addValue:res];
				}
				else if( [resNum isKindOfClass:[NSDictionary class]] )
				{
					double res = [[(NSDictionary *)resNum objectForKey:label] doubleValue];
					[view addValue:res];
				}
				else
				{
					NSLog( @"Unknown type in plugin results" );
					[view addNoValue];
				}
			}
			else
			{
				[view addNoValue];
			}
		}
	}
	
	NSImage *image = [pluginResults objectForKey:@"CERT.image"];	
	if( [self displayGray] || !image )
	{
		MPT_RimageAdaptor *rimageAdaptor = [pluginResults objectForKey:@"CERT.rimage"];
		image = [rimageAdaptor image];
	}
	
	NSArray *faces = [pluginResults objectForKey:@"CERT.faces"];
	
	[imageView setFaces:faces];
	[imageView setImage:image];	
	
	NSString *imagePath = [pluginResults objectForKey:@"CERT.path"];
	if( imagePath )
	{
		[imageView setLastDropPath:imagePath];

		NSNumber *movieFrame = [pluginResults objectForKey:@"CERT.movieFrame"];
		if( movieFrame )
		{
			imagePath = [NSString stringWithFormat:@"%@:%d", imagePath, [movieFrame intValue]];
		}
		[fileNameField setStringValue:imagePath];
	}
}

- (IBAction)startVideo:(id)sender
{
	if( [self videoTimer] )
	{
		[self stopVideo:self];
	}
	else
	{
		[self prepareForProcessing];
		videoBatchFrame = frameNum;
		[self setVideoTimer:YES];
		[fileNameField setStringValue:@""];
		[imageView resetFaceAndEyes];
		[videoController start];

		if( ![videoController cameraStarted] )
		{
			[self stopVideo:self];
			if( [self autostartWebCam] )
			{
				NSLog( @"Quitting %@: unable to connect to a camera in autostart mode.", appName );
				[NSApp terminate:self];
			}
			else
			{
				NSRunAlertPanel( @"Video Error", @"No video source was found, or an error was encountered while trying to access the video.", @"OK", nil, nil );
			}
		}
		else
		{
			// My testing suggests that even on single processor machines, running this in a separate thread doesn't hurt and may even help a bit. So don't use this timer option.
			if( [[NSUserDefaults standardUserDefaults] boolForKey:@"useVideoTimer"] )
				[NSTimer scheduledTimerWithTimeInterval:0.0 target:self selector:@selector(videoTimerFired:) userInfo:NULL repeats:NO];
			else
				[NSThread detachNewThreadSelector:@selector(threadedVideo) toTarget:self withObject:nil];
		}
	}
}

- (IBAction)stopVideo:(id)sender
{
	[videoController stop];
	[self setVideoTimer:NO];
	//	[self doProcessingFinished];
	[self sourceProcessingFinished];
}

- (void)handleVideoError:(NSError *)error
{
	if( [self autostartWebCam] )
	{
		NSLog( @"Quitting %@: lost camera connection in autostart mode.", appName );
		[NSApp terminate:self];
	}
	else
	{
		NSAlert *theAlert = [NSAlert alertWithError:error];
		if( -2211 == [error code] )
			[theAlert setInformativeText:@"Possible lost camera connection. Try unplugging it then plugging it back in again."];
		int button = [theAlert runModal];
		if( button != NSAlertFirstButtonReturn )
		{
			// handle
		}
		[self stopVideo:self];
	}
}

- (void)videoTimerFired:(NSTimer *)timer
{
	if( interrupt )
	{
		[self stopVideo:self];
		return;
	}
	
	if( [self videoTimer] )
	{
		NSImage *vImage = (NSImage *)[videoController lastImage];
		MPT_RimageAdaptor *adaptor = [videoController lastRimage];
		NSError *error = [videoController lastError];
		
		if( error )
		{
			[self handleVideoError:error];
			return;
		}
		
		if( vImage )
		{
			NSString *imagePath = [NSString stringWithFormat:@"frame%d", frameNum];
			NSMutableDictionary *inputs = [NSMutableDictionary dictionary];
			if( vImage )
				[inputs setObject:vImage forKey:@"CERT.image"];
			if( adaptor )
				[inputs setObject:adaptor forKey:@"CERT.rimage"];
			[inputs setObject:imagePath forKey:@"CERT.path"];
			[inputs setObject:[NSNumber numberWithUnsignedInt:frameNum++] forKey:@"CERT.frame"];
			[inputs setObject:[NSDate date] forKey:@"CERT.date"];
			
			if( ![doClient doesScheduleInput:inputs] )
				[scheduler scheduleInput:inputs];
		}
		else
		{
			// So no errors, but also no images. Usually that's because the iSight iris is closed.
		}
		[NSTimer scheduledTimerWithTimeInterval:0.0 target:self selector:@selector(videoTimerFired:) userInfo:NULL repeats:NO];
	}
}

- (void)threadedVideo
{
//	static const float maxFramesPerSecond = 30.0;
	
	NSMethodSignature *imagesMethod = [TestController instanceMethodSignatureForSelector:@selector(addImagesToInputs:)];
	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
	int poolCount = 1;

	while( !interrupt )
	{
#ifdef CERT_TIMING
		MPT_Timing *timer = [[MPT_Timing alloc] init];
		STAMPTIME(timer,@"Before lastImage");
#endif
		
		NSMutableDictionary *inputs = [NSMutableDictionary dictionary];
		
		NSInvocation *imagesInvocation = [NSInvocation invocationWithMethodSignature:imagesMethod];
		
		[imagesInvocation setTarget:videoController];
		[imagesInvocation setSelector:@selector(addImagesToInputs:)];
		[imagesInvocation setArgument:&inputs atIndex:2];
		
//		[inputs setObject:imagesInvocation forKey:@"CERT.promise"];
		[imagesInvocation invoke];
		
//[videoController addImagesToInputs:inputs];
//		NSError *error = [inputs objectForKey:@"CERT.error"];
//		
//		if( error )
//		{
//			[self performSelectorOnMainThread:@selector(handleVideoError:) withObject:error waitUntilDone:YES];
//			[pool release];
//#ifdef CERT_TIMING
//			[timer release];
//#endif
//			return;
//		}
//		
//		if( [inputs objectForKey:@"CERT.image"] || [inputs objectForKey:@"CERT.rimage"] )
		{
			[inputs setObject:[NSString stringWithFormat:@"frame%d", frameNum] forKey:@"CERT.path"];
			[inputs setObject:[NSNumber numberWithUnsignedInt:frameNum++] forKey:@"CERT.frame"];
			[inputs setObject:[NSDate date] forKey:@"CERT.date"];
#ifdef CERT_TIMING
			[inputs setObject:timer forKey:@"CERT.timing"];
			STAMPTIME(timer,@"Before schedule");
#endif
			NSDate *limit = [NSDate dateWithTimeIntervalSinceNow:(1.0/maxFramesPerSecond)];
			if( ![doClient doesScheduleInput:inputs] )
				[scheduler scheduleInput:inputs];
			STAMPTIME(timer,@"After schedule");
			NSTimeInterval elapsed = [[NSDate date] timeIntervalSinceDate:limit];
			if( elapsed < 0.0 )
			{
				//NSLog( @"Schedule elapsed time: %lf", elapsed );
				STAMPTIME(timer,@"Before sleep");
				[NSThread sleepUntilDate:limit];
				STAMPTIME(timer,@"After sleep");
			}
		}
		
		if( 0 == (poolCount % 30) )
		{
			STAMPTIME(timer,@"Before pool release");
			[pool release];
			pool = [[NSAutoreleasePool alloc] init];
			STAMPTIME(timer,@"After pool alloc");
		}
		++poolCount;
		
		// We have to throttle it in this case, otherwise we overwhelm the main thread with calls to handleResults:
		if( ![self findFaces] )
			[NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:(1.0/maxFramesPerSecond)]];
#ifdef CERT_TIMING
		STAMPTIME(timer,@"End of video loop");
		[timer release];
#endif
	}
	[pool release];
	
	[self performSelectorOnMainThread:@selector(stopVideo:) withObject:self waitUntilDone:NO];
}

- (IBAction)showPreferences:(id)sender
{
	if( !preferences )
	{
		preferences = [[MPT_Preferences alloc] initWithWindowNibName:@"MPTPrefs"];
		[preferences setAppDelegate:self];
		[preferences showWindow:sender];
	}
	else
	{
		[[preferences window] makeKeyAndOrderFront:sender];
	}
}

- (IBAction)showDataWindow:(id)sender
{
	if( ![[MPT_Demo sharedDemo] isEvaluation] )
		[dataWindow makeKeyAndOrderFront:sender];
}
- (IBAction)saveDataWindow:(id)sender
{
	if( ![[MPT_Demo sharedDemo] isEvaluation] )
		[dataRecorder saveDataModalToWindow:[self window]];
}

- (void)displayAboutPanel:(id)sender
{
	NSBundle *certBundle = [NSBundle bundleWithIdentifier:@"edu.ucsd.mplab.CERTFramework"];
	NSString *mptVersion = [certBundle objectForInfoDictionaryKey:@"MPTVersion"];
	NSString *certVersion = [certBundle objectForInfoDictionaryKey:@"CERTVersion"];

//	int rev = getCertRevision();
////	char *revDate = getCertRevisionDate();
//	const char *mptRev = getMPTRevisionString();
	NSString *demoString = @"";

	if( [[MPT_Demo sharedDemo] isEvaluation] )
		demoString = @" Eval";

	NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:[NSString stringWithFormat:@"build %@::%@%@", mptVersion, certVersion, demoString], @"Version", appName, @"ApplicationName", nil]; // , @"1.0", @"ApplicationVersion", nil
	[NSApp orderFrontStandardAboutPanelWithOptions:options];
//	[NSApp orderFrontStandardAboutPanel:sender];
}

- (IBAction)mailVersionReport:(id)sender
{
	[versionReporter performSelector:@selector(sendToMail:) withObject:sender];
}
- (IBAction)saveVersionReport:(id)sender
{
	[versionReporter performSelector:@selector(saveToFile:) withObject:[self window]];
}

/*** Mail an image as a bug report.
 * Ues the Scripting Bridge to create a new message in Mail.app with the current image as an attachment.
 */
- (IBAction)mailBugReport:(id)sender
{
	NSFileManager *fileManager = [NSFileManager defaultManager];
	BOOL isDirectory = YES;
	NSImage *image = [imageView origImage];
	NSData *bitmapData = NULL;
	NSString *ext = @"tiff";
	NSData *data = NULL;

	// Uncomment the next three lines if you want to send bug report images as jpegs instead of tiffs.
//	NSDictionary *properties = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithFloat:1.0],NSImageCompressionFactor, NULL];    
//	NSBitmapImageRep *imageRep = [image bitmapImageRep];
//	bitmapData = [imageRep representationUsingType:NSJPEGFileType properties:properties];
	
	if( [bitmapData length] > 0 )
	{
		data = bitmapData;
		ext = @"jpeg";
	}
	else
	{
		data = [image TIFFRepresentation];
		ext = @"tiff";
	}
	NSString *guid = [[NSProcessInfo processInfo] globallyUniqueString];
	NSString *path = [[NSTemporaryDirectory() stringByAppendingPathComponent:guid] stringByAppendingPathExtension:ext];
	
	if( !image || (0 == [data length]) )
	{
		NSAlert *alert = [NSAlert alertWithMessageText:@"No image found for submitting a bug report."
										 defaultButton:@"Continue"
									   alternateButton:nil
										   otherButton:nil
							 informativeTextWithFormat:@""];
		[alert runModal];
		return;
	}
	
	if( ![data writeToFile:path atomically:NO] )
	{	
		NSAlert *alert = [NSAlert alertWithMessageText:@"Unable to write image to a temporary file for submitting a bug report."
										 defaultButton:@"Continue"
									   alternateButton:nil
										   otherButton:nil
							 informativeTextWithFormat:@"Temporary file location was: %@", path];
		[alert runModal];
		return;		
	}
#ifdef DEBUG
	NSLog( @"Writing bug report image to: %@", path );
#endif
	
	MailApplication *mail = [SBApplication applicationWithBundleIdentifier:@"com.apple.mail"];
	
	NSString *body = @"";
//	body = [[self latestResults] description]; // Uncomment this if we want to include AFECT outputs in the email
	
	MailOutgoingMessage *mailMessage = [[[[mail classForScriptingClass:@"outgoing message"] alloc]
										 initWithProperties:[NSDictionary dictionaryWithObjectsAndKeys:
															 [NSString stringWithFormat:@"%@ Mistake", appName], @"subject",
															 body, @"content",
															 nil]] autorelease];
	[[mail outgoingMessages] addObject:mailMessage];

	MailToRecipient *recipient = [[[[mail classForScriptingClass:@"to recipient"] alloc] 
								   initWithProperties:[NSDictionary dictionaryWithObjectsAndKeys:
											//		   [NSString stringWithFormat:@"%@ Bug Reports", appName], @"name",
													   @"mistakes@mpt4u.com", @"address",
													   nil]] autorelease];
	[[mailMessage toRecipients] addObject:recipient];
	
	if( [path length] && [fileManager fileExistsAtPath:path isDirectory:&isDirectory] && !isDirectory )
	{
		MailAttachment *attachment = [[[[mail classForScriptingClass:@"attachment"] alloc]
									   initWithProperties:[NSDictionary dictionaryWithObjectsAndKeys:
														   path, @"filename",
														   nil]] autorelease];
//		[[[mailMessage content] paragraphs] addObject:attachment]; // put the image at the end
		[[[mailMessage content] paragraphs] insertObject:attachment atIndex:0]; // put the image at the beginning
	}
	else
	{
		[mailMessage setContent:@"Unable to find image path."];
	}
	
	[mailMessage setVisible:YES];
	[mail activate];
}

- (void)faceDisplayTypeChanged:(NSNotification *)notification
{
	[imageView setFaceType:[[NSUserDefaults standardUserDefaults] integerForKey:@"FaceDisplayType"]];
}

- (BOOL)autostartWebCam
{
	return [[NSUserDefaults standardUserDefaults] boolForKey:@"autostartWebCam"];
}

- (MPT_PluginController *)pluginController {
    return [[pluginController retain] autorelease];
}

@end
