// Copyright 1997-2000 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 <OmniFoundation/OFBundleRegistry.h>

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

#import <OmniFoundation/NSString-OFExtensions.h>
#import <OmniFoundation/OFBundledClass.h>

#import "NSBundle-OFFixes.h"


RCS_ID("$Header: /Network/Source/CVS/OmniGroup/Frameworks/OmniFoundation/OFBundleRegistry.m,v 1.39 2000/11/16 12:29:09 wjs 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

static NSDictionary *environmentDictionary;
static NSMutableSet *registeredBundleNames;
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];
    registeredBundleNames = [[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:@"Contents"] stringByAppendingPathComponent:@"PlugIns"];

    // 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", "/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 Mac OS X library directories
            [NSString pathWithComponents:[NSArray arrayWithObjects:@"/", @"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:[candidateName stringByDeletingPathExtension]] ||
            [disabledBundleNames containsObject:bundlePath]) {
            [description setObject:NSLocalizedStringFromTableInBundle(@"Disabled by user preference", @"OmniFoundation", [self bundle], invalid bundle reason) 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:NSLocalizedStringFromTableInBundle(@"Not a valid bundle", @"OmniFoundation", [self bundle], invalid bundle reason) forKey:@"invalid"];
            continue;
        }

        [description setObject:bundle forKey:@"bundle"];
    }

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

+ (void)registerDictionary:(NSDictionary *)registrationDictionary forBundle:(NSDictionary *)bundleDescription;
{
    NSString *bundlePath, *registrationClassName;
    NSEnumerator *registrationClassEnumerator, *registrationDictionaryEnumerator;
    NSBundle *bundle;

    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] : NSLocalizedStringFromTableInBundle(@"local configuration file", @"OmniFoundation", [self bundle], local bundle path readable string);

    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])) {
        NSBundle *bundle;
        NSDictionary *infoDictionary;
        NSString *bundlePath;
        NSString *bundleName;
        NSString *softwareVersion;
        NSDictionary *registrationDictionary;

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

        bundlePath = [bundle bundlePath];
        bundleName = [bundlePath lastPathComponent];
        infoDictionary = [bundle infoDictionary];

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

            // Look up the bundle's required software
            requiredVersionsDictionary = [infoDictionary objectForKey:OFRequiredSoftwareVersions];
            if (!requiredVersionsDictionary) {
                if (OFBundleRegistryDebug)
                    NSLog(@"OFBundleRegistry: Skipping %@ (obsolete)", bundlePath);
                [description setObject:NSLocalizedStringFromTableInBundle(@"Bundle is obsolete", @"OmniFoundation", [self bundle], invalid bundle reason) forKey:@"invalid"];
                continue;
            }
            // Check whether we have the bundle's required software
            if (![self haveSoftwareVersionsInDictionary:requiredVersionsDictionary]) {
                if (OFBundleRegistryDebug)
                    NSLog(@"OFBundleRegistry: Skipping %@ (requires software)", bundlePath);
                [description setObject:NSLocalizedStringFromTableInBundle(@"Bundle requires additional software", @"OmniFoundation", [self bundle], invalid bundle reason) forKey:@"invalid"];
                continue;
            }
            // Check whether we've already registered another copy of this bundle
            if ([registeredBundleNames containsObject:bundleName]) {
                // A possible enhancement would be to keep track of the duplicates rather than just throwing them out, and fall back on loading them if the first copy fails to load.
                if (OFBundleRegistryDebug)
                    NSLog(@"OFBundleRegistry: Skipping %@ (duplicate bundle name)", bundlePath);
                [description setObject:NSLocalizedStringFromTableInBundle(@"Duplicate bundle name", @"OmniFoundation", [self bundle], invalid bundle reason) forKey:@"invalid"];
                continue;
            }
        }
        // OK, we're going to register this bundle
        [registeredBundleNames addObject:bundleName];

        // Look up the bundle's software version
        softwareVersion = [infoDictionary objectForKey:OFSoftwareVersion];
        if (softwareVersion) {
            [softwareVersionDictionary setObject:softwareVersion forKey:[bundleName stringByDeletingPathExtension]];
        }
        if (softwareVersion == nil || [softwareVersion isEqualToString:@""]) {
            // For logging purposes, let's say "unknown" rather than being blank
            softwareVersion = @"unknown";
        }

        // Register the bundle
        registrationDictionary = [infoDictionary objectForKey:OFRegistrations];
        if (registrationDictionary) {
            if (OFBundleRegistryDebug)
                NSLog(@"OFBundleRegistry: Registering %@ (version %@)", bundlePath, softwareVersion);
            [self registerDictionary:registrationDictionary forBundle:description];
        } else {
            if (OFBundleRegistryDebug)
                NSLog(@"OFBundleRegistry: Found %@ (version %@) (no registrations)", bundlePath, softwareVersion);
        }
   }
}

+ (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
