/*
 * Puppeteer 1.1
 *
 * Copyright (c) 1994 Primitive Software Ltd.  All rights reserved.
 *
 * Author: Dave Griffiths <dave@prim.demon.co.uk>
 */

#import "Puppeteer.h"
#import "WindowInfo.h"
#import "CyberPost.h"
#import <assert.h>

@implementation Puppeteer

+ connectToApp:(const char *)theName launch:(BOOL)launch
/*
 * Return an instance of puppeteer connected to the given app, or nil on failure.
 */
{
	id puppet = [[self alloc] init];
	
	if ([puppet connect:theName launch:launch])
		return puppet;
	else {
		[puppet free];
		return nil;
	}
}

- (BOOL)connect:(const char *)theName launch:(BOOL)launch
/*
 * Connect to the specified app. Returns YES on success.
 */
{
	/*
	 * Because the WindowInfo object sends postscript commands, we may need
	 * a postscript context for it to run in.
	 */
	if (!DPSGetCurrentContext()) {
		ctxt = DPSCreateContext(0, 0, NULL, NULL);
		DPSSetContext(ctxt);
	}

	appName = NXCopyStringBuffer(theName);
	appSpeaker = [[Speaker alloc] init];
	journalSpeaker = [[Speaker alloc] init];
	if (launch) {
		if ((appPort = NXPortFromName(appName, NULL)) == PORT_NULL)
			return NO;
	} else {
		if ((appPort = NXPortNameLookup(appName, NULL)) == PORT_NULL)
			return NO;
	}
	[appSpeaker setSendPort:appPort];

	return YES;
}

- free
{
	if (appName)
		free(appName);
	if (appSpeaker)
		[appSpeaker free];
	if (journalSpeaker)
		[journalSpeaker free];
	if (ctxt)
		DPSDestroyContext(ctxt);
	
	return [super free];
}

- postEvent:(NXEvent *)event
/*
 * This is the method which actually posts the events to the puppet.
 */
{
	assert(enabled);
	
	[journalSpeaker selectorRPC:
		"_playJournalEventType:x:y:time:flags:window:subtype:miscL0:miscL1:ctxt:" 		
		paramTypes:"iddiiiiiii", 
		event->type, 
		event->location.x, 
		event->location.y, 
		event->time, 
		event->flags | 0x80000000, 
		event->window, 
		event->data.compound.subtype, 
		event->data.compound.misc.L[0], 
		event->data.compound.misc.L[1], 
		event->ctxt];
	
	return self;
}

- postKeyboardString:(const char *)keyString flags:(int)flags
{
	int i, length = strlen(keyString);

	for (i=0; i<length; i++) {
		[self postKeyboardEvent:NX_KEYDOWN window:NX_KEYWINDOW flags:flags 
			charCode:keyString[i]];
		[self postKeyboardEvent:NX_KEYUP window:NX_KEYWINDOW flags:flags 
			charCode:keyString[i]];
	}
	
	return self;
}

- postKeyboardEvent:(int)eventType window:(int)window flags:(int)flags 
	charCode:(char)charCode
{
	NXEvent event;
	
	bzero(&event, sizeof(event));
	
	event.type = eventType;
	event.flags = flags;
	event.window = window;
	event.data.key.charCode = charCode;

	[self postEvent:&event];
	
	return self;
}

- postKeyCode:(char)charCode window:(int)window flags:(int)flags
{
	[self postKeyboardEvent:NX_KEYDOWN window:window flags:flags charCode:charCode];
	[self postKeyboardEvent:NX_KEYUP window:window flags:flags charCode:charCode];
	
	return self;
}

- postMouseEvent:(int)eventType window:(int)window flags:(int)flags 
	x:(double)x y:(double)y click:(int)click
{
	NXEvent event;
	
	bzero(&event, sizeof(event));
	
	event.type = eventType;
	event.flags = flags;
	event.window = window;
	event.location.x = x;
	event.location.y = y;
	event.data.mouse.click = click;
	
	[self postEvent:&event];
	
	return self;
}

- postSingleClick:(int)window flags:(int)flags x:(double)x y:(double)y
{
	[self postMouseEvent:NX_MOUSEDOWN window:window flags:flags x:x y:y click:1];
	[self postMouseEvent:NX_MOUSEUP window:window flags:flags x:x y:y click:1];
	
	return self;
}

- postDoubleClick:(int)window flags:(int)flags x:(double)x y:(double)y
{
	[self postSingleClick:window flags:flags x:x y:y];
	[self postMouseEvent:NX_MOUSEDOWN window:window flags:flags x:x y:y click:2];
	[self postMouseEvent:NX_MOUSEUP window:window flags:flags x:x y:y click:2];
	
	return self;
}

- postTripleClick:(int)window flags:(int)flags x:(double)x y:(double)y
{
	[self postDoubleClick:window flags:flags x:x y:y];
	[self postMouseEvent:NX_MOUSEDOWN window:window flags:flags x:x y:y click:3];
	[self postMouseEvent:NX_MOUSEUP window:window flags:flags x:x y:y click:3];
	
	return self;
}

- postActivate:(BOOL)activate
{
	NXEvent event;
	
	bzero(&event, sizeof(event));
	
	event.type = NX_KITDEFINED;
	event.data.compound.subtype = activate ? NX_APPACT : NX_APPDEACT;
	
	[self postEvent:&event];
	
	return self;
}

- dragWindow:(int)winNumber deltaX:(double)x deltaY:(double)y
{
	NXEvent event;
	
	bzero(&event, sizeof(event));
	
	event.type = NX_JOURNALEVENT;
	event.data.compound.subtype = NX_WINDRAGGED;
	event.window = winNumber;
	event.location.x = x;
	event.location.y = y;
	event.data.mouse.click = 1;
	
	[self postEvent:&event];

	return self;
}

- attachStrings
{
	port_t retPort;
	
	if (enabled)
		return self;
		
	[appSpeaker selectorRPC:"_initJournaling:::" paramTypes:"isS", 0, 0, &retPort];
	[journalSpeaker setSendPort:retPort];
	[journalSpeaker selectorRPC:"_setStatus:" paramTypes:"i", 1];
	
	enabled = YES;
		
	return self;
}

- releaseStrings
{
	if (!enabled)
		return self;
		
	[journalSpeaker selectorRPC:"_setStatus:" paramTypes:"i", 0];
	
	enabled = NO;
		
	return self;
}

- (int)getPid
/*
 * Returns the pid of the application, or -1 if it can't be found.
 */
{
	char psLine[500];
	FILE *psPipe;
	int length = strlen(appName);
	
	if (pid)
		return pid;
		
	if ((psPipe = popen("ps -axc", "r")) == NULL) {
		NXRunAlertPanel(NULL, "Could not open pipe to \"ps\"", 
			NULL, NULL, NULL);
		return -1;
	}
	fgets(psLine, 500, psPipe);	// Skip first line
	while (fgets(psLine, 500, psPipe) != NULL) {
		if (!strncmp(&psLine[20], appName, length)) {
			sscanf(psLine, "%d", &pid);
			break;
		}
	}
	pclose(psPipe);
	
	if (pid)
		return pid;
	else
		return -1;
}

- (int)getContext
/*
 * Returns the application's postscript context.
 */
{
	int numWins, *winList, i, windowPid;
	
	if (context)
		return context;
		
	[self getPid];
	if (pid < 0)
		return 0;
	PScountwindowlist(0, &numWins);
	winList = (int *)calloc(sizeof(int), numWins);
	PSwindowlist(0, numWins, winList);
	
	for (i=0; i<numWins; i++) {
		myCurrentOwner(winList[i], &context);
		myPid(context, &windowPid);
		if (windowPid == pid)
			break;
	}
	if (i == numWins) {
		fprintf(stderr, "Puppeteer: failed to find application's context\n");
		context = 0;
	}
	
	free(winList);
		
	return context;
}

- windowList
/*
 * Returns a list of this application's windows. A new list is created each time this
 * method is called, and it is the caller's responsibility to free it and it's contents.
 */
{
	int numWins, *winList, i;
	unsigned int winNumber;
	id window, windowList;
	
	[self getContext];
	if (!context)
		return NULL;
	PScountwindowlist(context, &numWins);
	winList = (int *)calloc(sizeof(int), numWins);
	PSwindowlist(context, numWins, winList);
	
	windowList = [[List alloc] init];
	for (i=0; i<numWins; i++) {
		NX_DURING
		NXConvertGlobalToWinNum(winList[i], &winNumber);
		NX_HANDLER
    		switch (NXLocalHandler.code) {
        	case dps_err_ps:
			winNumber = 0;
            		break;
       		default:
            		NX_RERAISE();
    		}
		NX_ENDHANDLER

		window = [[WindowInfo alloc] initLocalNumber:winNumber
			globalNumber:winList[i]];
		[windowList addObject:window];
	}
	free(winList);
		
	return windowList;
}

- (int)windowCount
/*
 * Return the number of windows belonging to puppet.
 */
{
	int windowCount;
	
	[self getContext];
	if (!context)
		return 0;
		
	PScountwindowlist(context, &windowCount);
	
	return windowCount;
}

#define DELTA_X 1

- windowForPseudoNumber:(int)pseudoNumber
/*
 * Returns a WindowInfo object for the given pseudo window number (eg NX_KEYWINDOW),
 * or nil if it can't be determined.
 */
{
	id iList, fList, iWin, fWin;
	int i, f, iCount, fCount, retry, iNum, fNum;
	NXRect *iFrame, fFrame;

	assert(pseudoNumber < 0);

	/*
	 * First save the initial positions of all the windows.
	 */
	iList = [self windowList];
	iCount = [iList count];
	iFrame = (NXRect *)malloc(iCount*sizeof(NXRect));
	for (i=0; i<iCount; i++) {
		iWin = [iList objectAt:i];
		[iWin getFrame:&iFrame[i]];
	}
	
	/*
	 * Now move the window by one pixel.
	 */
	[self dragWindow:pseudoNumber deltaX:DELTA_X deltaY:0];
	
	for (retry=0; retry<10; retry++) {
		fList = [self windowList];
		for (i=0; i<iCount; i++) {
			iWin = [iList objectAt:i];
			iNum = [iWin localWindowNumber];
			fCount = [fList count];
			for (f=0; f<fCount; f++) {
				fWin = [fList objectAt:f];
				fNum = [fWin localWindowNumber];
				if (fNum == iNum)
					break;
			}
			if (f<fCount) {
				[fWin getFrame:&fFrame];
				if (iFrame[i].origin.x == (fFrame.origin.x - DELTA_X)) {
					[[iList freeObjects] free];
					//[[fList freeObjects] free];
					free(iFrame);
					return fWin;
				}
			}
		}
		[[fList freeObjects] free];
		sleep(1);
		printf("Failed...\n");
	}
	
	[[iList freeObjects] free];
	free(iFrame);
	printf("Failed utterly!\n");
	
	return NULL;
}

- keyWindow
{
	return [self windowForPseudoNumber:NX_KEYWINDOW];
}

- mainWindow
{
	return [self windowForPseudoNumber:NX_MAINWINDOW];
}

- mainMenu
{
	return [self windowForPseudoNumber:NX_MAINMENU];
}

- ping
/*
 * This method simply sends a message to the puppet application and waits for it to
 * return. This ensures that the application has processed all outstanding events.
 * We use the standard msgVersion:ok: method and ignore the return value.
 */
{
	int ret, ok = 0;
	char *const *version = 0;
	
	ret = [journalSpeaker msgVersion:version ok:&ok];
	
	return self;
}

- appSpeaker
{
	return appSpeaker;
}

@end
