// Copyright 1997-1999 Omni Development, Inc.  All rights reserved.
//
// This software may only be used and reproduced according to the
// terms in the file OmniSourceLicense.html, which should be
// distributed with this project and can also be found at
// http://www.omnigroup.com/DeveloperResources/OmniSourceLicense.html.

#import "OFUserDefaults.h"

#import <Foundation/Foundation.h>
#import <OmniBase/OmniBase.h>

#import "OFObject-Queue.h"

RCS_ID("$Header: /Network/Developer/Source/CVS/OmniGroup/OmniFoundation/OFUserDefaults.m,v 1.19 1999/03/30 21:25:33 kc Exp $")

#if !defined(NeXT_PDO) && !defined(sun) && !defined(WIN32)
#define USE_NETINFO
#endif

#ifdef USE_NETINFO
#import <netinfo/ni.h>
#endif

@interface OFUserDefaults (Private)
- _init;
- (id)_propertyListFromValue:(id)value;
- (void)updateDefaultForKey:(NSString *)defaultName;
// Late initialization
- (void)lockedLazyInitialization;
#ifdef USE_NETINFO
- (void)readNetInfo;
#endif
- (void)readNetworkDefaults;
@end

@implementation OFUserDefaults

static NSUserDefaults *personalNSUserDefaults;
static OFUserDefaults *sharedUserDefaults;
static NSRecursiveLock *lock;
#ifdef USE_NETINFO
static BOOL OFUserDefaultsDebugNetInfo = NO;
#endif

+ (void)initialize;
{
    static BOOL initialized = NO;

    [super initialize];
    if (initialized)
        return;
    initialized = YES;

    lock = [[NSRecursiveLock alloc] init];
    sharedUserDefaults = nil;
    personalNSUserDefaults = nil;
}

+ (OFUserDefaults *)sharedUserDefaults;
{
    if (sharedUserDefaults)
        return sharedUserDefaults;

    [lock lock];
    if (!sharedUserDefaults) {
        sharedUserDefaults = [[self alloc] _init];
        [sharedUserDefaults setOwnerName:[[NSProcessInfo processInfo] processName]];
    }
    [lock unlock];
    return sharedUserDefaults;
}

// OFBundleRegistryTarget informal protocol

+ (void)registerItemName:(NSString *)itemName bundle:(NSBundle *)bundle description:(NSDictionary *)description;
{
    if ([itemName isEqualToString:@"defaultsDictionary"]) {
        [[self sharedUserDefaults] registerDefaults:description];
    }
}

// NSObject subclass

- (id)retain;
{
    return self;
}

- (id)autorelease;
{
    return self;
}

- (void)release;
{
}

- (void)dealloc;
{
}


// API

- (void)setOwnerName:(NSString *)newOwnerName;
{
    OBPRECONDITION(newOwnerName && !ownerName);
    ownerName = [newOwnerName copy];
}

- (NSString *)ownerName;
{
    return ownerName;
}

//

- (void)readDefaultsDatabase;
{
    unsigned int defaultsCount;
    NSEnumerator *defaultEnumerator;
    NSString *defaultName;
    NSUserDefaults *appDefaults;
    NSUserDefaults *globalDefaults;

    OBPRECONDITION(ownerName != nil);

    defaultsCount = [registrationDictionary count];
    OBASSERT(defaultsCount != 0);

    if (!defaultsDictionary) {
        OBPRECONDITION(globalDefaultsDictionary == nil);

        // This can get called multiple times if some defaults are looked up before all are registered
        defaultsDictionary = [[NSMutableDictionary alloc] initWithCapacity:defaultsCount];
        globalDefaultsDictionary = [[NSMutableDictionary alloc] initWithCapacity:defaultsCount];
    }

    // It might be cool to subclass NSUserDefaults, add locking, and create volatile domains for the two netinfo dictionaries

    appDefaults = [[NSUserDefaults alloc] init];
    [appDefaults setSearchList:[NSArray arrayWithObject:ownerName]];
    if (![appDefaults synchronize])
        NSLog(@"Error reading user defaults for domain '%@'", ownerName);

    globalDefaults = [[NSUserDefaults alloc] init];
    [globalDefaults setSearchList:[NSArray arrayWithObject:NSGlobalDomain]];
    if (![globalDefaults synchronize])
        NSLog(@"Error reading user defaults for domain '%@'", NSGlobalDomain);

    defaultEnumerator = [unresolvedKeys objectEnumerator];
    while ((defaultName = [defaultEnumerator nextObject])) {
	id defaultValue;

        if ((defaultValue = [appDefaults objectForKey:defaultName]))
            [defaultsDictionary setObject:defaultValue forKey:defaultName];

        if ((defaultValue = [globalDefaults objectForKey:defaultName]))
            [globalDefaultsDictionary setObject:defaultValue forKey:defaultName];
    }

    [appDefaults release];
    [globalDefaults release];

    [unresolvedKeys removeAllObjects];
}

//

- (NSDictionary *)defaultsDictionary;
{
    return defaultsDictionary;
}

- (NSDictionary *)registrationDictionary;
{
    return registrationDictionary;
}

- (NSDictionary *)overrideNetworkDictionary;
{
    return overrideNetworkDictionary;
}

- (NSDictionary *)advisoryNetworkDictionary;
{
    return advisoryNetworkDictionary;
}

- (unsigned int)changeCount;
{
    return changeCount;
}

- (BOOL)defaultRegisteredForKey:(NSString *)defaultName;
{
    BOOL defaultRegistered;

    OBPRECONDITION(defaultName != nil);

    [lock lock];
    [self lockedLazyInitialization];
    defaultRegistered = [registrationDictionary objectForKey:defaultName] != nil;
    [lock unlock];
    return defaultRegistered;
}

- (id)objectForKey:(NSString *)defaultName;
{
    id value;

    OBPRECONDITION(defaultName != nil);

    [lock lock];
    [self lockedLazyInitialization];

    if ((value = [overrideNetworkDictionary objectForKey:defaultName]))
	goto unlockAndReturnValue;

    if ((value = [defaultsDictionary objectForKey:defaultName]))
	goto unlockAndReturnValue;

    if ((value = [advisoryNetworkDictionary objectForKey:defaultName]))
	goto unlockAndReturnValue;

    if ((value = [globalDefaultsDictionary objectForKey:defaultName]))
	goto unlockAndReturnValue;

    value = [registrationDictionary objectForKey:defaultName];
	
unlockAndReturnValue:
    [lock unlock];

    if (value == nil) {
        [NSException raise:OFUserDefaultsNotRegisteredException format:@"OFUserDefaults:  No default registered for key '%@'", defaultName];
    }

    return value;
}

//

- (NSArray *)arrayForKey:(NSString *)defaultName;
{
    id value;

    value = [self _propertyListFromValue:[self objectForKey:defaultName]];
    if ([value isKindOfClass:[NSString class]])
	return [NSArray arrayWithObject:value];
    if (![value isKindOfClass:[NSArray class]])
	return nil;
    return value;
}

- (BOOL)boolForKey:(NSString *)defaultName;
{
    id value;

    value = [self objectForKey:defaultName];
    if ([value isKindOfClass:[NSString class]])
	if ([[value lowercaseString] isEqualToString:@"yes"])
	    return YES;
    if ([value respondsToSelector:@selector(intValue)]) {
	if ([value intValue])
	    return YES;
	else
	    return NO;
    }
    return NO;
}

- (NSData *)dataForKey:(NSString *)defaultName;
{
    return [[self objectForKey:defaultName] dataUsingEncoding:NSUnicodeStringEncoding];
}

- (NSDictionary *)dictionaryForKey:(NSString *)defaultName;
{
    id value;

    value = [self _propertyListFromValue:[self objectForKey:defaultName]];
    if (![value isKindOfClass:[NSDictionary class]])
	return nil;
    return value;
}

- (float)floatForKey:(NSString *)defaultName;
{
    id value;

    value = [self stringForKey:defaultName];
    if (!value)
	return 0.0;
    return [value floatValue];
}

- (int)integerForKey:(NSString *)defaultName;
{
    id value;

    value = [self stringForKey:defaultName];
    if (!value)
	return 0;
    return [value intValue];
}

- (NSArray *)stringArrayForKey:(NSString *)defaultName;
{
    id value;
    NSEnumerator *enumerator;
    id nextObject;

    value = [self _propertyListFromValue:[self objectForKey:defaultName]];
    if (![value isKindOfClass:[NSArray class]])
	return nil;
    enumerator = [value objectEnumerator];
    while ((nextObject = [enumerator nextObject]))
	if (![nextObject isKindOfClass:[NSString class]])
	    return nil;
    return value;
}

- (NSString *)stringForKey:(NSString *)defaultName
{
    id value;

    value = [self objectForKey:defaultName];
    if (![value isKindOfClass:[NSString class]])
	return nil;
    return value;
}

//

- (void)removeObjectForKey:(NSString *)defaultName;
{
    [lock lock];
    [self lockedLazyInitialization];
        
    changeCount++;
    [defaultsDictionary removeObjectForKey:defaultName];
    [lock unlock];
    [self queueSelectorOnce:@selector(updateDefaultForKey:) withObject:defaultName];
}

//

- (void)setObject:(id <NSObject>)value forKey:(NSString *)defaultName;
{
    NSString *newDefaultValueString;
    NSString *defaultValueString;
    NSString *globalDefaultValueString;

    newDefaultValueString = [(NSObject *)value description];
    [lock lock];
    [self lockedLazyInitialization];
    
    changeCount++; // could be better...
    // If this is overridden, you can't set it
    if ([overrideNetworkDictionary objectForKey:defaultName])
	goto removeDefault;
    
    if ((defaultValueString = [(NSObject *)[advisoryNetworkDictionary
                               objectForKey:defaultName] description])) {
	// If it's already the same in netinfo, don't write it
	if ([newDefaultValueString isEqualToString:defaultValueString] )
	    goto removeDefault;
	else // must write or netinfo will override
	    goto writeDefault;
    }
	
    globalDefaultValueString = [(NSObject *)[globalDefaultsDictionary
			objectForKey:defaultName] description];
			
    // If it's the same in the defaults dictionary, don't bother writing
    if ([newDefaultValueString isEqualToString:[[registrationDictionary objectForKey:defaultName] description]]) {
	// unless it's different than in the global defaults dictionary
	if (globalDefaultValueString && (![newDefaultValueString
		isEqualToString:globalDefaultValueString]))
	    goto writeDefault;
	goto removeDefault;
    }
    
    // if it's the same in the global defaults, don't bother writing it
    if (globalDefaultValueString && [newDefaultValueString isEqualToString:globalDefaultValueString])
	goto removeDefault;

writeDefault:   
    if (!newDefaultValueString)
	newDefaultValueString = [[NSString alloc] init];
    [defaultsDictionary setObject:newDefaultValueString forKey:defaultName];
    [lock unlock];
    [self queueSelectorOnce:@selector(updateDefaultForKey:) withObject:defaultName];
    return;

removeDefault:
    [lock unlock];
    [self removeObjectForKey:defaultName];
}

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

- (void)setFloat:(float)value forKey:(NSString *)defaultName;
{
    [self setObject:[NSNumber numberWithFloat:value] forKey:defaultName];
}

- (void)setInteger:(int)value forKey:(NSString *)defaultName;
{
    [self setObject:[NSNumber numberWithInt:value] forKey:defaultName];
}


//

- (void)registerDefaults:(NSDictionary *)dictionary;
{
    if (!dictionary)
	return;

    [lock lock];
    [unresolvedKeys addObjectsFromArray: [dictionary allKeys]];
    [registrationDictionary addEntriesFromDictionary:dictionary];
    [lock unlock];
}

@end


@implementation OFUserDefaults (Private)

- _init;
{
    if (![super init])
	return nil;

    ownerName = nil;
    unresolvedKeys = [[NSMutableArray alloc] init];
    registrationDictionary = [[NSMutableDictionary alloc] initWithCapacity:16];
    changeCount = 1;

    return self;
}

- (id)_propertyListFromValue:(id)value;
{
    if ([value isKindOfClass:[NSString class]])
	return [value propertyList];
    return value;
}

- (void)updateDefaultForKey:(NSString *)defaultName;
{
    NSException *raisedException = nil;

    OBPRECONDITION(ownerName);
    OBPRECONDITION(defaultName);

    [lock lock];

    NS_DURING {
        id defaultValue;

        [self lockedLazyInitialization];

        if (!personalNSUserDefaults) {
            personalNSUserDefaults = [[NSUserDefaults alloc] init];
            [personalNSUserDefaults setSearchList:[NSArray arrayWithObject:ownerName]];
        }

        if (![personalNSUserDefaults synchronize])
            NSLog(@"Error reading user defaults for domain '%@'", ownerName);

        defaultValue = [defaultsDictionary objectForKey:defaultName];
        if (defaultValue)
            [personalNSUserDefaults setObject:defaultValue forKey:defaultName];
        else
            [personalNSUserDefaults removeObjectForKey:defaultName];

        if (![personalNSUserDefaults synchronize])
            NSLog(@"Error updating user default for key '%@' in domain '%@'", defaultName, ownerName);

    } NS_HANDLER {
        raisedException = localException;
    } NS_ENDHANDLER;

    [lock unlock];

    if (raisedException)
        [raisedException raise];
}

// Late initialization

- (void)lockedLazyInitialization;
{
    if (![unresolvedKeys count])
        return;
    
    [self readDefaultsDatabase];
    [self readNetworkDefaults];
}

#ifdef USE_NETINFO
// This could read NIS+ on Solaris, if we care that much someday

- (void)readNetInfo;
{
    NSString *preferencesDirectoryName;
    void *handle;
    
    OBPRECONDITION(ownerName != nil);

    if (overrideNetworkDictionary)
	return; // Already read

    overrideNetworkDictionary = [[NSMutableDictionary alloc] init];
    advisoryNetworkDictionary = [[NSMutableDictionary alloc] init];
    preferencesDirectoryName = [NSString stringWithFormat:@"/application_preferences/%s", [ownerName cString]];

    if (ni_open(NULL, ".", &handle) != NI_OK)
	return;

    while (1) {
	ni_id defaultsDirectory;
	ni_proplist propertyList;
	unsigned int propertyIndex;
	void *oldHandle;
	ni_status status;

	if (ni_root(handle, &defaultsDirectory) != NI_OK)
	    goto loopToHigherDomain;

	if (ni_pathsearch(handle, &defaultsDirectory, [preferencesDirectoryName cString]) != NI_OK)
	    goto loopToHigherDomain;

	changeCount++;

	ni_read(handle, &defaultsDirectory, &propertyList);

	for (propertyIndex = 0; propertyIndex < propertyList.ni_proplist_len; propertyIndex++) {
	    ni_property property;
	    ni_namelist namelist;
	    
	    property = propertyList.ni_proplist_val[propertyIndex];

	    if (strcmp(property.nip_name, "name") != 0) {
		namelist = property.nip_val;
                switch (namelist.ni_namelist_len) {
                    default:
                        break;
                    case 2:
                        if (strcmp("override", namelist.ni_namelist_val[1]) == 0) {
                            if (OFUserDefaultsDebugNetInfo)
                                NSLog(@"Read protected '%s' = '%s'", property.nip_name, namelist.ni_namelist_val[0]);
                            [overrideNetworkDictionary setObject:[NSString stringWithCString:namelist.ni_namelist_val[0]] forKey:[NSString stringWithCString:property.nip_name]];
                            break;
                        }
                    case 1:
                        if (OFUserDefaultsDebugNetInfo)
                            NSLog(@"Read property '%s' = '%s'", property.nip_name, namelist.ni_namelist_val[0]);
                        [advisoryNetworkDictionary setObject:[NSString stringWithCString:namelist.ni_namelist_val[0]] forKey:[NSString stringWithCString:property.nip_name]];
                        break;
                }
	    }
	}
	ni_proplist_free(&propertyList);

loopToHigherDomain:
	oldHandle = handle;
        status = ni_open(oldHandle, "..", &handle);
	ni_free(oldHandle);
	if (status != NI_OK)
	    return;
    }
}
#endif

- (void)readNetworkDefaults;
{
#ifdef USE_NETINFO
    [self readNetInfo];
#endif
}

//

- (NSMutableDictionary *)debugDictionary;
{
    NSMutableDictionary *debugDictionary;

    debugDictionary = [super debugDictionary];

    if (defaultsDictionary)
	[debugDictionary setObject:defaultsDictionary forKey:@"defaultsDictionary"];
    if (globalDefaultsDictionary)
	[debugDictionary setObject:globalDefaultsDictionary forKey:@"globalDefaultsDictionary"];
    if (registrationDictionary)
	[debugDictionary setObject:registrationDictionary forKey:@"registrationDictionary"];
    if (overrideNetworkDictionary)
	[debugDictionary setObject:overrideNetworkDictionary forKey:@"overrideNetworkDictionary"];
    if (advisoryNetworkDictionary)
	[debugDictionary setObject:advisoryNetworkDictionary forKey:@"advisoryNetworkDictionary"];
    [debugDictionary setObject:[NSString stringWithFormat:@"%d", changeCount] forKey:@"changeCount"];

    return debugDictionary;
}

@end

DEFINE_NSSTRING(OFUserDefaultsNotRegisteredException);
