/*
   Project: AdunServer

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

   Author: Michael Johnston

   Created: 2005-05-31 15:41:02 +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 "AdServer.h"
#include <wait.h>

@implementation AdServer

- (void) kernelTermination: (NSNotification*) aNotification
{
	NSError* error;

	/*
	 * Core Exit Procedure : 
	 * On controlled exit - Posts an exit (error) message with the server via closeConnection: 
	 * On uncontrolled exit - Does nothing
	 *
	 * Here we check if the process posted an error message eariler via closeConnection.
	 * If it didn't we create an error using the exit status code and use that instead.
	 */

	int status = [[aNotification object] terminationStatus];
	int pid;
	id task, process;
	NSMutableDictionary* errorInfo;

	task = [aNotification object];
	pid = [task processIdentifier];

       	NSWarnLog(@"Kernel process %d exited with status %d. (%s)", pid, status, strerror(status));
	
	error = nil;
	NSDebugLLog(@"AdunServer", @"Checking for error from AdunCore");
	if((error = [processErrors objectForKey: [NSNumber numberWithInt: pid]]) != nil)
		NSWarnLog(@"Kernel posted error %@", [error userInfo]);
	else if(status != 0)
	{
		NSDebugLLog(@"AdunServer", @"No error posted - creating one now.");
		errorInfo =  [NSMutableDictionary dictionary];
		[errorInfo setObject: @"Simulation terminated by uncaught signal.\n"
			forKey: @"NSLocalizedDescriptionKey"];
		[errorInfo setObject: [NSString stringWithFormat:
					@"Exit code %d - %s.\n", status, strerror(status)]
			forKey: @"AdDetailedDescriptionKey"];
		[errorInfo setObject: @"Please submit this event along with the system and options used\
 to the support tracker at gna.org/projects/adun.\n"
			forKey: @"NSRecoverySuggestionKey"];

		error = [NSError errorWithDomain: @"AdCoreErrorDomain"
				code: 5
				userInfo: errorInfo];
	}

	NSDebugLLog(@"AdunServer", @"Retrieving relevant process object");
	process = [processes objectForKey: [NSNumber numberWithInt: pid]];
	NSDebugLLog(@"AdunServer", @"Notifying process object of termination");
	[process processDidTerminate: error];
	[processes removeObjectForKey: [NSNumber numberWithInt: pid]];
	[tasks removeObjectForKey: [NSNumber numberWithInt: pid]];
	[state removeObjectForKey: [NSNumber numberWithInt: pid]];
	[processErrors removeObjectForKey: [NSNumber numberWithInt: pid]];
	[interfaces removeObjectForKey: [NSNumber numberWithInt: pid]];
}

- (void) _adunCoreError
{
	[NSException raise: NSInvalidArgumentException
		format: [NSString stringWithFormat: 
			@"Couldnt execute AdunCore. Set AdunCorePath default to the correct file \
and ensure its executable. Check AdServer.log for more information."]];

}

- (BOOL) _testFileExistsIsExecutable: (NSString*) path
{
	if([[NSFileManager defaultManager] fileExistsAtPath: path])
	{
		if([[NSFileManager defaultManager] isExecutableFileAtPath: path])
			return YES;
		else
			return NO;
	}
	else
		return NO;
}

- (void) _redirectOutput
{
	id logFile;

	logFile = [[NSUserDefaults standardUserDefaults] stringForKey: @"LogFile"];
	if(![[NSFileManager defaultManager] isWritableFileAtPath:
		 [logFile stringByDeletingLastPathComponent]])
	{
		logFile = [[[NSUserDefaults standardUserDefaults] 
				volatileDomainForName: NSRegistrationDomain]
				valueForKey:@"LogFile"];
		NSWarnLog(@"Invalid value for user default 'LogFile'. The specificed directory is not writable");
		NSWarnLog(@"Switching to registered default %@", logFile);
		if(![[NSFileManager defaultManager] 
			isWritableFileAtPath: [logFile stringByDeletingLastPathComponent]])
		{
			[NSException raise: NSInternalInconsistencyException
				format: [NSString stringWithFormat:
				 @"Fallback logfile (%@) not writable. Cannot start.", logFile]];
		}
	} 

	freopen([logFile cString], "w", stderr);
}

- (id) init
{
	id adunFile;
	id portOne, portTwo;

	if((self = [super init]))
	{
		//redirect output
		
		if([[NSUserDefaults standardUserDefaults] boolForKey: @"RedirectOutput"])
			[self _redirectOutput];
			
		if([[NSUserDefaults standardUserDefaults] boolForKey: @"AdServerIsDistributed"])
		{

			GSPrintf(stderr, @"Using socket ports to enable distributed computing.\n");

			portOne = [NSSocketPort port];
			portTwo = [NSSocketPort port];
			connection = [[NSConnection alloc] initWithReceivePort: portOne 
					sendPort: portTwo];
			[connection setRootObject: self];
			if([connection registerName: @"AdunServer" withNameServer:
					 [NSSocketPortNameServer sharedInstance]] == NO)
			{
				NSWarnLog(@"Failed to vend!");
				exit(1);
			}
		}
		else
		{
			GSPrintf(stderr, 
				@"Using default message ports. Distributed computing facilites disabled.\n");
			
			portOne = [NSMessagePort port];
			portTwo = [NSMessagePort port];
			connection = [[NSConnection alloc] initWithReceivePort: portOne 
					sendPort: portTwo];
			[connection setRootObject: self];
			
			if([connection registerName: @"AdunServer" withNameServer:
					 [NSMessagePortNameServer sharedInstance]] == NO)
			{
				NSWarnLog(@"Failed to vend!");
				exit(1);
			}
		}

		[[NSNotificationCenter defaultCenter] addObserver: self
			selector: @selector(kernelTermination:)
			name: NSTaskDidTerminateNotification
			object: nil];

		tasks = [NSMutableDictionary new];
		interfaces = [NSMutableDictionary new];	
		processes = [NSMutableDictionary new];
		state = [NSMutableDictionary new];
		processErrors = [NSMutableDictionary new];

		//see if we can locate the adun core executable

		adunCorePath = [[[NSUserDefaults standardUserDefaults] stringForKey: @"AdunCorePath"] 
					stringByAppendingPathComponent: @"AdunCore"];
		[adunCorePath retain];

		if(![self _testFileExistsIsExecutable: adunCorePath])
		{
			NSWarnLog(@"Defaults value for AdunCorePath (%@) is not valid", adunCorePath);
			adunFile = [[[NSUserDefaults standardUserDefaults] 
					volatileDomainForName: NSRegistrationDomain]
					valueForKey:@"AdunCorePath"];
			
			if(![adunCorePath isEqual: adunFile])
			{
				NSWarnLog(@"Falling back on registered default.");
				adunCorePath = adunFile;
				if(![self _testFileExistsIsExecutable: adunCorePath])
				{
					NSWarnLog(@"Adun was installed into a non standard directory.");
					[self _adunCoreError];
				}
			}
			else
				[self _adunCoreError];	
		}
		else
			GSPrintf(stderr, @"Found Adun Core Executable at %@\n", adunCorePath);
	}
	
	fflush(stderr);

	return self;
}

- (void) dealloc
{
	[processes release];
	[tasks release];
	[interfaces release];
	[state release];
	[adunCorePath release];	
	[processErrors release];

	[super dealloc];
}

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

- (NSError*) startSimulation: (id) process;
{
	id task;
	NSMutableDictionary* userInfo;
	NSError* error;

	NSMutableArray* arguments;

	NSDebugLLog(@"AdunServer", @"Recieved a start simulation message");
	NSDebugLLog(@"AdunServer", @"Process Object is %@", [process description]);
	NSDebugLLog(@"AdunServer", @"Found Adun Core Executable at %@", adunCorePath);
	
	NS_DURING
	{
		task = [NSTask launchedTaskWithLaunchPath: adunCorePath
			arguments: nil];

		[process setProcessIdentifier: [task processIdentifier]];
		[processes setObject: process 
			forKey: [NSNumber numberWithInt: [task processIdentifier]]];
		[tasks setObject: task 
			forKey: [NSNumber numberWithInt: [task processIdentifier]]];
		[state setObject: [NSNumber numberWithBool: NO] 
			forKey: [NSNumber numberWithInt: [task processIdentifier]]];

		return nil;
	}
	NS_HANDLER
	{
		userInfo = [NSMutableDictionary dictionaryWithCapacity: 1];
		[userInfo setObject: 
			[NSString stringWithFormat: @"Unable to lauch simulation - %@", [localException reason]]
			forKey: NSLocalizedDescriptionKey];

		return [NSError errorWithDomain: @"AdServerErrorDomain"
				code: 1
				userInfo: userInfo];
	}
	NS_ENDHANDLER

	return nil;
}

- (void) haltProcess: (id) process
{
	NSNumber* pid;
	id task;

	pid = [NSNumber numberWithInt: [process processIdentifier]];
	task = [tasks objectForKey: pid];
	[task suspend];
}

- (void) terminateProcess: (id) process
{
	NSNumber* pid;
	id task;

	pid = [NSNumber numberWithInt: [process processIdentifier]];
	task = [tasks objectForKey: pid];
	[task terminate];
}

- (void) restartProcess: (id) process
{
	NSNumber* pid;
	id task;

	pid = [NSNumber numberWithInt: [process processIdentifier]];
	task = [tasks objectForKey: pid];
	[task resume];
}

//Process Core Interface Methods
/*
 *N.B. If you declare in the protocol for DO that an paramter is "out" as in the
pointer to NSError below. It seems you have to assign a value to it or else
you get a seg fault
*/

- (id) execute: (NSDictionary*) commandDict error: (NSError**) error process: (id) process
{
	NSNumber* pid;
	id interface;
	id returnVal;

	NSDebugLLog(@"AdunServer", @"Recieved command request for process %@", process);
	pid = [NSNumber numberWithInt: [process processIdentifier]];
	interface = [interfaces objectForKey:  pid];
	if(interface == nil)
	{
		NSDebugLLog(@"AdunServer", @"The specified process has not provided its interface");
		*error = [NSError errorWithDomain: @"AdServerCommandInterfaceErrorDomain"
				code: 0
				userInfo: [NSDictionary dictionaryWithObject:
					@"The specified process has not provided its interface"
					forKey:
					NSLocalizedDescriptionKey]];

		return nil;
	}
	NSDebugLLog(@"AdunServer", @"Retrieved interface. Checking availability", process);

	if([[state objectForKey: pid] boolValue] == NO)
	{
		NSDebugLLog(@"AdunServer", @"The specified process is not accepting requests");
		*error = [NSError errorWithDomain: @"AdServerCommandInterfaceErrorDomain"
				code: 0
				userInfo: [NSDictionary dictionaryWithObject:
					@"The specified process is not accepting requests"
					forKey:
					NSLocalizedDescriptionKey]];
		return nil;
	}
	
	NSDebugLLog(@"AdunServer", @"Exectuting command %@", commandDict);

	returnVal = [[[interface execute: commandDict
			error: error] retain] autorelease];

	return returnVal;
}

- (NSMutableDictionary*) optionsForCommand: (NSString*) name process: (id) process
{
	NSNumber* pid;
	id interface;
	
	pid = [NSNumber numberWithInt: [process processIdentifier]];
	interface = [interfaces objectForKey:  pid];
	if(interface == nil)
		return nil;

	return [interface optionsForCommand: name];
}

- (NSArray*) validCommandsForProcess: (id) process
{
	NSNumber* pid;
	id interface;
	
	pid = [NSNumber numberWithInt: [process processIdentifier]];
	interface = [interfaces objectForKey:  pid];
	if(interface == nil)
		return nil;

	return [interface validCommands];
}

- (void) useInterface: (id) object forProcess: (int) pid
{
	[interfaces setObject: object forKey: [NSNumber numberWithInt: pid]];
}

- (void) acceptingRequests: (int) pid
{
	//Dictionary containing command name and options
	NSMutableDictionary* commandDict = [NSMutableDictionary dictionary];
	//The options for the command
	NSMutableDictionary* options = [NSMutableDictionary dictionary];
	NSError* error;
	id interface, process;

	NSDebugLLog(@"AdunServer", @"Process %d is accepting requests", pid);

	options = [NSMutableDictionary dictionary];
	interface = [interfaces objectForKey:  [NSNumber numberWithInt: pid]];

	NSDebugLLog(@"AdunServer", @"Create output directories");
	
	process = [processes objectForKey: [NSNumber numberWithInt: pid]];
	[options setObject: [process simulationOutputDirectory]
		forKey: @"simulationOutputDirectory"];

	//check if there is a controller	
	
	if([process controllerOutputDirectory] != nil)
		[options setObject: [process controllerOutputDirectory]
			forKey: @"controllerOutputDirectory"];

	[commandDict setObject: @"setOutputDirectories"
		forKey: @"command"];
	[commandDict setObject: options forKey: @"options"]; 
	
	[interface execute: commandDict error: &error];
	if(error != nil)
		NSLog(@"Error (%@) - %@",
			[error domain], 
			[[error userInfo] objectForKey: NSLocalizedDescriptionKey]); 
		
	[options removeAllObjects];
	
	NSDebugLLog(@"AdunServer", @"Load process data");

	[options setObject: @"Server" forKey: @"inputSourceName"];
	[commandDict setObject: @"loadProcessData" forKey: @"command"];
	[commandDict setObject: options forKey: @"options"]; 
	[interface execute: commandDict error: &error];
	if(error != nil)
		NSLog(@"Error (%@) - %@",
			[error domain], 
			[[error userInfo] objectForKey: NSLocalizedDescriptionKey]); 
	
	NSDebugLLog(@"AdunServer", @"Load controller");
	
	[commandDict setObject: @"loadController" forKey: @"command"];
	[interface execute: commandDict error: &error];
	if(error != nil)
		NSLog(@"Error (%@) - %@",
			[error domain], 
			[[error userInfo] objectForKey: NSLocalizedDescriptionKey]); 

	NSDebugLLog(@"AdunServer", @"Create simulator");

	[commandDict removeObjectForKey: @"options"];
	[commandDict setObject: @"createSimulator" forKey: @"command"];
	[interface execute: commandDict error: &error];
	if(error != nil)
		NSLog(@"Error (%@) - %@",
			[error domain], 
			[[error userInfo] objectForKey: NSLocalizedDescriptionKey]); 
	
	NSDebugLLog(@"AdunServer", @"Create system");
	
	[commandDict setObject: @"createSystem" forKey: @"command"];
	[interface execute: commandDict error: &error];
	if(error != nil)
		NSLog(@"Error (%@) - %@",
			[error domain], 
			[[error userInfo] objectForKey: NSLocalizedDescriptionKey]); 
	
	NSDebugLLog(@"AdunServer", @"Start simulation");
	
	[commandDict setObject: @"main" forKey: @"command"];
	[interface execute: commandDict error: &error];
	if(error != nil)
		NSLog(@"Error (%@) - %@",
			[error domain], 
			[[error userInfo] objectForKey: NSLocalizedDescriptionKey]); 

	[state setObject: [NSNumber numberWithBool: YES] forKey: [NSNumber numberWithInt: pid]];
	
	NSDebugLLog(@"AdunServer", @"Process %d initialised", pid);
}

- (void) closeConnectionForProcess: (int) pid error: (NSError*) error
{
	[interfaces removeObjectForKey: [NSNumber numberWithInt: pid]];
	
	if(error != nil)
		[processErrors setObject: error forKey: [NSNumber numberWithInt: pid]];
}

- (id) getSystemsForProcess: (int) pid
{
	id process;

	process = [processes objectForKey: [NSNumber numberWithInt: pid]];
	
	return [process valueForKey: @"systems"];
}

- (bycopy NSMutableDictionary*) getOptionsForProcess: (int) pid
{
	id process;

	process = [processes objectForKey: [NSNumber numberWithInt: pid]];
	return [process transmitOptionsForProcess: 0];
}

- (void) controllerData: (id) results forProcess: (int) pid
{
	id  process;
	
	process = [processes objectForKey: [NSNumber numberWithInt: pid]];
	[process setControllerResults: results];
}

@end
