/*
   Project: UL

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

   Author: Michael Johnston

   Created: 2005-07-12 15:24:33 +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 "ULFramework/ULDatabaseIndex.h"

@implementation ULDatabaseIndex

- (id) initWithDirectory: (NSString*) dir
{
	if(self = [super init])
	{
		index = [NSMutableDictionary  dictionaryWithCapacity: 1];
		[index retain];
		databaseDir = [dir retain];
		lastNumber = 0;
		objectInputReferences = [NSMutableDictionary new];
		objectOutputReferences = [NSMutableDictionary new];
	}	

	return self;
}

- (void) dealloc
{
	if(indexArray != nil)
		[indexArray release];

	[index release];
	[databaseDir release];
	[objectInputReferences release];
	[objectOutputReferences release];
	[super dealloc];
}

- (void) updateMetadataForObject: (id) object
{
	id metadata;
	NSString* ident;

	//search for the metadata dict for this object

	metadata = [index objectForKey: [object identification]];

	if(metadata == nil)
		[NSException raise: NSInvalidArgumentException
			format: [NSString stringWithFormat: 
			@"Database - Object %@ not present. Cannot update metadata", [object description]]];

	ident = [object identification];
	[index setObject: [[object allData] mutableCopy] forKey: ident];
	[[index objectForKey: ident] setObject: ident forKey: @"Identification"];
	[[index objectForKey: ident] setObject: NSStringFromClass([object class]) 
		forKey: @"Class"];
	[indexArray release];
	indexArray = [[index allValues] retain];
}

- (void) updateOutputReferencesForObject: (id) object
{
	NSString* ident;

	//check the object is actually in the index

	ident = [object identification];
	if([index objectForKey: ident] == nil)
		[NSException raise: NSInternalInconsistencyException
			format: @"Object %@ is not present in the database index. Cannot add output references.",
			[object metadata]];

	[objectOutputReferences setObject: [object outputReferences] 
		forKey: ident];
}

- (BOOL) objectInIndex: (id) object
{
	id ident;

	ident = [object identification];
	if([index objectForKey: ident] != nil)
		return YES;
	else
		return NO;
}

- (void) addObject: (id) object
{
	NSMutableDictionary* metadata;
	NSKeyedArchiver* archiver;
	id ident;
	NSMutableData* data = [NSMutableData new];

	//extract metadata to index	

	NSDebugLLog(@"ULDatabaseIndex", @"Extracting object metadata to index");
	metadata = [[object allData] mutableCopy];
	NSDebugLLog(@"ULDatabaseIndex", @"Metdata is %@", metadata);

	//save to the database

	ident = [object identification];
	[index setObject: metadata forKey: ident];
	//add the objects identification to its metadata so other objects can access it
	[[index objectForKey: ident] setObject: ident forKey: @"Identification"];
	NSDebugLLog(@"ULDatabaseIndex", @"Object Ident is %@", ident);
	
	//add the objects class to the metadata

	[[index objectForKey: ident] setObject: NSStringFromClass([object class]) 
		forKey: @"Class"];

	[indexArray release];
	indexArray = [[index allValues] retain];
	NSDebugLLog(@"ULDatabaseIndex", @"Saving %@ to database in %@", object, databaseDir);

	//extract references 

	[objectInputReferences setObject: [object inputReferences] forKey: ident];
	//There are no output references when an object is first added
	//since it must be saved before it can be used to generate data.

	archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData: data];
	[archiver setOutputFormat: NSPropertyListXMLFormat_v1_0];
	[archiver encodeObject: object forKey: @"root"];
	[archiver finishEncoding];

	[data writeToFile: [databaseDir stringByAppendingPathComponent: ident]
		atomically: NO];

	[archiver release];
	[data release];
}

- (void) removeObjectWithId: (id) ident
{
	NSString* filePath;
	NSFileManager* fileManager;
	id object, temp;

	object = [index objectForKey: ident];

	if(object != nil)
	{
		filePath = [databaseDir stringByAppendingPathComponent:
				[object valueForKey: @"Identification"]];

		NSDebugLLog(@"ULDatabaseIndex", @"Removing object (%@) and file %@", 
				object, 
				filePath);

		fileManager = [NSFileManager defaultManager];	
		if([fileManager fileExistsAtPath: filePath])
		{
			if([fileManager isDeletableFileAtPath: filePath])
			{
				[fileManager removeFileAtPath: filePath
					handler: nil];

				//remove object from index

				[index removeObjectForKey: ident]; 
				NSDebugMLLog(@"ULDatabaseIndex", @"Index is %@",
					index);
				[indexArray release];
				indexArray = [[index allValues] retain];

				//remove related input and output references

				[objectOutputReferences removeObjectForKey: ident];
				[objectInputReferences removeObjectForKey: ident];
			}
			else
			{
				NSWarnLog(@"Failed to remove object from database");
				[NSException raise: NSInternalInconsistencyException
					format: [NSString stringWithFormat: 
					@"Unable to remove file %@ from database", filePath]];
			}
		}	
		else
		{
			//remove object from index

			[index removeObjectForKey: ident];
			NSDebugMLLog(@"ULDatabaseIndex", @"Index is	%@", index);
			[indexArray release];
			indexArray = [[index allValues] retain];
			[NSException raise: NSInternalInconsistencyException
				format: [NSString stringWithFormat: 
				@"File %@ not present in database!. This is probably due to a bug - Please notify devs."]];
		}
	}
	else
	{
		[NSException raise: NSInternalInconsistencyException
			format: [NSString stringWithFormat: 
			@"Object with ident %@ not present in index!. This is probably due to a bug - Please notify devs.",
			ident]];
	}

}

- (void) removeObjectsWithIds: (NSArray*) idents
{
	NSEnumerator* idEnum;
	id ident;

	idEnum = [idents objectEnumerator];
	while(ident = [idEnum nextObject])
		[self removeObjectWithId: ident];
}

//FIXME: Temporary implementation. Its too much to have
//to unarchive all the input objects to update them. However
//its tricky to synch the output references each time.
- (void) removeOutputReferenceToObjectWithId: (NSString*) identOne
		fromObjectWithId: (NSString*) identTwo
{
	NSString* temp;
	NSKeyedArchiver* archiver;
	NSMutableData* data = [NSMutableData new];
	id obj2;

	//An id can only appear once in an objects input and output references.
	
	temp = [databaseDir stringByAppendingPathComponent: identTwo];
	obj2 = [NSKeyedUnarchiver unarchiveObjectWithFile: temp];
	[obj2 removeOutputReferenceToObjectWithID: identOne];

	[objectOutputReferences setObject: [obj2 outputReferences] 
		forKey: identTwo];

	archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData: data];
	[archiver setOutputFormat: NSPropertyListXMLFormat_v1_0];
	[archiver encodeObject: obj2 forKey: @"root"];
	[archiver finishEncoding];

	[data writeToFile: temp atomically: NO];

	[archiver release];
	[data release];
}

- (id) unarchiveObjectWithId: (NSString*) ident
{
	NSString* temp;
	NSDictionary* indexMetadata;
	id object;

	temp = [databaseDir stringByAppendingPathComponent: ident];
	object = [NSKeyedUnarchiver unarchiveObjectWithFile: temp];

	//sync object metadata with current index metadata

	indexMetadata = [index objectForKey: ident];
	[object updateMetadata: indexMetadata];

	return object;
}

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

- (NSDictionary*) metadataForObjectWithID: (NSString*) ident
{
	return [index objectForKey: ident];
}

- (NSArray*) outputReferencesForObjectWithID: (NSString*) ident
{
	return [objectOutputReferences objectForKey: ident];
}

- (NSArray*) inputReferencesForObjectWithID: (NSString*) ident
{
	return [objectInputReferences objectForKey: ident];
}

- (void) reindexAll
{
	NSWarnLog(@"Not implemented %@", NSStringFromSelector(_cmd));
}

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

NSCoding

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

/**
Updates v1.3 objects
*/

- (void) _updateVersion
{
	NSString* dbName;
	id object;
	NSEnumerator* indexEnum;

	//assuming here that the index is always part of the local filesystem db
	//which is valid at the moment
	if((dbName = NSUserName()) == nil)
			dbName = @"unknown";
		
	dbName = [NSString stringWithFormat: @"%@@localhost", dbName];

	//create new ivars	

	objectInputReferences = [NSMutableDictionary new];
	objectOutputReferences = [NSMutableDictionary new];

	//update entries without the key "database"
	indexEnum = [index objectEnumerator];
	while(object = [indexEnum nextObject])
		if([object objectForKey: @"Database"] == nil)
			[object setObject: dbName forKey: @"Database"];
}

- (id) initWithCoder: (NSCoder*) decoder
{
	id lastSaveDate, date;

	if([decoder allowsKeyedCoding])
	{
		index = [[decoder decodeObjectForKey: @"Index"] retain];
		databaseDir = [[decoder decodeObjectForKey: @"DatabaseDir"] retain];
		lastSaveDate = [decoder decodeObjectForKey: @"SavedDate"];
		date = [NSCalendarDate calendarDate];
		if([lastSaveDate dayOfYear] == [date dayOfYear])
		{
			if([lastSaveDate yearOfCommonEra] == [date yearOfCommonEra])
				lastNumber = [decoder decodeIntForKey: @"LastIndex"];
		}		
		else
			lastNumber = 0;	

		//handle update to new version of ULDatabaseIndex - (version 1.2 - 1.3+)

		if([decoder decodeObjectForKey: @"objectInputReferences"] != nil)
		{
			objectInputReferences = [decoder decodeObjectForKey: @"objectInputReferences"];
			objectOutputReferences = [decoder decodeObjectForKey: @"objectOutputReferences"];
			[objectInputReferences retain];
			[objectOutputReferences retain];
		}
		else
			[self _updateVersion];
	}
	else
		[NSException raise: NSInvalidArgumentException
			format: @"Database does not support non-keyed coding"];
	
	indexArray = [[index allValues] retain];
	NSDebugLLog(@"ULDatabaseIndex", @"Last Saved %@. Todays date %@. Last Index %d", lastSaveDate, date, lastNumber);

	return self;
}

- (void) encodeWithCoder: (NSCoder*) encoder
{
	if([encoder allowsKeyedCoding])
	{
		[encoder encodeInt: lastNumber forKey: @"LastIndex"];
		[encoder encodeObject: [NSCalendarDate calendarDate] forKey: @"SavedDate"];
		[encoder encodeObject: index forKey: @"Index"];
		[encoder encodeObject: databaseDir forKey: @"DatabaseDir"];
		[encoder encodeObject: objectInputReferences 
			forKey: @"objectInputReferences"];
		[encoder encodeObject: objectOutputReferences 
			forKey: @"objectOutputReferences"];
	}
	else
		[NSException raise: NSInvalidArgumentException
			format: @"Database does not support non-keyed coding"];
}

@end
