//
//  LinkBack.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 "LinkBack.h"
#import "LinkBackServer.h"

NSString* LinkBackPboardType = @"LinkBackData" ;

// LinkBack data keys.  These are used in a LinkBack object, which is currently a dictionary.  Do not depend on these values.  They are public for testing purposes only.
NSString* LinkBackServerActionKey = @"serverActionKey" ;
NSString* LinkBackServerApplicationNameKey = @"serverAppName" ;
NSString* LinkBackServerNameKey = @"serverName" ;
NSString* LinkBackServerBundleIdentifierKey = @"bundleId" ;
NSString* LinkBackVersionKey = @"version" ;
NSString* LinkBackApplicationDataKey = @"appData" ;
NSString* LinkBackSuggestedRefreshKey = @"refresh" ;
NSString* LinkBackApplicationURLKey = @"ApplicationURL" ;

NSString* LinkBackEditActionName = @"_Edit" ;
NSString* LinkBackRefreshActionName = @"_Refresh" ;

// ...........................................................................
// Support Functions
//

id MakeLinkBackData(NSString* serverName, id appData) 
{
	return [NSDictionary linkBackDataWithServerName: serverName appData: appData] ;
}

id LinkBackGetAppData(id LinkBackData) 
{
	return [LinkBackData linkBackAppData] ;
}

NSString* LinkBackUniqueItemKey() 
{
	static int counter = 0 ;
	
	NSString* base = [[NSBundle mainBundle] bundleIdentifier] ;
	unsigned long time = [NSDate timeIntervalSinceReferenceDate] ;
	return [NSString stringWithFormat: @"%@%.8x.%.4x",base,time,counter++] ;
}

BOOL LinkBackDataBelongsToActiveApplication(id data) 
{
	return [data linkBackDataBelongsToActiveApplication] ;
}

NSString* LinkBackEditMultipleMenuTitle() 
{
	NSBundle* bundle = [NSBundle bundleForClass: [LinkBack class]] ;
	NSString* ret = [bundle localizedStringForKey: @"_EditMultiple" value: @"Edit LinkBack Items" table: @"Localized"] ;
	return ret ;
}

NSString* LinkBackEditNoneMenuTitle() 
{
	NSBundle* bundle = [NSBundle bundleForClass: [LinkBack class]] ;
	NSString* ret = [bundle localizedStringForKey: @"_EditNone" value: @"Edit LinkBack Item" table: @"Localized"] ;
	return ret ;
}

// ...........................................................................
// LinkBack Data Category
//

// Use these methods to create and access linkback data objects.  You can also use the helper functions above.

@implementation NSDictionary (LinkBackData)

+ (NSDictionary*)linkBackDataWithServerName:(NSString*)serverName appData:(id)appData 
{
	return [self linkBackDataWithServerName: serverName appData: appData actionName: nil suggestedRefreshRate: 0];
}

+ (NSDictionary*)linkBackDataWithServerName:(NSString*)serverName appData:(id)appData suggestedRefreshRate:(NSTimeInterval)rate 
{
	return [self linkBackDataWithServerName: serverName appData: appData actionName: LinkBackRefreshActionName suggestedRefreshRate: rate] ;
}

+ (NSDictionary*)linkBackDataWithServerName:(NSString*)serverName appData:(id)appData actionName:(NSString*)action suggestedRefreshRate:(NSTimeInterval)rate ;
{
	NSDictionary* appInfo = [[NSBundle mainBundle] infoDictionary] ;

	NSMutableDictionary* ret = [[NSMutableDictionary alloc] init] ;
	NSString* bundleId = [[NSBundle mainBundle] bundleIdentifier] ;
	NSString* url = [appInfo objectForKey: @"LinkBackApplicationURL"] ;
	NSString* appName = [[NSProcessInfo processInfo] processName] ;
	id version = @"A" ;

	if (nil==serverName) [NSException raise: NSInvalidArgumentException format: @"LinkBack Data cannot be created without a server name."] ;
	
	// callback information
	[ret setObject: bundleId forKey: LinkBackServerBundleIdentifierKey]; 
	[ret setObject: serverName forKey: LinkBackServerNameKey] ;
	[ret setObject: version forKey: LinkBackVersionKey] ;
	
	// additional information
	if (appName) [ret setObject: appName forKey: LinkBackServerApplicationNameKey] ;
	if (action) [ret setObject: action forKey: LinkBackServerActionKey] ;
	if (appData) [ret setObject: appData forKey: LinkBackApplicationDataKey] ;
	if (url) [ret setObject: url forKey: LinkBackApplicationURLKey] ;
	[ret setObject: [NSNumber numberWithFloat: rate] forKey: LinkBackSuggestedRefreshKey] ;
	
	return [ret autorelease] ;
}

- (BOOL)linkBackDataBelongsToActiveApplication 
{
	NSString* bundleId = [[NSBundle mainBundle] bundleIdentifier] ;
	NSString* dataId = [self objectForKey: LinkBackServerBundleIdentifierKey] ;
	return (dataId && [dataId isEqualToString: bundleId]) ;
}

- (id)linkBackAppData 
{
	return [self objectForKey: LinkBackApplicationDataKey] ;
}

- (NSString*)linkBackSourceApplicationName 
{
	return [self objectForKey: LinkBackServerApplicationNameKey] ;
}

- (NSString*)linkBackActionName 
{
	NSBundle* bundle = [NSBundle bundleForClass: [LinkBack class]] ;
	NSString* ret = [self objectForKey: LinkBackServerActionKey] ;
	if (nil==ret) ret = LinkBackEditActionName ;
	
	ret = [bundle localizedStringForKey: ret value: ret table: @"Localized"] ;
	return ret ;
}

- (NSString*)linkBackEditMenuTitle
{
	NSBundle* bundle = [NSBundle bundleForClass: [LinkBack class]] ;
	NSString* appName = [self linkBackSourceApplicationName] ;
	NSString* action = [self linkBackActionName] ;
	NSString* ret = [bundle localizedStringForKey: @"_EditPattern" value: @"%@ in %@" table: @"Localized"] ;
	ret = [NSString stringWithFormat: ret, action, appName] ;
	return ret ;
}

- (NSString*)linkBackVersion 
{
	return [self objectForKey: LinkBackVersionKey] ;
}

- (NSTimeInterval)linkBackSuggestedRefreshRate 
{
	id obj = [self objectForKey: LinkBackSuggestedRefreshKey] ;
	return (obj) ? [obj floatValue] : 0 ;
}

- (NSURL*)linkBackApplicationURL 
{
	id obj = [self objectForKey: LinkBackApplicationURLKey] ;
	if (obj) obj = [NSURL URLWithString: obj] ;
	return obj ;
}

@end

// ...........................................................................
// LinkBackServer 
//
// one of these exists for each registered server name.  This is the receiver of server requests.

// ...........................................................................
// LinkBack Class
//

NSMutableDictionary* keyedLinkBacks = nil ;

@implementation LinkBack

+ (void)initialize
{
	static BOOL inited = NO ;
	if (inited) return ;
	inited=YES; [super initialize] ;
	keyedLinkBacks = [[NSMutableDictionary alloc] init] ;
}

+ (LinkBack*)activeLinkBackForItemKey:(id)aKey 
{
	return [keyedLinkBacks objectForKey: aKey] ;
}

- (id)initServerWithClient: (LinkBack*)aLinkBack delegate: (id<LinkBackServerDelegate>)aDel 
{
	if ((self = [super init])) {
		peer = [aLinkBack retain] ;
		sourceName = [[peer sourceName] copy] ;
		sourceApplicationName = [[peer sourceApplicationName] copy] ;
		key = [[peer itemKey] copy] ;
		isServer = YES ;
		delegate = aDel ;
		[keyedLinkBacks setObject: self forKey: key] ;
	}
	
	return self ;
}

- (id)initClientWithSourceName:(NSString*)aName delegate:(id<LinkBackClientDelegate>)aDel itemKey:(NSString*)aKey ;
{
	if ((self = [super init])) {
		isServer = NO ;
		delegate = aDel ;
		sourceName = [aName copy] ;
		sourceApplicationName = [[NSProcessInfo processInfo] processName] ;
		pboard = [[NSPasteboard pasteboardWithUniqueName] retain] ;
		key = [aKey copy] ;
	}
	
	return self ;
}

- (void)dealloc
{
	[repobj release] ;
	[sourceName release] ;
	
	if (peer) [self closeLink] ;
	[peer release] ;
	
	if (!isServer) [pboard releaseGlobally] ; // client owns the pboard.
	[pboard release] ;
	
	[super dealloc] ;
}

// ...........................................................................
// General Use methods

- (NSPasteboard*)pasteboard 
{
	return pboard ;
}

- (id)representedObject 
{
	return repobj ;
}

- (void)setRepresentedObject:(id)obj 
{
	[obj retain] ;
	[repobj release] ;
	repobj = obj ;
}

- (NSString*)sourceName
{
	return sourceName ;
}

- (NSString*)sourceApplicationName 
{
	return sourceApplicationName ;
}

- (NSString*)itemKey
{
	return key ;
}

// this method is called to initial a link closure from this side.
- (void)closeLink 
{
	// inform peer of closure
	if (peer) {
		[peer remoteCloseLink] ; 
		[peer release] ;
		peer = nil ;
		[self release] ;
		[keyedLinkBacks removeObjectForKey: [self itemKey]]; 
	}
}

// this method is called whenever the link is about to be or has been closed by the other side.
- (void)remoteCloseLink 
{
	if (peer) {
		[peer release] ;
		peer = nil ;
		[self release] ;
		[keyedLinkBacks removeObjectForKey: [self itemKey]]; 
	}

	if (delegate) [delegate linkBackDidClose: self] ;
}

// ...........................................................................
// Server-side methods
//
+ (BOOL)publishServerWithName:(NSString*)name delegate:(id<LinkBackServerDelegate>)del 
{
	return [LinkBackServer publishServerWithName: name delegate: del] ;
}

+ (void)retractServerWithName:(NSString*)name 
{
	LinkBackServer* server = [LinkBackServer LinkBackServerWithName: name] ;
	if (server) [server retract] ;
}

- (void)sendEdit 
{
	if (!peer) [NSException raise: NSGenericException format: @"tried to request edit from a live link not connect to a server."] ;
	[peer refreshEditWithPasteboardName: [pboard name]] ;
}

// FROM CLIENT LinkBack
- (void)requestEditWithPasteboardName:(bycopy NSString*)pboardName
{
	// get the new pasteboard, if needed
	if ((!pboard) || ![pboardName isEqualToString: [pboard name]]) pboard = [[NSPasteboard pasteboardWithName: pboardName] retain] ;

	// pass onto delegate
	[delegate performSelectorOnMainThread: @selector(linkBackClientDidRequestEdit:) withObject: self waitUntilDone: NO] ;
}

// ...........................................................................
// Client-Side Methods
//
+ (LinkBack*)editLinkBackData:(id)data sourceName:(NSString*)aName delegate:(id<LinkBackClientDelegate>)del itemKey:(NSString*)aKey
{
	// if an active live link already exists, use that.  Otherwise, create a new one.
	LinkBack* ret = [keyedLinkBacks objectForKey: aKey] ;
	
	if(nil==ret) {
		BOOL ok = [data isKindOfClass: [NSDictionary class]] ;
		
		if (ok) {
			// collect server contact information from data.
			NSString* serverName = [data objectForKey: LinkBackServerNameKey] ;
			NSString* serverId = [data objectForKey: LinkBackServerBundleIdentifierKey];
			NSString* appName = [data linkBackSourceApplicationName] ;
			NSURL* url = [data linkBackApplicationURL] ;

			if ( !serverName || !serverId)
				[NSException raise: NSInvalidArgumentException format: @"LinkBackData is not of the correct format: %@", data] ;

			// create the live link object and try to connect to the server.
			ret = [[LinkBack alloc] initClientWithSourceName: aName delegate: del itemKey: aKey] ;
		
			if (![ret connectToServerWithName: serverName inApplication: serverId fallbackURL: url appName: appName]) {
				[ret release] ;
				ret = nil ;
			}
		} else {
			[NSException raise: NSInvalidArgumentException format: @"LinkBackData is not of the correct format: %@", data] ;
		}
	}
	
	// now with a live link in hand, request an edit
	if (ret) {
		// if connected to server, publish data and inform server.
		NSPasteboard* my_pboard = [ret pasteboard] ;
		[my_pboard declareTypes: [NSArray arrayWithObject: LinkBackPboardType] owner: ret] ;
		[my_pboard setPropertyList: data forType: LinkBackPboardType] ;
		
		[ret requestEdit] ;
		
	// if connection to server failed, return nil.
	}

	return ret ;
}

- (BOOL)connectToServerWithName:(NSString*)aName inApplication:(NSString*)bundleIdentifier fallbackURL:(NSURL*)url appName:(NSString*)appName 
{
	// get the LinkBackServer.
	LinkBackServer* server = [LinkBackServer LinkBackServerWithName: aName inApplication: bundleIdentifier launchIfNeeded: YES fallbackURL: url appName: appName] ;
	if (!server) return NO ; // failed to get server
	
	peer = [[server initiateLinkBackFromClient: self] retain] ;
	if (!peer) return NO ; // failed to initiate session
	
	// if we connected, then add to the list of active keys
	[keyedLinkBacks setObject: self forKey: [self itemKey]] ;
	
	return YES ;
}

- (void)requestEdit 
{
	if (!peer) [NSException raise: NSGenericException format: @"tried to request edit from a live link not connect to a server."] ;
	[peer requestEditWithPasteboardName: [pboard name]] ;
}

// RECEIVED FROM SERVER
- (void)refreshEditWithPasteboardName:(bycopy NSString*)pboardName
{
	// if pboard has changes, change to new pboard.
	if (![pboardName isEqualToString: [pboard name]]) {
		[pboard release] ;
		pboard = [[NSPasteboard pasteboardWithName: pboardName] retain] ;
	} 
	
	// inform delegate
	[delegate performSelectorOnMainThread: @selector(linkBackServerDidSendEdit:) withObject: self waitUntilDone: NO] ;
}

@end
