// ToolLoader.m
// By Charles G. Fleming, Educational Computing Services, Allegheny College.
// Copyright 1993, Allegheny College.
// You may freely copy, distribute and reuse this code. 
// Allegheny College and the author disclaim any warranty of any kind, 
// expressed or implied, as to its fitness for any particular use.

#import "ToolLoader.h"
#import "InspectorLoader.h"
#import "WBProtocols.h"
#import "AcceptDragScrollView.h"
#import "AcceptDragText.h"
#import "Output.h"
#import "Input.h"

#import <objc/NXBundle.h>
#import <objc/objc-runtime.h>

#include <sys/types.h>
#include <sys/dir.h>

@implementation ToolLoader

- appDidInit:sender
{
	NXBundle *mainBundle, *loadableBundle;
	char resourceFullPath[MAXPATHLEN+1];
	id ClassObject, classObject;
	const char *appDirectory;
	DIR *directoryStream;
	struct direct *directoryEntry;
	char *lastPeriod, *className, *bundleDirectory, *errorMessage;

	// Load any connectors that are in the app directory.  Store them in a 
	// list.
	[patiencePanel makeKeyAndOrderFront:self];	
	NXPing();
	mainBundle = [NXBundle mainBundle];	
	appDirectory = [mainBundle directory];
	directoryStream = opendir(appDirectory);
	while(directoryEntry = readdir(directoryStream))
	{
		className = (char *)malloc(directoryEntry->d_namlen + 1);
		strcpy(className, directoryEntry->d_name); 
		lastPeriod = rindex(className, '.');
		if(lastPeriod)
			if(strcmp(lastPeriod+1, "wbResource") == 0)
			{
				bundleDirectory = (char *)malloc(directoryEntry->d_namlen + 1);
				strcpy(bundleDirectory, className);
				*lastPeriod = '\0';

				if(objc_lookUpClass(className) == nil)
				{	
					sprintf(resourceFullPath, "%s/%s", appDirectory,
							bundleDirectory);
					loadableBundle = [[NXBundle allocFromZone:[self zone]]
							initForDirectory:resourceFullPath];

					// Get the class object from the bundle.
					if(ClassObject = [loadableBundle principalClass])
					{
						// Allocate and initialize an object.
						classObject = [[ClassObject allocFromZone:[self zone]]
								init];
						
						// Add the object to the list of connectors.
						if(classObject)			
							[connectorList addObject:classObject];
						else
						{
							errorMessage = (char *)malloc(strlen(className)+
									21);
							sprintf(errorMessage, "Unable to load %s.",
									className);
							NXRunAlertPanel("Error", errorMessage, NULL, 
									NULL, NULL);
							free(errorMessage);
						}
					}
					else
					{
						errorMessage = (char *)malloc(strlen(className) + 21);
						sprintf(errorMessage, "Unable to load %s.", className);
						NXRunAlertPanel("Error", errorMessage, NULL, 
								NULL, NULL);
						free(errorMessage);
					} 	
				}
				free(bundleDirectory);
			}
		free(className);
	}
	closedir(directoryStream);

	// The input text should accept dragged files.
	[inputText free];
	inputText = acceptDragText;
	
	// Set the input text's outputWindow and currentInspectorWindow variables.
	[acceptDragText setOutputWindow:[outputText window]];
	[acceptDragText setInspectorPanel:inspectorPanel];
	
	// The input and output windows need to have their inputText variables set.
	[outputWindow setInputText:acceptDragText];
	[inputWindow setInputText:acceptDragText];
	
	// Set up the menu so that we can print the input text.
	[printInputMenuCell setTarget:acceptDragText];
	[printInputMenuCell setAction:@selector(printPSCode:)];
	
	[patiencePanel orderOut:self];	
	return self;
}

// Miscellaneous initializations - create a list to hold the tools,
// create a default icon for tools that don't provide their own, set up a
// prototype cell for the tool matrix, and set up the matrix to hold the button
// cells that will represent loaded tools.
- init
{
	NXRect matrixRect = {0, 0, 636, 36};
	const NXSize cellSize = {36, 36};

	[super init];

	openPanel = [OpenPanel new];
	[openPanel allowMultipleFiles:YES];

	//make self the delegate of the help panel
	[[NXHelpPanel new] setDelegate:self];
		
	// Create a list to hold the loaded connectors.
	connectorList = [[List allocFromZone:[self zone]] initCount:6];

	// Create a list to hold the loaded tools.
	toolList = [[List allocFromZone:[self zone]] initCount:6];

	// Create a storage object to hold the paths of the loaded tools.
	toolPathsStorage = [[Storage allocFromZone:[self zone]] initCount:0
							elementSize:sizeof(char *) description:"*"];

	// This icon will be used if a tool to be loaded doesn't provide one.
	defaultToolIcon = [[NXImage allocFromZone:[self zone]] 	
			initFromSection:"defaultToolIcon.tiff"];
			
	// Set up a button cell that will be used to represent a loaded tool.		
	prototypeCell = [[ButtonCell allocFromZone:[self zone]]
			initIconCell:"defaultToolIcon.tiff"];
	[prototypeCell setType:NX_ONOFF];
	[prototypeCell setTarget:self];
	[prototypeCell setAction:@selector(selectTool:)];

	// Set up the matrix which will hold button cells that represent the
	// loaded tools.
	toolMatrix = [[Matrix allocFromZone:[self zone]] initFrame:&matrixRect
			mode:NX_RADIOMODE prototype:prototypeCell numRows:1 
			numCols:0];
	[toolMatrix setCellSize:&cellSize];
	return self;
}

// Insert into the input scroll view a text object that can accept dragged
// files.
- setInputScrollView:anObject
{
	NXRect contentRect = {0, 0, 0, 0};
	NXSize textSize;
	
	inputScrollView = anObject;
	
	[inputScrollView setHorizScrollerRequired:YES];
	
	[inputScrollView getContentSize:&contentRect.size];
	acceptDragText = [[AcceptDragText allocFromZone:[self zone]]
			initFrame:&contentRect text:NULL alignment:NX_LEFTALIGNED];
			
	[acceptDragText notifyAncestorWhenFrameChanged:YES];
	[acceptDragText setVertResizable:YES];
	[acceptDragText setHorizResizable:YES];
	[acceptDragText setOpaque:YES];
	
	textSize.width = contentRect.size.width;
	textSize.height = contentRect.size.height;
	[acceptDragText setMinSize:&textSize];
	
	textSize.width = 1E38;
	textSize.height = 1E38;
	[acceptDragText setMaxSize:&textSize];
	
	[acceptDragText setFont:[Font newFont:"Courier" size:14]];
	
	[inputScrollView setDocView:acceptDragText];
	[[inputScrollView superview] setAutoresizeSubviews:YES];
	[[inputScrollView superview] setAutosizing:NX_HEIGHTSIZABLE |
			NX_WIDTHSIZABLE];				
	return self;
}
	
// Add the tool matrix to the tool box.
- setToolBox:anObject
{
	NXRect scrollViewRect;

	toolBox = anObject;

	// Set up the scrollView that will hold the tools.
	[[toolBox contentView] getBounds:&scrollViewRect];
	toolScrollView = [[AcceptDragScrollView allocFromZone:[self zone]] 	
			initFrame:&scrollViewRect];
	[toolScrollView setToolLoader:self];
	[toolScrollView setHorizScrollerRequired:YES];
	[toolScrollView setDocView:toolMatrix];
	[toolScrollView setAutosizing:NX_WIDTHSIZABLE];
	
	[toolBox addSubview:toolScrollView];
	[[toolBox contentView] setAutoresizeSubviews:YES];
	return self;
}

// Load a tool if it hasn't already been loaded.  If we do load a tool,
// add a new button cell to the tool matrix to represent the new tool.  
- loadTool
{
	id	ClassObject, classObject;
	NXBundle *loadableBundle;
	const char *fileTypes[2] = {"wbTool", NULL};
	char className[MAXPATHLEN+1], tiffFilePath[MAXPATHLEN+1];
	char *period, bundleDirectory[MAXPATHLEN+1], *savePath;
	int rows, cols, toolCount;
	BOOL tiffFound;
	NXImage *toolIcon;	

	for(; *filenames; filenames++)
	{
		sprintf(bundleDirectory, "%s/%s", directory, *filenames);

		strcpy(className, *filenames);
		period = rindex(className, '.');
		*period = '\0';

		if(![self toolLoaded:className])
		{	
			loadableBundle = [[NXBundle allocFromZone:[self zone]]
					initForDirectory:bundleDirectory];

			// Get the class object from the bundle.
			if(ClassObject = [loadableBundle principalClass])
			{
				// Allocate and initialize an object.
				classObject = [[ClassObject allocFromZone:[self zone]] init];

				// Set the object's connectors.
				if(![self setToolConnectors:classObject])
					[classObject free];
				else
				{
					// Set the object's instance variables.
					if([classObject respondsTo:@selector(setInputText:)])
 						[classObject setInputText:inputText];
						
					if([classObject respondsTo:@selector(setOutputText:)])
 						[classObject setOutputText:outputText];
						
					if([classObject 
								respondsTo:@selector(setOutputViewBox:)])
 						[classObject setOutputViewBox:outputViewBox];
						
					// Add the object to the list of tools and save 
					// its path.
					[toolList addObject:classObject];
					savePath = (char *)malloc(strlen(bundleDirectory) + 1);
					strcpy(savePath, bundleDirectory);
					[toolPathsStorage addElement:&savePath];
										
					// Add a new button cell to the tool matrix.
					tiffFound = [loadableBundle getPath:tiffFilePath
							forResource:className ofType:"tiff"];
					if(tiffFound)
					{
						toolIcon = [[NXImage allocFromZone:[self zone]]
								initFromFile:tiffFilePath];
						[prototypeCell setImage:toolIcon];
					}
					else
						[prototypeCell setImage:defaultToolIcon];

					[toolMatrix addCol];
					[toolMatrix sizeToCells];
					[toolMatrix display];
					[toolMatrix getNumRows:&rows numCols:&cols];
 					[toolMatrix selectCellAt:0 :cols-1];
					[toolMatrix scrollCellToVisible:0 :cols-1];

					// Load the tool's inspector, if it has one.
					currentInspector = [classObject inspector];
					if(currentInspector)
					{	
						[self setTextFieldsTextDelegates: 
								[[currentInspector window] contentView]];
						[currentInspector revert:self];
						[inspectorLoader setCurrentInspector:
								[currentInspector window]];
						[inspectorPanel setTitle:
								[currentInspector inspectorTitle]];
					}
					
					// Load the tool's help TOC.
 					[[NXHelpPanel new] addSupplement:"Help" 
								inPath:bundleDirectory];
				}
			}
		}
		else
			NXRunAlertPanel("Error", "The tool selected has already "
					"been loaded.", NULL, NULL, NULL);
	}

	// If any tools were successfully loaded, set up the Apply Tool
	// button's target to be the last object loaded.  Show the last
	// tool's inspector if it has one. 
	toolCount = [toolList count];
	if(toolCount)
	{
		// Show the inspector of the last tool loaded.
		if(currentInspector)
			[inspectorPanel makeKeyAndOrderFront:self];
		else	
			[inspectorLoader setCurrentInspector:nil];
		
		// The target of the Apply Tool button will be the last 
		// object loaded.  The action will be "evaluate:".
		[evaluateButton setTarget:[toolList objectAt:toolCount-1]];	
		[evaluateButton setAction:@selector(evaluate:)];
	}

	// The application will not be active if tools were dragged into the 
	// tool box from the Workspace.
	if(![NXApp isActive])
	{
		[NXApp activateSelf:YES];
		[[outputText window] orderFront:self];
		[[inputText window] orderFront:self];
	}
    return self;
}

- loadTool:sender
{
	const char *fileTypes[2] = {"wbTool", NULL};

	// Get the paths to the bundles selected and load the tools.
	if([openPanel runModalForTypes:fileTypes])
	{
		filenames = [openPanel filenames];
		directory = [openPanel directory];
		[self loadTool];
	}
	return self;
}

- setToolDirectory:(const char *)aDirectory
{
	directory = aDirectory;
	return self;
}

- setToolFiles:(const char *const *)fileList
{	
	filenames = fileList;	
	return self;
}

// This is sent when one of the button cells (representing tools) is clicked.
// Make sure the correct inspector is displayed and set the target of the
// evaluate button to be the correct tool.
- selectTool:sender
{
	int column;
	id tool;

	column = [sender selectedCol];

	tool = [toolList objectAt:column];
	[evaluateButton setTarget:tool];

	currentInspector = [tool inspector];
	if(currentInspector)
	{
		[inspectorPanel setTitle:[currentInspector inspectorTitle]];
		[currentInspector revert:self];
		[inspectorLoader setCurrentInspector:[currentInspector window]];
		[inspectorPanel setDocEdited:NO];
		[inspectorPanel makeKeyAndOrderFront:self];
	}
	else
	{
		currentInspector = nil;
		[inspectorLoader setCurrentInspector:nil];
	}

	return self;
}

// Make sure the inspector is marked dirty if some editing has been done
// in one of the inspector's text fields.
- textDidChange:sender
{
	[inspectorPanel setDocEdited:YES];
	return self;
}	

// Make sure the inspector is marked clean after editing is completed.
- textDidEnd:textObject endChar:(unsigned short)whyEnd
{
	if(whyEnd == NX_RETURN)
		[inspectorPanel setDocEdited:NO];
	return self;
}		
			
// Find all text fields that are subviews of aView and assign ourself
// as their textDelegate.
- setTextFieldsTextDelegates:aView
{
	int index, count;
	List *subviewList;
	id view;
	
	subviewList = [aView subviews];
	if(subviewList)
	{
		count = [subviewList count];
		for(index = 0; index < count; index++)
		{
			view = [subviewList objectAt:index];
			if([view respondsTo:@selector(setTextDelegate:)])
				[view setTextDelegate:self];
			else
				[self setTextFieldsTextDelegates:view];			
		}		
	}
	return self;
}

// For each connector that a tool needs, see if it has been loaded.  If it
// hasn't, run an alert panel.  If the connectors have been loaded, call
// the tools set methods to set the connectors. 
- (BOOL)setToolConnectors:tool
{
	id 	connector;
	SEL setMethod;
	char *setMethodString, *errorMessage;
	char **connectors;
	int connectorCount, count, toolNameLength;
	BOOL connectorFound = YES, match;
	const char *className = NULL, *toolName;
	
	// Ask the tool what connectors it needs.		
	connectors = [tool requiredConnectors];

	toolName = [tool name];
	toolNameLength = strlen(toolName)+1;

	connectorCount = [connectorList count];
	for(; *connectors && connectorFound; connectors++)
	{
		match = NO;
		for(count = 0; count < connectorCount && !match; count++)
		{
			connector = [connectorList objectAt:count];
			className = [connector name]; 
			if(strcmp(*connectors, className) == 0)
			{
				setMethodString = (char *)malloc(5 + strlen(className));
				sprintf(setMethodString, "set%s:", className);
				setMethod = sel_getUid(setMethodString);
				[tool perform:setMethod with:connector];
 				match = YES;
				free(setMethodString);
			}
		}

		if(!match && connectorCount)
		{
			connectorFound = NO;
			errorMessage = (char *)malloc(strlen(className) + toolNameLength +
					60);
			sprintf(errorMessage, "%s cannot be loaded because the "
					"resource %s is not loaded.", toolName, *connectors);
			NXRunAlertPanel("Error", errorMessage, NULL, NULL, NULL);
			free(errorMessage);
		}
	}
	return connectorFound;
}

// See if tool has already been loaded.
- (BOOL)toolLoaded:(char *)tool
{
	int count, entry;
	BOOL found = NO;
	id loadedTool;

	count = [toolList count];
	for(entry=0; entry < count && !found; entry++)
	{
		loadedTool = [toolList objectAt:entry];
		if(strcmp([loadedTool name], tool) == 0)
			found = YES;		
	}
	return found;
}

// These delegate messages may be sent from the help panel, the input window,
// or the output window.
- windowWillClose:sender
{
	char helpFilePath[MAXPATHLEN+1];
	
	if ((sender != inputWindow) && (sender != outputWindow))
	{
		sprintf(helpFilePath, "%s/English.lproj/Help/LoadingTools.rtf", 
										[[NXBundle mainBundle] directory]);
		[sender showFile:helpFilePath atMarker:NULL];
	}
	return self;
}

- windowDidMove:sender
{
	id reply;
	
	if ((sender != inputWindow) && (sender != outputWindow))
		reply = [[NXHelpPanel new] windowDidMove:sender];
	else
		reply = self;
	
	return reply;
}
	
- windowWillResize:sender toSize:(NXSize *)frameSize;
{
	id reply;
	int numberOfTools;
	
	if ((sender != inputWindow) && (sender != outputWindow))
		reply = [[NXHelpPanel new] windowWillResize:sender toSize:frameSize];
	else
	{
		reply = self;
		if (sender == inputWindow)
		{
			// do the usual size restraints so it can't go too small
			if (frameSize -> width < 600)
				frameSize -> width = 600;
			if (frameSize -> height < 231)
				frameSize -> height = 231;
			
			// make sure the number of tools visible is always a whole number	
			numberOfTools = (int)(frameSize->width - 21) / 37;
			frameSize -> width = numberOfTools * 37 + 21;
			
		}
		else		// sender is outputWindow
		{
			if (frameSize -> width < 687)
				frameSize -> width = 687;
			if (frameSize -> height < 141)
				frameSize -> height = 141;
		}
	}
	return reply;
}

@end
