/* 
   NSUserDefaults.m

   Copyright (C) 1995, 1996 Ovidiu Predescu and Mircea Oancea.
   All rights reserved.

   Author: Mircea Oancea <mircea@jupiter.elcom.pub.ro>

   This file is part of libFoundation.

   Permission to use, copy, modify, and distribute this software and its
   documentation for any purpose and without fee is hereby granted, provided
   that the above copyright notice appear in all copies and that both that
   copyright notice and this permission notice appear in supporting
   documentation.

   We disclaim all warranties with regard to this software, including all
   implied warranties of merchantability and fitness, in no event shall
   we be liable for any special, indirect or consequential damages or any
   damages whatsoever resulting from loss of use, data or profits, whether in
   an action of contract, negligence or other tortious action, arising out of
   or in connection with the use or performance of this software.
*/

#include <Foundation/common.h>
#include <Foundation/NSString.h>
#include <Foundation/NSData.h>
#include <Foundation/NSValue.h>
#include <Foundation/NSArray.h>
#include <Foundation/NSDictionary.h>
#include <Foundation/NSDate.h>
#include <Foundation/NSNotification.h>
#include <Foundation/NSProcessInfo.h>
#include <Foundation/NSDistributedLock.h>
#include <Foundation/NSBundle.h>
#include <Foundation/NSUserDefaults.h>
#include <Foundation/NSFileManager.h>
#include <Foundation/exceptions/GeneralExceptions.h>

/*
 * User defaults strings
 */

/* Defaults Domains */
NSString*  NSArgumentDomain = @"NSArgumentDomain";
NSString*  NSGlobalDomain = @"NSGlobalDomain";
NSString*  NSRegistrationDomain = @"NSRegistrationDomain";

/* Notification name */
NSString*  NSUserDefaultsDidChangeNotification = @"NSUserDefaultsDidChangeNotification";

/* Defaults names */
NSString*  NSWeekDayNameArray = @"NSWeekDayNameArray";
NSString*  NSShortWeekDayNameArray = @"NSShortWeekDayNameArray";
NSString*  NSMonthNameArray = @"NSMonthNameArray";
NSString*  NSShortMonthNameArray = @"NSShortMonthNameArray";
NSString*  NSTimeFormatString = @"NSTimeFormatString";
NSString*  NSDateFormatString = @"NSDateFormatString";
NSString*  NSTimeDateFormatString = @"NSTimeDateFormatString";
NSString*  NSShortTimeDateFormatString = @"NSShortTimeDateFormatString";
NSString*  NSCurrencySymbol = @"NSCurrencySymbol";
NSString*  NSDecimalSeparator = @"NSDecimalSeparator";
NSString*  NSThousandsSeparator = @"NSThousandsSeparator";
NSString*  NSInternationalCurrencyString = @"NSInternationalCurrencyString";
NSString*  NSCurrencyString = @"NSCurrencyString";
NSString*  NSDecimalDigits = @"NSDecimalDigits";
NSString*  NSAMPMDesignation = @"NSAMPMDesignation";

/* 
 * User defaults shared class 
 */


@interface NSSharedUserDefaults : NSUserDefaults
@end

@implementation NSSharedUserDefaults
- retain {return self;}
- autorelease {return self;}
- (void)release{}
- (unsigned int)retainCount {return 1;}
@end

/* 
 * User defaults class 
 */

@implementation NSUserDefaults : NSObject

/* Getting and Setting a Default */

- (NSArray*)arrayForKey:(NSString*)defaultName
{
    id obj = [self objectForKey:defaultName];
    if ([obj isKindOfClass:[NSArray class]])
	return obj;
    return nil;
}

- (NSDictionary*)dictionaryForKey:(NSString*)defaultName
{
    id obj = [self objectForKey:defaultName];
    if ([obj isKindOfClass:[NSDictionary class]])
	return obj;
    return nil;
}

- (NSData*)dataForKey:(NSString*)defaultName;
{
    id obj = [self objectForKey:defaultName];
    if ([obj isKindOfClass:[NSData class]])
	return obj;
    return nil;
}

- (NSArray*)stringArrayForKey:(NSString*)defaultName
{
    id obj = [self objectForKey:defaultName];
    if ([obj isKindOfClass:[NSArray class]]) {
	int n;
	Class strClass = [NSString class];
	
	for (n = [obj count]-1; n >= 0; n--)
	    if (![[obj objectAtIndex:n] isKindOfClass:strClass])
		return nil;

	return obj;
    }
    return nil;
}

- (NSString*)stringForKey:(NSString*)defaultName
{
    id obj = [self objectForKey:defaultName];
    if ([obj isKindOfClass:[NSString class]])
	return obj;
    return nil;
}

- (BOOL)boolForKey:(NSString*)defaultName
{
    id obj = [self objectForKey:defaultName];
    if (obj && [obj isKindOfClass:[NSString class]]) {
	if ([obj compare:@"YES" options:NSCaseInsensitiveSearch] == 
		NSOrderedSame)
	    return YES;
	else
	    if ([obj respondsToSelector:@selector(intValue)])
		    return [obj intValue] ? YES : NO;
    }
    return NO;
}

- (float)floatForKey:(NSString*)defaultName
{
    id obj = [self stringForKey:defaultName];
    if (obj) 
	return [obj floatValue];
    return 0;
}

- (int)integerForKey:(NSString*)defaultName
{
    id obj = [self stringForKey:defaultName];
    if (obj) 
	return [obj intValue];
    return 0;
}

- (void)setBool:(BOOL)value forKey:(NSString*)defaultName
{
    [self setObject:(value ? @"YES" : @"NO") 
	    forKey:defaultName];
}

- (void)setFloat:(float)value forKey:(NSString*)defaultName
{
    [self setObject:[NSString stringWithFormat:@"%f", value]
	    forKey:defaultName];
}

- (void)setInteger:(int)value forKey:(NSString*)defaultName
{
    [self setObject:[NSString stringWithFormat:@"%d", value] 
	    forKey:defaultName];
}

/* Accessing app domain defaults */

- (id)objectForKey:(NSString*)defaultName
{
    int i, n = [searchList count];
    
    for (i=0; i<n; i++) {
	NSString* name = [searchList objectAtIndex:i];
	NSDictionary* domain;
	id obj;
	
	if ((domain = [volatileDomains objectForKey:name]))
	    if ((obj = [domain objectForKey:defaultName]))
		return obj;
	if ((domain = [persistentDomains objectForKey:name]))
	    if ((obj = [domain objectForKey:defaultName]))
		return obj;
    }
    return nil;
}

- (void)setObject:(id)value forKey:(NSString*)defaultName
{
    [(NSMutableDictionary*)[self persistentDomainForName:appDomain]
	setObject:value forKey:defaultName];
    [self persistentDomainHasChanged:appDomain];
}

- (void)removeObjectForKey:(NSString*)defaultName
{
    [(NSMutableDictionary*)[self persistentDomainForName:appDomain] 
	removeObjectForKey:defaultName];
    [self persistentDomainHasChanged:appDomain];
}

/* Returning the Search List */

- (NSMutableArray*)searchList
{
    return searchList;
}

/* Making Advanced Use of Defaults */

- (NSDictionary*)dictionaryRepresentation
{
    NSMutableDictionary* dict = [[NSMutableDictionary new] autorelease];
    
    int i, n = [searchList count];
    
    for (i = n-1; i >= 0; i--) {
	NSString* name = [searchList objectAtIndex:i];
	NSDictionary* domain;
	
	if ((domain = [volatileDomains objectForKey:name]))
	    [dict addEntriesFromDictionary:domain];
	if ((domain = [persistentDomains objectForKey:name]))
	    [dict addEntriesFromDictionary:domain];
    }

    return dict;
}

- (void)registerDefaults:(NSDictionary*)dictionary
{
    NSMutableDictionary* regDomain = [self 
	volatileDomainForName:NSRegistrationDomain];
    
    if ([searchList indexOfObjectIdenticalTo:regDomain] == NSNotFound)
	[searchList addObject:NSRegistrationDomain];
    
    [regDomain addEntriesFromDictionary:dictionary];
}

/* Maintaining Volatile Domains */

- (void)removeVolatileDomainForName:(NSString*)domainName
{
    [searchList removeObject:domainName];
    [volatileDomains removeObjectForKey:domainName];
}

- (void)setVolatileDomain:(NSDictionary*)domain  
  forName:(NSString*)domainName
{
    if ([volatileDomains objectForKey:domainName])
	THROW([[InvalidArgumentException alloc]
		    initWithFormat:@"volatile domain %@ already exists",
				    domainName]);

    [volatileDomains setObject:
	    [[NSMutableDictionary alloc] initWithDictionary:domain] 
	forKey:domainName];
}

- (NSDictionary*)volatileDomainForName:(NSString*)domainName
{
    return [volatileDomains objectForKey:domainName];
}

- (NSArray*)volatileDomainNames
{
    return [volatileDomains allKeys];
}

/* Maintaining Persistent Domains */

- (NSDictionary*)loadPersistentDomainNamed:(NSString*)domainName
{
    int n;
    NSDistributedLock* lock = nil;
    NSDictionary* dict = nil;
    NSString* domainPath;
    NSString* path;
    
    /* Declare here `format' to work around a bug in compiler */
    NSString* format = @"caught exception %@ with reason %@ "
	      @"while loading user defaults domain %@";

    domainPath = [[directoryForSaving 
	stringByAppendingPathComponent:domainName] 
	stringByAppendingPathExtension:@"plist"];
    /* Take the address of `path' to force the compiler to not allocate it
       in a register. */
    *(&path) = [domainPath stringByResolvingSymlinksInPath];

    // if (!domainName || [domainName isEqual:@""] || !path) {
    //	NSLog(@"could not load user defaults domain %@ from path %@",
    //	    domainName, domainPath);
    // }

    if (path) {
	lock = [NSDistributedLock lockWithPath:
	    [[path stringByDeletingPathExtension]
		stringByAppendingPathExtension:@"lock"]];
	for (n = 0;  n < 3; n++)
	    if ([lock tryLock])
		break;
	    else
		sleep(1);
	
	if (n >= 3) {
	    NSLog(@"could not lock user defaults domain %@", domainName);
	    return nil;
	}
	
	TRY {
	    dict = [[NSString stringWithContentsOfFile:path] propertyList];
	} END_TRY
	OTHERWISE {
	    NSLog(format, [localException name], [localException reason]);
	    dict = nil;
	} END_CATCH
	
	[lock unlock];
    }
    
    if (path == nil || dict == nil) {
	domainPath = [[[[NSBundle libraryResourceDirectory] 
	    stringByAppendingPathComponent:@"Defaults"] 
	    stringByAppendingPathComponent:domainName] 
	    stringByAppendingPathExtension:@"plist"];
	path = [domainPath stringByResolvingSymlinksInPath];
    
	if (path) {
	    TRY {
		dict = [[NSString stringWithContentsOfFile:path] propertyList];
	    } END_TRY
	    OTHERWISE {
		NSLog(format, [localException name], [localException reason]);
		dict = nil;
	    } END_CATCH
	}
	// else
	//    NSLog(@"could not load user defaults domain %@ from path %@",
	//	domainName, domainPath);
    }
    
    if (dict == nil)
	return nil;
    
    return [[NSMutableDictionary alloc] initWithDictionary:dict];
}

- (BOOL)savePersistentDomainNamed:(NSString*)domainName
{
    int n;
    BOOL ok = YES;
    NSDistributedLock* lock = nil;
    NSDictionary* dict = [persistentDomains objectForKey:domainName];
    NSString* path = [[directoryForSaving
			    stringByAppendingPathComponent:domainName]
			    stringByAppendingPathExtension:@"plist"];

    /* Declare here `format' to work around a bug in compiler */
    NSString* format = @"caught exception %@ with reason %@ "
	      @"while saving user defaults domain %@";

    lock = [NSDistributedLock lockWithPath:
	[path stringByAppendingPathExtension:@"lock"]];
    for (n = 0;  n < 3; n++)
	if (![lock tryLock])
	    sleep(1);
	else
	    break;
    if (n >= 3) {
	NSLog(@"could not lock user defaults domain %@", domainName);
	return NO;
    }
    
    TRY {
	ok = [dict writeToFile:path atomically:YES];
    } END_TRY
    OTHERWISE {
	NSLog(format, [localException name], [localException reason]);
	ok = NO;
    } END_CATCH
    
    [lock unlock];
    return ok;
}

- (void)removePersistentDomainForName:(NSString*)domainName
{
    [persistentDomains removeObjectForKey:domainName];
}

- (NSDictionary*)persistentDomainForName:(NSString*)domainName
{
    NSDictionary* domain;
    
    domain = [persistentDomains objectForKey:domainName];
    
    if (!domain) {
	domain = [self loadPersistentDomainNamed:domainName];
	if (domain)
	    [persistentDomains setObject:domain forKey:domainName];
    }
    
    return domain;
}

- (void)setPersistentDomain:(NSDictionary*)domain
  forName:(NSString*)domainName
{
    if ([persistentDomains objectForKey:domainName])
	THROW([[InvalidArgumentException alloc]
		    initWithFormat:@"persistent domain %@ already exists",
				    domainName]);
    [persistentDomains setObject:
	    [[NSMutableDictionary alloc] initWithDictionary:domain] 
	forKey:domainName];
    [self persistentDomainHasChanged:domainName];
}

- (NSArray*)persistentDomainNames
{
    return [persistentDomains allKeys];
}

/* Creation of defaults */

static NSUserDefaults* sharedDefaults = nil;
static NSString* sharedDefaultsDir = nil;

+ (void)setStandardDefaultsDirectory:(NSString*)dir
{
    [dir retain];
    [sharedDefaultsDir release];
    sharedDefaultsDir = dir;
}

+ (NSUserDefaults*)standardUserDefaults
{
    if (!sharedDefaults) {
	if (sharedDefaultsDir)
	    sharedDefaults = (id)[[NSSharedUserDefaults alloc] 
		initWithPath:sharedDefaultsDir];
	else {
	    NSString* defdir = [[NSHomeDirectoryForUser(NSUserName())
		stringByAppendingPathComponent:@".libFoundation"]
		stringByAppendingPathComponent:@"Defaults"];
	    if ([defdir stringByResolvingSymlinksInPath] == nil)
		[[NSFileManager defaultManager]
		    createDirectoryAtPath:defdir attributes:nil];
	    sharedDefaults = (id)[[NSSharedUserDefaults alloc] 
		initWithPath:defdir];
	}
	[sharedDefaults makeStandardDomainSearchList];
    }
    return sharedDefaults;
}

+ (void)sycronizeStandardUserDefaults:sender
{
    [sharedDefaults synchronize];
}

/* Initializing the User Defaults */

- (id)init
{
    return [self initWithUser:NSUserName()];
}

- (id)initWithUser:(NSString*)aUserName
{
    return [self initWithPath:[[NSHomeDirectoryForUser(aUserName)
		    stringByAppendingPathComponent:@".libFoundation"]
		    stringByAppendingPathComponent:@"Defaults"]];
}

- (id)initWithPath:(NSString*)pathName
{
    NSDictionary* dict;
    NSArray* languages;
    int i, n;
    
    directoryForSaving = [pathName retain];
    persistentDomains = [NSMutableDictionary new];
    volatileDomains  = [NSMutableDictionary new];
    searchList = [NSMutableArray new];
    appDomain = [[NSProcessInfo processInfo] processName];
    
    [self setVolatileDomain:[[NSDictionary new] autorelease] 
	forName:NSArgumentDomain];
    [self setVolatileDomain:[[NSDictionary new] autorelease] 
	forName:NSRegistrationDomain];
    
    dict = [self persistentDomainForName:appDomain];
    if (!dict)
	[self setPersistentDomain:[[NSDictionary new] autorelease] 
	    forName:appDomain];
    dict = [self persistentDomainForName:NSGlobalDomain];
    if (!dict)
	[self setPersistentDomain:[[NSDictionary new] autorelease] 
	    forName:NSGlobalDomain];
    languages = [[self persistentDomainForName:NSGlobalDomain] 
	objectForKey:@"Languages"];
    for (i = 0, n = [languages count]; i < n; i++) {
	dict = [self persistentDomainForName:[languages objectAtIndex:i]];
	if (dict)
	    break;
    }
    
    dirty = NO;
    
    return self;
}

- (void)dealloc
{
    [directoryForSaving release];
    [appDomain release];
    [persistentDomains release];
    [volatileDomains release];
    [searchList release];
    [super dealloc];
}

- (void)makeStandardDomainSearchList
{
    int i,n;
    NSArray* languages;
    
    /* make clear list */
    [searchList removeAllObjects];
    
    /* make argument domain */
    [searchList addObject:NSArgumentDomain];
    
    /* make app domain */
    [searchList addObject:appDomain];
    
    /* make global domain */
    [searchList addObject:NSGlobalDomain];

    /* add languages domains */
    languages = [[self persistentDomainForName:NSGlobalDomain] 
	objectForKey:@"Languages"];
    if (!languages) {
	languages = [NSArray arrayWithObject:@"English"];
    }
    for (i = 0, n = [languages count]; i < n; i++) {
	NSString* lang = [languages objectAtIndex:i];
	/* check that the domain exists */
	if ([self persistentDomainForName:lang]) {
	    [searchList addObject:lang];
	}
    }
    
    /* add catch-all registration domain */
    [searchList addObject:NSRegistrationDomain];
}

- (BOOL)synchronize
{
    NSEnumerator* penum = [persistentDomains keyEnumerator];
    NSString* pname;
    NSMutableDictionary* pdomain;
    NSDictionary* ndomain;
    BOOL allOk = YES;
    
    if (!dirty)
	return YES;
    
    while ((pname = [penum nextObject])) {
	pdomain = [persistentDomains objectForKey:pname];
	ndomain = [self loadPersistentDomainNamed:pname];
	if (!ndomain || ![ndomain isEqual:pdomain]) {
	    NSEnumerator* nenum = [ndomain keyEnumerator];
	    NSString* key;
	    while ((key = [nenum nextObject])) {
		if (![pdomain objectForKey:key])
		    [pdomain setObject:[ndomain objectForKey:key] forKey:key];
	    }
	    allOk = [self savePersistentDomainNamed:pname];
	}
    }
    
    dirty = NO;
    return allOk;
}

- (void)persistentDomainHasChanged:(NSString*)domainName
{
    if (!dirty && [persistentDomains objectForKey:domainName]) {
	dirty = YES;
	[[NSNotificationCenter defaultCenter]
	    postNotificationName:NSUserDefaultsDidChangeNotification
	    object:self
	    userInfo:nil];
    }
}

@end
