/*
 * tkMacOSXInit.c --
 *
 *	This file contains Mac OS X -specific interpreter initialization
 *	functions.
 *
 * Copyright (c) 1995-1997 Sun Microsystems, Inc.
 * Copyright 2001-2009, Apple Inc.
 * Copyright (c) 2005-2009 Daniel A. Steffen <das@users.sourceforge.net>
 *
 * See the file "license.terms" for information on usage and redistribution of
 * this file, and for a DISCLAIMER OF ALL WARRANTIES.
 */

#include "tkMacOSXPrivate.h"

#include <sys/stat.h>
#include <sys/utsname.h>
#include <dlfcn.h>
#include <objc/objc-auto.h>

static char tkLibPath[PATH_MAX + 1] = "";

/*
 * If the App is in an App package, then we want to add the Scripts directory
 * to the auto_path.
 */

static char scriptPath[PATH_MAX + 1] = "";

long tkMacOSXMacOSXVersion = 0;

#pragma mark TKApplication(TKInit)

#if MAC_OS_X_VERSION_MIN_REQUIRED < 1060
#define NSTextInputContextKeyboardSelectionDidChangeNotification @"NSTextInputContextKeyboardSelectionDidChangeNotification"
static void keyboardChanged(CFNotificationCenterRef center, void *observer, CFStringRef name, const void *object, CFDictionaryRef userInfo) {
    [[NSNotificationCenter defaultCenter] postNotificationName:NSTextInputContextKeyboardSelectionDidChangeNotification object:nil userInfo:nil];
}
#endif

@interface TKApplication(TKKeyboard)
- (void) keyboardChanged: (NSNotification *) notification;
@end

#if MAC_OS_X_VERSION_MAX_ALLOWED >= 1060
#define TKApplication_NSApplicationDelegate <NSApplicationDelegate>
#else
#define TKApplication_NSApplicationDelegate
#endif
@interface TKApplication(TKWindowEvent) TKApplication_NSApplicationDelegate
- (void) _setupWindowNotifications;
@end

@interface TKApplication(TKMenus)
- (void) _setupMenus;
@end

@implementation TKApplication
@synthesize poolProtected = _poolProtected;
@end

@implementation TKApplication(TKInit)
- (void) _resetAutoreleasePool
{
    if(![self poolProtected]) {
	[_mainPool drain];
	_mainPool = [NSAutoreleasePool new];
    }
}
#ifdef TK_MAC_DEBUG_NOTIFICATIONS
- (void) _postedNotification: (NSNotification *) notification
{
    TKLog(@"-[%@(%p) %s] %@", [self class], self, _cmd, notification);
}
#endif

- (void) _setupApplicationNotifications
{
    NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
#define observe(n, s) \
	[nc addObserver:self selector:@selector(s) name:(n) object:nil]
    observe(NSApplicationDidBecomeActiveNotification, applicationActivate:);
    observe(NSApplicationDidResignActiveNotification, applicationDeactivate:);
    observe(NSApplicationDidUnhideNotification, applicationShowHide:);
    observe(NSApplicationDidHideNotification, applicationShowHide:);
    observe(NSApplicationDidChangeScreenParametersNotification, displayChanged:);
    observe(NSTextInputContextKeyboardSelectionDidChangeNotification, keyboardChanged:);
#undef observe
#if MAC_OS_X_VERSION_MIN_REQUIRED < 1060
    CFNotificationCenterAddObserver(CFNotificationCenterGetDistributedCenter(), NULL, &keyboardChanged, kTISNotifySelectedKeyboardInputSourceChanged, NULL, CFNotificationSuspensionBehaviorCoalesce);
#endif
}

- (void) _setupEventLoop
{
    NSAutoreleasePool *pool = [NSAutoreleasePool new];
    [self finishLaunching];
    [self setWindowsNeedUpdate:YES];
    [pool drain];
}

- (void) _setup: (Tcl_Interp *) interp
{
    _eventInterp = interp;
    _mainPool = [NSAutoreleasePool new];
    [NSApp setPoolProtected:NO];
    _defaultMainMenu = nil;
    [self _setupMenus];
    [self setDelegate:self];
#ifdef TK_MAC_DEBUG_NOTIFICATIONS
    [[NSNotificationCenter defaultCenter] addObserver:self
	    selector:@selector(_postedNotification:) name:nil object:nil];
#endif
    [self _setupWindowNotifications];
    [self _setupApplicationNotifications];
}

- (NSString *) tkFrameworkImagePath: (NSString *) image
{
    NSString *path = nil;
    NSAutoreleasePool *pool = [NSAutoreleasePool new];
    if (tkLibPath[0] != '\0') {
	path = [[NSBundle bundleWithPath:[[NSString stringWithUTF8String:
		tkLibPath] stringByAppendingString:@"/../.."]]
		pathForImageResource:image];
    }
    if (!path) {
	const char *tk_library = Tcl_GetVar2(_eventInterp, "tk_library", NULL,
		TCL_GLOBAL_ONLY);

	if (tk_library) {
	    NSFileManager *fm = [NSFileManager defaultManager];

	    path = [[NSString stringWithUTF8String:tk_library]
		    stringByAppendingFormat:@"/%@", image];
	    if (![fm isReadableFileAtPath:path]) {
		path = [[NSString stringWithUTF8String:tk_library]
			stringByAppendingFormat:@"/../macosx/%@", image];
		if (![fm isReadableFileAtPath:path]) {
		    path = nil;
		}
	    }
	}
    }
#ifdef TK_MAC_DEBUG
    if (!path && getenv("TK_SRCROOT")) {
	path = [[NSString stringWithUTF8String:getenv("TK_SRCROOT")]
		stringByAppendingFormat:@"/macosx/%@", image];
	if (![[NSFileManager defaultManager] isReadableFileAtPath:path]) {
	    path = nil;
	}
    }
#endif
    [path retain];
    [pool drain];
    return path;
}
@end

#pragma mark -

/*
 *----------------------------------------------------------------------
 *
 * DoWindowActivate --
 *
 *	Idle handler that sets the application icon to the generic Tk icon.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

static void
SetApplicationIcon(
    ClientData clientData)
{
    NSAutoreleasePool *pool = [NSAutoreleasePool new];
    NSString *path = [NSApp tkFrameworkImagePath:@"Tk.icns"];
    if (path) {
	NSImage *image = [[NSImage alloc] initWithContentsOfFile:path];
	if (image) {
	    [NSApp setApplicationIconImage:image];
	    [image release];
	}
    }
    [pool drain];
}

/*
 *----------------------------------------------------------------------
 *
 * TkpInit --
 *
 *	Performs Mac-specific interpreter initialization related to the
 *	tk_library variable.
 *
 * Results:
 *	Returns a standard Tcl result. Leaves an error message or result in
 *	the interp's result.
 *
 * Side effects:
 *	Sets "tk_library" Tcl variable, runs "tk.tcl" script.
 *
 *----------------------------------------------------------------------
 */

int
TkpInit(
    Tcl_Interp *interp)
{
    static int initialized = 0;

    /*
     * Since it is possible for TkInit to be called multiple times and we
     * don't want to do the following initialization multiple times we protect
     * against doing it more than once.
     */

    if (!initialized) {
	int bundledExecutable = 0;
	CFBundleRef bundleRef;
	CFURLRef bundleUrl = NULL;
	struct utsname name;
	struct stat st;

	initialized = 1;

	/*
	 * Initialize/check OS version variable for runtime checks.
	 */

#if MAC_OS_X_VERSION_MIN_REQUIRED < 1050
#   error Mac OS X 10.5 required
#endif

	if (!uname(&name)) {
	    tkMacOSXMacOSXVersion = (strtod(name.release, NULL) + 96) * 10;
	}
       /*Check for new versioning scheme on Yosemite (10.10) and later.*/
	if (MAC_OS_X_VERSION_MIN_REQUIRED > 100000) {
		tkMacOSXMacOSXVersion = MAC_OS_X_VERSION_MIN_REQUIRED/100;
	    }
	if (tkMacOSXMacOSXVersion && MAC_OS_X_VERSION_MIN_REQUIRED < 100000 &&
		tkMacOSXMacOSXVersion/10 < MAC_OS_X_VERSION_MIN_REQUIRED/10) {
	    Tcl_Panic("Mac OS X 10.%d or later required !",
		    (MAC_OS_X_VERSION_MIN_REQUIRED/10)-100);
	}


#ifdef TK_FRAMEWORK
	/*
	 * When Tk is in a framework, force tcl_findLibrary to look in the
	 * framework scripts directory.
	 * FIXME: Should we come up with a more generic way of doing this?
	 */

	if (Tcl_MacOSXOpenVersionedBundleResources(interp,
		"com.tcltk.tklibrary", TK_FRAMEWORK_VERSION, 0, PATH_MAX,
		tkLibPath) != TCL_OK) {
	    TkMacOSXDbgMsg("Tcl_MacOSXOpenVersionedBundleResources failed");
	}
#endif

	{
	    NSAutoreleasePool *pool = [NSAutoreleasePool new];
		[[NSUserDefaults standardUserDefaults] registerDefaults:
		     [NSDictionary dictionaryWithObjectsAndKeys:
		     [NSNumber numberWithBool:YES],
		     @"_NSCanWrapButtonTitles",
		     [NSNumber numberWithInt:-1],
		     @"NSStringDrawingTypesetterBehavior",
		     nil]];
	    [TKApplication sharedApplication];
	    [pool drain];
	    [NSApp _setup:interp];
	}

	/* Check whether we are a bundled executable: */
	bundleRef = CFBundleGetMainBundle();
	if (bundleRef) {
	    bundleUrl = CFBundleCopyBundleURL(bundleRef);
	}
	if (bundleUrl) {
	    /*
	     * A bundled executable is two levels down from its main bundle
	     * directory (e.g. Wish.app/Contents/MacOS/Wish), whereas an
	     * unbundled executable's main bundle directory is just the
	     * directory containing the executable. So to check whether we are
	     * bundled, we delete the last three path components of the
	     * executable's url and compare the resulting url with the main
	     * bundle url.
	     */

	    int j = 3;
	    CFURLRef url = CFBundleCopyExecutableURL(bundleRef);

	    while (url && j--) {
		CFURLRef parent =
			CFURLCreateCopyDeletingLastPathComponent(NULL, url);

		CFRelease(url);
		url = parent;
	    }
	    if (url) {
		bundledExecutable = CFEqual(bundleUrl, url);
		CFRelease(url);
	    }
	    CFRelease(bundleUrl);
	}

	if (!bundledExecutable) {
	    /*
	     * If we are loaded into an executable that is not a bundled
	     * application, the window server does not let us come to the
	     * foreground. For such an executable, notify the window server
	     * that we are now a full GUI application.
	     */

	    OSStatus err = procNotFound;
	    ProcessSerialNumber psn = { 0, kCurrentProcess };

	    err = ChkErr(TransformProcessType, &psn,
		    kProcessTransformToForegroundApplication);

	    /*
	     * Set application icon to generic Tk icon, do it at idle time
	     * instead of now to ensure tk_library is setup.
	     */

	    Tcl_DoWhenIdle(SetApplicationIcon, NULL);
	}

	{
	    NSAutoreleasePool *pool = [NSAutoreleasePool new];
	    [NSApp _setupEventLoop];
	    TkMacOSXInitAppleEvents(interp);
	    TkMacOSXUseAntialiasedText(interp, -1);
	    TkMacOSXInitCGDrawing(interp, TRUE, 0);
	    [pool drain];
	}

	/*
	 * FIXME: Close stdin & stdout for remote debugging otherwise we will
	 * fight with gdb for stdin & stdout
	 */

	if (getenv("XCNOSTDIN") != NULL) {
	    close(0);
	    close(1);
	}

	/*
	 * If we don't have a TTY and stdin is a special character file of
	 * length 0, (e.g. /dev/null, which is what Finder sets when double
	 * clicking Wish) then use the Tk based console interpreter.
	 */

	if (getenv("TK_CONSOLE") ||
		(!isatty(0) && (fstat(0, &st) ||
		(S_ISCHR(st.st_mode) && st.st_blocks == 0)))) {
	    Tk_InitConsoleChannels(interp);
	    Tcl_RegisterChannel(interp, Tcl_GetStdChannel(TCL_STDIN));
	    Tcl_RegisterChannel(interp, Tcl_GetStdChannel(TCL_STDOUT));
	    Tcl_RegisterChannel(interp, Tcl_GetStdChannel(TCL_STDERR));

	    /*
	     * Only show the console if we don't have a startup script
	     * and tcl_interactive hasn't been set already.
	     */

	    if (Tcl_GetStartupScript(NULL) == NULL) {
		const char *intvar = Tcl_GetVar2(interp,
			"tcl_interactive", NULL, TCL_GLOBAL_ONLY);

		if (intvar == NULL) {
		    Tcl_SetVar2(interp, "tcl_interactive", NULL, "1",
			    TCL_GLOBAL_ONLY);
		}
	    }
	    if (Tk_CreateConsoleWindow(interp) == TCL_ERROR) {
		return TCL_ERROR;
	    }
	}
    }

    Tk_MacOSXSetupTkNotifier();

    if (tkLibPath[0] != '\0') {
	Tcl_SetVar2(interp, "tk_library", NULL, tkLibPath, TCL_GLOBAL_ONLY);
    }

    if (scriptPath[0] != '\0') {
	Tcl_SetVar2(interp, "auto_path", NULL, scriptPath,
		TCL_GLOBAL_ONLY|TCL_LIST_ELEMENT|TCL_APPEND_VALUE);
    }

    Tcl_CreateObjCommand(interp, "::tk::mac::standardAboutPanel",
	    TkMacOSXStandardAboutPanelObjCmd, NULL, NULL);
    Tcl_CreateObjCommand(interp, "::tk::mac::iconBitmap",
	    TkMacOSXIconBitmapObjCmd, NULL, NULL);

    return TCL_OK;
}

/*
 *----------------------------------------------------------------------
 *
 * TkpGetAppName --
 *
 *	Retrieves the name of the current application from a platform specific
 *	location. For Unix, the application name is the tail of the path
 *	contained in the tcl variable argv0.
 *
 * Results:
 *	Returns the application name in the given Tcl_DString.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

void
TkpGetAppName(
    Tcl_Interp *interp,
    Tcl_DString *namePtr)	/* A previously initialized Tcl_DString. */
{
    const char *p, *name;

    name = Tcl_GetVar2(interp, "argv0", NULL, TCL_GLOBAL_ONLY);
    if ((name == NULL) || (*name == 0)) {
	name = "tk";
    } else {
	p = strrchr(name, '/');
	if (p != NULL) {
	    name = p+1;
	}
    }
    Tcl_DStringAppend(namePtr, name, -1);
}

/*
 *----------------------------------------------------------------------
 *
 * TkpDisplayWarning --
 *
 *	This routines is called from Tk_Main to display warning messages that
 *	occur during startup.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Generates messages on stdout.
 *
 *----------------------------------------------------------------------
 */

void
TkpDisplayWarning(
    const char *msg,		/* Message to be displayed. */
    const char *title)		/* Title of warning. */
{
    Tcl_Channel errChannel = Tcl_GetStdChannel(TCL_STDERR);

    if (errChannel) {
	Tcl_WriteChars(errChannel, title, -1);
	Tcl_WriteChars(errChannel, ": ", 2);
	Tcl_WriteChars(errChannel, msg, -1);
	Tcl_WriteChars(errChannel, "\n", 1);
    }
}

/*
 *----------------------------------------------------------------------
 *
 * TkMacOSXDefaultStartupScript --
 *
 *	On MacOS X, we look for a file in the Resources/Scripts directory
 *	called AppMain.tcl and if found, we set argv[1] to that, so that the
 *	rest of the code will find it, and add the Scripts folder to the
 *	auto_path. If we don't find the startup script, we just bag it,
 *	assuming the user is starting up some other way.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Tcl_SetStartupScript() called when AppMain.tcl found.
 *
 *----------------------------------------------------------------------
 */

MODULE_SCOPE void
TkMacOSXDefaultStartupScript(void)
{
    NSAutoreleasePool *pool = [NSAutoreleasePool new];
    CFBundleRef bundleRef = CFBundleGetMainBundle();

    if (bundleRef != NULL) {
	CFURLRef appMainURL = CFBundleCopyResourceURL(bundleRef,
		CFSTR("AppMain"), CFSTR("tcl"), CFSTR("Scripts"));

	if (appMainURL != NULL) {
	    CFURLRef scriptFldrURL;
	    char startupScript[PATH_MAX + 1];

	    if (CFURLGetFileSystemRepresentation (appMainURL, true,
		    (unsigned char *) startupScript, PATH_MAX)) {
		Tcl_SetStartupScript(Tcl_NewStringObj(startupScript,-1), NULL);
		scriptFldrURL = CFURLCreateCopyDeletingLastPathComponent(NULL,
			appMainURL);
		if (scriptFldrURL != NULL) {
		    CFURLGetFileSystemRepresentation(scriptFldrURL, true,
			    (unsigned char *) scriptPath, PATH_MAX);
		    CFRelease(scriptFldrURL);
		}
	    }
	    CFRelease(appMainURL);
	}
    }
    [pool drain];
}

/*
 *----------------------------------------------------------------------
 *
 * TkMacOSXGetNamedSymbol --
 *
 *	Dynamically acquire address of a named symbol from a loaded dynamic
 *	library, so that we can use API that may not be available on all OS
 *	versions.
 *
 * Results:
 *	Address of given symbol or NULL if unavailable.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

MODULE_SCOPE void*
TkMacOSXGetNamedSymbol(
    const char* module,
    const char* symbol)
{
    void *addr = dlsym(RTLD_NEXT, symbol);
    if (!addr) {
	(void) dlerror(); /* Clear dlfcn error state */
    }
    return addr;
}

/*
 *----------------------------------------------------------------------
 *
 * TkMacOSXGetStringObjFromCFString --
 *
 *	Get a string object from a CFString as efficiently as possible.
 *
 * Results:
 *	New string object or NULL if conversion failed.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

MODULE_SCOPE Tcl_Obj*
TkMacOSXGetStringObjFromCFString(
    CFStringRef str)
{
    Tcl_Obj *obj = NULL;
    const char *c = CFStringGetCStringPtr(str, kCFStringEncodingUTF8);

    if (c) {
	obj = Tcl_NewStringObj(c, -1);
    } else {
	CFRange all = CFRangeMake(0, CFStringGetLength(str));
	CFIndex len;

	if (CFStringGetBytes(str, all, kCFStringEncodingUTF8, 0, false, NULL,
		0, &len) > 0 && len < INT_MAX) {
	    obj = Tcl_NewObj();
	    Tcl_SetObjLength(obj, len);
	    CFStringGetBytes(str, all, kCFStringEncodingUTF8, 0, false,
		    (UInt8*) obj->bytes, len, NULL);
	}
    }
    return obj;
}

/*
 * Local Variables:
 * mode: objc
 * c-basic-offset: 4
 * fill-column: 79
 * coding: utf-8
 * End:
 */
