/*
   Project: Adun

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

   Author: 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.
*/
#define _GNU_SOURCE
#include <fenv.h>
#include "AdunKernel/AdunCore.h"

static id appCore = nil;

@class SphericalBox;

@implementation AdCore

/*
 * Loading Controller
 */

- (BOOL)controllerClassIsValid:(Class)controllerClass
{
	if([controllerClass  conformsToProtocol:@protocol(AdController)])
        	return YES;
    
	return NO;
}

- (void) _loadController
{
	NSString* controllerDir;
	NSBundle *controllerBundle;
	NSString *controllerPath;
	NSString *temp;
	Class controllerClass; 

	controllerDir = [ioManager valueForKey: @"controllerDir"];
	controllerBundle = [NSBundle bundleWithPath: 
				[controllerDir stringByAppendingPathComponent: 
				[environment valueForKey: @"Controller"]]];

	[environment printBanner: @"Controller"];

	GSPrintf(stderr, @"Dynamicaly Loading controller from Directory: %@.\n\n",
		 [controllerBundle bundlePath]);
	NSDebugLLog(@"StandardCore", @"Searching for controller\n");

	if(controllerClass = [controllerBundle principalClass])
	{ 
		NSDebugLLog(@"StandardCore", @"Found controller = %@).\n", 
			[controllerClass description]);
		NSDebugLLog(@"StandardCore", 
			@"Testing if controller conforms to AdController protocol.\n");

		if([self controllerClassIsValid: controllerClass])
		{
			NSDebugLLog(@"StandardCore", @"Controller class validated. Instantiating object.\n"); 
			controller = [[controllerClass alloc] initWithEnvironment: environment observe: YES];
		}
		else
		{
			NSWarnLog(@"Specified controller does not implement Adcontroller protocol!");
			[NSException raise: NSInvalidArgumentException
				format: @"Specified controller does not implement Adcontroller protocol."];
		}
	}
	else
	{
		NSWarnLog(@"Error: No controller Found");	
		[NSException raise: NSInvalidArgumentException
			format: @"Error: No controller Found"];
	}

	GSPrintf(stderr, @"\nLoading Complete.\n");
	[environment printSeperator];
}


/*
 * Creation/Destruction
 */

+ (id) appCore
{
	if(appCore == nil)
		return [[AdCore alloc] init];
	else
		return appCore;
}

- (id) init
{
	if(appCore != nil)
		return appCore;
	else if(self = [super init])
	{	
		corePool = [[NSAutoreleasePool alloc] init];
		commandErrors = [NSMutableDictionary new];
		//\note This may not be necessary - Leaving as a test
		commandResults = [NSMutableDictionary new];
		validCommands = [NSArray arrayWithObjects: 
					@"flushEnergies", 
					@"status", 
					@"reload",
					@"endSimulation", 
					@"controllerResults",
					 nil];
		[validCommands retain];

		//If the core has to exit for any reason this
		//notification will be posted

		[[NSNotificationCenter defaultCenter] addObserver: self
			selector: @selector(simulationFinished:)
			name: @"AdSimulationFinishedNotification"
			object: nil];

		GSPrintf(stderr, @"\n******************** environment *******************\n\n");
		GSPrintf(stderr, @"Initialising the environment.\n\n");
		environment = [[AdEnvironment alloc] init];
		GSPrintf(stderr, @"\nInitialisation Complete.\n\n");
		[environment printSeperator];
		memoryManager = [[AdMemoryManager alloc] initWithEnvironment: environment];
		runLoopIsRunning = NO;
		endSimulation = NO;
		ioManager = [[AdIOManager alloc] initWithEnvironment: environment];
		[ioManager setCore: self];
		energyDict = [NSMutableDictionary new];
		appCore = self;
	}	
	return self;
}

- (BOOL) connectToServer: (NSError**) error
{
	return [ioManager connectToServer: error];
}

- (void) startRunLoop
{
	/*
	 * We stay in the run loop until the simulation finishes.
	 * When this happens we disconnect from the server and the
	 * runloop should end.
	 * Unfortunately I cannot get the runloop to exit gracefully. 
	 * Even though all the connections are invalidated limitDateForMode: still
	 * returns [NSDate distantFuture] for NSConnectionReplyMode. This implies
	 * there is still an input source in the runloop however its impossible	
	 * to find what it is. 
	 * Instead we have to use a flag which is set to true when the core 
	 * receives a simulationFinished notification - less elegant but what
	 * can you do?
	**/

	if(!runLoopIsRunning)
	{
		runLoopIsRunning = YES;
		//send server message indicating core is ready to recieve requests
		//FIXME: consider adding this as a timed request to the runLoop
		[ioManager acceptRequests];
		while(!endSimulation)
			[[NSRunLoop currentRunLoop] 
				runMode: NSConnectionReplyMode 
				beforeDate: nil];

		runLoopIsRunning = NO;
	}
}

/**
This method is called when the core recieves a AdSimulationDidFinishNotification. 
We check if the notification contains an NSError object and retain it. This will
be posted to AdunServer (or logged) when clean up is called. We also set endSimulation
to YES so we will break out of the runloop if it is running.
*/

- (void) simulationFinished: (NSNotification*) aNotification
{
	if((terminationError = [[aNotification userInfo] 
		objectForKey: @"AdTerminationErrorKey"]) != nil)
	{
		NSWarnLog(@"Error %@", [terminationError userInfo]);
		[terminationError retain];
	}

	endSimulation = YES;
}

- (void) dealloc
{	
	[corePool release];
	
	//Make sure the thread gets a chance to finish. If it doesnt it
	//wont be deallocated.
	//This is a problem since the thread retains the object 
	//it was detached from (i.e. the controller) and hence 
	//that object wont be released when the core exits if the thread
	//hasnt exited.
	//If the controller retain count is not one the thread has not
	//exited so we wait until it is one so we can be sure it will
	//be released right now.

	if(controller != nil)
		while([controller retainCount] != 1)
			sleep(1);

	[containerDataSources release]; //temporary
	[controller release];
	[date release];
	[energyDict release];
	[environment release];
	[simulator release];
	[forceField release];
	[system release];
	[dataSources release];
	[validCommands release];
	[terminationError release];
	[ioManager release];
	[memoryManager release];
	[super dealloc];
}

/*
 * Base Commands
 */

- (id) setOutputDirectories: (NSDictionary*) dict
{
	NSDebugLLog(@"AdunCore", 
		@"Setting output directories %@", dict);

	[ioManager setSimulationOutputDirectory:
		[dict objectForKey: @"simulationOutputDirectory"]];
	if([dict objectForKey: @"controllerOutputDirectory"] != nil)	
		[ioManager setControllerOutputDirectory: 
			[dict objectForKey: @"controllerOutputDirectory"]];
	
	return nil;
}

- (id) loadProcessData: (NSDictionary*) dict
{
	//FIXME: add some error checking here
	[ioManager loadProcessData: dict];
	//\note Maybe these should be in a seperate method?
	[environment loadOptions];

	return nil;
}

- (id) loadController: (NSDictionary*) dict
{	
	NSString* controllerName;
	
	//find and load the contoller that will perform the calculation

	controllerName = [environment valueForKey: @"Controller"];
	if([controllerName isEqual: @"Standard"])
		controller = [[AdController alloc] 
				initWithEnvironment: environment];
	else
		[self _loadController];

	return nil;
}	

- (id) createSimulator: (NSDictionary*) dict 
{
	[environment printBanner: @"ForceField"];
	GSPrintf(stderr, @"Initialising the Force Field.\n\n");
	forceField = [[AdForceFieldManager alloc] initWithEnvironment: environment];
	GSPrintf(stderr, @"\nInitialisation Complete.\n\n");

	[environment printBanner: @"Simulator"];
	GSPrintf(stderr, @"Initialising the Simulator.\n\n");
	simulator = [[AdSimulator objectForEnvironment: environment] retain];
	[simulator setForceField: forceField];
	GSPrintf(stderr, @"\nInitialisation Complete.\n\n");

	[[NSNotificationCenter defaultCenter] addObserver: self
		selector: @selector(simulationFinished:)
		name: @"AdSimulationDidFinishNotification"
		object: nil];

	/*
	 * Set the timer that sits in the simulatino loop 
	 * to checkpoint at the required intervals
	 */

	scheduler = [simulator timer];
	[scheduler sendMessage: @selector(checkpoint)
		toObject: self
		interval: [[environment valueForKey: @"ConfigWriteInterval"] intValue]
		name: @"Checkpoint"];
	[scheduler sendMessage: @selector(checkpointState)
		toObject: self
		interval: [[environment valueForKey: @"EnergyWriteInterval"] intValue]
		name: @"CheckpointState"];

	//only register energy dumping if the specified interval is
	//less than the number of steps
	if([[environment valueForKey: @"EnergyDumpInterval"] intValue] < 
		[[environment valueForKey: @"NumberConfigurations"] intValue])
	{
		[scheduler sendMessage: @selector(energyDump)
			toObject: self
			interval: [[environment valueForKey: @"EnergyDumpInterval"] intValue]
			name: @"EnergyDump"];
	}

	trajectoryFile = [ioManager getStreamForName: @"TrajectoryFile"];
	energyFile = [ioManager getStreamForName: @"EnergyFile"];
	energyCount = 0;	

	return nil;
}

- (id) createSystem: (NSDictionary*) dict
{
	id relOne, relTwo, relArray, box, dataSource;
	NSMutableArray *array;
	NSEnumerator* dataSourceEnum;
	pool = [[NSAutoreleasePool alloc] init];

	dataSources = [ioManager systemDataSources];
	[dataSources retain];

	/**
	 * FIXME: This is a temporary solution. Later
	 * AdSystem will create the containing box if
	 * requested and the AdRelationship objects
	 * will be created in UL
	 */
	
	array = [NSMutableArray array];
	//temp ivar for holding containter dataSources
	containerDataSources = [NSMutableArray new];
	dataSourceEnum = [dataSources objectEnumerator];
	while(dataSource = [dataSourceEnum nextObject])
	{
		system = [[AdSystem alloc] initWithEnvironment: environment];
		[system autorelease];

		/**Create container if necessary*/

		if([[dataSource systemName] isEqual: @"Solvent"])
		{
			box = [[SphericalBox alloc] initWithEnvironment: environment];
			[box setDataSource: dataSource];
			[box reloadData];
			[containerDataSources addObject: box];
			dataSource = box;
		}	

		[system setDataSource: dataSource];
		[system reloadData];
		[array addObject: system];
		
		//FIXME: We have to move the center of mass of each system to its
		//defined location

		[system moveCentreOfMassToOrigin];
	}

	//FIXME: Hack since we since UL still doesnt send information on relationships
	//Plus currently if there are two systems they must be solute & solvent
	if([dataSources count] == 2)
	{
		relOne = [[AdRelationship alloc] initWithObject: [[array objectAtIndex: 0] systemName]
				relationship: @"Interacts"
				object: [[array objectAtIndex: 1] systemName]];
		[relOne autorelease];
		relTwo = [[AdRelationship alloc ] initWithObject: [[array objectAtIndex: 1] systemName]
				relationship: @"Contains"
				object: [[array objectAtIndex: 0] systemName]];
		[relTwo autorelease];
		relArray = [NSArray arrayWithObjects: relOne, relTwo, nil];
	}
	else
		relArray = nil;

	[environment printBanner: @"System"];
	GSPrintf(stderr, @"Initialising the system.\n\n");
	//temporary initialisation method
	system = [[AdSystemNode alloc] 
			initWithSystems: array 
			relationships: relArray
			environment: environment];
	GSPrintf(stderr, @"\nInitialisation Complete.\n\n");
	[environment printSeperator];
	
	[forceField setSystem: system];
	[simulator setSystem: system];
	
	//checkpoint of system

	[self checkpointSystem: @"system.ad"];
	[self initialCheckpoint];
	
	[pool release];
	return nil;
}

- (id) main: (NSDictionary*) dict
{
	GSPrintf(stderr, @"Calling controller %@\n", NSStringFromClass([controller class]));

	NS_DURING
	{
		[controller coreWillStartSimulation: self];
		//move timing to Simulator class
		date = [[NSDate date] retain]; 
		terminationError = nil;
		[controller runThreadedSimulation];

		//start the runLoop now if it hasnt been started already
		[self startRunLoop];
	}
	NS_HANDLER
	{
		NSWarnLog(@"Exception - %@\nReason - %@\nInformation %@", 
			[localException name], [localException reason], [localException userInfo]);
		[localException raise];
	}
	NS_ENDHANDLER
	
	return nil;
}

- (void) cleanUp
{
	id results;

	GSPrintf(stderr, @"Cleaning Up\n");

	if((results = [controller simulationResults]) != nil)
		[ioManager sendControllerResults: results];
	
	[controller cleanUp];
	
	GSPrintf(stderr, @"Closing connections\n");
	[ioManager closeConnection: terminationError];
	[[NSConnection defaultConnection] invalidate];

	//use perform selector since the command is in a category
	//whose header is in a separate file
	
	if(energyFile != NULL)
		[self performSelector: @selector(flushEnergies:) withObject:nil];

	GSPrintf(stderr, @"Saving system state\n");
	[self checkpointSystem: @"restart.ad"];
		
	//FIXME: we have to this while AdTimer uses strong refs
	//First remove all AdCore messages so core will dealloc
	//Them call clearTimer on simulator so it can remove
	//itself from the timer hence along it to dealloc and
	//finally release it.

	[scheduler removeMessageWithName: @"CheckpointState"];
	[scheduler removeMessageWithName: @"Checkpoint"];
	[scheduler removeMessageWithName: @"EnergyDump"];
	[simulator clearTimer];
}

- (NSMutableDictionary*) optionsForCommand: (NSString*) name
{
	SEL methodSelector;
	NSString* methodName;

	methodName = [NSString stringWithFormat: @"%@Options", name];
	methodSelector = NSSelectorFromString(methodName);
	if(![self respondsToSelector: methodSelector])
		return nil;
	else
		return [self performSelector: methodSelector];
}

- (BOOL) validateCommand: (NSString*) name
{
	SEL command;

	NSDebugLLog(@"Execute", @"Validating command %@", name);

	command = NSSelectorFromString([NSString stringWithFormat: @"%@:", name]);
	return [self respondsToSelector: command];
}

- (NSError*) errorForCommand: (NSString*) name
{
	return [commandErrors objectForKey: name];
}

- (NSArray*) validCommands
{
	return validCommands;
}

- (void) setErrorForCommand: (NSString*) name description: (NSString*) description
{
	id error;

	error = [NSError errorWithDomain: AdCoreCommandErrorDomain
			code: 1
			userInfo: [NSDictionary dictionaryWithObject: description
					forKey:
					NSLocalizedDescriptionKey]];

	[commandErrors setValue: error forKey: name];
}

/***********

CheckPointing

************/

- (void) checkpointSystem: (NSString*) filename
{
	int count;
	NSMutableData* data;
	FILE* systemFile;

	systemFile = [ioManager openFile:
			[[ioManager simulationOutputDirectory] stringByAppendingPathComponent: filename]
 			usingName: @"SystemFile"
		 	flag: @"w+"];

	data = [NSMutableData new];
	archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData: data];
	[archiver setOutputFormat: NSPropertyListXMLFormat_v1_0];
	[archiver encodeObject: [environment options] forKey: @"environmentOptions"];
	[archiver encodeObject: system forKey: @"system"];
	[archiver encodeObject: dataSources forKey: @"dataSources"];
	[archiver finishEncoding];
	count = fwrite([data bytes], 1, [data length], systemFile);
	[data release];
	[archiver release];
	[ioManager closeStreamWithName: @"SystemFile"];
}

- (void) initialCheckpoint
{
	int count;
	NSMutableData* data;
	id subsystem, source;
	NSMutableArray* subsystemNames, *sources;
	NSEnumerator* subsystemEnum;

	data = [NSMutableData new];
	archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData: data];
	[archiver setOutputFormat: NSPropertyListXMLFormat_v1_0];
	subsystemNames = [NSMutableArray arrayWithCapacity: 1];
	sources = [NSMutableArray arrayWithCapacity: 1];
	subsystemEnum = [[system allSystems] objectEnumerator];
	while(subsystem = [subsystemEnum nextObject])
	{
		[archiver encodeObject:[subsystem valueForKey: @"dynamics"]  
			forKey: [NSString stringWithFormat: @"%@.dynamics", [subsystem systemName]]];
		source = [[subsystem valueForKey: @"dynamics"] dataSource];
		[archiver encodeObject: source 
			forKey: [NSString stringWithFormat: @"%@.Source", [subsystem systemName]]];
		[subsystemNames addObject: [subsystem systemName]];
	}
	[archiver encodeObject: dataSources forKey: @"DataSources"];
	[archiver encodeObject: subsystemNames forKey: @"SubsystemNames"];
	[archiver finishEncoding];
	count = fwrite([data bytes], 1, [data length], trajectoryFile);
	[data release];
	[archiver release];
}

- (void) checkpoint
{
	int count;
	NSMutableData* data;
	id subsystem;
	NSEnumerator* subsystemEnum;

	data = [NSMutableData new];
	archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData: data];
	[archiver setOutputFormat: NSPropertyListXMLFormat_v1_0];
	subsystemEnum = [[system allSystems] objectEnumerator];
	while(subsystem = [subsystemEnum nextObject])
		[[subsystem valueForKey: @"dynamics"] captureStateWithArchiver: archiver 
			key: [NSString stringWithFormat: @"%@.dynamics", [subsystem systemName]]];

	[archiver finishEncoding];
	count = fwrite([data bytes], 1, [data length], trajectoryFile);
	[data release];
	[archiver release];
}

- (void) checkpointState
{
	id subsystem;
	NSEnumerator* subsystemEnum;

	energyCount++;
	subsystemEnum = [[system allSystems] objectEnumerator];
	while(subsystem = [subsystemEnum nextObject])
		[energyDict setObject: [[subsystem valueForKey: @"state"] allEnergies]
			forKey: [NSString stringWithFormat: @"%@.State.%d", [subsystem systemName],
					energyCount]];
}

//this is a wrapper around a call to flushEnergies
//We need it because AdTimer can only send messages
//that have no arguements.
//This will be removed when AdTimers functionality is
//expanded

- (void) energyDump
{
	[self performSelector: @selector(flushEnergies:) withObject: nil];
}

- (BOOL) simulationIsRunning
{
	return runLoopIsRunning;
}

- (id) controllerResults: (id) options
{
	return [controller simulationResults];
}

- (id) simulator
{
	return simulator;
}

- (id) forceField
{
	return forceField;
}

- (id) system
{
	return system;
}

- (id) controller
{
	return controller;
}

- (id) scheduler
{
	return [simulator timer];
}

@end
