/* 
   NSBundle.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 <sys/types.h>
#include <sys/stat.h>

#include <Foundation/common.h>
#include <Foundation/NSBundle.h>
#include <Foundation/NSArray.h>
#include <Foundation/NSDictionary.h>
#include <Foundation/NSString.h>
#include <Foundation/NSAutoreleasePool.h>
#include <Foundation/NSProcessInfo.h>
#include <Foundation/NSNotification.h>
#include <Foundation/NSUserDefaults.h>
#include <extensions/objc-runtime.h>

/*
 * Static class variables
 */

typedef struct {
    Class class;
    Category* category;
} LoadingClassCategory;

static NSMapTable*	    bundleClasses;	// class -> bundle mapping
static NSMapTable*	    bundleNames;	// path  -> bundle mapping
static NSBundle*	    mainBundle;		// application bundle
static LoadingClassCategory*load_Classes;	// used while loading
static int		    load_classes_size;	// used while loading
static int		    load_classes_used;	// used while loading

/*
 * NSBundle methods
 */

@implementation NSBundle

// Library resource directory

static NSString* resourcesPath = nil;

+ (NSString*)libraryResourceDirectory
{
    if (!resourcesPath) {
	char* res_path = getenv("LIB_FOUNDATION_RESOURCES_PATH");
    
	resourcesPath = (res_path == NULL) ?
	    @RESOURCES_PATH : 
	    [[NSString stringWithCString:res_path] retain];
    }
    
    return resourcesPath;
}

// Bundle initialization

+ (void)initialize
{
    mainBundle = nil;
    bundleClasses = NSCreateMapTable(NSNonOwnedPointerMapKeyCallBacks,
	NSNonRetainedObjectMapValueCallBacks, 23);
    bundleNames = NSCreateMapTable(NSObjectMapKeyCallBacks,
	NSNonRetainedObjectMapValueCallBacks, 23);
}

// Load info for bundle

- (void)loadInfo
{
    NSString* file;

    if (infoDictionary)
	return;
    
    file = [self pathForResource:@"Info" ofType:@"plist"];
    
    if (file)
	infoDictionary = [[[NSString stringWithContentsOfFile:file] 
	    propertyList] retain];
    
    if (!infoDictionary)
	infoDictionary = [[NSDictionary alloc] init];
}

// Internal code loading

static void load_callback(Class class, Category* category)
{
    if (load_classes_used >= load_classes_size) {
	load_classes_size += 128;
	load_Classes = Realloc(load_Classes,
	    load_classes_size*sizeof(LoadingClassCategory));
    }
    load_Classes[load_classes_used].class = class;
    load_Classes[load_classes_used].category = category;
    load_classes_used++;
}

- (BOOL)loadCode
{
    int i;
    NSString* file;
    NSString* rfile;
    BOOL status;
    int objc_load_module(const char*, void (*)(Class, Category*));
    
    if (codeLoaded)
	return YES;
    else
	codeLoaded = YES;
    
    // Find file to load
    if ((file = [[self infoDictionary] objectForKey:@"NSExecutable"]))
	file = [fullPath stringByAppendingPathComponent:file];
    else
	file = [fullPath stringByAppendingPathComponent:
	    [fullPath lastPathComponent]];
    
    rfile = [file stringByResolvingSymlinksInPath];
    if (!rfile) {
	NSLog(@"%@:NSBundle: cannot find executable file %@",
	    [[NSProcessInfo processInfo] processName], file);
	return NO;
    }
    
    // Prepare to keep classes/categories loaded
    load_classes_size = 128;
    load_classes_used = 0;
    load_Classes = Malloc(load_classes_size*sizeof(LoadingClassCategory));

    status = objc_load_module([file fileSystemRepresentation], load_callback);
    
    if (status) {
	NSDictionary* info;
	firstLoadedClass = Nil;
	for (i = 0; i < load_classes_used; i++) {
	    // get first class from bundle
	    if (!firstLoadedClass && !load_Classes[i].category)
		firstLoadedClass = load_Classes[i].class;
	    
	    // TODO - call class/category load method
	    
	    // insert in bundle hash
	    if (!load_Classes[i].category)
		NSMapInsert(bundleClasses, load_Classes[i].class, self);
	    
	    // post notification
	    if (!load_Classes[i].category)
		info = [NSDictionary dictionaryWithObjectsAndKeys:
			    [NSString stringWithCStringNoCopy:
			    (char*)(load_Classes[i].class->name)
			    freeWhenDone:NO], 
			@"NSClass", 
			    nil];
	    else
		info = [NSDictionary dictionaryWithObjectsAndKeys:
			    [NSString stringWithCStringNoCopy:
			    (char*)(load_Classes[i].class->name)
			    freeWhenDone:NO],
			@"NSClass",
			    [NSString stringWithCStringNoCopy:
			    (char*)(load_Classes[i].category->category_name)
			    freeWhenDone:NO], 
			@"NSCategory",
			     nil];
	    [[NSNotificationCenter defaultCenter] 
		postNotificationName:@"NSBundleDidLoadNotification"
		object:nil
		userInfo:info];
	}
        [[NSNotificationCenter defaultCenter] 
	    postNotificationName:@"NSBundleDidLoadNotification"
	    object:nil
	    userInfo:nil];
    }
    
    Free(load_Classes);
    
    return status;
}

// Initializing an NSBundle 

static BOOL canReadDirectory(NSString* path)
{
    const char* cpath = [path fileSystemRepresentation];
    struct stat statbuf;
    
    if (stat(cpath, &statbuf) < 0)
	return NO;
    
    if (S_IFDIR != (S_IFMT & statbuf.st_mode))
	return NO;
    
    if (access(cpath, R_OK | X_OK) < 0)
	return NO;

    return YES;
}

static BOOL canReadFile(NSString* path)
{
    const char* cpath = [path fileSystemRepresentation];
    if (access(cpath, R_OK) < 0)
	return NO;

    return YES;
}

- (id)initWithPath:(NSString*)path
{
    NSBundle* old;

    path = [path stringByResolvingSymlinksInPath];
    
    if (!path || !canReadDirectory(path)) {
	[self release];
	return nil;
    }
    
    old = (NSBundle*)NSMapGet(bundleNames, path);
    if (old) {
	[self release];
	return [old retain];
    }
    
    NSMapInsert(bundleNames, path, self);
    fullPath = [path retain];
    return self;
}

// TODO - now bundle is not capable of dealloc & code unloading

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

- (void)dealloc
{
    NSMapRemove(bundleNames, fullPath);
    [fullPath release];
    [infoDictionary release];
    [stringTables release];
    [super dealloc];
}

// Getting an NSBundle 

+ (NSBundle*)bundleForClass:(Class)aClass
{
    NSBundle* bundle = (NSBundle*)NSMapGet(bundleClasses, aClass);
    
    return bundle ? bundle : [self mainBundle];
}

+ (NSBundle*)bundleWithPath:(NSString*)path
{
    return [[[self alloc] initWithPath:path] autorelease];
}

+ (NSBundle*)mainBundle
{
    if (!mainBundle) {
	NSString* path = [[[NSProcessInfo processInfo] processName]
			      stringByDeletingLastPathComponent];
	if ([path isEqual:@""])
	    path = @".";
	mainBundle = [[NSBundle alloc] initWithPath:path];
    }
    return mainBundle;
}

// Getting a Bundled Class 

- (Class)classNamed:(NSString*)className
{
    Class class;

    [self loadCode];
    
    class = NSClassFromString(className);
    if (class && (NSBundle*)NSMapGet(bundleClasses, class) == self)
	return class;

    return nil;
}

- (Class)principalClass
{
    NSString* className;
    Class class;
    
    [self loadCode];
    className = [[self infoDictionary] objectForKey:@"NSPrincipalClass"];
    
    class = NSClassFromString(className);
    
    return class ? class : firstLoadedClass;
}

// Finding a Resource 

+ (NSString*)pathForResource:(NSString*)name ofType:(NSString*)ext
  inDirectories:(NSArray*)directories
{
    int i, n;
    NSAutoreleasePool* pool;
    NSString* file;
    
    pool = [NSAutoreleasePool new];
    
    if (ext)
	name = [name stringByAppendingPathExtension:ext];
    
    n = [directories count];
    
    for (i = 0; i < n; i++) {
	file = [[directories objectAtIndex:i]
	    stringByAppendingPathComponent:name];
	if (canReadFile(file))
	    goto found;
    }
    
    file = nil;
    
    found:
    
    [file retain];
    [pool release];
    
    return [file autorelease];
}

- (NSString*)pathForResource:(NSString*)name ofType:(NSString*)ext		
  inDirectory:(NSString*)directory
{
    int i, n;
    NSString* path;
    NSString* file;
    NSAutoreleasePool* pool;
    NSMutableArray* languages;
    
    pool = [NSAutoreleasePool new];
    
    // Get languages and translate list by adding "lproj" extension
    // {English, German, ...} to {English.lproj, German.lproj, ...}
    languages = [[[NSUserDefaults standardUserDefaults] 
	stringArrayForKey:@"Languages"] mutableCopy];
    n = [languages count];
    for (i = 0; i < n; i++) {
	file = [[languages objectAtIndex:i] 
	    stringByAppendingPathExtension:@"lproj"];
	[languages replaceObjectAtIndex:i withObject:file];
    }
    
    // make file name name.ext if extension is present
    if (ext)
	name = [name stringByAppendingPathExtension:ext];
    
    // look for fullPath/Resources/directory/...
    path = [fullPath stringByAppendingPathComponent:@"Resources"];
    if (directory && ![directory isEqualToString:@""])
	path = [path stringByAppendingPathComponent:directory];
    if (canReadDirectory(path)) {
	// check languages
	for (i = 0; i < n; i++) {
	    file = [[path stringByAppendingPathComponent:
		[languages objectAtIndex:i]]
		    stringByAppendingPathComponent:name];
	    if (canReadFile(file))
		goto found;
	}
	// check base
	file = [path stringByAppendingPathComponent:name];
	if (canReadFile(file))
	    goto found;
    }
    
    // look for fullPath/directory/...
    if (directory && ![directory isEqualToString:@""])
	path = [fullPath stringByAppendingPathComponent:directory];
    else
	path = fullPath;
    if (canReadDirectory(path)) {
	// check languages
	for (i = 0; i < n; i++) {
	    file = [[path stringByAppendingPathComponent:
		[languages objectAtIndex:i]]
		    stringByAppendingPathComponent:name];
	    if (canReadFile(file))
		goto found;
	}
	// check base
	file = [path stringByAppendingPathComponent:name];
	if (canReadFile(file))
	    goto found;
    }

    file = nil;
    
    found:
    
    [file retain];
    [pool release];
    
    return [file autorelease];
}

- (NSArray*)pathsForResourcesOfType:(NSString*)extension
  inDirectory:(NSString*)bundlePath
{
    // TODO - needs NSFileManager
    [self notImplemented:_cmd];
    return nil;
}

- (NSString*)pathForResource:(NSString*)name ofType:(NSString*)ext
{
    return [self pathForResource:name ofType:ext inDirectory:nil];
}

- (NSString*)resourcePath
{
    return fullPath;
}

// Getting bundle information

- (NSDictionary*)infoDictionary
{
    [self loadInfo];
    return infoDictionary;
}

// Getting the Bundle Directory 

- (NSString*)bundlePath
{
    return fullPath;
}

// Managing Localized Resources

- (NSString*)localizedStringForKey:(NSString*)key value:(NSString*)value
  table:(NSString*)tableName
{
    NSDictionary* table;
    NSString* string;

    if (!stringTables)
	stringTables = [NSMutableDictionary new];
    
    table = [stringTables objectForKey:tableName];
    if (!table) {
	string = [NSString stringWithContentsOfFile:
		[self pathForResource:tableName ofType:@"strings"]];
	if (!string)
		return value;
	table = [string propertyListFromStringsFileFormat];
	if (table)
	    [stringTables setObject:table forKey:tableName];
    }

    string = [table objectForKey:key];
    if (!string)
	string = value;

    return string;
}

- (void)releaseStringtableCache
{
    [stringTables release];
    stringTables = nil;
}

@end /* NSBundle */
