/* 
   Project: UL

   Copyright (C) 2006 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.
*/

#include <AppKit/AppKit.h>
#include "ULDatabaseBrowser.h"

@implementation ULDatabaseBrowserPath

- (id) init
{
	if(self = [super init])
	{
		path = [NSMutableArray new];
	}	
		
	return self;
}

- (void) dealloc
{
	[path release];
	[super dealloc];
}

- (void) setItem: (id)  object forLevel: (int) level
{
	//NSLog(@"Setting item %@ at level %d", object, level);

	//truncate to given level
	if([self truncateToLevel: level])
	{
		//remove the object currently at the given level
		[path removeLastObject];
	}	
	//add the new object in its place
	[self addItem: object];
}

- (BOOL) truncateToLevel: (int) value
{
	int i;

	if(value > [path count])
	{
		[NSException raise: NSInvalidArgumentException
			format: @"Attempt to set item at level %d - current level %d", 
			value, 
			[self currentLevel]];
	}		
	else if(value < [path count])
	{
		//remove all objects at index higher then level
		
		for(i=[self currentLevel]; i>value; i--)
			[path removeObjectAtIndex: i];
		
		return YES;

	}

	return NO;
}

- (void) addItem: (id) object
{
	[path addObject: object];
}

- (id) itemForLevel: (int) level
{
	return [path objectAtIndex: level];
}

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

- (int) currentLevel
{
	return [path count] - 1;
}

- (void) clearPath
{
	[path removeAllObjects];
}

@end

@implementation ULDatabaseBrowser

- (void) deselectAllRows: (id) sender
{
	int row;
	id selectedRows;
	
	selectedRows = [browserView selectedRowIndexes];
	if([selectedRows count] == 0)
		return;

	row = [selectedRows firstIndex];
	while(row != NSNotFound)
	{
		[browserView deselectRow: row];
		row = [selectedRows indexGreaterThanIndex: row];
	}
	
	[browserView setNeedsDisplay: YES];
} 

- (id) init
{
	if(self = [super init])
	{
		isActive = NO;
		selectedSystems = [NSMutableArray new];
		selectedOptions = [NSMutableArray new];
		selectedDataSets = [NSMutableArray new];
		selectedSimulations = [NSMutableArray new];
		path = [ULDatabaseBrowserPath new];
		allowedActions = [[NSArray alloc] initWithObjects:
					@"copy:",
					@"cut:", 
					@"paste:",
					@"remove:",
					@"import:",
					@"export:",
					nil];
		cut = NO;
		editedObject = nil;
		progressPanel = nil;
	}

	return self;
}

- (void) awakeFromNib
{
	[browserView setDataSource: self];
	[browserView setDelegate: self];
	[referenceList selectItemWithTitle: @"No References"];
	[[NSNotificationCenter defaultCenter]
		addObserver: self
		selector: @selector(updateView:)
		name: @"ULDatabaseInterfaceDidAddObjectNotification"
		object: nil];
	[[NSNotificationCenter defaultCenter]
		addObserver: self
		selector: @selector(updateView:)
		name: @"ULDatabaseInterfaceDidUpdateMetadataNotification"
		object: nil];
	databaseInterface = [[ULDatabaseInterface databaseInterface] retain];
	[browserView reloadData];

}

- (void) dealloc
{
	[allowedActions release];
	[path release];
	[databaseInterface release];
	[selectedDataSets release];
	[selectedSystems release];
	[selectedOptions release];
	[selectedSimulations release];
	[super dealloc];
}

- (BOOL) isActive;
{
	return isActive;
}

- (void) setActive: (BOOL) value
{
	if(!value)
		[self deselectAllRows: self];
	isActive = value;
}

- (void) updateView: (NSNotification*) aNotification
{
	if(progressPanel != nil)
	{	
		[progressPanel setProgressInfo: @"Complete"];
		sleep(1.5);
		[progressPanel endPanel];
		[progressPanel release];
		progressPanel = nil;
	}	
	[browserView reloadData];
}


- (BOOL) validateMenuItem: (NSMenuItem*) menuItem
{
	int selectedRow, itemLevel;
	
	//deselectAllRows is active when there are more than 0 rows selected
	if([menuItem action] == @selector(deselectAllRows:))
	{
		if([[browserView selectedRowIndexes] count] > 0)
			return YES;	
		else
			return NO;
	}		

	//selectedRow is not updated when the browser
	//collapses its view (until another selection is made). 
	//This means that if the index of the last row in the collapsed view 
	//is less than the index of the last selected row
	//calling [browserView itemAtRow: [browserView selectedRow]]
	//will crash the program (or raise an exception).
	//\note File this as a bug

	selectedRow = [browserView selectedRow];
	if(selectedRow >= [browserView numberOfRows])
		return NO;
	
	if(selectedRow == -1)
		return NO;

	if([[browserView selectedRowIndexes] count] > 1)
		return NO;

	itemLevel = [browserView levelForRow: selectedRow];
	if([menuItem action] == @selector(paste:))
	{
		if(editedObject != nil && itemLevel == 1)
			return YES;
		else
			return NO;
	}		
	
	//import is active when a database schema is selected
	if([NSStringFromSelector([menuItem action]) isEqual: @"import:"])
	{
		if(itemLevel == 1)
			return YES;
		else
			return NO;
	}		

	if([allowedActions containsObject: NSStringFromSelector([menuItem action])])
	{
		if(itemLevel == 3)
			return YES;
		else	
			return NO;
	}		

	return NO;
}

- (void) remove: (id) sender
{
	int row, lastRow;
	NSIndexSet* selectedRows;
	id item;

	//FIXME: Currently remove: is activated if only one object
	//is selected

	selectedRows = [browserView selectedRowIndexes];
	row = [selectedRows firstIndex];
	
	NS_DURING
	{
		while(row != NSNotFound)
		{
			lastRow = row;
			item = [browserView itemAtRow: row];
		
			//remove references to this object from all the objects that
			//generated it.
			//FIXME: Not sure if we should remove input references.
			//FIXME: This only works for the file system database 

			[databaseInterface removeOutputReferencesToObjectWithID: [item objectForKey: @"Identification"]
				ofClass: [item objectForKey:@"Class"]
				inSchema: [item objectForKey: @"Schema"]
				ofClient: [item objectForKey: @"DatabaseClient"]];
				
			[databaseInterface removeObjectOfClass: [item objectForKey:@"Class"]
				withID: [item objectForKey: @"Identification"]
				fromSchema: [item objectForKey: @"Schema"]
				ofClient: [item objectForKey: @"DatabaseClient"]];
		
			row = [selectedRows indexGreaterThanIndex: row];
		}
	}
	NS_HANDLER
	{
		NSWarnLog(@"Caught exception while attempting to remove object from client %@.",
				[item objectForKey: @"Database"]);
		NSWarnLog(@"Item class %@. Item id %@", 
				[item objectForKey: @"Class"],
				[item objectForKey: @"Identification"]);
		NSWarnLog(@"Exception name %@. Reason %@. Information %@", 
				[localException name],
				[localException reason],
				[localException userInfo]);
		NSRunAlertPanel(@"Error - Unable to remove selected object",
			[localException reason],
			@"Dismiss", 
			nil,
			nil);
	}
	NS_ENDHANDLER

	[browserView reloadData];
	//update selection
	if(lastRow <= [browserView numberOfRows])
		row = [browserView numberOfRows] - 1;

	[browserView selectRowIndexes: [NSIndexSet indexSetWithIndex: lastRow]
		byExtendingSelection: NO];
}

- (void) cut: (id) sender
{
	int selectedRow;
	id item;

	selectedRow = [browserView selectedRow];
	item = [browserView itemAtRow: selectedRow];

	[editedObject release];
	editedObject = [item retain];
	NSLog(@"Edited obejct %@", editedObject);
	cut = YES;
}

- (void) copy: (id) sender
{
	int selectedRow;
	id item;

	selectedRow = [browserView selectedRow];
	item = [browserView itemAtRow: selectedRow];
	[editedObject release];
	editedObject = [item retain];
	cut = NO;
}

- (void) _cutAndPasteItem: (id) item 
	from: (NSString*) source 
	to: (NSString*) destination
{

	int retVal;
	id realObject; //the actual unarchived object

	progressPanel = [ULProgressPanel progressPanelWithTitle:
				[NSString stringWithFormat: @"Moving %@ to %@", 
					[editedObject objectForKey: @"Name"],
					[item objectForKey: @"ULDatabaseClientName"]]
				message: @"Moving"	
				progressInfo: @"Retrieving"];
	[progressPanel retain];			
	[progressPanel setIndeterminate: YES];	
	
	retVal = NSRunAlertPanel(@"Move",
			[NSString stringWithFormat: @"Move %@ from\n %@\n to\n %@?", 
				[editedObject objectForKey: @"Name"],
				source,
				destination],
			@"OK", 
			@"Dismiss",
			nil);
			
	if(retVal == NSOKButton)
	{
		[progressPanel runProgressPanel: NO];		

		//get the real object
		realObject = [databaseInterface unarchiveObjectWithID: 
				[editedObject objectForKey: @"Identification"]
			ofClass: [editedObject objectForKey: @"Class"]
			fromSchema: [editedObject objectForKey: @"Schema"]
			ofClient: [editedObject objectForKey: @"DatabaseClient"]];


		//FIXME: We should check remove and add permissions
		//Aswell as database availability before commiting 
		//to anything here.

		//delete it from its old database
		[databaseInterface removeObjectOfClass: [editedObject valueForKey:@"Class"]
		withID: [editedObject valueForKey: @"Identification"]
		fromSchema: [editedObject objectForKey: @"Schema"]
		ofClient: [editedObject objectForKey: @"DatabaseClient"]];

		[progressPanel setProgressInfo: @"Adding"];
		//add it to its new database
		[databaseInterface addObject: realObject
		toSchema: [item objectForKey: @"ULSchemaName"]
		ofClient: [item objectForKey: @"ULDatabaseClientName"]];

		sleep(1.0);
	}
	else
	{
		[progressPanel release];
		progressPanel = nil;
		editedObject = nil;
		cut = NO;
	}
}

- (void) _copyAndPasteItem: (id) item 
	from: (NSString*) source 
	to: (NSString*) destination
{
	int retVal;
	id realObject; //the actual unarchived object
	
	progressPanel = [ULProgressPanel progressPanelWithTitle: 
				[NSString stringWithFormat: @"Copying %@ to %@", 
					[editedObject objectForKey: @"Name"],
					[item objectForKey: @"ULDatabaseClientName"]]	
				message: @"Copying"
				progressInfo: @"Estimated Time - Unknown"];
	[progressPanel retain];			
	[progressPanel setIndeterminate: YES];	
	[NSApp updateWindows];

	retVal = NSRunAlertPanel(@"Copy",
			[NSString stringWithFormat: @"Copy %@ from\n %@\n to\n %@?", 
				[editedObject objectForKey: @"Name"],
				source,
				destination],
			@"OK", 
			@"Dismiss",
			nil);
	if(retVal == NSOKButton)
	{
		[progressPanel runProgressPanel: NO];		
		realObject = [databaseInterface unarchiveObjectWithID: 
					[editedObject objectForKey: @"Identification"]
				ofClass: [editedObject objectForKey: @"Class"]
				fromSchema: [editedObject objectForKey: @"Schema"]
				ofClient: [editedObject objectForKey: @"DatabaseClient"]];
		
		[databaseInterface addObject: realObject	
			toSchema: [item objectForKey: @"ULSchemaName"]
			ofClient: [item objectForKey: @"ULDatabaseClientName"]];
		sleep(1.0);
	}
	else
	{
		[progressPanel release];
		progressPanel = nil;
		editedObject = nil;
		cut = NO;
	}
}

- (void) paste: (id) sender
{
	int selectedRow;
	NSString* source, *destination;
	id item;

	selectedRow = [browserView selectedRow];
	item = [browserView itemAtRow: selectedRow];

	source = [NSString stringWithFormat: @"%@/%@",
			[editedObject objectForKey: @"Database"],
			[editedObject objectForKey: @"Schema"]]; 
	destination = [NSString stringWithFormat: @"%@/%@",
			[item objectForKey: @"ULDatabaseClientName"],
			[item objectForKey: @"ULSchemaName"]]; 

	if(cut)
	{	
		[self _cutAndPasteItem: item 
			from: source 
			to: destination];
	}
	else
	{	
		[self _copyAndPasteItem: item 
			from: source 
			to: destination];
	}	
}

- (void) export: (id) sender
{
	BOOL retVal;
	int selectedRow, result;
	id realObject; //the actual unarchived object
	id item, savePanel, filename;
	NSError *error;
	NSMutableData *data;
	NSKeyedArchiver* archiver;
	NSString* storagePath, *destinationPath;

	selectedRow = [browserView selectedRow];
	item = [browserView itemAtRow: selectedRow];
	
	NS_DURING
	{
		savePanel = [NSSavePanel savePanel];	
		[savePanel setTitle: 
			[NSString stringWithFormat: @"Export Data - %@",
				[item objectForKey: @"Name"]]];
		[savePanel setDirectory: 
			[NSHomeDirectory() stringByAppendingPathComponent: @"adun"]];
		result = [savePanel runModal];
		filename = [savePanel filename];

		if(result == NSOKButton)
		{
			realObject = [databaseInterface unarchiveObjectWithID: [item objectForKey: @"Identification"]
					ofClass: [item objectForKey: @"Class"]
					fromSchema: [item objectForKey: @"Schema"]
					ofClient: [item objectForKey: @"DatabaseClient"]];
			
			//If its a simulation we also have to export the simulation
			//data directory. This involves copying the directory to the
			//chosen location. 	
			if([realObject isKindOfClass: [ULSimulation class]])
			{
				storagePath = [[realObject dataStorage] storagePath];
				destinationPath =  [filename stringByAppendingString: @"_Data"];
				retVal = [[NSFileManager defaultManager]
						copyPath: storagePath
						toPath: destinationPath
						handler: nil];

				if(!retVal)
				{
					//Abort
					NSRunAlertPanel(@"Error",
						@"Unable to extract simulation data - Aborting",
						@"Dismiss", 
						nil,
						nil);

					return;
				}	
			}
			
			data = [NSMutableData new];
			archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData: data];
			[archiver setOutputFormat: NSPropertyListXMLFormat_v1_0];
			[archiver encodeObject: realObject forKey: @"root"];
			[archiver finishEncoding];
			[archiver release];
			
			retVal = [[ULIOManager appIOManager]
					writeObject: data 
					toFile: filename 
					error: &error];

			[data release];
			if(!retVal)
			{
				NSRunAlertPanel(@"Error",
					[[error userInfo] objectForKey:NSLocalizedDescriptionKey],
					@"Dismiss", 
					nil,
					nil);
			}
		}
	}
	NS_HANDLER
	{
		NSRunAlertPanel(@"Alert", [localException reason], @"Dismiss", nil, nil);
	}
	NS_ENDHANDLER
}

- (BOOL) _canImportObject: (id) object error: (NSError**) error toSchema: (id) info
{
 	/*
	  If the object already exists in the file system database
	  we dont add it. If we do it will cause problems in the case of simulation
	  data since the data storage being imported will clash with the data 
	  storage already present. 
	  FIXME: There is no method for checking if an object is present in
	  a remote database so we have to add everything. Luckily the 
	  problem doesnt exist in the SQL backend case however it would be better
	  to check first.
	 */ 

	//FIXME: Related to above - We dont know here what database we are
	//adding to. At the moment it is always the fileSystemDatabase. In the future
	//we should use a method objectInSchema:ofDatabase etc using the schema information
	//passed instead.
	if([databaseInterface objectInFileSystemDatabase: object])
	{
		*error = [NSError errorWithDomain: @"ULErrorDomain"
				code: 10
				userInfo: [NSDictionary dictionaryWithObjectsAndKeys: 
						@"Data already present in database", 
						NSLocalizedDescriptionKey,
						nil]];
		return NO;
	}

	//Check the file storage is valid. 
	if([object isKindOfClass: [ULSimulation class]])
		if(![[object dataStorage] isAccessible])
		{
			*error = [[object dataStorage] accessError];
			return NO;
		}
	
	return YES;
}

- (void) import: (id) sender
{
	BOOL retVal;
	int selectedRow, result;
	id object; 
	id item, openPanel, filename;
	NSError *error;
	NSMutableData *data = [NSMutableData new];
	NSString* storagePath;
	ULFileSystemSimulationStorage *dataStorage;

	selectedRow = [browserView selectedRow];
	item = [browserView itemAtRow: selectedRow];
	
	NS_DURING
	{
		progressPanel = [ULProgressPanel progressPanelWithTitle: @"Import"
					message: @"Importing"
					progressInfo: @"Estimated time - Unknown"];
		[progressPanel retain];			
		[progressPanel setIndeterminate: YES];	
		[NSApp updateWindows];

		openPanel = [NSOpenPanel openPanel];	
		[openPanel setTitle: @"Import Data"];
		[openPanel setDirectory: 
			[NSHomeDirectory() stringByAppendingPathComponent: @"adun"]];
		result = [openPanel runModal];
		filename = [openPanel filename];

		if(result == NSOKButton)
		{
			[progressPanel setMessage: 
				[NSString stringWithFormat: @"Importing file - %@", [filename lastPathComponent]]];
			[progressPanel runProgressPanel: NO];		
			object = [NSKeyedUnarchiver unarchiveObjectWithFile: filename];

			//If its a simulation we have to import its data aswell.
			//This is a directory with the same name as filename but 
			//with _Data appended (see export: above).
			if([object isKindOfClass: [ULSimulation class]])
			{
				storagePath = [filename stringByAppendingString: @"_Data"];
				//Create a ULFileSystemSimulationStorage object for the data
				dataStorage = [[ULFileSystemSimulationStorage alloc]
						initForReadingSimulationDataAtPath: storagePath];
				[dataStorage autorelease];
				[object setDataStorage: dataStorage];
			}

			if(![self _canImportObject: object error: &error toSchema: item])
			{
				[progressPanel endPanel];
				[progressPanel release];
				progressPanel = nil;
				NSRunAlertPanel(@"Error - Import Failed", 
					[[error userInfo] objectForKey: NSLocalizedDescriptionKey], 
					@"Dismiss", 
					nil, 
					nil);
			}
			else
			{
				//We add non-ULSimulation objects in the standard way
				[databaseInterface addObject: object	
					toSchema: [item objectForKey: @"ULSchemaName"]
					ofClient: [item objectForKey: @"ULDatabaseClientName"]];
			}		
		}
		else
		{
			//If the user chose cancel we have to
			//destroy the progress panel we set up above
			[progressPanel release];
			progressPanel = nil;
		}
	}
	NS_HANDLER
	{
		//There was an exception during the import.
		//End the progress panel and display an alert panel.
		[progressPanel endPanel];
		[progressPanel release];
		progressPanel = nil;
		NSRunAlertPanel(@"Alert", [localException reason], @"Dismiss", nil, nil);
	}
	NS_ENDHANDLER
}

/**
ULPasteboard delegate methods
**/

- (NSArray*) availableTypes
{
	NSMutableArray* array = [NSMutableArray array];

	if([selectedSystems count] != 0)
		[array addObject: @"ULSystem"];
	
	if([selectedOptions count] != 0)
		[array addObject: @"ULOptions"];
	
	if([selectedDataSets count] != 0)
		[array addObject: @"AdDataSet"];
	
	if([selectedSimulations count] != 0)
		[array addObject: @"ULSimulation"];

	return array;	
}

- (id) objectForType: (NSString*) type
{
	id item; 
	id object;

	NS_DURING
	{
		if([type isEqual: @"ULSystem"])
			item = [selectedSystems objectAtIndex: 0];
		else if([type isEqual: @"ULOptions"])
			item = [selectedOptions objectAtIndex: 0];
		else if([type isEqual: @"AdDataSet"])
			item = [selectedDataSets objectAtIndex: 0];
		else if([type isEqual: @"ULSimulation"])
			item = [selectedSimulations objectAtIndex: 0];
		
		object = [databaseInterface 
				unarchiveObjectWithID: [item objectForKey: @"Identification"]
				ofClass: [item objectForKey: @"Class"]
				fromSchema: [item objectForKey: @"Schema"]
				ofClient: [item objectForKey: @"DatabaseClient"]];

		return object;		
	}
	NS_HANDLER
	{
		//Objects should call availableTypes (or countOfObjectsForType:)
		//to determine if the neccessary type is available. However
		//in case they dont we must catch the NSRangeException that will be
		//raised if there is no object of the requested type available
		if([[localException name] isEqual: NSRangeException])
		{
			NSWarnMLog(@"Recieved request for unavailable object type %@"
					, type);
		}
		else
		{
			//Deal with an exception from the database interface
			NSWarnLog(@"Caught exception while attempting to retrieve object from client %@.",
					[item objectForKey: @"Database"]);
			NSWarnLog(@"Item class %@. Item id %@", 
					[item objectForKey: @"Identification"],
					[item objectForKey: @"Class"]);
			NSWarnLog(@"Exception name %@. Reason %@. Information %@", 
					[localException name],
					[localException reason],
					[localException userInfo]);
			NSRunAlertPanel(@"Error - Unable to retrieve selected object",
				[localException reason],
				@"Dismiss", 
				nil,
				nil);
		}
	}
	NS_ENDHANDLER

	return nil;
}

- (NSArray*) objectsForType: (NSString*) type
{
	NSMutableArray* array = [NSMutableArray array];
	NSEnumerator* selectionEnum;
	id item, object;

	if([type isEqual: @"ULSystem"])
		selectionEnum = [selectedSystems objectEnumerator];
	else if([type isEqual: @"ULOptions"])
		selectionEnum = [selectedOptions objectEnumerator];
	else if([type isEqual: @"AdDataSet"])
		selectionEnum = [selectedDataSets objectEnumerator];
	else if([type isEqual: @"ULSimulation"])
		selectionEnum = [selectedSimulations objectEnumerator];
	
	NS_DURING
	{
		while(item = [selectionEnum nextObject])
		{
			object = [databaseInterface unarchiveObjectWithID: 
					[item objectForKey: @"Identification"]
					ofClass: [item objectForKey: @"Class"]
					fromSchema: [item objectForKey: @"Schema"]
					ofClient: [item objectForKey: @"DatabaseClient"]];
			[array addObject: object];

		}			
	}
	NS_HANDLER
	{
		NSWarnLog(@"Caught exception while attempting to retrieve object from client %@.",
				[item objectForKey: @"Database"]);
		NSWarnLog(@"Item class %@. Item id %@", 
				[item objectForKey: @"Identification"],
				[item objectForKey: @"Class"]);
		NSWarnLog(@"Exception name %@. Reason %@. Information %@", 
				[localException name],
				[localException reason],
				[localException userInfo]);
		NSRunAlertPanel(@"Error - Unable to retrieve selected object",
			[localException reason],
			@"Dismiss", 
			nil,
			nil);
	}
	NS_ENDHANDLER

	return array;
}

- (int) countOfObjectsForType: (NSString*) type
{

	if([type isEqual: @"ULSystem"])
		return [selectedSystems count];

	if([type isEqual: @"ULOptions"])
		return [selectedOptions count];

	if([type isEqual: @"AdDataSet"])
		return [selectedDataSets count];
	
	if([type isEqual: @"ULSimulation"])
		return [selectedSimulations count];
}

- (void) pasteboardChangedOwner: (id) pasteboard
{
	[self deselectAllRows: self];
	isActive = NO;
}

@end



