#import <appkit/appkit.h>
#import <objc/List.h>
#import <sys/file.h>
#import "MoveMatrix.h"
#import "StateManager.h"
#import "Filter.h"
#import "Script.h"
#import "FilterInspector.h"
#import "ScriptInspector.h"
#import "HelpManager.h"
#import "support.h"
#import "pscode.h"

@implementation Script

#define SCRIPT_VERSION 1
#define NOTSET strdup("")
static id	speaker;

+ initialize
{
	[Script setVersion: SCRIPT_VERSION];
	return self;
}

+ alloc
{
	self = [super alloc];
	filterList = [[List alloc] init];
	scriptName = strdup("Untitled");
	resultIconPath = NOTSET;
	helpText = NOTSET;
	return self;
}

- setStateManager: anObject
{
	stateManager = anObject;
	inspector = [stateManager scriptInspector];
	return self;
}

- createWindow
{
	static	NXRect wRect={{160.0,500.0},{211.0,119.0}};
	static	NXSize intercell = { 0.0, 1.0 };
	NXRect	wBounds;
	NXRect	optionFrame, textFrame, filterFrame, aFrame;
	NXSize	cellSize;
	id		scrollView, aButtonCell, aTextCell, aFilterCell;

	window = [[Window alloc] initContent:&wRect
		style:NX_TITLEDSTYLE
		backing:NX_BUFFERED
    		buttonMask:NX_ALLBUTTONS
    		defer:NO];
	[window setMiniwindowIcon: "script"];
	[window setDelegate: self];
	[[window contentView] getFrame: &aFrame];
	scrollView = [[ScrollView alloc] initFrame: &aFrame];
	[scrollView setVertScrollerRequired:YES];
	[scrollView setHorizScrollerRequired:YES];
	[ScrollView getContentSize: &docFrame.size
				forFrameSize: &aFrame.size
				horizScroller:(BOOL) YES
				vertScroller:(BOOL) YES
				borderType: NX_NOBORDER];
	docFrame.origin.x = docFrame.origin.y = 0.0;
	optionFrame = docFrame;
	optionFrame.origin.x += 48.0;
	optionFrame.size.width -= 96.0;
	filterFrame = textFrame = optionFrame;
	docView = [[View alloc] initFrame: &docFrame];
	[docView setClipping: NO];
	[scrollView setDocView: docView];
	[window setContentView: scrollView];

	aButtonCell = [[ButtonCell alloc] initTextCell: ""];
	[aButtonCell setIcon: "popup"];
	[aButtonCell setIconPosition: NX_ICONRIGHT];
	[aButtonCell setAltIcon: "popupH"];
	[aButtonCell setHighlightsBy: NX_NONE];
	[aButtonCell setAlignment: NX_LEFTALIGNED];
	[aButtonCell sendActionOn: NX_MOUSEDOWNMASK];
	optionFrame.origin.y = 0.0;
	optionFrame.size.height = 20.0;
	optionMatrix = [[Matrix alloc] initFrame: &optionFrame
				mode: NX_TRACKMODE
				prototype: aButtonCell
				numRows: 1 numCols: 0];
	cellSize.width = 96.0;
	cellSize.height = 20.0;
	[optionMatrix setCellSize: &cellSize];
	[optionMatrix sizeToCells];
	[optionMatrix setIntercell: &intercell];
	[docView addSubview: optionMatrix];

	aTextCell = [[TextFieldCell alloc] initTextCell: "empty"];
	[aTextCell setEditable: YES];
	[aTextCell setAlignment: NX_CENTERED];
	textFrame.origin.y = 20.0;
	textFrame.size.height = 32.0;
	textMatrix = [[Matrix alloc] initFrame: &textFrame
				mode: NX_TRACKMODE
				prototype: aTextCell
				numRows: 1 numCols: 0];
	cellSize.height = 32.0;
	[textMatrix setCellSize: &cellSize];
	[textMatrix sizeToCells];
	[textMatrix setIntercell: &intercell];
	[textMatrix setTarget: self];
	[textMatrix setAction: @selector(filterNameChanged)];
	[docView addSubview: textMatrix];

	aFilterCell = [[ButtonCell alloc] initIconCell: "filterArrow"];	// create prototype
	[aFilterCell setAltIcon: "filterArrowH"];
	[aFilterCell setBordered: NO];
	[aFilterCell setType: NX_RADIOBUTTON];
	filterFrame.origin.y = 52.0;
	matrix = [[MoveMatrix alloc] initFrame: &filterFrame
					mode: NX_RADIOMODE
					prototype: aFilterCell
					numRows: 1  numCols: 0];
	[matrix setMoveDelegate: self];	// notify me
	[matrix allowEmptySel: YES];
	[matrix setIntercell: &intercell];
	[matrix setTarget: self];
	[matrix setAction: @selector(buttonPressed)];
	cellSize.height = 48.0;
	[matrix setCellSize: &cellSize];
	[matrix sizeToCells];
	[docView addSubview: matrix];
	[window display];

	//	register this window to window server (for icon dragging)
	speaker = [NXApp appSpeaker];
	NXConvertWinNumToGlobal([window windowNum], &globalWindowNum);
	[speaker setSendPort:
	 		NXPortFromName(NX_WORKSPACEREQUEST, NULL)];
        [speaker registerWindow: globalWindowNum
			toPort:[[NXApp appListener] listenPort]];

        return self;
}

- addFilters
{
	int i, n;
	id aFilter;

	n = [filterList count];
	for (i = 0; i < n; ++i) {
		[matrix addCol];
		[textMatrix addCol];
		[optionMatrix addCol];
		aFilter= [filterList objectAt: i];
		[aFilter setStateManager: stateManager];
		[aFilter setNameCell: [textMatrix cellAt: 0 : i]];
		[aFilter setOptionCell: [optionMatrix cellAt: 0 : i]];
		[aFilter update];
	}
	sizeChanged = YES;
	return self;
}

- addFilter: newFilter
{
	int index;

	[matrix addCol];
	[textMatrix addCol];
	[optionMatrix addCol];
	[filterList addObject: newFilter];
	index = [filterList count] -1;
	[[filterList lastObject] setNameCell: [textMatrix cellAt: 0 : index]];
	[[filterList lastObject] setOptionCell: [optionMatrix cellAt: 0 : index]];
	sizeChanged = YES;
	return self;
}

- removeFilter
{
	int col;
	[filterList removeObject: activeFilter];
	activeFilter = nil;
	col = [matrix selectedCol];
	[matrix clearSelectedCell];
	[matrix removeColAt: col andFree: YES];
	[textMatrix removeColAt: col andFree: YES];
	[optionMatrix removeColAt: col andFree: YES];
	[[matrix cellList] removeObjectAt: col];
	[[textMatrix cellList] removeObjectAt: col];
	[[optionMatrix cellList] removeObjectAt: col];
	sizeChanged = YES;
	return self;
}

- workspaceBitmap:(char *)path
{
	int	   ok, length;
	char	   *tiffData;
	NXStream   *bitmapStream;
	id 	   aBitmap;

	[speaker getFileIconFor:path TIFF:&tiffData
		TIFFLength:&length ok:&ok];

	if (!ok) {
	  return nil;
	}

	bitmapStream = NXOpenMemory(tiffData, length, NX_READONLY);
	if (!bitmapStream) {
	  return nil;
	}

	aBitmap = [[NXImage alloc] initFromStream: bitmapStream];
	NXClose(bitmapStream);

	return aBitmap;
}

- drawBitmap: aBitmap side: (int) side
{
	static NXPoint aPoint = {0.0, 52.0};

	[docView lockFocus];
	if (side == 0) {
		aPoint.x = 0.0;
		[aBitmap composite: NX_SOVER toPoint: &aPoint];
	} else {
		aPoint.x = docFrame.size.width - 48.0;
		[aBitmap composite: NX_SOVER toPoint: &aPoint];
	}
	[docView unlockFocus];
}
    
// Delegate for Window class
- windowDidBecomeKey: sender
{
	[stateManager setActiveScript: self];
	if ([matrix selectedCol] >= 0) {
		activeFilter = [filterList objectAt: [matrix selectedCol]];
	} else {
		activeFilter = nil;
	}
	[stateManager setActiveFilter: activeFilter];
	return self;
}

- windowWillClose: sender
{
	int response;

	if ([window isDocEdited])  {
		response = NXRunAlertPanel("Save",
				pathname ? "Save changes to %s?" : "Save changes?",
				"Save", "No", "Cancel",
				pathname);
		switch (response) {
		case NX_ALERTDEFAULT:
			[stateManager saveScript: self];
			break;
		case NX_ALERTALTERNATE:
			break;
		case NX_ALERTOTHER:
		default:
			return nil;
		}
	}
	[speaker unregisterWindow: globalWindowNum];
	[stateManager removeScript: self];
	[stateManager removeFromActivate: self];
	return self;
}

- windowWillMiniaturize: sender toMiniwindow: aWindow
{
	miniwindow = aWindow;
	NXConvertWinNumToGlobal([miniwindow windowNum], &globalMiniwindowNum);
	[stateManager miniaturized: self];
	miniaturized = YES;
	[speaker unregisterWindow: globalWindowNum];
	[speaker registerWindow: globalMiniwindowNum
			toPort:[[NXApp appListener] listenPort]];
	return self;
}

- windowDidDeminiaturize: sender
{
	[stateManager deminiaturized: self];
	miniaturized = NO;
	[speaker unregisterWindow: globalMiniwindowNum];
	[speaker registerWindow: globalWindowNum
			toPort:[[NXApp appListener] listenPort]];
	return self;
}

// Delegate for MoveMatrix class
- matrixDidExchange: (int) index1 : (int) index2
{
	id		aFilter, list1, list2;
	char		*str1, str2;
	if (index1 < index2) {
		aFilter = [filterList removeObjectAt: index1];
		[filterList insertObject: aFilter at: index2-1];
		aFilter = [filterList removeObjectAt: index2];
		[filterList insertObject: aFilter at: index1];
	} else {
		aFilter = [filterList removeObjectAt: index2];
		[filterList insertObject: aFilter at: index1-1];
		aFilter = [filterList removeObjectAt: index1];
		[filterList insertObject: aFilter at: index2];
	}
	[[filterList objectAt: index1] setNameCell: [textMatrix cellAt: 0 : index1]];
	[[filterList objectAt: index2] setNameCell: [textMatrix cellAt: 0 : index2]];
	[[[filterList objectAt: index1] setOptionCell: [optionMatrix cellAt: 0 : index1]] update];
	[[[filterList objectAt: index2] setOptionCell: [optionMatrix cellAt: 0 : index2]] update];
	return self;
}

- (unsigned int) globalWindowNum
{
	return globalWindowNum;
}

- (unsigned int) globalMiniwindowNum
{
	return globalMiniwindowNum;
}

- window
{
	return window;
}

- miniwindow
{
	return miniwindow;
}

- update
{
	NXRect mRect, tRect, oRect;
	NXSize sSize;

	if (sizeChanged) {
		[matrix sizeToCells];
		[textMatrix sizeToCells];
		[optionMatrix sizeToCells];
		[matrix getFrame: &mRect];
		[textMatrix getFrame: &tRect];
		[optionMatrix getFrame: &oRect];
		docFrame = mRect;
		docFrame.size.height = mRect.size.height + tRect.size.height + oRect.size.height;
		docFrame.size.width += 96.0;
		[docView setFrame: &docFrame];
		sizeChanged = NO;
		if ([stateManager autosize]) {
			if (docFrame.size.width < 144.0) docFrame.size.width = 144.0;
			if (docFrame.size.width > 960.0) docFrame.size.width = 960.0;
			[ScrollView getFrameSize: &sSize
						forContentSize: &docFrame.size
						horizScroller: YES
						vertScroller:YES
						borderType:NX_NOBORDER];
			[window sizeWindow: sSize.width : sSize.height];
		}
	}
	[window setTitle: scriptName];
	[stateManager removeFromActivate: self];
	[stateManager addToActivate: self];
	[window display];
	return self;
}

- activate: sender		// glue for activate menu
{
	[window makeKeyAndOrderFront: self];
	return self;
}

//	methods to manipulate filter objects

- parameterChanged
{
	if (strcmp(scriptName, [inspector scriptName])) {
		[stateManager removeFromActivate: self];
		scriptName = stringDup(scriptName, [inspector scriptName]);
		[stateManager addToActivate: self];
	} else {
		scriptName = stringDup(scriptName, [inspector scriptName]);
	}
	confirm = [inspector confirm];
	errorTerminate = [inspector errorTerminate];
	foreground = [inspector foreground];
	iconType = [inspector iconType];
	resultIconPath = stringDup(resultIconPath, [inspector resultIconPath]);
	helpText = stringDup(helpText, [inspector helpText]);
	return self;
}

- (char *) scriptName	{return scriptName;}
- (int) confirm			{return confirm;}
- (int) errorTerminate	{return errorTerminate;}
- (int) foreground		{return foreground;}
- (int) iconType		{return iconType;}
- (char *) resultIconPath{return resultIconPath; }
- (char *) helpText		{return helpText; }

- setPathname: (char *) aString
{
	pathname = stringDup(pathname, aString);
	return self;
}
- (char *) pathname
{
	return pathname;
}

- write: (NXTypedStream *)ts	
{
	[super write: ts];
	NXWriteTypes(ts, "*iiii**@",  &scriptName, &confirm, &errorTerminate, &foreground,
				&iconType, &resultIconPath, &helpText, &filterList);
	return self;
}

- read: (NXTypedStream *)ts
{
	[super read: ts];
	if (NXTypedStreamClassVersion(ts, [self name]) != SCRIPT_VERSION) {
		NXRunAlertPanel(NULL, "Specified file contains contains incompatible data.\n"
							"You might have specified non-script file.",
					"OK", NULL, NULL);
		return nil;
	}
	NXReadTypes(ts, "*iiii**@", &scriptName, &confirm, &errorTerminate, &foreground,
				&iconType, &resultIconPath, &helpText, &filterList);
	return self;
}

- buttonPressed
{
	activeFilter = [filterList objectAt: [matrix selectedCol]];
	[stateManager setActiveFilter: activeFilter];
}

- filterNameChanged
{
	[[filterList objectAt: [textMatrix selectedCol]] nameChanged];
	return self;
}

- (int)iconEntered:(int)windowNum at:(double)x :(double)y
	iconWindow:(int)iconWindowNum 
	iconX:(double)iconX iconY:(double)iconY
	iconWidth:(double)iconWidth 
	iconHeight:(double)iconHeight
	pathList:(char *)pathList
{
	NXSize	size = {48.0, 48.0};
	NXRect	bounds;

	bounds.origin.x = bounds.origin.y = 0.0;
	bounds.size = size;
	bitmap = [[NXImage alloc] initSize: &size];
	[bitmap lockFocus];
	PSsetgray(NX_LTGRAY);
	NXRectFill(&bounds);
	copyIconPicture(iconWindowNum, (float) iconX, (float) iconY,
				(float) iconWidth, (float) iconHeight);
	[bitmap unlockFocus];

	iconPathList = stringDup(iconPathList, pathList);
	return 0;
}

- fromFile: (char *) inFile toFile: (char **) outFile
	pipeTo: (int *) pipefd waitFor: (int *) waitpid
{
	int		infd = 0, outfd = 1, pfd[2], pid;
	char		*infile, *outfile, *commandline;
	id		filter, nextFilter;
	int		invoke(char *, int, char *, int, char *);

	filter = [filterList objectAt: filterIndex];
	if (++filterIndex < [matrix cellCount])
		nextFilter = [filterList objectAt: filterIndex];
	else
		nextFilter = nil;
	if ([filter inputFromStdin]) {
		if (inFile != NULL) {
			//	namely, previous process wrote output to file; although
			//	this process demands input from stdin
			infile = inFile;
			infd = -1;
		} else {
			//	conventional piped execution (right side)
			pipe(pfd);
			*pipefd = pfd[1];
			infd = pfd[0];
		}
	} else {
		if (inFile != NULL) {
			//	conventional file-to-file, sequential execution
			//	no redirection needed
		} else {
			//	this must not occur.
		}
	}
	commandline = strdup([filter assignVariable: inFile to: [filter command]]);
	if ([filter outputToStdout]) {
		if (nextFilter == nil || ! [nextFilter inputFromStdin]) {
			//	needs redirection to temporary file
			outfile = strdup("/tmp/faXXXXXX");
			mktemp(outfile);
			*outFile = outfile;
			outfd = -1;
		} else {
			//	conventional piped execution (left side)
			[self fromFile: NULL toFile: outFile pipeTo: &outfd waitFor: waitpid];
		}
	} else {
		*outFile = [filter destination];
		if (*outFile[0] == '\0') {
			// discard result
		} else {
			*outFile = strdup([filter assignVariable: inFile to: *outFile]);
		}
	}
	pid = invoke(commandline, infd, infile, outfd, outfile);
	free(commandline);
	if (outfd <= 1) {
		*waitpid = pid;
	}
}

- showTargetIcon
{
	NXStream *bitmapStream;
	char filename[256];

	if (iconType)  {
		[self drawBitmap: [self workspaceBitmap: finalOutputFile] side: 1];
	} else {
		filename[0] = '\0';
		if (resultIconPath[0] != '/') {
			strcpy(filename, NXGetDefaultValue(APPNAME, "IconOpenPath"));
			strcat(filename, "/");
		}
		strcat(filename, resultIconPath);
		tilde_expand(filename);
		bitmapStream = NXOpenFile(open(filename, O_RDONLY), NX_READONLY);
		if (!bitmapStream) {
			return nil;
		}
		[self drawBitmap: [[NXImage alloc] initFromStream: bitmapStream] side: 1];
	}
}

- animate
{
	int	i;
	if (!iconShown && (iconType == 0 && resultIconPath || iconType && finalOutputFile))  {
		[self showTargetIcon];
		iconShown = YES;
	}
	for (i = firstFilter; i < lastFilter; ++i) {
		[[matrix cellAt: 0 : i] setIcon:
			(flip == 0 ? "filterAnim1" : (flip == 1 ? "filterAnim2" : "filterAnim3"))];
	}
	flip = ++flip % 3;
	return self;
}

void DrawIt (DPSTimedEntry te, double timeNow, void *data)
{
	[(id)data animate];
}

- (int)iconReleasedAt:(double)x :(double)y ok:(int *)flag
{
	int		response;

	if (processing && foreground) {
		*flag = NO;
		return 0;
	}
	if (confirm) {
		response = NXRunAlertPanel("Do it?", "Apply %s to \"%s\" ?", "OK", "Cancel", NULL,
							scriptName, iconPathList);
		if (response == NX_ALERTALTERNATE) {
			*flag = NO;
			return 0;
		}
	}
	[self drawBitmap: bitmap side: 0];
	[window flushWindow];
	fromFile = strdup(iconPathList);
	filterIndex = 0;
	firstFilter = lastFilter = 0;
	iconShown = NO;
	finalOutputFile = NULL;
	processing = YES;
	chdir([[filterList objectAt: 0] assignVariable: iconPathList to: "$s"]);
	[self executeOneStep];
	if (foreground) {
		selectedCol = [matrix selectedCol];
		[matrix clearSelectedCell];
		timedEntry = DPSAddTimedEntry(0.8, &DrawIt, self, NX_BASETHRESHOLD);
	}
	*flag = YES;
	return 0;
}

- executeOneStep
{
	char		*toFile;
	int		waitpid, i, n;

	if (filterIndex < [filterList count]) {
		for (i = firstFilter; i < lastFilter; ++i) {
				[[matrix cellAt: 0 : i] setIcon: "filterArrow"];
		}
		firstFilter = filterIndex;
		[self fromFile: fromFile toFile: &toFile pipeTo: NULL waitFor: &waitpid];
		lastFilter = filterIndex;
		if (fromFile)
			free(fromFile);
		fromFile = toFile;
		if (lastFilter == [filterList count])	// goal
			finalOutputFile = fromFile;
		[stateManager registerProcess: self id: waitpid];
	} else {
		processing = NO;
		if (foreground) {
			n = [matrix cellCount];
			for (i = 0; i < n; ++i) {
				[[matrix cellAt: 0 : i] setIcon: "filterArrow"];
			}
			[matrix selectCellAt: 0 : selectedCol];
			[window display];
			DPSRemoveTimedEntry(timedEntry);
		}
	}
	return self;
}

- childTerminated: (int) pid  status: (union wait *) status
{
	int response;
	int proceed = 1;

	if (WIFSTOPPED(*status)) {
		response = NXRunAlertPanel(NULL,
				"Process %d has stopped due to signal #%d. What now?",
				"Kill", "Continue", NULL,
				pid, status->w_stopsig);
		switch (response) {
		case NX_ALERTDEFAULT:
			kill(pid, SIGKILL);
			proceed = 0;
			/* fallthrough */
		case NX_ALERTALTERNATE:
			kill(pid, SIGCONT);
			break;
		default:
			;	// ignore
		}
	} else if (WIFSIGNALED(*status)) {
		NXRunAlertPanel(NULL, "Process %d was terminated by signal #%d. %s",
				"OK", NULL, NULL,
				pid, status->w_termsig, (status->w_coredump) ? "(Core dumped)" : "");
		proceed = 0;
	} else if (WIFEXITED(*status)) {
		if (errorTerminate && status->w_retcode)
			proceed = 0;
	}
	if (!proceed)
		filterIndex = 1000;		// such that (filterIndex > [filterList count]) holds
	[self executeOneStep];
	return self;
}

@end
