/*
   Project: UL

   Copyright (C) 2005 Michael Johnston & Jordi Villa-Freixa

   Author: Michael Johnston

   Created: 2005-12-09 14:47:28 +0100 by michael johnston

   This application is free software; you can redistribute it and/or
   modify it under the terms of the GNU General Public
   License as published by the Free Software Foundation; either
   version 2 of the License, or (at your option) any later version.

   This application is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   Library General Public License for more details.

   You should have received a copy of the GNU General Public
   License along with this library; if not, write to the Free
   Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111 USA.
*/

#include "ULFramework/ULAnalysisManager.h"

@implementation ULAnalysisManager

- (Class) _loadBundle: (NSString*) pluginName fromDir: (NSString*) dir 
{
	NSBundle *pluginBundle;
	NSString *pluginPath;
	NSString *temp;
	Class pluginClass;

	NSDebugLLog(@"ULAnalysisManager", @"Plugin dir is %@. Plugin Name is %@", 
		dir, 
		pluginName);

	//add check to see if bundle actually exists

	pluginBundle = [NSBundle bundleWithPath: 
				[dir stringByAppendingPathComponent: 
				pluginName]];
	if(pluginBundle == nil)
		[NSException raise: NSInvalidArgumentException format: @"Specified plugin does not exist"];	

	NSDebugLLog(@"ULAnalysisManager", @"Plugin Bundle is %@", pluginBundle);
	NSDebugLLog(@"ULAnalysisManager", 
		@"Dynamicaly Loading Plugin (if neccessary) from Directory: %@.\n\n",
		[pluginBundle bundlePath]);

	if(pluginClass = [pluginBundle principalClass])
		NSDebugLLog(@"ULAnalysisManager", @"Found plugin (plugin=%@).\n",
			 [pluginClass description]);
	else
		[NSException raise: NSInternalInconsistencyException 
			format: @"Specified plugin has no principal class"];

	NSDebugLLog(@"ULAnalysisManager", @"Loaded plugin\n");

	return pluginClass;
}

- (void) _setCurrentPlugin: (NSString*) name
{
	Class pluginClass;
	id holder;
	
	//check if this is the current plugin

	if(![name isEqual: currentPluginName])
	{
		//get the principal class of the plugin	

		pluginClass = [self _loadBundle: name fromDir: pluginDir];		
		holder = [pluginClass new];
		[currentPlugin release];
		currentPlugin = holder;
		[currentPluginName release];
		currentPluginName = [name retain];

		if(![currentPlugin conformsToProtocol:@protocol(ULAnalysisPlugin)])
		{
			[currentPlugin release];
			[currentPluginName release];
			currentPluginName = nil;
			currentPlugin = nil;
			NSWarnLog(@"Plugin doesnt not conform to ULAnalysisPlugin protocol");
			[NSException raise: NSInternalInconsistencyException 
				format: @"Specified plugins (%@) principal class does not conform to ULAnalysisPlugin protocol", 
				[pluginClass description]];
		}
	}
}

- (void) _findAvailablePlugins
{
	BOOL isDir;
	NSFileManager *fileManager = [NSFileManager defaultManager];
	NSEnumerator *pluginDirEnum;
	NSString *contentObject, *path;
	NSDictionary * inputInfo, *infoDict;
	NSBundle* bundle;

	//scan the analysis plugins directory and get all the bundle names

	pluginDirEnum = [[fileManager directoryContentsAtPath: pluginDir]
				objectEnumerator];
	availablePlugins = [NSMutableArray new];
	pluginInfoDict = [NSMutableDictionary new];
	while(contentObject = [pluginDirEnum nextObject])
	{
		path = [pluginDir stringByAppendingPathComponent: contentObject];
		[fileManager fileExistsAtPath: path isDirectory: &isDir];
		if(isDir)
		{
			//retrieve the info dict
			bundle = [NSBundle bundleWithPath: path];
			infoDict = [bundle infoDictionary];
			inputInfo = [infoDict objectForKey: @"ULAnalysisPluginInputInformation"];
			if(infoDict == nil)
			{
				NSWarnLog(@"Plugin %@ contains no Info.plist", contentObject);
			}	
			else if(inputInfo == nil)
			{
				NSWarnLog(@"%@ plugin Info.plist contains no input object information"
						,contentObject);
			}
			else
			{
				[availablePlugins addObject: contentObject];
				[pluginInfoDict setObject: inputInfo
					forKey: contentObject];
			}		
		}	
	}		

	NSDebugLLog(@"ULAnalysisManager", @"Available plugins %@", availablePlugins);
	NSDebugLLog(@"ULAnalysisManager", @"Plugin information %@", pluginInfoDict);
}

- (id) init
{
	id defaults;

	if((self = [super init]) != nil)
	{
		currentPluginName = nil;
		results = nil;
		pluginDir = [NSHomeDirectory() stringByAppendingPathComponent: @"adun/Plugins/Analysis"];
		[pluginDir retain];
		inputObjects = [NSMutableArray new];
		objectsCountDict = [NSMutableDictionary new];
		outputObjectsReferences = [NSMutableArray new];
		//Locate what plugins are available along with their input requirements.
		[self _findAvailablePlugins];
	}

	return self;
}

- (void) dealloc
{
	[results release];
	[outputObjectsReferences release];
	[availablePlugins release];
	[pluginInfoDict release];
	[pluginDir release];
	[inputObjects release];
	[objectsCountDict release];
	[currentPluginName release];
	[currentPlugin release];
}

//FIXME: This method needs to be updated
//Need to implement ULSimulations loadData
//so it can be called multiple times
/*
- (void) reloadResultsAtPath: (NSString*) path
{
	id holder;

	//FIXME: Ideally we should have more rigourous checking that
	//new energies/dynamics have actually been output since the
	//last time they were read in - A method in ULResults?

	holder = [[ULSimulation alloc] initWithTrajectoryAtPath: path];
	[currentResults release];
	currentResults = holder;
	
}*/

- (id) applyPlugin: (NSString*) name withOptions: (NSMutableDictionary*) options
{
	NSEnumerator *inputObjectsEnum, *dataSetsEnum;
	id dataSet, inputObject;
	NSArray* dataSets;
	
	[self _setCurrentPlugin: name];
	[results release];
	[outputObjectsReferences removeAllObjects];

	NS_DURING
	{
		results = [currentPlugin processInputs: inputObjects
				userOptions: options];
		[results retain];		
		dataSets = [results objectForKey: @"ULAnalysisPluginDataSets"];

		//set input references 
		//note: output references cant be added until the object is saved 
		//to a database.
		inputObjectsEnum = [inputObjects objectEnumerator];
		while(inputObject = [inputObjectsEnum nextObject])
		{
			//FIXME: Unable to add input references for moltalk structures.
			if([inputObject isKindOfClass: [AdModelObject class]])
			{
				//loop over every data set returned
				dataSetsEnum = [dataSets objectEnumerator];
				while(dataSet = [dataSetsEnum nextObject])
				{
					[dataSet addInputReferenceToObject: inputObject];
					NSDebugMLLog(@"ULAnalysisManager", @"Data set input references %@", 
						[dataSet inputReferences]);
				}	
			}
		}	

		//Keep track of the objects that generated these data sets.
		[outputObjectsReferences  addObjectsFromArray: inputObjects];
	}
	NS_HANDLER
	{
		results  = nil;
		NSWarnLog(@"Results analysis failed due to an exception");
		NSWarnLog(@"%@ %@ %@", [localException name], 
			[localException reason], 
			[localException userInfo]);
		[localException raise];
	}
	NS_ENDHANDLER

	NSDebugLLog(@"ULAnalysisManager", @"Completed analysis. Returning %@", results);

	return results;
}

- (void) saveOutputDataSet: (AdDataSet*) dataSet
{
	NSArray* dataSets;
	NSEnumerator* inputsEnum;
	id input;

	//check this object is one of the current output objects

	dataSets = [results objectForKey: @"ULAnalysisPluginDataSets"];
	if(![dataSets containsObject: dataSet])
		[NSException raise: NSInvalidArgumentException
			format: @"Dataset is not among the current outputs"];

	[[ULDatabaseInterface databaseInterface]
		addObjectToFileSystemDatabase: dataSet];

	//Go through all the objects that created this dataSet and 
	//add an output reference to each
	inputsEnum = [outputObjectsReferences objectEnumerator];
	while(input = [inputsEnum nextObject])
	{
		if([input isKindOfClass: [AdModelObject class]])
		{
			[input addOutputReferenceToObject: dataSet];
			[[ULDatabaseInterface databaseInterface]
				updateOutputReferencesForObject: input];
		}		
	}
}

- (id) optionsForPlugin: (NSString*) name
{
	[self _setCurrentPlugin: name];	
	//FIXME
	return [currentPlugin pluginOptions: inputObjects];
}

- (void) addInputObject: (id) object
{
	NSNumber* objectsCount;
	NSString* type;

	[inputObjects addObject: object];
	type = NSStringFromClass([object class]);
	if((objectsCount = [objectsCountDict objectForKey: type]) != nil)
	{	
		objectsCount = [NSNumber numberWithInt: 
				[objectsCount intValue] +1];
		[objectsCountDict setValue: objectsCount forKey: type];
	}
	else
		[objectsCountDict setValue: [NSNumber numberWithInt: 1]	
			forKey: type];
}

- (void) removeInputObject: (id) object
{
	NSNumber* objectsCount;
	NSString* type;

	if(![inputObjects containsObject: object])
		return

	[inputObjects removeObject: object];
	type = NSStringFromClass([object class]);
	objectsCount = [objectsCountDict objectForKey: type];
	if([objectsCount intValue] == 1)
		[objectsCountDict removeObjectForKey: type];
	else
	{	
		objectsCount = [NSNumber numberWithInt: 
				[objectsCount intValue] -1];
		[objectsCountDict setValue: objectsCount forKey: type];
	}
}

- (void) removeAllInputObjects
{
	[inputObjects removeAllObjects];
	[objectsCountDict removeAllObjects];
}

- (NSArray*) inputObjects
{
	return [[inputObjects copy] autorelease];
}

- (BOOL) containsInputObjects
{
	if([inputObjects count] != 0)
		return YES;
	else
		return NO;
}

- (int) countOfInputObjectsOfClass: (NSString*) className
{
	NSNumber* count;

	count = [objectsCountDict objectForKey: className];
	if(count != nil)
		return [count intValue];
	else
		return 0;
}

- (BOOL) _pluginCanProcessCurrentInputs: (NSString*) pluginName
{
	NSNumber* number, *minimumNumber, *maximumNumber;
	NSArray* pluginInputs;
	NSMutableArray *inputTypes;
	NSEnumerator *inputsEnum;
	id input, inputType;

	pluginInputs = [pluginInfoDict objectForKey: pluginName];

	//Create an array containing the input types which are present
	//We will check this against the types the plugin can handle.
	//The plugin must be able to accept all these types

	inputTypes = [NSMutableArray array];
	inputsEnum = [objectsCountDict keyEnumerator];
	while(input = [inputsEnum nextObject])
		if([[objectsCountDict objectForKey: input] intValue] >0)
			[inputTypes addObject: input];

	NSDebugLLog(@"ULAnalysisManager", @"Available input types %@", inputTypes);		
	
	//Check that all the plugins requirements are present.
	//When we find that a required  plugin input is present 
	//we remove it from the inputTypes array.
	//At the end if there are any entries left in inputTypes
	//it means the plugin cant process them and we have to return NO.
	
	inputsEnum = [pluginInputs objectEnumerator];
	while(input = [inputsEnum nextObject])
	{
		//check if the input type exists
		inputType = [input objectForKey: @"ULInputObject"];
		number = [objectsCountDict objectForKey: inputType];
		NSDebugLLog(@"ULAnalysisManager", @"Checking for input objects of type %@", inputType);
		minimumNumber = [input objectForKey: @"ULInputObjectMinimumNumber"];
		maximumNumber = [input objectForKey: @"ULInputObjectMaximumNumber"];

		if(minimumNumber == nil)
		{
			NSWarnLog(@"ULInputObjectMinimumNumber key missing from plugin input information");
			return NO;
		}

		if(maximumNumber == nil)
		{
			NSWarnLog(@"ULInputObjectMaximumNumber key missing from plugin input information");
			return NO;
		}

		
		NSDebugLLog(@"ULAnalysisManager", @"Mininum required number %@ maximum number %@", 
			minimumNumber, 
			maximumNumber);
		
		//if there are no inputs of this type check if its optional
		//if its required return NO
		if(number == nil)
		{
			NSDebugLLog(@"ULAnalysisManager", 
				@"No inputs of this type! Checking if optional");
			if([minimumNumber intValue] != 0)
			{
				NSDebugLLog(@"ULAnalysisManager", 
					@"Required type - plugin cant process current inputs");
				return NO;
			}
				
			NSDebugLLog(@"ULAnalysisManager", 
				@"Input optional - continuing");	
		}		
		else
		{
			//check if its greater than or equal to the minimum number

			if(!([number intValue] <= [maximumNumber intValue] && 
				[number intValue] >= [minimumNumber intValue]))
			{
				NSDebugLLog(@"ULAnalysisManager", 
					@"The number of inputs of type %@ (%@) is incorrect", 
					inputType, 
					number);
				return NO;
			}
			else
					//remove this object from the inputTypes array
				[inputTypes removeObject: inputType];
		}
	}	

	if([inputTypes count] != 0)
		return NO;
		
	return YES;
}

- (NSArray*) pluginsForCurrentInputs
{
	NSEnumerator *pluginEnum;
	NSMutableArray *array = [NSMutableArray array];
	NSString *plugin;

	if([inputObjects count] != 0)
	{
		pluginEnum = [availablePlugins objectEnumerator];
		while(plugin = [pluginEnum nextObject])
			if([self _pluginCanProcessCurrentInputs: plugin])
				[array addObject: plugin];
	}			

	return array;
}

- (id) outputDataSets 
{ 
	return [results objectForKey: @"ULAnalysisPluginDataSets"];
}

- (id) outputString 
{ 
	return [results objectForKey: @"ULAnalysisPluginString"];
}

@end
