//
//  KSExtensibleManagedObject.m
//
//  Created by Mike Abdullah on 25/08/2007.
//  Copyright 2007-2008 Karelia Software. All rights reserved.
//
//  THIS SOFTWARE IS PROVIDED BY KARELIA SOFTWARE AND ITS CONTRIBUTORS "AS-IS"
//  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
//  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
//  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
//  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
//  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
//  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
//  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
//  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
//  ARISING IN ANY WAY OUR OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
//  POSSIBILITY OF SUCH DAMAGE.
//
//
//	A special kind of managed object that allows you to use -valueForKey: and
//	-setValueForKey: using any key. If the object does not normally accept this
//	key, it is stored internally in a dictionary and then archived as data.


#import "KSExtensibleManagedObject.h"


@interface KSExtensibleManagedObject (Private)

- (NSMutableDictionary *)_extensibleProperties;
+ (NSSet *)modifiedKeysBetweenDictionary:(NSDictionary *)dict1 andDictionary:(NSDictionary *)dict2;
- (NSDictionary *)archivedExtensibleProperties;

@end


#pragma mark -


@implementation KSExtensibleManagedObject

#pragma mark -
#pragma mark Class Methods

+ (NSString *)extensiblePropertiesDataKey
{
	return @"extensiblePropertiesData";
}

#pragma mark -
#pragma mark Accessors

- (NSDictionary *)extensibleProperties
{
	NSDictionary *result = [NSDictionary dictionaryWithDictionary:[self _extensibleProperties]];
	return result;
}

#pragma mark -
#pragma mark Core Data

/*	Throw away our internal dictionary just like normal Core Data faulting behavior.
 */
- (void)didTurnIntoFault
{
	[myExtensibleProperties release];	myExtensibleProperties = nil;
	[super didTurnIntoFault];
}

- (NSMutableDictionary *)_extensibleProperties
{
	// Fault in the properties on-demand
	if (!myExtensibleProperties)
	{
		myExtensibleProperties = [[self archivedExtensibleProperties] mutableCopy];
		
		if (!myExtensibleProperties)
		{
			myExtensibleProperties = [[NSMutableDictionary alloc] init];
		}
	}
	
	return myExtensibleProperties;
}

#pragma mark -
#pragma mark KVC

/*	We catch all undefined keys and pull them from the extensible properties dictionary.
 */
- (id)valueForUndefinedKey:(NSString *)key
{
	id result = [[self _extensibleProperties] valueForKey:key];
	return result;
}

- (void) setValue: (id) value forExtensibleKey:(NSString *)key
{
	[self willChangeValueForKey:key];
	
	[[self _extensibleProperties] setValue:value forKey:key];
	
	// Archive the new properties. This has to be done every time so Core Data knows
	// that some kind of change was made.
	[self setValue:[self archiveExtensibleProperties:[self _extensibleProperties]]
			forKey:[[self class] extensiblePropertiesDataKey]];
	
	[self didChangeValueForKey:key];
}


/*	Undefined keys are caught and A) stored in-memory B) archived persistently
 */
- (void)setValue:(id)value forUndefinedKey:(NSString *)key
{
	// Archive the new properties. This has to be done every time so Core Data knows
	// that some kind of change was made.
	[self setValue: value
			forExtensibleKey: key];
}

/*	Whenever a change to our dictionary data is made due to an undo or redo, match the changes to
 *	our in-memory dictionary
 */
- (void)didChangeValueForKey:(NSString *)key
{
	if ([key isEqualToString:[[self class] extensiblePropertiesDataKey]])
	{
		NSUndoManager *undoManager = [[self managedObjectContext] undoManager];
		if ([undoManager isUndoing] || [undoManager isRedoing])
		{
			// Comparison of the old and new dictionaries in order to to send out approrpriate KVO notifications
			// We specifically access the ivar directly to avoid faulting it in.
			NSDictionary *replacementDictionary = [self archivedExtensibleProperties];
			NSSet *modifiedKeys =
				[KSExtensibleManagedObject modifiedKeysBetweenDictionary:myExtensibleProperties
														   andDictionary:replacementDictionary];
			
			
			// Change each of the modified keys in our in-memory dictionary
			NSEnumerator *keysEnumerator = [modifiedKeys objectEnumerator];
			NSString *aKey;
			while (aKey = [keysEnumerator nextObject])
			{
				[self willChangeValueForKey:aKey];
				[[self _extensibleProperties] setValue:[replacementDictionary valueForKey:aKey] forKey:aKey];
				[self didChangeValueForKey:aKey];
			}
		}
	}
	
	
	// Finally go ahead and do the default behavior. This is required to balance the
	// earlier -willChangeValueForKey: that must have ocurred.
	[super didChangeValueForKey:key];
}

#pragma mark -
#pragma mark Support

+ (NSSet *)modifiedKeysBetweenDictionary:(NSDictionary *)dict1 andDictionary:(NSDictionary *)dict2
{
	// It's easy if either dictionary is nil
	if (!dict1) return [NSSet setWithArray:[dict1 allKeys]];
	if (!dict2) return [NSSet setWithArray:[dict2 allKeys]];
	
	
	// Build the set containing all the keys that exist in either dictionary
	NSMutableSet *allKeys = [[NSMutableSet alloc] initWithArray:[dict1 allKeys]];
	[allKeys addObjectsFromArray:[dict2 allKeys]];
	
	
	// Then run through these building a list of keys which the two dictionaries have different values for
	NSEnumerator *enumerator = [allKeys objectEnumerator];
	NSString *aKey;
	NSMutableSet *result = [NSMutableSet set];
	
	while (aKey = [enumerator nextObject])
	{
		if (![[dict1 valueForKey:aKey] isEqual:[dict2 valueForKey:aKey]]) {
			[result addObject:aKey];
		}
	}
	
	// Tidy up
	[allKeys release];
	
	return result;
}

/*	Fetches all custom values from the persistent store rather than the in-memory representation.
 */
- (NSDictionary *)archivedExtensibleProperties
{
	NSData *data = [self valueForKey:[[self class] extensiblePropertiesDataKey]];
	NSDictionary *result = [self unarchiveExtensibleProperties:data];
	return result;
}

- (NSDictionary *)unarchiveExtensibleProperties:(NSData *)propertiesData
{
	NSMutableDictionary *result = nil;
	
	if (propertiesData)
	{
		id unarchivedDictionary = [NSKeyedUnarchiver unarchiveObjectWithData:propertiesData];
		if ([unarchivedDictionary isKindOfClass:[NSMutableDictionary class]])
		{
			result = unarchivedDictionary;
		}
	}
	
	return result;
}

- (NSData *)archiveExtensibleProperties:(NSDictionary *)properties;
{
	NSData *result = [NSKeyedArchiver archivedDataWithRootObject:properties];
	return result;
}

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

@end
