// 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 "OFBundleRegistry.h"

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

#import "NSString-OFExtensions.h"
#import "OFBundledClass.h"

#ifdef __MACH__
#import <mach-o/dyld.h>
#endif

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

/* Bundle descriptions are currently NSMutableDictionaries with the following keys:

    path      --- the path to the bundle, if known
    bundle    --- the NSBundle, if any
    invalid   --- if the bundle isn't valid, indicates why
    loaded    --- indicates whether the bundle is loaded (maybe)
    preloaded --- indicates that bundle was loaded at startup time

It may be better to make an OFBundleDescription class eventually.

NB that the "loaded" key isn't actually updated right now --- we either need to add a method to NSBundle to extract the loaded flag, or make OFBundledClass know about class descriptions and set the flag when it loads.

NB also, if this dictionary changes, the OmniBundlePreferences bundle (in OmniComponents/Other) should be updated.
*/
    

@interface OFBundleRegistry (Private)
+ (void)readConfigDictionary;
+ (NSArray *)standardPath;
+ (NSArray *)bundlesFromStandardPath;
+ (NSArray *)bundlesInDirectory:(NSString *)directoryPath;
+ (void)registerDictionary:(NSDictionary *)registrationDictionary forBundle:(NSDictionary *)bundleDescription;
+ (void)registerBundlesFromStandardPath;
+ (void)registerAdditionalRegistrations;
+ (BOOL)haveSoftwareVersionsInDictionary:(NSDictionary *)requiredVersionsDictionary;
@end

#ifdef __MACH__
@interface NSBundle (OFFixes)
- (void)_fixFrameworkVersioning;
@end
#endif

static NSDictionary *environmentDictionary;
static NSMutableSet *loadedBundleNames;
static NSMutableDictionary *softwareVersionDictionary;
static NSMutableArray *knownBundles;

@implementation OFBundleRegistry

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

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

    environmentDictionary = [[[NSProcessInfo processInfo] environment] retain];
    loadedBundleNames = [[NSMutableSet alloc] init];
    softwareVersionDictionary = [[NSMutableDictionary alloc] init];
}

+ (void)didLoad;
{
    [self readConfigDictionary];
    [self registerBundlesFromStandardPath];
    [self registerAdditionalRegistrations];
    [OFBundledClass processImmediateLoadClasses];
}

+ (NSDictionary *)softwareVersionDictionary;
{
    return softwareVersionDictionary;
}

+ (NSArray *)knownBundles
{
    return knownBundles;
}

@end

@implementation OFBundleRegistry (Private)

static DEFINE_NSSTRING(OFBundleRegistryConfig);
static DEFINE_NSSTRING(OFSoftwareVersion);
static DEFINE_NSSTRING(OFRequiredSoftwareVersions);
static DEFINE_NSSTRING(OFRegistrations);

static NSString *OFBundleRegistryConfigSearchPaths = @"SearchPaths";
static NSString *OFBundleRegistryConfigAppWrapperPath = @"AppWrapper";
static NSString *OFBundleRegistryConfigBundleExtensions = @"BundleExtensions";
static NSString *OFBundleRegistryConfigAdditionalRegistrations = @"AdditionalRegistrations";
static NSString *OFBundleRegistryConfigLogBundleRegistration = @"LogBundleRegistration";
static NSString *OFBundleRegistryConfigDisabledBundles = @"DisabledBundles";

static NSString *frameworkExtension = @"framework";

static NSDictionary *configDictionary = nil;
static BOOL OFBundleRegistryDebug = NO;

+ (void)readConfigDictionary;
{
    NSString *logBundleRegistration;

    configDictionary = [[[[NSBundle mainBundle] infoDictionary] objectForKey:OFBundleRegistryConfig] retain];
    if (!configDictionary)
        configDictionary = [[NSDictionary alloc] init];

    logBundleRegistration = [configDictionary objectForKey:OFBundleRegistryConfigLogBundleRegistration];
    if (logBundleRegistration && [[logBundleRegistration lowercaseString] hasPrefix:@"y"])
        OFBundleRegistryDebug = YES;
}

+ (NSArray *)standardPath;
{
    static NSArray *standardPath = nil;
    NSArray *configPathArray;
    NSString *mainBundlePath, *mainBundleResourcesPath;

    if (standardPath)
        return standardPath;

    // Bundles are stored in the Resources directory of the applications, but tools might have bundles in the same directory as their binary.  Use both paths.
    mainBundlePath = [[NSBundle mainBundle] bundlePath];
    mainBundleResourcesPath = [mainBundlePath stringByAppendingPathComponent:@"Resources"];

    // Search for the config path array in defaults, then in the app wrapper's configuration dictionary.  (In gdb, we set the search path on the command line where it will appear in the NSArgumentDomain, overriding the app wrapper's configuration.)
    if ((configPathArray = [[NSUserDefaults standardUserDefaults] arrayForKey:OFBundleRegistryConfigSearchPaths]) ||
        (configPathArray = [configDictionary objectForKey:OFBundleRegistryConfigSearchPaths])) {
        unsigned int pathIndex, pathCount;
        NSMutableArray *newPath;

        pathCount = [configPathArray count];

        // The capacity of the newPath array is pathCount + 1 because AppWrapper expands to two entries.
        newPath = [[NSMutableArray alloc] initWithCapacity:pathCount + 1];
        for (pathIndex = 0; pathIndex < pathCount; pathIndex++) {
            NSString *path;

            path = [configPathArray objectAtIndex:pathIndex];
            if ([path isEqualToString:OFBundleRegistryConfigAppWrapperPath]) {
                [newPath addObject:mainBundleResourcesPath];
                [newPath addObject:mainBundlePath];
            } else
                [newPath addObject:path];
        }

        standardPath = [newPath copy];
        [newPath release];
    } else {
        // standardPath = ("~/Library/Components", "/Local/Library/Components", "/Network/Library/Components", "/System/Library/Components", "/LocalLibrary/Components", "/NextLibrary/Components", "AppWrapper");
        standardPath = [[NSArray alloc] initWithObjects:[NSString pathWithComponents:
            // User's library directory
            [NSArray arrayWithObjects:NSHomeDirectory(), @"Library", @"Components", nil]],

            // Standard Rhapsody library directories
            [NSString pathWithComponents:[NSArray arrayWithObjects:@"/", @"Local", @"Library", @"Components", nil]],
            [NSString pathWithComponents:[NSArray arrayWithObjects:@"/", @"Network", @"Library", @"Components", nil]],
            [NSString pathWithComponents:[NSArray arrayWithObjects:@"/", @"System", @"Library", @"Components", nil]],

            // Standard OPENSTEP library directories
            [NSString pathWithComponents:[NSArray arrayWithObjects:@"/", @"LocalLibrary", @"Components", nil]],
            [NSString pathWithComponents:[NSArray arrayWithObjects:@"/", @"NextLibrary", @"Components", nil]],

            // App wrapper
            mainBundleResourcesPath,
            mainBundlePath,

            nil];
    }

    return standardPath;
}

/* Returns an NSArray of bundle descriptions */
+ (NSArray *)bundlesFromStandardPath;
{
    static NSMutableArray *bundlesFromStandardPath = nil;
    NSArray *standardPath;
    NSBundle *framework;
    NSEnumerator *frameworkEnumerator;
    unsigned int pathIndex, pathCount;

    if (bundlesFromStandardPath)
        return bundlesFromStandardPath;

    bundlesFromStandardPath = [[NSMutableArray alloc] init];

    // The frameworks and main bundle are already loaded, so we should register them first.
    frameworkEnumerator = [[NSBundle allFrameworks] objectEnumerator];
    while ((framework = [frameworkEnumerator nextObject])) {
#ifdef __MACH__
        [framework _fixFrameworkVersioning];
#endif
        [bundlesFromStandardPath addObject:[NSMutableDictionary dictionaryWithObjectsAndKeys:framework, @"bundle", @"YES", @"loaded", @"YES", @"preloaded", nil]];
    }
    [bundlesFromStandardPath addObject:[NSMutableDictionary dictionaryWithObjectsAndKeys:[NSBundle mainBundle], @"bundle", @"YES", @"loaded", @"YES", @"preloaded", nil]];

    // Now register the bundles from the standard path
    standardPath = [self standardPath];
    pathCount = [standardPath count];
    for (pathIndex = 0; pathIndex < pathCount; pathIndex++) {
        NSString *pathElement;

        pathElement = [[standardPath objectAtIndex:pathIndex] stringByReplacingKeysInDictionary:environmentDictionary startingDelimiter:@"$(" endingDelimiter:@")"];
        NS_DURING {
            [bundlesFromStandardPath addObjectsFromArray:[self bundlesInDirectory:pathElement]];
        } NS_HANDLER {
            NSLog(@"+[OFBundleRegistry bundlesFromStandardPath]: %@", [localException reason]);
        } NS_ENDHANDLER;
    }

    /* Keep track of future bundle loads */
    knownBundles = [[NSMutableArray alloc] initWithArray:bundlesFromStandardPath];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(recordBundleLoading:) name:NSBundleDidLoadNotification object:nil];
        
    return bundlesFromStandardPath;
}

/* Invoked whenever NSBundle loads something */
+ (void)recordBundleLoading:(NSNotification *)note
{
    int knownBundlesCount, knownBundlesIndex;
    NSBundle *theBundle = [note object];

//    NSLog(@"NSBundle %@ loaded %@", theBundle, [note userInfo]);

    knownBundlesCount = [knownBundles count];
    for(knownBundlesIndex = 0; knownBundlesIndex < knownBundlesCount; knownBundlesIndex ++) {
        NSMutableDictionary *aBundleDict;
        NSBundle *aBundle;
        aBundleDict = [knownBundles objectAtIndex:knownBundlesIndex];
        aBundle = [aBundleDict objectForKey:@"bundle"];
        if (aBundle == theBundle) {
            [aBundleDict setObject:@"YES" forKey:@"loaded"];
            return;
        }
    }

    [knownBundles addObject:[NSMutableDictionary dictionaryWithObjectsAndKeys:theBundle, @"bundle", @"YES", @"loaded", nil]];
}

/* Returns an array of bundle descriptions (currently NSMutableDictionaries) */
+ (NSArray *)bundlesInDirectory:(NSString *)directoryPath;
{
    NSString *expandedDirectoryPath;
    NSArray *bundleExtensions, *disabledBundleNamesArray;
    NSSet *disabledBundleNames;
    NSMutableArray *bundles;
    NSArray *candidates;
    unsigned int candidateIndex, candidateCount;
    NSFileManager *fileManager;

    if (!(bundleExtensions = [configDictionary objectForKey:OFBundleRegistryConfigBundleExtensions]))
        bundleExtensions = [NSArray arrayWithObjects:@"omni", frameworkExtension, nil];

    disabledBundleNamesArray = [[NSUserDefaults standardUserDefaults] arrayForKey:OFBundleRegistryConfigDisabledBundles]; 
    if (disabledBundleNamesArray)
        disabledBundleNames = [NSSet setWithArray:disabledBundleNamesArray];
    else
        disabledBundleNames = [NSSet set];
    
    fileManager = [NSFileManager defaultManager];
    bundles = [NSMutableArray arrayWithCapacity:0];
    expandedDirectoryPath = [directoryPath stringByExpandingTildeInPath];
    candidates = [[fileManager directoryContentsAtPath:expandedDirectoryPath] sortedArrayUsingSelector:@selector(compare:)];
    candidateCount = [candidates count];
    for (candidateIndex = 0; candidateIndex < candidateCount; candidateIndex++) {
        NSString *candidateName;
        NSString *bundlePath;
        NSBundle *bundle;
        NSMutableDictionary *description;

        candidateName = [candidates objectAtIndex:candidateIndex];
        if (![bundleExtensions containsObject:[candidateName pathExtension]])
            continue;

        bundlePath = [expandedDirectoryPath stringByAppendingPathComponent:candidateName];

        description = [NSMutableDictionary dictionary];
        [description setObject:bundlePath forKey:@"path"];
        [bundles addObject:description];

        if ([disabledBundleNames containsObject:candidateName] ||
            [disabledBundleNames containsObject:bundlePath] ||
            [disabledBundleNames containsObject:[candidateName stringByDeletingPathExtension]]) {
            [description setObject:@"Disabled by user preference" forKey:@"invalid"];
            continue;
        }
        
        if ([loadedBundleNames containsObject:candidateName]) {
            [description setObject:@"Duplicate bundle name" forKey:@"invalid"];
            continue;
        }

        bundle = [NSBundle bundleWithPath:bundlePath];
        if (!bundle) {
            // bundle might be nil if the candidate is not a directory or is a symbolic
            // link to a path that doesn't exist or doesn't contain a valid bundle.
            [description setObject:@"Not a valid bundle" forKey:@"invalid"];
            continue;
        }

        [description setObject:bundle forKey:@"bundle"];
        [loadedBundleNames addObject:candidateName];
    }

    return [bundles count] > 0 ? bundles : nil;
}

+ (void)registerDictionary:(NSDictionary *)registrationDictionary forBundle:(NSDictionary *)bundleDescription;
{
    NSString *bundlePath, *registrationClassName;
    NSEnumerator *registrationClassEnumerator, *registrationDictionaryEnumerator;
    NSBundle *bundle = [bundleDescription objectForKey:@"bundle"];

    /* this is just temporary ...wim */
    if (bundle) {
        bundlePath = [bundleDescription objectForKey:@"path"];
        if (bundlePath && ![bundlePath isEqual:[bundle bundlePath]])
            NSLog(@"OFBundleRegistry:warning:%@!=%@", bundlePath, [bundle bundlePath]);
    }
    
    bundlePath = bundle ? [bundle bundlePath] : @"local configuration file";

    registrationClassEnumerator = [registrationDictionary keyEnumerator];
    registrationDictionaryEnumerator = [registrationDictionary objectEnumerator];

    while ((registrationClassName = [registrationClassEnumerator nextObject])) {
        NSDictionary *registrationDictionary;
        NSEnumerator *itemEnumerator;
        NSString *itemName;
        NSEnumerator *descriptionEnumerator;
        Class registrationClass;

        if (!registrationClassName || [registrationClassName length] == 0)
            break;

        registrationDictionary = [registrationDictionaryEnumerator nextObject];
        registrationClass = NSClassFromString(registrationClassName);
        if (!registrationClass) {
            NSLog(@"OFBundleRegistry warning: registration class '%@' from bundle '%@' not found.", registrationClassName, bundlePath);
            continue;
        }
        if (![registrationClass respondsToSelector:@selector(registerItemName:bundle:description:)]) {
            NSLog(@"OFBundleRegistry warning: registration class '%@' from bundle '%@' doesn't accept registrations", registrationClassName, bundlePath);
            continue;
        }

        itemEnumerator = [registrationDictionary keyEnumerator];
        descriptionEnumerator = [registrationDictionary objectEnumerator];

        while ((itemName = [itemEnumerator nextObject])) {
            NSDictionary *descriptionDictionary;

            descriptionDictionary = [descriptionEnumerator nextObject];
            [registrationClass registerItemName:itemName bundle:bundle description:descriptionDictionary];
        }
    }
}

+ (void)registerBundlesFromStandardPath;
{
    NSEnumerator *bundleEnumerator;
    NSMutableDictionary *description;

    if (!configDictionary)
        return;

    bundleEnumerator = [[self bundlesFromStandardPath] objectEnumerator];
    while ((description = [bundleEnumerator nextObject])) {
        NSDictionary *infoDictionary;
        NSString *softwareVersion;
        NSDictionary *registrationDictionary;
        NSBundle *bundle;

        // skip invalidated bundles
        if ([description objectForKey:@"invalid"] != nil)
            continue;
        
        bundle = [description objectForKey:@"bundle"];

        infoDictionary = [bundle infoDictionary];

        // If the bundle isn't already loaded, decide whether to register it
        if (![[description objectForKey:@"loaded"] isEqualToString:@"YES"]) {
            NSDictionary *requiredVersionsDictionary;

            requiredVersionsDictionary = [infoDictionary objectForKey:OFRequiredSoftwareVersions];
            if (!requiredVersionsDictionary) {
                if (OFBundleRegistryDebug)
                    NSLog(@"OFBundleRegistry: Skipping %@ (obsolete)", [bundle bundlePath]);
                [description setObject:@"Bundle is obsolete" forKey:@"invalid"];
                continue;
            }
            if (![self haveSoftwareVersionsInDictionary:requiredVersionsDictionary]) {
                if (OFBundleRegistryDebug)
                    NSLog(@"OFBundleRegistry: Skipping %@ (requires software)", [bundle bundlePath]);
                [description setObject:@"Bundle requires additional software" forKey:@"invalid"];
                continue;
            }
        }

        softwareVersion = [infoDictionary objectForKey:OFSoftwareVersion];
        if (softwareVersion) {
            [softwareVersionDictionary setObject:softwareVersion forKey:[[[bundle bundlePath] lastPathComponent] stringByDeletingPathExtension]];
        }
        registrationDictionary = [infoDictionary objectForKey:OFRegistrations];

        if (softwareVersion == nil || [softwareVersion isEqualToString:@""]) {
	    // For logging purposes, let's say "unknown" rather than being blank
            softwareVersion = @"unknown";
	}
        if (!registrationDictionary) {
            if (OFBundleRegistryDebug)
                NSLog(@"OFBundleRegistry: Found %@ (version %@) (no registrations)", [bundle bundlePath], softwareVersion);
            continue;
        }

        if (OFBundleRegistryDebug)
            NSLog(@"OFBundleRegistry: Registering %@ (version %@)", [bundle bundlePath], softwareVersion);
        [self registerDictionary:registrationDictionary forBundle:description];
    }
}

+ (void)registerAdditionalRegistrations;
{
    NSEnumerator *registrationPathEnumerator;
    NSString *registrationPath;

    if (!configDictionary)
        return;

    registrationPathEnumerator = [[configDictionary objectForKey:OFBundleRegistryConfigAdditionalRegistrations] objectEnumerator];
    while ((registrationPath = [registrationPathEnumerator nextObject])) {
        NSDictionary *registrationDictionary;

        registrationPath = [registrationPath stringByReplacingKeysInDictionary:environmentDictionary startingDelimiter:@"$(" endingDelimiter:@")"];
        registrationPath = [registrationPath stringByExpandingTildeInPath];
        registrationDictionary = [[NSDictionary alloc] initWithContentsOfFile:registrationPath];
        if (registrationDictionary)
            [self registerDictionary:registrationDictionary forBundle:nil];
    }
}

+ (BOOL)haveSoftwareVersionsInDictionary:(NSDictionary *)requiredVersionsDictionary;
{
    NSEnumerator *softwareEnumerator, *requiredVersionEnumerator;
    NSString *software;

    softwareEnumerator = [requiredVersionsDictionary keyEnumerator];
    requiredVersionEnumerator = [requiredVersionsDictionary objectEnumerator];
    while ((software = [softwareEnumerator nextObject])) {
        NSString *requiredVersion, *currentVersion;

        requiredVersion = [requiredVersionEnumerator nextObject];
        currentVersion = [softwareVersionDictionary objectForKey:software];
        if (!currentVersion || ![currentVersion isEqualToString:requiredVersion])
            return NO;
    }
    return YES;
}

@end


#ifdef __MACH__
@implementation NSBundle (OFFixes)

- (void)_fixFrameworkVersioning;
{
    NSString *tmp2;
    NSString *frameworkName;
    NSSymbol sym;
    NSModule mod;
    const char *symbolName;
    const char *fullName;

    if (_infoDictionary != nil || _tmp2 == nil)
        return;
    tmp2 = (NSString *)_tmp2;
    if (![[tmp2 pathExtension] isEqualToString:@"framework"])
        return;

    frameworkName = [[tmp2 lastPathComponent] stringByDeletingPathExtension];

    symbolName = [[@".objc_class_name_NSFramework_" stringByAppendingString:frameworkName] cString];
    if (!NSIsSymbolNameDefined(symbolName))
        return;
    sym = NSLookupAndBindSymbol(symbolName);
    mod = NSModuleForSymbol(sym);
    fullName = NSLibraryNameForModule(mod);


    [(id)_tmp2 release];
    _tmp2 = [[[NSString stringWithCString:fullName] stringByDeletingLastPathComponent] retain];

//    NSLog(@"Fixing up %@", (id)_tmp2);
}

@end
#endif
