/*
   Project: UL

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

   Author: Michael Johnston

   Created: 2005-05-23 14:10:58 +0200 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 "ULProcessManager.h"

static id processManager;

@implementation ULProcessManager 

- (BOOL) _checkForServerOnHost: (NSString*) host
{
	NSPort* port;

	port = nil;
	if([host isEqual: [[NSHost currentHost] name]])
		port = [[NSMessagePortNameServer sharedInstance] portForName: @"AdunServer"];
		
	if(port == nil)
		port = [[NSSocketPortNameServer sharedInstance] portForName: @"AdunServer" onHost: host];

	if(port == nil)
		return NO;

	return YES;
}

- (void) _handleConnectionsDidDie: (NSNotification*) aNotification
{
	id host, process;
	NSEnumerator* processEnum;
	NSString* errorString, *suggestionString;
	NSError* error;
	NSMutableDictionary* errorInfo;
	NSMutableDictionary* userInfo;

	[[NSNotificationCenter defaultCenter] removeObserver: self
		name: NSConnectionDidDieNotification
		object: [aNotification object]];
	
	//check the host

	host = [[connections allKeysForObject: [aNotification object]] 
			objectAtIndex: 0];

	/*
	 * We receive this when the last simulation run by the server dies OR
	 * when the server crashes. We must distinguish between these two possibilities.
	 * 	In the first case simply remove the invalid connection from the connections dict.
	 * When the next simulation is started a new one will be established
	 *	In the second case we have to remove the connection, set all the processes running on 
	 * the host controlled by the server to "Disconnected", set an error and post a notification
	 * of the servers death.
	 */

	[connections removeObjectForKey: host];

	if(![self _checkForServerOnHost: host])
	{	
		NSWarnLog(@"Detected server death");

		//set all current processes on the host as disconnected

		processEnum = [spawnedStack objectEnumerator];
		while(process = [processEnum nextObject])
			if([[process processHost] isEqual: host] && [[process processStatus] isEqual: @"Running"])
				[process setProcessStatus: @"Disconnected"];

		errorString = [NSString stringWithFormat: 
				@"Unexpected disconnection from AdunServer on host %@\nPossible server crash.", host];
		suggestionString = @"There is currently no way to reaquire a dynamic connection to disconnected simulations.\
\nHowever it is likely they are still running.\nExamine the AdunCore.logs for the disconnected process to determine the\
situation.\nDisconnected simulations will continue to collect results which can still be accessed through the results\
interface.\nPease send the file AdunServerCrash.log in the adun directory to the developers.";

		errorInfo = [NSMutableDictionary dictionary];
		[errorInfo setObject: errorString forKey: NSLocalizedDescriptionKey];
		[errorInfo setObject: suggestionString forKey: @"NSLocalizedRecoverySuggestionKey"];

		error = [NSError errorWithDomain: @"ULServerConnectionDomain" 
				code: 1
				userInfo: errorInfo];

		userInfo = [NSMutableDictionary dictionary];
		[userInfo setObject: error forKey: @"ULDisconnectionErrorKey"];
		[userInfo setObject: host forKey: @"ULDisconnectedHostKey"];

		[[NSNotificationCenter defaultCenter] postNotificationName: @"ULDisconnectedFromServerNotification"
			object: self
			userInfo: userInfo];
	}
}

//The exceptions here should be errors

- (id) _proxyForHost: (NSString*) hostname
{
	id connection;
	NSPort* port;

	if((connection = [connections objectForKey: hostname]) == nil)
	{
		if([hostname isEqual: [[NSHost currentHost] name]])
		{
			//if we're trying to connect to the local host first try to connect to
			//AdunServer through message ports then socket ports

			connection =  [NSConnection connectionWithRegisteredName: @"AdunServer" 
					host: nil 
					usingNameServer: [NSMessagePortNameServer sharedInstance]]; 
		
			if(connection == nil)
				connection =  [NSConnection connectionWithRegisteredName: @"AdunServer" 
						host: hostname 
						usingNameServer: [NSSocketPortNameServer sharedInstance]]; 
		}	
		else
		{
			//if we're not trying to connect to the local host their must be
			//an AdunServer using NSSocketPorts on the remote machine

			connection =  [NSConnection connectionWithRegisteredName: @"AdunServer" 
					host: hostname 
					usingNameServer: [NSSocketPortNameServer sharedInstance]]; 
		}
		
		if(connection == nil)
		{
			[[NSException exceptionWithName: @"ULCouldNotConnectToServerException" 
				reason: [NSString stringWithFormat: @"Couldn't connect to host %@.", hostname]
				userInfo: [NSDictionary dictionaryWithObject: hostname forKey: @"host"]]
				raise];
		}		

		[connections setObject: connection forKey: hostname];

		//register for notifications

		[[NSNotificationCenter defaultCenter] addObserver: self
			selector: @selector(_handleConnectionsDidDie:)
			name: NSConnectionDidDieNotification
			object: connection];
	}

	//check is connection still valid 

	if(![connection isValid])
	{
		//try to reconnect
			
		port = [connection sendPort];
	
		if([port isMemberOfClass: [NSMessagePort class]])
			connection =  [NSConnection connectionWithRegisteredName: @"AdunServer" 
					host: hostname 
					usingNameServer: [NSMessagePortNameServer sharedInstance]]; 
		else
			connection =  [NSConnection connectionWithRegisteredName: @"AdunServer" 
					host: hostname 
					usingNameServer: [NSSocketPortNameServer sharedInstance]]; 
	
		if(connection == nil)
			[NSException raise: NSInternalInconsistencyException 
				format: @"Connection for host %@ invalid and couldnt reconnect.", hostname];
		
		[connections setObject: connection forKey: hostname];
	}

	return [connection rootProxy];
}

- (void) dealloc
{
	[newStack release];
	[spawnedStack release];
	[finishedStack release];
	[connections release];
	[hosts release];
}

+ (id) appProcessManager
{
	return processManager;
}

- (id) init
{
	if(processManager != nil)
		return processManager;

	if(self = [super init])
	{
		newStack = [NSMutableArray arrayWithCapacity: 1];
		spawnedStack = [NSMutableArray arrayWithCapacity: 1];
		finishedStack = [NSMutableArray arrayWithCapacity: 1];
		[newStack retain];
		[spawnedStack retain];
		[finishedStack retain];
		connections = [NSMutableDictionary new];

		hosts = [[ULIOManager appIOManager] valueForKey:@"AdunHosts"];
		[hosts retain];

		NSDebugLLog(@"ULProcessManager", @"Available hosts %@", hosts);
		processManager = self;
	}

	return self;
}

- (void) newProcessWithSystem: (id) newSystem options: (id) newOptions host: (NSString*) host
{
	int i;
	id anObj;
	NSEnumerator* anEnum;
	id process;

	//add the process to the newStack
	
	NSDebugLLog(@"ULProcessManager", @"Creating process with system %@ and options %@", 
			newSystem, 
			[newOptions valueForKey:@"Options"]);

	process = [ULProcess processWithSystem: newSystem options: newOptions];
	[process setProcessStatus: @"Waiting"];
	[process setProcessHost: host];
	[newStack addObject: process]; 
				 
	[[NSNotificationCenter defaultCenter] postNotificationName: @"ULDidCreateNewProcessNotification"
		object: self];

	NSDebugLLog(@"ULProcessManager", @"Added process to newStack. Currently %d objects on newStack", 
		 [newStack count]);
	NSDebugLLog(@"ULProcessManager", @"New object id %@", 
		[[newStack objectAtIndex: 0] valueForKey: @"Identification"]);
}

- (void) newProcessWithSystems: (id) newSystems options: (id) newOptions host: (NSString*) host;
{
	int i;
	id anObj;
	NSEnumerator* anEnum;
	id process;

	//add the process to the newStack

	NSDebugLLog(@"ULProcessManager", @"Creating process with systems %@ and options %@", 
			newSystems, 
			[newOptions valueForKey:@"Options"]);

	process = [ULProcess processWithSystems: newSystems options: newOptions];
	[process setProcessStatus: @"Waiting"];
	[process setProcessHost: host];
	[newStack addObject: process]; 
		 
	[[NSNotificationCenter defaultCenter] postNotificationName: @"ULDidCreateNewProcessNotification"
		object: self];

	NSDebugLLog(@"ULProcessManager", @"Added process to newStack. Currently %d objects on newStack",  
			[newStack count]);
	NSDebugLLog(@"ULProcessManager", @"New object id %@",
			 [[newStack objectAtIndex: 0] valueForKey: @"Identification"]);
	NSDebugLLog(@"ULProcessManager", @"Systems %@",	
			 [process valueForKey:@"Systems"]);
}

- (void) spawnNewProcess
{
	id proxy;
	id process;
	id host;
	id error;

	NSDebugLLog(@"ULProcessManager", @"Preparing process for launch");

	if([newStack count] == 0)
		[NSException raise: NSInternalInconsistencyException
			format: @"There are no simulations waiting to be spawned"];

	process = [[newStack objectAtIndex: 0] retain];
	host = [process processHost]; 
	
	NSDebugLLog(@"ULProcessManager", @"Spawining process on host %@", host);
	
	proxy = [self _proxyForHost: host];

	GSPrintf(stdout, @"Starting Sim using proxy %@\n", proxy);
	NSDebugLLog(@"ULProcessManager", @"Process is %@ , %@", 
		[process valueForKey:@"systems"], [process valueForKeyPath: @"options.Options"]);
	[process processWillStart];	
	error = [proxy startSimulation: process];
	/**
	Clear up created directories if the above error isnt nill
	(Thought maybe this could be done by ULProcess¿)
	*/
	[process setValue: [NSDate date] forKey: @"started"];
	
	if(error == nil)
	{
		[[NSNotificationCenter defaultCenter] addObserver: self
			selector: @selector(processTermination:)
			name: @"ULProcessDidFinishNotification"
			object: process];
		[newStack removeObject: process];
		[spawnedStack addObject: process];
		[process setProcessStatus: @"Running"];
		[process release];	
		[[NSNotificationCenter defaultCenter] postNotificationName: @"ULDidLaunchProcessNotification"
			object: self];
	}
	else
		[NSException raise: NSInternalInconsistencyException
			format: [error localizedDescription]];
}

- (void) processTermination: (NSNotification*) aNotification
{
	id process, dataSet;
	ULSimulation *simulation;
	NSArray* dataSets;
	NSEnumerator* dataSetEnum;
		
	NSDebugLLog(@"ULProcessManager", @"Received a process termination message");

	process = [aNotification object];
	[[NSNotificationCenter defaultCenter] removeObserver: self
		name: nil
		object: process];
	[spawnedStack removeObject: process];
	[finishedStack addObject: process];

	//Add the simulation data to the database.
	simulation = [process simulationData];
	[[ULDatabaseInterface databaseInterface] 
		addObjectToFileSystemDatabase: simulation];

	/**
	Import any dataSets that the simulation created to the database.
	Add input references to the data sets and output references to the simulation.
	*/
	
	if((dataSets = [process controllerResults]) != nil)
	{
		dataSetEnum = [dataSets objectEnumerator];
		while(dataSet = [dataSetEnum nextObject])
		{
			//Add input references for the data sets.		
			[dataSet addInputReferenceToObject: simulation];
			NSDebugMLLog(@"ULAnalyser", @"Data set input references %@", 
				[dataSet inputReferences]);

			[[ULDatabaseInterface databaseInterface]
				addObjectToFileSystemDatabase: dataSet];

			//Add output reference to the simulation
			[simulation addOutputReferenceToObject: dataSet];
		}		
		
		NSDebugMLLog(@"ULAnalyser", @"Simulation output references %@", 
			[simulation outputReferences]);

		//update the database with the new output references for the simulation

		[[ULDatabaseInterface databaseInterface]
			updateOutputReferencesForObject: simulation];
	}
	

	[[NSNotificationCenter defaultCenter] 
		postNotificationName: @"ULProcessDidFinishNotification"
		object: self
		userInfo: [aNotification userInfo]];
}

- (int) numberWaitingProcesses
{
	return [newStack count];
}

- (int) numberSpawnedProcesses
{
	return [spawnedStack count];
}

- (NSMutableArray*) WaitingProcesses
{
	return newStack;
}

- (NSMutableArray*) SpawnedProcesses
{
	return spawnedStack;
}

- (NSMutableArray*) FinishedProcesses
{
	return finishedStack;
}

- (NSMutableArray*) allProcesses
{
	id array;

	array = [newStack arrayByAddingObjectsFromArray: spawnedStack];
	return [[array arrayByAddingObjectsFromArray: finishedStack] mutableCopy];
}

- (NSMutableArray*) Hosts
{
	return hosts;
}

//Basic Commands
//Refactor these messages to use execute:error:process

- (BOOL) exportProcess: (ULProcess*) process 
		toFile: (NSString*) filename 
		overwrite: (BOOL) flag
		error: (NSError**) error 
{
	NSMutableData* data;
	NSKeyedArchiver* archiver;
	BOOL isDir;
	NSString* dir, *reason;
	NSMutableDictionary* userInfo;
	id temp, name, fileManager;

	fileManager = [NSFileManager defaultManager];
	userInfo = [NSMutableDictionary dictionary];
	
	//check file
	name = 	[[filename lastPathComponent] stringByTrimmingCharactersInSet: 
						[NSCharacterSet whitespaceCharacterSet]];
	NSDebugLLog(@"Export", @"Name of file is %@", name);
	if([name isEqual: @""])
	{
		reason = @"Filename invalid - Must contain characters other than whitespace";
		[userInfo setObject: reason forKey: NSLocalizedDescriptionKey];
		*error = [NSError errorWithDomain: @"ULErrorDomain"
				code: 1
				userInfo: userInfo];
		return NO;
	}

	temp = [[filename pathComponents] mutableCopy];
	[temp removeLastObject];
	dir = [NSString pathWithComponents: temp];

	NSDebugLLog(@"Export", @"Exporting to dir %@", dir);
	if(![fileManager fileExistsAtPath: dir isDirectory: &isDir])
	{
		reason = [NSString stringWithFormat: @"The specfied directory (%@) does not exist", dir];
		[userInfo setObject: reason forKey: NSLocalizedDescriptionKey];
		*error = [NSError errorWithDomain: @"ULErrorDomain"
				code: 1
				userInfo: userInfo];
		return NO;
	}

	if(![fileManager isWritableFileAtPath: dir])
	{
		reason = [NSString stringWithFormat: @"Specified directory (%@) is not writable", dir];
		[userInfo setObject: reason forKey: NSLocalizedDescriptionKey];
		*error = [NSError errorWithDomain: @"ULErrorDomain"
				code: 1
				userInfo: userInfo];
		return NO;
	}

	if(!isDir)
	{
		reason = [NSString stringWithFormat: @"%@ is not a directory", dir];
		[userInfo setObject: reason forKey: NSLocalizedDescriptionKey];
		*error = [NSError errorWithDomain: @"ULErrorDomain"
				code: 1
				userInfo: userInfo];
		return NO;
	}


	if([fileManager fileExistsAtPath: filename] && flag == NO)
	{
		reason = @"File already exists - Really overwrite?";
		[userInfo setObject: reason forKey: NSLocalizedDescriptionKey];
		*error = [NSError errorWithDomain: @"ULErrorDomain"
				code: 10
				userInfo: userInfo];
		return NO;
	}
	else if([fileManager fileExistsAtPath: filename] && flag == YES)
	{
		if(![fileManager isWritableFileAtPath:filename]) 
		{
			reason = @"Cannot overwrite file - write protected";
			[userInfo setObject: reason forKey: NSLocalizedDescriptionKey];
			*error = [NSError errorWithDomain: @"ULErrorDomain"
					code: 1
					userInfo: userInfo];
			return NO;
		}
	}

	data = [NSMutableData new];
	archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData: data];
	[archiver setOutputFormat: NSPropertyListXMLFormat_v1_0];
	[archiver encodeObject: process forKey: @"process"];
	[archiver finishEncoding];

	if(![data writeToFile: filename atomically: NO])
	{
		reason = @"Unable to export file - Reason unknown";
		[userInfo setObject: reason forKey: NSLocalizedDescriptionKey];
		*error = [NSError errorWithDomain: @"ULErrorDomain"
				code: 1
				userInfo: userInfo];
		return NO;
	}

	[archiver release];
	[data release];

	return YES;
}

- (void) haltProcess: (ULProcess*) process
{
	id proxy;

	if([[process processStatus] isEqual: @"Running"])
	{
		proxy = [self _proxyForHost: [process processHost]];
		[proxy haltProcess: process];
		[process setProcessStatus: @"Suspended"];
	}
	else
		[NSException raise: @"NSInvalidArgumentException"
			format: @"Selected process is not running"];
	
	[[NSNotificationCenter defaultCenter] 
		postNotificationName: @"ULProcessStatusDidChangeNotification"
		object: self];
}

- (void) restartProcess: (ULProcess*) process
{
	id proxy;

	NSDebugMLLog(@"ULProcessManager", @"Restarting %@, status %@", 
		process, [process processStatus]);

	if([[process processStatus] isEqual: @"Suspended"])
	{
		proxy = [self _proxyForHost: [process processHost]];
		[proxy restartProcess: process];
		[process setProcessStatus: @"Running"];
	}
	else
		[NSException raise: @"NSInvalidArgumentException"
			format: @"Selected process is not suspended"];

	[[NSNotificationCenter defaultCenter] 
		postNotificationName: @"ULProcessStatusDidChangeNotification"
		object: self];
}

- (void) terminateProcess: (ULProcess*) process
{
	id proxy;
	id status;

	status = [process processStatus];
	NSDebugMLLog(@"ULProcess", @"Terminating %@, status %@", 
		process, [process processStatus]);

	if([status isEqual: @"Running"] || 
		[status isEqual: @"Suspended"])
	{
		proxy = [self _proxyForHost: [process processHost]];
		[proxy terminateProcess: process];
	}
	else
		[NSException raise: @"NSInvalidArgumentException"
			format: @"Selected process has not been started"];
}

- (void) startProcess: (ULProcess*) process
{
	if([[process processStatus] isEqual: @"Waiting"])
	{
		[newStack removeObject: process];
		[newStack insertObject: process atIndex: 0];
		[self spawnNewProcess];
	}
	else
		[NSException raise: @"NSInvalidArgumentException"
			format: @"Selected process is not waiting"];

	[[NSNotificationCenter defaultCenter] 
		postNotificationName: @"ULProcessStatusDidChangeNotification"
		object: self];
}

- (void) removeProcess: (ULProcess*) process
{
	if([[process processStatus] isEqual: @"Finished"])
		[finishedStack removeObject: process];
	else if([[process processStatus] isEqual: @"Waiting"])
		[newStack removeObject: process];
	else
		[NSException raise: @"NSInvalidArgumentException"
			format: @"Selected process is running"];

	[[NSNotificationCenter defaultCenter] 
		postNotificationName: @"ULProcessStatusDidChangeNotification"
		object: self];
}

- (id) execute: (NSDictionary*) commandDict error: (NSError**) error process: (id) process
{
	id proxy;
	id result;
	id receivePort, connection;
	NSMutableDictionary* errorInfo;
	NSString* descriptionString, *suggestionString;
	
	if([[process processStatus] isEqual: @"Running"])
		proxy = [self _proxyForHost: [process processHost]];
	else
		[NSException raise: @"NSInvalidArgumentException"
			format: @"Selected process is not running"];

	NS_DURING
	{
		[[proxy connectionForProxy] setReplyTimeout: 10.0];
		[[proxy connectionForProxy] setRequestTimeout: 10.0];
		result = [proxy execute: commandDict 
				error: error
				process: process];
	}
	NS_HANDLER
	{
		connection = [proxy connectionForProxy];
		
		//if the port is still valid - set an error
		if([connection isValid])
		{
			errorInfo = [NSMutableDictionary dictionary];
			
			descriptionString = [NSString stringWithFormat: @"Attempt to send command %@ to %@ timed out.\n",
						[commandDict objectForKey: @"command"], 
						[process processHost]];
			suggestionString = @"This could be due to the server being busy or the underlying operating\
system.\nTry again later.";

			[errorInfo setObject: descriptionString 
				forKey: NSLocalizedDescriptionKey];
			[errorInfo setObject: suggestionString 
				forKey: @"NSLocalizedRecoverySuggestionKey"];
			*error = [NSError errorWithDomain: @"ULServerConnectionDomain"
					code: 1
					userInfo: errorInfo];
		}	
		
		/*
		If either port (i.e. send or recieve) was invalidated and was an NSMessagePort 
		we will receive an NSConnectionDidDieNotification and we will handle the results then. 
		However if we are using socket ports we wont recieve this notification 
		for an exception on the remote (the receive) port and 
		must check for it here and raise the exception ourselves
		*/

		receivePort = [connection receivePort];
		if([receivePort isMemberOfClass: [NSSocketPort class]])
			if(![receivePort isValid])
				[[NSNotificationCenter defaultCenter] 
					postNotificationName: NSConnectionDidDieNotification
					object: [proxy connectionForProxy]];
	}
	NS_ENDHANDLER

	return result;
}

- (NSMutableDictionary*) optionsForCommand: (NSString*) name process: (id) process
{
	id proxy;
	
	if([[process processStatus] isEqual: @"Running"])
		proxy = [self _proxyForHost: [process processHost]];
	else
		[NSException raise: @"NSInvalidArgumentException"
			format: @"Selected process is not running"];

	return [proxy optionsForCommand: name
			process: process];
}

- (NSArray*) validCommandsForProcess: (id) process
{
	id proxy;
	

	if([[process processStatus] isEqual: @"Running"])
		proxy = [self _proxyForHost: [process processHost]];
	else
		[NSException raise: @"NSInvalidArgumentException"
			format: @"Selected process is not running"];

	return [proxy validCommandsForProcess: process];
}

@end
