/*
 * RZBundle
 *
 * Copyright 1994 Ralph Zazula.  All Rights Reserved.
 *
 * No part of this code may be reproduced in any form, compiled
 * or source code, nor used for any purpose without the express
 * written permission of the copyright holder.
 *
 * 11/14/94 - permission granted to use for any purpose excluding
 * 	resale or use as a dietary suppliment.
 *
 * By: Ralph Zazula (rzazula@next.com, zazula@hitower.com)
 */

/*$Log: RZBundle.m,v $
 * Revision 1.5  1994/06/29  07:02:26  zazula
 * *** empty log message ***
 **/

#import "RZBundle.h"
#import <objc/objc-runtime.h>
#import <sys/dir.h>

@interface NXBundle(SomePrivateStuff)
- (BOOL)codeLoaded;
- ensureLoaded;
@end

static id _bundleList = nil;              /* list of all allocated RZBundles */
static NXZone *_bundleZone = NULL;        /* a zone for our stuff */

@implementation RZBundle

int rzbundle_objc_classHandler(const char *name)
/*
 * This function will be installed as the class lookup error
 * handler.  This is called when objc_getClass() fails to try
 * and load the proper bundle for the class "name".  Returns
 * the newly load class, or 'Nil' on failure.
 */
{
   return (int)[RZBundle classNamed:name];
}

+ addBundlesInPath:(const char *)path withExtension:(const char *)extension
/*
 * Creates RZBundle instances for all files *."extension" in the folder
 * "path".
 */
{
   DIR *thedir;
   struct direct *entry;
   char *ext;
   char full_path[MAXPATHLEN+1];
   
   thedir = opendir(path);
   if(thedir) {
      while ((entry = readdir(thedir))) {
         if (*entry->d_name != '.') {
            ext = strrchr(entry->d_name, '.');
            if(ext) {
               if(!strncmp(++ext, extension, strlen(extension))) {
                  sprintf(full_path, "%s/%s",path, entry->d_name);
                  [[self alloc] initForDirectory:full_path];
               }
            }
         }
      }
      closedir(thedir);
   }
   return self;         
}

+ addBundlesInPath:(const char *)path
/*
 * Creates RZBundle instances for all files *.bundle in the folder
 * "path".
 */
{
   return [self addBundlesInPath:path withExtension:"bundle"];
}

+ setup
/*
 * Installs the class lookup error handling function for automatic
 * bundle loading.  This method also registers bundles found in the
 * following directories:
 *    ~/Library/Bundles
 *    ~/Developer/Bundles
 *    /LocalLibrary/Bundles
 *    /LocalDeveloper/Bundles
 *    <Path to Executable>/<AppName>.app
 *    <Path to Executable>/<AppName>.app/Bundles
 *    /NextLibrary/Bundles
 *    /NextDeveloper/Bundles
 */
{
   static BOOL alreadySetup = NO;
   
   if(!alreadySetup) {
      char path[MAXPATHLEN+1];

      _bundleZone = NXCreateZone(vm_page_size, vm_page_size, YES);
      _bundleList = [[List allocFromZone:_bundleZone] init];
      objc_setClassHandler(rzbundle_objc_classHandler);
      
      /* relative to the users home directory */
      sprintf(path, "%s/Library/Bundles", NXHomeDirectory());
      [self addBundlesInPath:path];
      sprintf(path, "%s/Developer/Bundles", NXHomeDirectory());
      [self addBundlesInPath:path];
      
      /* in the local system */
      [self addBundlesInPath:"/LocalLibrary/Bundles"];
      [self addBundlesInPath:"/LocalDeveloper/Bundles"];
      
      /* in the app */
      [self addBundlesInPath:[[RZBundle mainBundle] directory]];
      sprintf(path, "%s/Bundles",[[RZBundle mainBundle] directory]);
      [self addBundlesInPath:path];
      
      /* in the system directories */
      [self addBundlesInPath:"/NextLibrary/Bundles"];
      [self addBundlesInPath:"/NextDeveloper/Bundles"];
      
      alreadySetup = YES;
   }
   return self;
}

+ alloc
{
   return [self allocFromZone:_bundleZone];
}

+ allocFromZone:(NXZone *)zone
{
   id newBundle = nil;
   
   newBundle = [super allocFromZone:zone];
   [_bundleList addObjectIfAbsent:newBundle];
   
   return newBundle;
}

+ (RZBundle *)bundleContainingClass:(const char *)className
/*
 * Returns the first bundle found containing the class "className" or 'nil' if
 * no bundles currently initialized contain the class "className".  
 */
{
   int i;
   
   for(i=0; i<[_bundleList count]; i++) {
      if([[_bundleList objectAt:i] containsClass:className]) {
         return [_bundleList objectAt:i];
      }
   }
   
   return nil;
}

+ (List *)bundlesWithValue:(const char *)value forString:(const char *)key
/*
 * Returns a List of RZBundle instances whose 'bundle.table' file contain
 * the key/value pair: "key" = "value".  Returns 'nil' if no bundles contain
 * the key/value pair.  The List should be freed by the caller.
 */
{
   int i;
   const char *_value;
   id list = nil;
   
   for(i=0; i<[_bundleList count]; i++) {
      _value = [[_bundleList objectAt:i] valueForString:key];
      if(_value ? strcmp(_value, value) == 0 : NO) {
         if(!list) {
            list = [[List alloc] init];
         }
         [list addObject:[_bundleList objectAt:i]];
      }
   }
   
   return list;
}

+ (BOOL)doPreloadsFor:(RZBundle *)bundle
/*
 * Attempts to load any bundles containing classes required to load "bundle".
 * Returns YES on success (all external classes found) and NO on failure.
 * Upon success, the code for "bundle" and the code of any required bundles 
 * will be loaded.  Upon failure, the code for "bundle" will not be loaded but 
 * some or all of the prerequisite bundles may be loaded.
 */
{
   id table;
   NXHashState state;
   void *key, *value;
   id _bundle;
   
   if(!bundle) {
      return NO;
   }
   
   if([bundle codeLoaded]) {
      /* safe if code is already loaded */
      return YES;
   }
   
   if(!(table = [bundle dependTable]) || ![table count]) {
      /* no dependancies */
      return YES;
   }
   
   state = [table initState];
   while ([table nextState: &state key: &key value: &value]) {
      if(!objc_lookUpClass(key)) {
         _bundle = [self bundleContainingClass:key];
         if(!_bundle) {
            return NO;
         }
         if(![self doPreloadsFor:_bundle]) {
            return NO;
         }
         [_bundle ensureLoaded];  // force it to load
      }
   }
   
   return YES;
}

+ classNamed:(const char *)className
/*
 * Searches all initialized bundles for one containing "className".  If a bundle
 * is found, the receiver performs any preloading required and then returns the
 * class for "className".  Returns 'Nil' if the class "className" cannot be found,
 * or if the preloading could not be performed.
 */
{
   id bundle = [self bundleContainingClass:className];

   return bundle ? [bundle classNamed:className] : Nil;
}

+ (BOOL)loadNib:(const char *)name for:anObject
/*
 * Locates and loads the nib "name" with "anObject" as the owner.
 */
{
   id bundle;
   char path[MAXPATHLEN+1];
      
   bundle = [self bundleForClass:[anObject class]];
   if([bundle getPath:path forResource:name ofType:"nib"]) {
      [NXApp loadNibFile:path owner:anObject];
      return YES;
   }
   fprintf(stderr, "couldn't load nib %s.\n",name);
   return NO;
}

- (NXStringTable *)classTable
/*
 * Returns the NXStringTable instance representing the 'class.table' file
 * for the reciever.  This table has the names of the classes defined
 * in the receiver.
 */
{
   if(!classTable) {
      char path[MAXPATHLEN+1];
      
      if([self getPath:path forResource:"class" ofType:"table"]) {
         classTable = [[NXStringTable allocFromZone:[self zone]] init];
         [classTable readFromFile:path];
      }
   }
   return classTable;
}

- (NXStringTable *)dependTable
/*
 * Returns the NXStringTable instance representing the 'depend.table' file
 * for the reciever.  This table has the names of the external classes 
 * required by the receiver.  The class list may contain classes already
 * present in the runtime system (i.e., it is not pruned against objc_lookUpClass()).
 */
{
   if(!dependTable) {
      char path[MAXPATHLEN+1];
      
      if([self getPath:path forResource:"depend" ofType:"table"]) {
         dependTable = [[NXStringTable allocFromZone:[self zone]] init];
         [dependTable readFromFile:path];
      }
   }
   return dependTable;
}

- (NXStringTable *)infoTable
/*
 * Returns the NXStringTable instance representing the 'bundle.table' file
 * for the reciever.  This table has the user-defined key/value pairs.
 */
{
   if(!infoTable) {
      char path[MAXPATHLEN+1];
      
      if([self getPath:path forResource:"info" ofType:"table"]) {
         infoTable = [[NXStringTable allocFromZone:[self zone]] init];
         [infoTable readFromFile:path];
      }
   }
   return infoTable;
}

- classNamed:(const char *)className
/*
 * Overriden to perform dependency checking and bundle pre-loading
 * before loading the code for the receiving bundle.
 */
{
   if(!className) {
       return nil;
   }
   if(![self doPreloads]) {
      return nil;
   }
   [self ensureLoaded];
   return [super classNamed:className];
}

- principalClass
/*
 * Overriden to perform dependency checking and bundle pre-loading
 * before loading the code for the receiving bundle.
 */
{
   if(![self doPreloads]) {
      return nil;
   }
   [self ensureLoaded];
   return [super principalClass];
}

- free
{
   if(infoTable) {
      infoTable = [infoTable free];
   }
   if(classTable) {
      classTable = [classTable free];
   }
   if(dependTable) {
      dependTable = [dependTable free];
   }
   [_bundleList removeObject:self];
   return [super free];
}

- (BOOL)containsClass:(const char *)name
/*
 * Returns YES if this bundle contains the class named 'name'.  This information
 * is based on the contents of the file 'class.table' in the receivers bundle.
 */
{
   return (name) ? ([[self classTable] valueForStringKey:name] != NULL) : NO;
}

- (BOOL)doPreloads
/*
 * Checks for any external class dependancies and tries to resolve them.  Returns
 * YES on success and NO on failure.  Upon success, the recivers code and the code
 * of any required bundles will be loaded.  Upon failure, the receivers code will
 * not be loaded but some or all of the prerequisite bundles may be loaded.
 */
{
   return [[self class] doPreloadsFor:self];
}

- (const char *)valueForString:(const char *)s
/*
 * Returns the string value for keyword 's' found in the resource
 * 'bundle.table'.  Returns NULL if the keyword is not found.
 */
{  
   id table = [self infoTable];
   
   return table ? [table valueForKey:(const void *)s] : NULL;
}

@end
