//
//  main.m
//  AsynchronousReplies
//
//  Created by Bob Ippolito on 6/20/04.
//  Copyright (c) 2004 Bob Ippolito. All rights reserved.
//

#import <Cocoa/Cocoa.h>
#include <mach-o/dyld.h>
#include <sys/types.h>
#include <sys/stat.h>

//
// Constants
//
NSString *ERR_REALLYBADTITLE = @"The application could not be launched.";
NSString *ERR_TITLEFORMAT = @"%@ has encountered a fatal error, and will now terminate.";
NSString *ERR_NONAME = @"The Info.plist file must have values for the CFBundleName or CFBundleExecutable strings.";
NSString *ERR_PYRUNTIMELOCATIONS = @"The Info.plist file must have a PyRuntimeLocations array containing string values for preferred Python runtime locations.  These strings should be \"otool -L\" style mach ids; \"@executable_stub\" and \"~\" prefixes will be translated accordingly.";
NSString *ERR_NOPYTHONRUNTIME = @"A Python runtime could be located.  You may need to install a framework build of Python, or edit the PyRuntimeLocations array in this application's Info.plist file.\rThese runtime locations were attempted:\r\r";
NSString *ERR_NOPYTHONSCRIPT = @"A main script could not be located in the Resources folder.\rThese files were tried:\r\r";
NSString *ERR_LINKERRFMT = @"An internal error occurred while attempting to link with:\r\r%s\r\rSee the Console for a detailed dyld error message";
NSString *ERR_PYTHONEXCEPTION = @"An uncaught exception was raised during execution of the main script:\r\r%@: %@\r\rThis may mean that an unexpected error has occurred, or that you do not have all of the dependencies for this application.\r\rSee the Console for a detailed traceback.";
NSString *ERR_DEFAULTURLTITLE = @"Visit Website";
NSString *ERR_CONSOLEAPP = @"Console.app";
NSString *ERR_CONSOLEAPPTITLE = @"Open Console";
NSString *ERR_TERMINATE = @"Terminate";
#define PYMACAPP_NSIMAGEFLAGS (NSADDIMAGE_OPTION_RETURN_ON_ERROR | NSADDIMAGE_OPTION_WITH_SEARCHING)
#define PYMACAPP_NSLOOKUPSYMBOLINIMAGEFLAGS (NSLOOKUPSYMBOLINIMAGE_OPTION_BIND | NSLOOKUPSYMBOLINIMAGE_OPTION_RETURN_ON_ERROR)
//
// THIS WILL NOT WORK WITH Py_TRACE_REFS / Py_DEBUG ON!!! WARNING!!
//
#define Py_XDECREF(op) if ((op) == NULL) ; else --(*op)

//
// Typedefs
//

typedef int PyObject;
typedef void (*Py_SetProgramNamePtr)(const char *);
typedef void (*Py_InitializePtr)(void);
typedef int (*PyRun_SimpleFilePtr)(FILE *, const char *);
typedef void (*Py_FinalizePtr)(void);
typedef PyObject *(*PySys_GetObjectPtr)(const char *);
typedef int *(*PySys_SetArgvPtr)(int argc, char **argv);
typedef PyObject *(*PyObject_StrPtr)(PyObject *);
typedef const char *(*PyString_AsStringPtr)(PyObject *);
typedef PyObject *(*PyObject_GetAttrStringPtr)(PyObject *, const char *);

//
// Signatures
//

int report_error(NSString *err);
int report_linkEdit_error(void);
int report_script_error(NSString *err, NSString *errClassName, NSString *errName);
NSString *pyStandardizePath(NSString *pyLocation);
BOOL doesPathExist(NSString *path);
NSString *getApplicationName(void);
NSString *getErrorTitle(NSString *applicationName);
int pyobjc_main(int argc, char * const *argv, char * const *envp);
int main(int argc, char * const *argv, char * const *envp);


//
// Implementation
//

int report_script_error(NSString *err, NSString *errClassName, NSString *errName) {

	NSArray *errorScripts = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"PyErrorScripts"];
	if ( !errorScripts )
		errorScripts = [NSArray array];
	// find main python file.  __main__.py seems to be a standard, so we'll go ahead and add defaults.
	errorScripts = [errorScripts arrayByAddingObjectsFromArray:[NSArray arrayWithObjects:
		@"__error__",
		@"__error__.py",
		@"__error__.sh",
		nil]];
	NSEnumerator *errorScriptsEnumerator = [errorScripts objectEnumerator];
	NSString *path = nil;
	NSString *nextFileName;
	
	while (nextFileName = [errorScriptsEnumerator nextObject]) {
		path = [[NSBundle mainBundle] pathForResource: nextFileName ofType: nil];
		if ( path )
			break;
	}
	
	if ( !path )
		return report_error(err);
	
	NSTask *task = [[NSTask alloc] init];
	NSPipe *stdoutPipe = [NSPipe pipe];
	[task setLaunchPath: path];
	[task setArguments: [NSArray arrayWithObjects:getApplicationName(),errClassName,errName,nil]];
	[task setStandardOutput: stdoutPipe];

	NS_DURING
		[task launch];
		[task waitUntilExit];
	NS_HANDLER
		NSLog(@"Could not execute %@: %@", [path lastPathComponent], localException);
		[task release];
		return report_error(err);
	NS_ENDHANDLER

	NSData *taskData = [[stdoutPipe fileHandleForReading] readDataToEndOfFile];
	[task autorelease];
	
	if ([task terminationStatus])
		return report_error(err);
	NSString *result = [[[NSString alloc] initWithData:taskData encoding:NSUTF8StringEncoding] autorelease];
	NSArray *lines = [result componentsSeparatedByString:@"\n"];
	while ([[lines lastObject] isEqualToString: @""])
		lines = [lines subarrayWithRange:NSMakeRange(0, [lines count]-1)];
	if ( ![lines count] )
		return report_error(err);
	NSURL *buttonURL = nil;
	NSString *buttonString = nil;
		
	if ([[lines lastObject] hasPrefix:@"ERRORURL: "]) {
		NSString *lastLine = [lines lastObject];
		lines = [lines subarrayWithRange:NSMakeRange(0, [lines count]-1)];
		NSArray *buttonArr = [lastLine componentsSeparatedByString:@" "];
		buttonArr = [buttonArr subarrayWithRange:NSMakeRange(1, [buttonArr count]-1)];
		while ([[buttonArr objectAtIndex:0] isEqualToString: @""])
			buttonArr = [buttonArr subarrayWithRange:NSMakeRange(1, [buttonArr count]-1)];
		buttonURL = [NSURL URLWithString:[buttonArr objectAtIndex:0]];
		if ( buttonURL ) {
			buttonArr = [buttonArr subarrayWithRange:NSMakeRange(1, [buttonArr count]-1)];
			while ([[buttonArr objectAtIndex:0] isEqualToString: @""])
				buttonArr = [buttonArr subarrayWithRange:NSMakeRange(1, [buttonArr count]-1)];
			buttonString = [buttonArr componentsJoinedByString:@" "];
			if ( !buttonString )
				buttonString = ERR_DEFAULTURLTITLE;
		}
	}

	NSString *title = nil;
	NSString *msg = nil;
	title = [lines objectAtIndex:0];
	if ( !title )
		return report_error(err);
	
	msg = [[lines subarrayWithRange:NSMakeRange(1, [lines count]-1)] componentsJoinedByString:@"\r"];
	if ( !msg )
		msg = @"";
	
	NSLog(title);
	NSLog(msg);
	[NSApplication sharedApplication];
	if ( !buttonURL ) {
		int choice = NSRunAlertPanel(title, msg, ERR_TERMINATE, ERR_CONSOLEAPPTITLE, NULL);
		switch (choice) {
			case NSAlertAlternateReturn:
				[[NSWorkspace sharedWorkspace] launchApplication:ERR_CONSOLEAPP];
				break;
			default:
				break;
		}
	} else {
		int choice = NSRunAlertPanel(title, msg, ERR_TERMINATE, buttonString, NULL);
		switch (choice) {
			case NSAlertAlternateReturn:
				[[NSWorkspace sharedWorkspace] openURL:buttonURL];
				break;
			default:
				break;
		}
	}
	
	return -1;
}

int report_error(NSString *err) {
	int choice;
	[NSApplication sharedApplication];
	NSLog(err);
	choice = NSRunAlertPanel(getErrorTitle(getApplicationName()), err, ERR_TERMINATE, ERR_CONSOLEAPPTITLE, NULL);
	switch (choice) {
		case NSAlertAlternateReturn:
			[[NSWorkspace sharedWorkspace] launchApplication:ERR_CONSOLEAPP];
			break;
		default:
			break;
	}
	return -1;
}

int report_linkEdit_error() {
	NSLinkEditErrors errorClass;
	int errorNumber;
	const char *fileName;
	const char *errorString;
	NSLinkEditError(&errorClass, &errorNumber, &fileName, &errorString);
	NSLog(@"%s", errorString);
	return report_error([NSString stringWithFormat:ERR_LINKERRFMT,fileName]);
}


NSString *pyStandardizePath(NSString *pyLocation) {
		if ([pyLocation hasPrefix:@"@executable_path/"]) {
			NSMutableArray *newComponents = [[pyLocation pathComponents] mutableCopy];
			[newComponents replaceObjectAtIndex:0 withObject:[[NSBundle mainBundle] privateFrameworksPath]];
			pyLocation = [NSString pathWithComponents: newComponents];
		}
		return [pyLocation stringByStandardizingPath];
};

BOOL doesPathExist(NSString *path) {
		struct stat sb;
		return (stat([path fileSystemRepresentation], &sb) == -1) ? NO : YES;
}


NSString *getApplicationName(void) {
	NSDictionary *infoDictionary = [[NSBundle mainBundle] infoDictionary];
	NSString *applicationName = [infoDictionary objectForKey:@"CFBundleName"];
	if (!applicationName) {
		applicationName = [infoDictionary objectForKey:@"CFBundleExecutable"];
	}
	return applicationName;
}

NSString *getErrorTitle(NSString *applicationName) {
	if (!applicationName)
		return ERR_REALLYBADTITLE;
	return [NSString stringWithFormat:ERR_TITLEFORMAT,applicationName];
}



int pyobjc_main(int argc, char * const *argv, char * const *envp) {
	// little sanity check.
	if ( !getApplicationName() )
		return report_error(ERR_NONAME);
	
	
	// XXX - I'm not going to default anything here.. explicit is better than implicit.

	// get the runtime locations from the Info.plist
	NSArray *pyLocations = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"PyRuntimeLocations"];
	if ( !pyLocations )
		return report_error(ERR_PYRUNTIMELOCATIONS);
	
	// XXX - *does not* inspect DYLD environment variables for overrides, fallbacks, suffixes, etc.
	//		 I don't really consider that a very bad thing, as it makes this search extremely deterministic.
	//		 Note that I use the env variables when the image is actually linked, so what you find here 
	//		 may not be what gets linked.  If this is the case, you deserve it :)

	// find a Python runtime
	NSString *pyLocation;
	NSEnumerator *pyLocationsEnumerator = [pyLocations objectEnumerator];
	while (pyLocation = [pyLocationsEnumerator nextObject]) {
		pyLocation = pyStandardizePath(pyLocation);
		if ( doesPathExist(pyLocation) )
			break;
	}
	if ( !pyLocation ) {
	
		return report_script_error([ERR_NOPYTHONRUNTIME stringByAppendingString:[pyLocations componentsJoinedByString:@"\r\r"]], nil, nil);
	}
	
	NSString *pythonPath = [[[NSProcessInfo processInfo] environment] objectForKey: @"PYTHONPATH"];
	NSString *resourcePath = [[NSBundle mainBundle] resourcePath];
	NSMutableArray *pythonPathArray = [NSMutableArray arrayWithObjects: resourcePath, [resourcePath stringByAppendingPathComponent:@"PyObjC"], nil];
	if (pythonPath != nil)
		[pythonPathArray addObjectsFromArray: [pythonPath componentsSeparatedByString: @":"]];

	// I *refuse* to set dirty DYLD environment variables..  
	// If you want that, you'll have to do it in your main script or fork this bootstrap ;)
	//
	setenv("PYTHONPATH", [[pythonPathArray componentsJoinedByString:@":"] UTF8String], 1);
	
	NSArray *possibleMains = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"PyMainFileNames"];
	if ( !possibleMains )
		possibleMains = [NSArray array];
	// find main python file.  __main__.py seems to be a standard, so we'll go ahead and add defaults.
	possibleMains = [possibleMains arrayByAddingObjectsFromArray:[NSArray arrayWithObjects:
		@"__main__.py",
		@"__main__.pyc",
		@"__main__.pyo",
		@"__realmain__.py",
		@"__realmain__.pyc",
		@"__realmain__.pyo",
		@"Main.py",
		@"Main.pyc",
		@"Main.pyo",
		nil]];
	NSEnumerator *possibleMainsEnumerator = [possibleMains objectEnumerator];
	NSString *mainPyPath = nil;
	NSString *nextFileName;
	
	while (nextFileName = [possibleMainsEnumerator nextObject]) {
		mainPyPath = [[NSBundle mainBundle] pathForResource: nextFileName ofType: nil];
		if ( mainPyPath )
			break;
	}

	if ( !mainPyPath )
		return report_error([ERR_NOPYTHONSCRIPT stringByAppendingString:[possibleMains componentsJoinedByString:@"\r"]]);
	
	const struct mach_header *py_dylib = NSAddImage([pyLocation fileSystemRepresentation], PYMACAPP_NSIMAGEFLAGS);
	if ( !py_dylib ) 
		return report_linkEdit_error();
	
	NSSymbol tmpSymbol;

#define LOOKUP(NAME) \
	tmpSymbol = NSLookupSymbolInImage(py_dylib, "_" #NAME, PYMACAPP_NSLOOKUPSYMBOLINIMAGEFLAGS); \
	if ( !tmpSymbol ) \
		return report_linkEdit_error(); \
	NAME ## Ptr NAME = (NAME ## Ptr)NSAddressOfSymbol(tmpSymbol)

	LOOKUP(Py_SetProgramName);
	LOOKUP(Py_Initialize);
	LOOKUP(PyRun_SimpleFile);
	LOOKUP(Py_Finalize);
	LOOKUP(PySys_GetObject);
	LOOKUP(PySys_SetArgv);
	LOOKUP(PyObject_Str);
	LOOKUP(PyString_AsString);
	LOOKUP(PyObject_GetAttrString);

#undef LOOKUP

	NSString *pythonProgramName;
	// XXX - this is NOT tested with dylib builds.. but it might work if you do things "right"
	if ([[[pyLocation stringByDeletingLastPathComponent] lastPathComponent] isEqualToString:@"lib"]) {
		// $PREFIX/lib/python.dylib -> $PREFIX
		pythonProgramName = [[pyLocation stringByDeletingLastPathComponent] stringByDeletingLastPathComponent];
	} else {
		// $PREFIX/Python -> $PREFIX
		pythonProgramName = [pyLocation stringByDeletingLastPathComponent];
	}
	
	
	// Python might not copy the strings, and some evil code may not create a new NSAutoreleasePool.
	// Who knows.  We retain things just in case...	
	NSString *pyExecutableName = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"PyExecutableName"];
	if ( !pyExecutableName )
		pyExecutableName = @"python";

	pythonProgramName = [[[pythonProgramName stringByAppendingPathComponent:@"bin"] stringByAppendingPathComponent:pyExecutableName] retain];
	Py_SetProgramName([pythonProgramName fileSystemRepresentation]);
	Py_Initialize();
	PySys_SetArgv(argc, (char **)argv);
	
	NSAutoreleasePool *pythonPool = [[NSAutoreleasePool alloc] init];

	[mainPyPath retain];
	FILE *mainPy = fopen([mainPyPath fileSystemRepresentation], "r");
	int rval = PyRun_SimpleFile(mainPy, [mainPyPath fileSystemRepresentation]);
	fclose(mainPy);
	[mainPyPath release];

	while ( rval ) {
		PyObject *exc = PySys_GetObject("last_type");		
		if ( !exc ) {
			rval = report_error([NSString stringWithFormat:ERR_PYTHONEXCEPTION,"<<PyMacAppException>>","The exception went away?"]);
			break;
		}

		PyObject *exceptionClassName = PyObject_GetAttrString(exc, "__name__");
		if ( !exceptionClassName ) {
			rval = report_error([NSString stringWithFormat:ERR_PYTHONEXCEPTION,"<<PyMacAppException>>","Could not get exception class name?"]);
			break;
		}
		
		PyObject *v = PySys_GetObject("last_value");
		PyObject *exceptionName = NULL;
		if ( v )
			exceptionName = PyObject_Str(v);
				
		NSString *nsExceptionClassName = [NSString stringWithCString:PyString_AsString(exceptionClassName)];
		Py_XDECREF(exceptionClassName);exceptionClassName = NULL;
		NSString *nsExceptionName;
		if ( exceptionName ) {
			nsExceptionName = [NSString stringWithCString:PyString_AsString(exceptionName)];
			Py_XDECREF(exceptionName);exceptionName = NULL;
		} else {
			nsExceptionName = @"";
		}
		rval = report_script_error([NSString stringWithFormat:ERR_PYTHONEXCEPTION, nsExceptionClassName, nsExceptionName], nsExceptionClassName, nsExceptionName);
		break;
	}

	[pythonPool release];

	Py_Finalize();

	[pythonProgramName release];
	
	return rval;
}


int main(int argc, char * const *argv, char * const *envp)
{
	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
	int rval = pyobjc_main(argc, argv, envp);
	[pool release];
	return rval;
}
