//
//  LinkBackServer.m
//  LinkBack
//
//  Created by Charles Jolley on Tue Jun 15 2004.
//  Copyright (c) 2004, Nisus Software, Inc.
//  All rights reserved.

//  Redistribution and use in source and binary forms, with or without
//  modification, are permitted provided that the following conditions are met:
//
//  Redistributions of source code must retain the above copyright notice,
//  this list of conditions and the following disclaimer.
//
//  Redistributions in binary form must reproduce the above copyright notice,
//  this list of conditions and the following disclaimer in the documentation
//  and/or other materials provided with the distribution.
//
//  Neither the name of the Nisus Software, Inc. nor the names of its
//  contributors may be used to endorse or promote products derived from this
//  software without specific prior written permission.
//
//  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
//  IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
//  THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
//  PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
//  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
//  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
//  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
//  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
//  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
//  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
//  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//

#import "LinkBackServer.h"
#import "LinkBack.h"

NSString* MakeLinkBackServerName(NSString* bundleIdentifier, NSString* name)
{
    return [bundleIdentifier stringByAppendingFormat: @":%@",name] ;
}

NSMutableDictionary* LinkBackServers = nil ;

@implementation LinkBackServer

+ (void)initialize
{
    static BOOL inited = NO ;
    if (inited) return ;

    [super initialize] ;
    inited = YES ;

    if (!LinkBackServers) LinkBackServers = [[NSMutableDictionary alloc] init];
}

+ (LinkBackServer*)LinkBackServerWithName:(NSString*)aName
{
    return [LinkBackServers objectForKey: aName] ;
}

+ (BOOL)publishServerWithName:(NSString*)aName delegate:(id<LinkBackServerDelegate>)del
{
    LinkBackServer* serv = [[LinkBackServer alloc] initWithName: aName delegate: del] ;
    BOOL ret = [serv publish] ; // retains if successful
    [serv release] ;
    return ret ;
}

BOOL LinkBackServerIsSupported(NSString* name, id supportedServers)
{
	BOOL ret = NO ;
#if defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && (__MAC_OS_X_VERSION_MAX_ALLOWED >= 1050)
	NSUInteger idx ;
#else
	int idx ;
#endif
	NSString* curServer = supportedServers ;

	// NOTE: supportedServers may be nil, an NSArray, or NSString.
	if (supportedServers) {
		if ([supportedServers isKindOfClass: [NSArray class]]) {
			idx = [supportedServers count] ;
			while((NO==ret) && (idx > 0)) {
				curServer = [supportedServers objectAtIndex: --idx] ;
				ret = [curServer isEqualToString: name] ;
			}
		} else ret = [curServer isEqualToString: name] ;
	}

	return ret ;
}

NSString* FindLinkBackServer(NSString* bundleIdentifier, NSString* serverName, NSString* dir, int level)
{
	NSString* ret = nil ;

	NSFileManager* fm = [NSFileManager defaultManager] ;
#if defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && (__MAC_OS_X_VERSION_MAX_ALLOWED >= 1050)
	NSArray* contents = [fm contentsOfDirectoryAtPath: dir error: nil] ;
	NSUInteger idx ;
#else
	NSArray* contents = [fm directoryContentsAtPath: dir] ;
	int idx ;
#endif

	// working info
	NSString* cpath ;
	NSBundle* cbundle ;
	NSString* cbundleIdentifier ;
	id supportedServers ;

	NSLog(@"searching for %@ in folder: %@", serverName, dir) ;

	// resolve any symlinks, expand tildes.
	dir = [dir stringByStandardizingPath] ;

	// find all .app bundles in the directory and test them.
	idx = (contents) ? [contents count] : 0 ;
	while((nil==ret) && (idx > 0)) {
		cpath = [contents objectAtIndex: --idx] ;

		if ([[cpath pathExtension] isEqualToString: @"app"]) {
			cpath = [dir stringByAppendingPathComponent: cpath] ;
			cbundle = [NSBundle bundleWithPath: cpath] ;
			cbundleIdentifier = [cbundle bundleIdentifier] ;

			if ([cbundleIdentifier isEqualToString: bundleIdentifier]) {
				supportedServers = [[cbundle infoDictionary] objectForKey: @"LinkBackServer"] ;
				ret= (LinkBackServerIsSupported(serverName, supportedServers)) ? cpath : nil ;
			}
		}
	}

	// if the app was not found, descend into non-app dirs.  only descend 4 levels to avoid taking forever.
	if ((nil==ret) && (level<4)) {
		idx = (contents) ? [contents count] : 0 ;
		while((nil==ret) && (idx > 0)) {
			BOOL isdir ;

			cpath = [contents objectAtIndex: --idx] ;
			[fm fileExistsAtPath: cpath isDirectory: &isdir] ;
			if (isdir && (![[cpath pathExtension] isEqualToString: @"app"])) {
				cpath = [dir stringByAppendingPathComponent: cpath] ;
				ret = FindLinkBackServer(bundleIdentifier, serverName, cpath, level+1) ;
			}
		}
	}

	return ret ;
}

void LinkBackRunAppNotFoundPanel(NSString* appName, NSURL* url)
{

	// strings for panel
	NSBundle* b = [NSBundle bundleForClass: [LinkBack class]] ;
	NSString* title ;
	NSString* msg ;
	NSString* ok ;
	NSString* urlstr ;

	title = NSLocalizedStringFromTableInBundle(@"_AppNotFoundTitle", @"Localized", b, @"app not found title") ;
	ok = NSLocalizedStringFromTableInBundle(@"_OK", @"Localized", b, @"ok") ;

	msg = (url) ? NSLocalizedStringFromTableInBundle(@"_AppNotFoundMessageWithURL", @"Localized", b, @"app not found msg") : NSLocalizedStringFromTableInBundle(@"_AppNotFoundMessageNoURL", @"Localized", b, @"app not found msg") ;

	urlstr = (url) ? NSLocalizedStringFromTableInBundle(@"_GetApplication", @"Localized", b, @"Get application") : nil ;

	title = [NSString stringWithFormat: title, appName] ;

	NSAlert* alert = [[NSAlert alloc] init];
	[alert setAlertStyle:NSAlertStyleCritical];
	[alert setMessageText:title];
	[alert setInformativeText:[NSString stringWithFormat:@"%@", msg]];
	[alert addButtonWithTitle:ok];
	[alert addButtonWithTitle:urlstr];
	[alert beginSheetModalForWindow:[NSApp mainWindow] completionHandler:^(NSModalResponse returnCode) {
		if (returnCode == NSAlertSecondButtonReturn)
				[[NSWorkspace sharedWorkspace] openURL: url] ;
	}];
}

+ (LinkBackServer*)LinkBackServerWithName:(NSString*)aName inApplication:(NSString*)bundleIdentifier launchIfNeeded:(BOOL)flag fallbackURL:(NSURL*)url appName:(NSString*)appName
{
	BOOL connect = YES ;
	NSString* serverName = MakeLinkBackServerName(bundleIdentifier, aName) ;
    id ret = nil ;
	NSTimeInterval tryMark ;

	// Try to connect
	ret = [NSConnection rootProxyForConnectionWithRegisteredName: serverName host: nil] ;

    // if launchIfNeeded, and the connection was not available, try to launch.
	if((!ret) && (flag)) {
		NSString* appPath ;
		id linkBackServers ;

		// first, try to find the app with the bundle identifier
		appPath = [[NSWorkspace sharedWorkspace] absolutePathForAppBundleWithIdentifier: bundleIdentifier] ;
		linkBackServers = [[[NSBundle bundleWithPath: appPath] infoDictionary] objectForKey: @"LinkBackServer"] ;
		appPath = (LinkBackServerIsSupported(aName, linkBackServers)) ? appPath : nil ;

		// if the found app is not supported, we will need to search for the app ourselves.
		if (nil==appPath) appPath = FindLinkBackServer(bundleIdentifier, aName, @"/Applications",0);

		if (nil==appPath) appPath = FindLinkBackServer(bundleIdentifier, aName, @"~/Applications",0);

		if (nil==appPath) appPath = FindLinkBackServer(bundleIdentifier, aName, @"/Network/Applications",0);

		// if app path has been found, launch the app.
		if (appPath) {
			[[NSWorkspace sharedWorkspace] launchApplication: appName] ;
		} else {
			LinkBackRunAppNotFoundPanel(appName, url) ;
			connect = NO ;
		}
	}

    // if needed, try to connect.
	// retry connection for a while if we did not succeed at first.  This gives the app time to launch.
	if (connect && (nil==ret)) {
		tryMark = [NSDate timeIntervalSinceReferenceDate] ;
		do {
			ret = [NSConnection rootProxyForConnectionWithRegisteredName: serverName host: nil] ;
		} while ((!ret) && (([NSDate timeIntervalSinceReferenceDate]-tryMark)<10)) ;

	}

	// setup protocol and return
    if (ret) [ret setProtocolForProxy: @protocol(LinkBackServer)] ;
    return ret ;
}

- (id)initWithName:(NSString*)aName delegate:(id<LinkBackServerDelegate>)aDel
{
    self = [super init];
    if (self) {
        name = [aName copy] ;
        delegate = aDel ;
        listener = nil ;
    }

    return self ;
}

- (void)dealloc
{
    if (listener) [self retract] ;
    [name release] ;
    [super dealloc] ;
}

- (BOOL)publish
{
    NSString* serverName = MakeLinkBackServerName([[NSBundle mainBundle] bundleIdentifier], name) ;
    BOOL ret = YES ;

    // create listener and connect
    NSPort* port = [NSPort port] ;
    listener = [NSConnection connectionWithReceivePort: port sendPort:port] ;
    [listener setRootObject: self] ;
    ret = [listener registerName: serverName] ;

    // if successful, retain connection and add self to list of servers.
    if (ret) {
        [listener retain] ;
        [LinkBackServers setObject: self forKey: name] ;
    } else listener = nil ; // listener will dealloc on its own.

    return ret ;
}

- (void)retract
{
    if (listener) {
        [listener invalidate] ;
        [listener release] ;
        listener = nil ;
    }

    [LinkBackServers removeObjectForKey: name] ;
}

- (LinkBack*)initiateLinkBackFromClient:(LinkBack*)clientLinkBack
{
    LinkBack* ret = [[LinkBack alloc] initServerWithClient: clientLinkBack delegate: delegate] ;

    // NOTE: we do not release because LinkBack will release itself when it the link closes. (caj)

    return ret ;
}

@end
