//** Craig Laurent
#import "Converse.h"
#import "TextAppend.h"			//** for appending to Text Object

#import "MessageProtocol.h"		//** communication protocols
#import <remote/NXProxy.h>		//* setProtocolForProxy

#import <stdlib.h>				//** for malloc()
#import <string.h>				//** for string functions

//** SERVERNAME used in Communication and Converse
#define SERVERNAME "ConverseServer"


//****************************************
/* Fix for Malloc leak with unarchiving.  from NeXT. */
//****************************************


@implementation Converse

//**********
- init
{
	if (self = [super init]) {
		//** initialize unfilled objects
		prefs = nil;
		infoCtlr = nil;
		commCtlr = nil;
		userInfo = nil;
		addressList = nil;
		filename = nil;
		msgSound = nil;
		animator = nil;

		[self setPrefs:[[Preferences alloc] init]];		//** create preferences data object
		//** load filename from prefs before loading address list
		[self setFilename:[prefs addressFile]];

		//** Start communication receiving
		[self createCommZone];
		[self setServerConnection:[NXConnection registerRoot:[self commCtlr] withName:SERVERNAME]];
		if ([self serverConnection]) {
			[[self serverConnection] runFromAppKit];
		}

		[self setUserInfo:[[User alloc] init]];
		[[self userInfo] loadUser];
		[self setMsgSoundWithName:[[prefs soundFile] cString]];

		return self;
	}
	return nil;
}

- free
{
	if ([self msgSound])
		[self setMsgSound:[[self msgSound] free]];
	if ([self addressList])
		[self setAddressList:nil];	//** set will autorelease
	if ([self infoCtlr]) {
		//** free unique memory zone also
		NSZone	*memZone = [[self infoCtlr] zone];
		[self setInfoCtlr:nil];
		NSRecycleZone(memZone);
	}
	if ([self commCtlr]) {
		//** free unique memory zone also
		NSZone	*memZone = [[self commCtlr] zone];
		[self setCommCtlr:nil];
		NSRecycleZone(memZone);
	}
	if ([self prefs])
		[self setPrefs:nil];
	if ([self serverConnection])
		[self setServerConnection:[[self serverConnection] free]];
	if ([self userInfo])
		[self setUserInfo:nil];	//** will autorelease
	if ([self animator])
		[self setAnimator:nil];
	return [super free];
}

//****************************************
//** instance methods
- (Preferences*)prefs
{	return prefs; }
- (void)setPrefs:(Preferences*)newPrefs
{
	[prefs autorelease];
	prefs = [newPrefs retain];
}

- (Communication*)commCtlr
{	return commCtlr; }
- (void)setCommCtlr:(Communication*)newComm
{
	[commCtlr autorelease];
	commCtlr = [newComm retain];
}

- (NXConnection*)serverConnection
{	return serverConnection; }
- (void)setServerConnection:(NXConnection*)newServer
{
	serverConnection = newServer;
}

- (InfoController*)infoCtlr
{	return infoCtlr; }
- (void)setInfoCtlr:(InfoController*)newInfo
{
	[infoCtlr autorelease];
	infoCtlr = [newInfo retain];
}

- (User*)userInfo
{	return userInfo; }
- (void)setUserInfo:(User*)newUser
{
	[userInfo autorelease];
	userInfo = [newUser retain];
}

- (NSMutableArray*)addressList
{	return addressList; }
- (void)setAddressList:(NSMutableArray*)anArray
{
	[addressList autorelease];
	addressList = [anArray retain];
}

- (NSString*)filename
{	return filename; }
- (void)setFilename:(NSString*)string
{
	[filename autorelease];
	filename = [string retain];
}

- (Sound*)msgSound
{	return msgSound; }
- (void)setMsgSound:(Sound*)newSound
{
	[msgSound free];
	msgSound = newSound;
}
- (void)setMsgSoundWithName:(const char*)newName
{
	Sound	*newSound;

	if (newName) {
		newSound = [[Sound alloc] initFromSoundfile:newName];
		if (!newSound)
			newSound = [Sound findSoundFor:newName];
		[self setMsgSound:newSound];
	} else
		[self setMsgSound:nil];
}

- (IconAnimator*)animator
{	return animator; }
- (void)setAnimator:(IconAnimator*)newIA
{
	[animator autorelease];
	animator = [newIA retain];
}


//****************************************
//** Communication & logging methods
/* createCommZone - creates a new memory zone and instantiates a Communication Object in the new zone. */
- (void)createCommZone
{
	NSZone *commZone;

	//** perform communication in separate memory zone
	if (![self commCtlr]) {
		commZone = NSCreateZone(vm_page_size, vm_page_size, YES);
		[self setCommCtlr:[[Communication allocWithZone:commZone] initWithCtlr:self]];
	}
}

/* sendMessage: - executed to send the entered message to the selected users. */
- sendMessage:sender
{
	NSString *newMessage;

	//** get message text, log to self, send to others
	newMessage = [self messageFromText];	//* message not retained
	//* log message on sending machine
	[self logMessage:newMessage fromAuthor:[self userInfo] alert:NO];
	//* send message to selected machines
	[self transferMessage:newMessage];

	return self;
}

/* messageFromText - returns the message that is entered in the input portion of the log window. */
- (NSString*)messageFromText
{
	int numChar;
	char *message;
	NSString *string;

	//** get message from Text in ScrollView
	numChar = [inputView textLength];
	message = malloc(numChar + 1);
	[inputView getSubstring:message start:0 length:numChar];
	message[numChar] = '\0';		//** ensure end of string!

	string = [NSString stringWithCString:message];
	free(message);

	return string;
}

/* logMessage:fromAuthor:alert: - display the provided message and user information in the log window.  Run alerts if indicated. */
- (void)logMessage:(NSString*)message fromAuthor:author alert:(BOOL)yn
{
	unsigned int	row;
	NSString *nickname;

	//** get nickname to identify author
	nickname = [author userNicknameWithID:[[self prefs] discloseID] andName:[[self prefs] discloseName] andMachine:[[self prefs] discloseMachine]];

	//** put new message at the end of the Log
	//** add Author (in bold)
	[logView appendString:nickname withFontStyle:NX_BOLD];

	//** add Author/message separator (not bold)
	[logView appendString:@"\t> " withFontStyle:NX_UNBOLD];

	//** add message (regular format)
	[logView appendString:message];
	//** add newline at the end
	[logView appendString:@"\n"];

//** run Alerts if indicated
	if (yn) {
		//** play Sound as alert
		if ([[self prefs] alertAudio]) {
			[[self msgSound] play];
		}
		//** flash Icon as alert (if application is not active)
		if ((![NXApp isActive]) && ([[self prefs] alertIcon])) {
			if (![self animator]) {
				//** prepare data and instantiate IconAnimator
				NSMutableArray *images;
				NSMutableArray *pattern;

				//** create array of images
				images = [NSMutableArray array];
				[images addObject:[NXImage findImageNamed:"IconConverse"]];
				[images addObject:[NXImage findImageNamed:"IconConverseNewColor"]];
				[images addObject:[NXImage findImageNamed:"IconConverseNew"]];

				//** create array of pattern order
				pattern = [NSMutableArray array];
				[pattern addObject:[NSNumber numberWithInt:0]];
				[pattern addObject:[NSNumber numberWithInt:1]];
				[pattern addObject:[NSNumber numberWithInt:2]];
				[pattern addObject:[NSNumber numberWithInt:1]];
				[pattern addObject:[NSNumber numberWithInt:2]];
				[pattern addObject:[NSNumber numberWithInt:1]];
				[pattern addObject:[NSNumber numberWithInt:2]];
				[pattern addObject:[NSNumber numberWithInt:1]];
				[pattern addObject:[NSNumber numberWithInt:2]];
				[pattern addObject:[NSNumber numberWithInt:1]];
				[pattern addObject:[NSNumber numberWithInt:2]];
				[pattern addObject:[NSNumber numberWithInt:1]];
				[pattern addObject:[NSNumber numberWithInt:0]];

				[self setAnimator:[[IconAnimator alloc] initWithImageArray:images animationPattern:pattern andPatternLoops:1]];
			}
			[[self animator] startAnimation:nil];
		}
		//** show Window as alert
		if ([[self prefs] alertWindow]) {
			[[inputView window] makeKeyAndOrderFront:self];
		}
		//** make this application the Active application
		[NXApp activateSelf:[[self prefs] alertApplication]];

		[inputView selectAll:self];	//** prep message for typeover

		//** if author unique (not already in list)
		if ([self uniqueAddress:author inRow:&row]) {
			//** set machine name to reflect incoming machine
			[machineField setStringValue:[[author userMachine] cString]];
			[machineField selectText:self];
			[addButton setEnabled:YES];
		} else {
			//** select row in the address list
			[[machineBrowser matrixInColumn:0] selectCellAt:row :0];
			[sendInButton setEnabled:YES];
		}
	}
}

/* senderIsInvalid: - sent by DO proxy when it is removed. */
- senderIsInvalid:sender
{
//** proxy connection lost
	NXRunAlertPanel("Communication Alert", "Connection to another machine was lost.", NULL, NULL, NULL);

	//** currently don't keep connections so this isn't an issue.
	//** FUTURE enhancements will be keeping connections.
	//** Then we will want to remove the connection from connection
	//**  list.  (maybe don't even notify about connection
	//**  being removed?)
    return self;
}

//****************************************
/* add: - add machine to address list */
- add:sender
{
	User	*newUser;

	//** verify that machine name exists
	if ((![machineField stringValue]) || (0 == strcmp("", [machineField stringValue]))) {
		[addButton setEnabled:NO];
		return nil;
	}

	//** create new User, fill with new machine information
	newUser = [[User alloc] init];
	[newUser setUserMachine:[NSString stringWithCString:[machineField stringValue]]];
	
	//** check if machine already in list before adding!!
	if ([uniqueCheck state]) {
		if ([self uniqueAddress:newUser inRow:NULL]) {
			//** object is unique, add to the list.
			[[self addressList] addObject:newUser];
		} else {
			NXRunAlertPanel("Add Address", "This machine was added previously.", NULL, NULL, NULL);
			[machineField selectText:self];
			return nil;
		}
	} else {
		[[self addressList] addObject:newUser];
	}

	[self refreshAddress];		//** refresh screen information
	[addressWindow setDocEdited:YES];		//** set window dirty
	[machineField selectText:self];

	return self;
}

/* delayRemove: - send remove: message after a delay. */
- delayRemove:sender
{
	[self perform: @selector(remove:) with:nil afterDelay:0 cancelPrevious:NO];	//** (delay to finish "click" processing
	return self;
}

- remove:sender
{
	List	*selectedList;
	Cell	*currentSelectedCell;
	Matrix	*columnMatrix;
	int	counter = 0;
	int	row, col;

	[removeButton setEnabled:NO];
	selectedList = [[List alloc] init];	//* create List to load
	columnMatrix = [machineBrowser matrixInColumn:0];

	//** load list of selected rows (cells in browser)
	[machineBrowser getSelectedCells:selectedList];
	counter = [selectedList count] - 1;	//* set to last index
	if (counter >= 0)
		[addressWindow setDocEdited:YES];		//** set window dirty

	//** loop through all valid selected rows in the browser
	//** work from end, otherwise addressList index would be wrong.
	//**   indexing starts at ZERO
	while (counter >= 0) {
		//** make sure get an object before messaging it
		if (currentSelectedCell = [selectedList objectAt:counter]) {
			[columnMatrix getRow:&row andCol:&col ofCell:currentSelectedCell];

			[[self addressList] removeObjectAtIndex:row];
		}
		counter--;
	}
	
	[selectedList free];
	[self refreshAddress];	//** refresh screen with address info

	return self;
}

/* machineClicked:  - sent when user clicks on machine in the address list. */
- machineClicked:sender
{
	if ([[machineBrowser matrixInColumn:0] selectedRow] >= 0) {
		[removeButton setEnabled:YES];
		[sendInButton setEnabled:YES];
	}
	return self;
}

/* refreshAddress - reload address, reset buttons appropriately. */
- (void)refreshAddress
{
	[machineBrowser loadColumnZero];	//** reload screen
//	[machineBrowser perform: @selector(loadColumnZero) with:nil afterDelay:0 cancelPrevious:NO];	//** reload screen (delay to finish "click" processing
	[machineField selectText:self];

	//** set Buttons to appropriate status
	if ([[machineBrowser matrixInColumn:0] selectedRow] >= 0) {
		[removeButton setEnabled:YES];
		[sendInButton setEnabled:YES];
	} else {
		[removeButton setEnabled:NO];
		[sendInButton setEnabled:NO];
	}
}


/* loadAddressList - load address list from archived file. */
- (void)loadAddressList
{
	[self setAddressList:[NSMutableArray array]];

	//** default load from a file
	if ([self filename]) {
		[self openFile:nil];
		[self showAddress:nil];	 //** show address window when open.
	}

	//** cause address list to be displayed
	[self refreshAddress];	//** refresh screen with address info
}

/* uniqueAddress: - determine if newAddr is unique to the address list (no other machine in the list with the same name). */
- (BOOL)uniqueAddress:(User*)newAddr inRow:(unsigned int*)row
{
	NSEnumerator	*enumerator;
	User		*anAddr;
	NSString	*newMachineName;

	newMachineName = [newAddr userMachine];
	//** loop through all machines in the address list.
	//**  if machine name matches, bail-out because not unique
	enumerator = [[self addressList] objectEnumerator];
	while (anAddr = [enumerator nextObject]) {
		//** if same machine name, new address is NOT unique
 	    if (NSOrderedSame == [newMachineName compare:[anAddr userMachine]]) {
			//** if row not NULL, fill with index
			if (row)
				*row = [addressList indexOfObject:anAddr];
			return NO;
		}
	}

	return YES;		//** object is unique
}

//****** file stuff
/* new: - create new address list */
- new:sender
{
//** set will autorelease old list and retain the new list.
	[self setAddressList:[NSMutableArray array]];	//** create new list
	[self setFilename:nil];				//** blank out filename

	[self refreshAddress];	//** refresh screen with address info
	[[sender findCellWithTag:15] setEnabled:NO];	//** disable REVERT menu item

	[addressWindow setDocEdited:NO];		//** set window clean
	[self showAddress:nil];	 //** show address window when open.
    return self;
}

//**********
/* open: - get filename and load new address list */
- open:sender
{
	if (NO == [self newOpenFilename])
		return nil;
	[self openFile:sender];
	[self showAddress:nil];	 //** show address window when open.
    return self;
}

//**********
/* newOpenFilename - get name of file to retrieve addresses */
- (BOOL)newOpenFilename
{
	OpenPanel	*oPanel = [OpenPanel new];
	const char	*types[] = {"cvs", NULL};
	const char	*tempName;
	const char	*tempDir = "~/Library/Converse/";

	if ([self filename])
		tempDir = [[[self filename] stringByDeletingLastPathComponent] cString];

	//** get file to open
	if ([oPanel runModalForDirectory:tempDir file:NULL types:types]) {
		tempName = [oPanel filename];
		if (NULL == tempName)
			return NO;
		else {
			[self setFilename:[NSString stringWithCString:tempName]];
			return YES;
		}
	}
    return NO;
}

//**********
/* openFile: - open file and load new address list */
- openFile:sender
{
	//** if no filename, get one
	if (nil == [self filename]) {
		if (NO == [self newOpenFilename])
			return nil;
	}

	//** open file and read data
	[self setAddressList:[NSUnarchiver unarchiveObjectWithFile:[self filename]]];
	if (![self addressList]) {
		NXRunAlertPanel("File Alert", "Open failed!  Unable to read file.", NULL, NULL, NULL);
		return nil;
	}

	[self refreshAddress];	//** refresh screen with address info
	[[sender findCellWithTag:15] setEnabled:YES];	//** enable REVERT menu item

	[addressWindow setDocEdited:NO];		//** set window clean
    return self;
}

//**********
/* saveAs: - get filename and save new address list */
- saveAs:sender
{
	if (NO == [self newSaveFilename])
		return nil;
	else
		return [self save:nil];
}

//**********
/* newSaveFilename - get name of file to save addresses */
- (BOOL)newSaveFilename
{
	SavePanel	*sPanel = [SavePanel new];
	const char	*tempFile = NULL;
	const char	*tempDir = "~/Library/Converse/";
	const char	*tempName = NULL;

	//** if filename, break apart to pull up in save panel
	if ([self filename]) {
		tempDir = [[[self filename] stringByDeletingLastPathComponent] cString];
		tempName = [[[self filename] lastPathComponent] cString];
	}

	if ([sPanel runModalForDirectory:tempDir file:tempName]) {
		tempFile = [sPanel filename];
		if (NULL == tempFile)
			return NO;
		else {
			[self setFilename:[NSMutableString stringWithCString:tempFile]];
			//** make sure ".cvs" at end of filename
//** change types in InfoController as well!!
			if (NSOrderedSame != [[[self filename] pathExtension] compare:@"cvs"]) {
				[(NSMutableString*)[self filename] appendString:@".cvs"];
			}
			return YES;
		}
	}
	return NO;
}

//**********
/* save: - open file and save address list */
- save:sender
{
	//** if no filename, get one
	if (NULL == [self filename]) {
		if (NO == [self newSaveFilename])
			return nil;
	}

	//** open file and save data
	if (![NSArchiver archiveRootObject:[self addressList] toFile:[self filename]]) {
		NXRunAlertPanel("File Alert", "Save failed!  Unable to write file.", NULL, NULL, NULL);
		return nil;
	}

	[addressWindow setDocEdited:NO];		//** set window clean
    return self;
}


//****************************************
//** menu methods
/* createInfoZone - create new memory zone and instantiate InfoController in this zone. */
- (void)createInfoZone
{
	NSZone *infoZone;

	//** create info & preference panels in their own memory zone
	if (![self infoCtlr]) {
		infoZone = NSCreateZone(vm_page_size, vm_page_size, YES);
		[self setInfoCtlr:[[InfoController allocWithZone:infoZone] initWithPrefMaster:self]];
	}
}

/* showInfoPanel: - display info panel. */
- showInfoPanel:sender
{
	if (![self infoCtlr])
		[self createInfoZone];
    return [[self infoCtlr] showInfoPanel:sender];
}

/* showPreferences: - display preferences panel */
- showPreferences:sender
{
	if (![self infoCtlr])
		[self createInfoZone];
    return [[self infoCtlr] showPreferences:sender];
}

/* showLogInput: - display log/input window. */
- showLogInput:sender
{
	[[inputView window] makeKeyAndOrderFront:self];
	return self;
}

/* showAddress: - display address window */
- showAddress:sender
{
	[[machineField window] makeKeyAndOrderFront:self];
	return self;
}

//****************************************
//** delegated methods
- awakeFromNib
{
	[self loadAddressList];	//** refresh screen with address info

	//** prep the machine browser
	[[inputView window] makeKeyAndOrderFront:self];
	[inputView selectAll:self];

	[machineBrowser setDoubleAction:(@selector(delayRemove:))];	//** set action for double-click

	return self;
}

- appDidBecomeActive:sender
{
	[[self animator] stopAnimation];
	return self;
}

- appWillTerminate:sender
{
	int result;
	
	if ([addressWindow isDocEdited]) {		//** if window dirty, verify exit
		result = NXRunAlertPanel("Exit Alert", "Address File not saved.  Do you want to save?", "Save", "Don't Save", "Cancel");
		if (NX_ALERTDEFAULT == result) {
			if (nil == [self save:nil])
				return nil;		//** didn't save, don't exit
			return self;
		}
		else if (NX_ALERTALTERNATE == result)
			return self;
		else if (NX_ALERTOTHER == result)
			return nil;
	}
	return self;
}

- textDidGetKeys:sender isEmpty:(BOOL)flag
{
	//** if machine name is blank, don't allow add
	if (flag) {
		[addButton setEnabled:NO];
	} else {
		[addButton setEnabled:YES];
	}

	return nil;
}

//**********
//** NXBrowser delegate methods
- (int)browser:sender getNumRowsInColumn:(int)column
{
	//** return number of objects in the list
	return [[self addressList] count];
}

- browser:sender loadCell:cell atRow:(int)row inColumn:(int)column
{
	NSString	*nickname;

	nickname = [[[self addressList] objectAtIndex:row] userNicknameWithID:[[self prefs] discloseID] andName:[[self prefs] discloseName] andMachine:[[self prefs] discloseMachine]];
	[cell setStringValue:[nickname cString]];
	[cell setLoaded:YES];	//** tell cell it is loaded
	[cell setLeaf:YES];		//** tell cell it is a leaf (no sub info)

	return self;
}


//****************************************
/* Future enhancement: this process should be made cleaner/faster, create list of connections, (if not already connected) add to connection list before sending, disconnect before exiting program. */
/* transferMessage:fromAuthor: - transfer message to selected users. */
- (void)transferMessage:(NSString*)message
{
	User	*msgAuthor;
	List	*selectedList;
	Cell	*currentSelectedCell;
	Matrix	*columnMatrix;
	int	updates = 0;
	int	counter = 0;
	int	row, col;
	User	*proxyUser;
	User	*newProxyUser;
	NSString	*proxyMachineName;
	id	proxyObj;

	msgAuthor = [self userInfo];

	selectedList = [[List alloc] init];	//* create List to load
	columnMatrix = [machineBrowser matrixInColumn:0];

	//** load list of selected rows (cells in browser)
	[machineBrowser getSelectedCells:selectedList];
	counter = [selectedList count] - 1;	//* set to last index
	//** loop through all valid selected rows in the browser
	//** work from end, otherwise addressList index would be wrong.
	//**   indexing starts at ZERO
	while (counter >= 0) {
		//** make sure get an object before messaging it
		if (currentSelectedCell = [selectedList objectAt:counter]) {
			[columnMatrix getRow:&row andCol:&col ofCell:currentSelectedCell];

			proxyUser = [[self addressList] objectAtIndex:row];
			proxyMachineName = [proxyUser userMachine];
			//** setup proxy connection
			proxyObj = [NXConnection connectToName:SERVERNAME onHost:[proxyMachineName cString]];
			if (proxyObj) {
				//** get connection from proxy and use that to register...
				[[proxyObj connectionForProxy] registerForInvalidationNotification:self];
				//** notify proxy what protocol will be conformed
				[proxyObj setProtocolForProxy:@protocol(MessageProtocol)];

				//** if this user info is incomplete, get full info
				if (([updateCheck state]) && (nil == [proxyUser userID])) {
					//** check same machine sending to and from
					if (NSOrderedSame == [[proxyUser userMachine] compare:[[self userInfo] userMachine]]) {
						[[self addressList] replaceObjectAtIndex:row withObject:[self userInfo]];
					} else {
//** the following code will fail if you are accessing the same
//**   machine that is sending this!!  client == server
						newProxyUser = [proxyObj author];
						[[self addressList] replaceObjectAtIndex:row withObject:newProxyUser];
					}
					updates++;	//** set counter for # of updates
				}

				//** transmit message
				[proxyObj postMessage:[message cString] fromAuthor:msgAuthor];

				//** message sent, disconnect
				//** get connection from proxy and use that to unregister...
				[[proxyObj connectionForProxy] unregisterForInvalidationNotification:self];

				//** free connection and proxy
				proxyObj = [proxyObj freeProxy];
			} else {
				NXRunAlertPanel("Communication Alert", "Unable to connection to machine named: %s.", NULL, NULL, NULL, [proxyMachineName cString]);
			}
		}
		counter--;
	}

	[selectedList free];
	//** if updates have been made, process needed changes
	if (updates > 0) {
		[self refreshAddress];	//** refresh screen with address info
		[addressWindow setDocEdited:YES];		//** set window dirty
	}
}


@end
