//  Thinker.m
//
//  This class is the brains behind the BackSpace app; it is the Application
//  object's delegate, and it watches the system to determine when to
//  initiate the screen saver mode.
//
//  You may freely copy, distribute, and reuse the code in this example.
//  NeXT disclaims any warranty of any kind, expressed or  implied, as to its
//  fitness for any particular use.


#import "Thinker.h"
#import "BackWindow.h"
#import "BackView.h"
#import "SpaceView.h"
#import "MySlider.h"
#import "Password.h"
#import "psfuncts.h"

#import <appkit/appkit.h>
#import <objc/NXBundle.h>

// convert vertical blank time to milliseconds
#define SEC2MS(x) ((x * 1000) + 20)

//#define SHOWITERATIONSPERSEC

extern NXRect getBoundsForAllScreens(void);

#ifdef SHOWITERATIONSPERSEC
unsigned iterations;
BStimeval then, now, targetTime;
#endif

static id _BSThinker;

id BSThinker()
{	return _BSThinker;
}


@implementation Thinker

- appDidInit:sender
{
	const char *autoLaunch;
	globalTier = BACKGROUNDTIER;
	openAnother = YES;
	_BSThinker = self;

	backZone = NXCreateZone(vm_page_size, vm_page_size, YES);

	NXSetRect(&windowRect, 475, 300, 500, 450);

	[NXApp getScreens:&screens count:&screenCount];

	[commonImageInspector getFrame: &inspectorFrame];
	currentInspector = commonImageInspector;
	
	[self getViewType];
	[self setVirtualViewIndexAndIncrement:NO];
	[self getWindowType];

	[self getScreenSaverSetting];
	[self getScreenLockerSetting];
	[self getPrioritySetting];
	[self getImageFile];
	[self getHotCornerSetting];

	autoLaunch = NXGetDefaultValue([NXApp appName], "NXAutoLaunch");
	if (strcmp(autoLaunch,"YES"))
	{
		[[windMatrix window] makeKeyAndOrderFront:self];
		windowHasBeenDisplayed = YES;
	}
	else [NXApp hide:self];
	
#ifdef SHOWITERATIONSPERSEC
	then = currentTimeInMs();
	targetTime = then + 10000;
#endif
	
	srandom(time(0));

	return self;
}

- appDidHide:sender
{
	if (windowType != BACKWINDOW) [self removeTimer];
	return self;
}

- appDidUnhide:sender
{
	if (!windowHasBeenDisplayed)
	{
		[[windMatrix window] makeKeyAndOrderFront:self];
		windowHasBeenDisplayed = YES;
	}

	if (windowType != NOWINDOW) [self createTimer];
	return self;
}



// Pretty much a dummy function to invoke the step method.

void timedEntryFunction (DPSTimedEntry timedEntry, double timeNow, void *theObject)
{	[(id)theObject doDistributorLoop];
}

- createTimer
{
	if (!timerValid)
	{
		timerValid = YES;
		timer = DPSAddTimedEntry(0.02, &timedEntryFunction, self, NX_BASETHRESHOLD);
	}
	return self;
}

- removeTimer
{
	if (timerValid) DPSRemoveTimedEntry (timer);
	timerValid = NO;
	return self;
}

- doDistributorLoop
{
    NXEvent dummyEvent;
	
	keepLooping = YES;
	[spaceView lockFocus];
	if ([spaceView respondsTo:@selector(didLockFocus)]) [spaceView didLockFocus];

	do {
		[spaceView oneStep];
		[spaceWindow flushWindow];
		NXPing ();	// Synchronize postscript for smoother animation

		[spaceView oneStep];
		[spaceWindow flushWindow];
		NXPing ();	// Synchronize postscript for smoother animation

#ifdef SHOWITERATIONSPERSEC
			iterations++;
			if ((now = currentTimeInMs()) > targetTime)
			{
				printf("BackSpace: %5.1f its/sec\n",
					(double)iterations*1000.0/(double)(now - then));
				iterations = 0;
				targetTime = now + 10000;
				then = now;
			}
#endif
			
	   } while (timerValid && keepLooping &&
			([NXApp peekNextEvent:NX_ALLEVENTS into:&dummyEvent 
				waitFor:0 threshold:NX_BASETHRESHOLD] == NULL));

	[spaceView unlockFocus];
	
	return self;

}

- installSpaceViewIntoWindow:w
{
	NXRect cvrect;
	int i;
	id subviews, contentView;
	
	if (!w) return nil;
	contentView = [w contentView];

	// get size of content view
	[contentView getBounds:&cvrect];
	
	// remove old subviews, this is overkill really...
	subviews = [contentView subviews];
	for (i=([subviews count]-1); i>=0; i--)
	{	[[subviews objectAt:i] removeFromSuperview];
	}
	
	// install it into the window's content view
	[contentView addSubview:spaceView];
	[contentView setAutoresizeSubviews:YES];
	[spaceView setAutosizing:NX_WIDTHSIZABLE | NX_HEIGHTSIZABLE];

	// size the spaceview
	[spaceView sizeTo:cvrect.size.width :cvrect.size.height];
	
	return self;
}

- useNormalWindow
{
	int myBacking;
	
	spaceView = [self backView];
	myBacking = [self backingTypeForView:spaceView];

	if (!normalWindow)
	{
		normalWindow = [[Window allocFromZone:backZone]
			initContent:&windowRect style:NX_RESIZEBARSTYLE
			backing:myBacking 
			buttonMask:NX_CLOSEBUTTONMASK
			defer:NO];
		
		[self setWindowTitle];
		[normalWindow useOptimizedDrawing:YES];
		[normalWindow setDynamicDepthLimit:YES]; //want window depth to match device!
		[normalWindow setOneShot:YES];
		[normalWindow setDelegate:self];
		[normalWindow setBackgroundGray:NX_BLACK];
	}

	spaceWindow = normalWindow;
	[self installSpaceViewIntoWindow:spaceWindow];

	if ([spaceView respondsTo:@selector(setImage:)])
		[spaceView setImage: image];
	if ([spaceView respondsTo:@selector(newWindow)]) [spaceView newWindow];
	[spaceWindow display];

	[spaceWindow makeKeyAndOrderFront:self];
	
	// need to do this so flushing always works!
	// must do it late because kit does lazy window creation ie the PostScript
	// window might not exist until you actually draw to it
	
	if (myBacking == NX_RETAINED)
		[spaceWindow setBackingType:NX_RETAINED];
	else [spaceWindow setBackingType:NX_BUFFERED];

	return self;
}

- (int) backingTypeForView:aView
{
	if ([aView respondsTo:@selector(useBufferedWindow)] 
		&& [aView useBufferedWindow])
		return NX_BUFFERED;
	return NX_RETAINED;
}

- useBackWindow:(int)tier
{
	NXRect r= getBoundsForAllScreens();
	int myBacking;
	
	spaceView = [self backView];
	myBacking = [self backingTypeForView:spaceView];
	
	[self createBigWindowIfNecessaryForView:spaceView];

	if (myBacking == NX_RETAINED)
	{	spaceWindow = bigUnbufferedWindow;
		tweakWindow([spaceWindow windowNum], tier);
	}
	else
	{	spaceWindow = bigBufferedWindow;
	}

	[self installSpaceViewIntoWindow:spaceWindow];

	if ([spaceView respondsTo:@selector(setImage:)])
		[spaceView setImage: image];

	[spaceWindow placeWindow:&r];
	if (myBacking == NX_BUFFERED) [spaceWindow display];

	[spaceWindow orderFront:self];
	if (myBacking == NX_BUFFERED) tweakWindow([spaceWindow windowNum], tier);
	else [spaceWindow display];

	if ([spaceView respondsTo:@selector(newWindow)]) [spaceView newWindow];

	return self;
}

- createBigWindowIfNecessaryForView:aView
{
	NXRect r=getBoundsForAllScreens();
	int myBacking = [self backingTypeForView:aView];
	
	if ((myBacking == NX_RETAINED) && !bigUnbufferedWindow)
	{
	
		bigUnbufferedWindow = [[BackWindow allocFromZone:backZone]
			initContent:&r style:NX_TOKENSTYLE
			backing:NX_NONRETAINED buttonMask:0 defer:NO];

		[bigUnbufferedWindow useOptimizedDrawing:YES];

		[bigUnbufferedWindow removeFromEventMask:(NX_LMOUSEDOWNMASK | NX_LMOUSEUPMASK
  			   | NX_MOUSEMOVEDMASK | NX_LMOUSEDRAGGEDMASK
			   | NX_MOUSEENTEREDMASK | NX_MOUSEEXITEDMASK
			   | NX_KEYDOWNMASK | NX_KEYUPMASK
			   | NX_CURSORUPDATEMASK)];
		[bigUnbufferedWindow setBackgroundGray:NX_BLACK];
	}

	if ((myBacking == NX_BUFFERED) && !bigBufferedWindow)
	{

		bigBufferedWindow = [[BackWindow allocFromZone:backZone]
			initContent:&r style:NX_TOKENSTYLE
			backing:NX_BUFFERED buttonMask:0 defer:NO];

		[bigBufferedWindow useOptimizedDrawing:YES];

		[bigBufferedWindow removeFromEventMask:(NX_LMOUSEDOWNMASK | NX_LMOUSEUPMASK
  			   | NX_MOUSEMOVEDMASK | NX_LMOUSEDRAGGEDMASK
			   | NX_MOUSEENTEREDMASK | NX_MOUSEEXITEDMASK
			   | NX_KEYDOWNMASK | NX_KEYUPMASK
			   | NX_CURSORUPDATEMASK)];

		[bigBufferedWindow setDynamicDepthLimit:YES]; //want window depth to match device!
		[bigBufferedWindow setOneShot:YES];
		[bigBufferedWindow setBackgroundGray:NX_BLACK];
	}

	return self;
}

- changeWindowType:sender
{
	[self changeWindowTypeAndRemember:YES];
	return self;
}

- changeWindowTypeAndRemember:(BOOL)rem
{
	char str[10];
	int newWindowType;

	newWindowType = [windMatrix selectedRow];
	if (newWindowType == windowType) return self;
	
	windowType = newWindowType;

	if (rem)
	{
		sprintf(str,"%1d", windowType);
		NXWriteDefault([NXApp appName], "windowType", str);
	}

	[spaceWindow orderOut:self];
	
	switch (windowType)
	{
		case NOWINDOW:
			[self removeTimer];
			break;
		case NORMALWINDOW:
			[self useNormalWindow];
			[self createTimer];
			break;
		case BACKWINDOW:
			[self useBackWindow: globalTier];
			[self createTimer];
			break;
	}
	
	return self;
}

- getWindowType
{
	int tWindowType = NORMALWINDOW;
	const char *ptr;
	int val;

	ptr = NXGetDefaultValue([NXApp appName], "windowType");
	if (ptr)
	{
		sscanf(ptr,"%d",&val);
		if (val >= 0 && val <= 2) tWindowType = val;
	}
	
	[windMatrix selectCellAt:tWindowType :0];
	[self changeWindowTypeAndRemember:NO];

	return self;
}

- getScreenSaverSetting
{
	const char *ptr;
	
	if((evs = NXOpenEventStatus()) == 0)
	{	perror("NXOpenEventStatus failed.");
        exit(10);
	}
	
	[self getDimBrightness:&dimBrightness];
	
	//in case the old dim brightness is somehow invalid, I reset it
	if (dimBrightness > .25)
	{
		dimBrightness = .25;
		[self _setDimBrightness:&dimBrightness];
	}

	[screenSaver setState:0];
	
	ptr = NXGetDefaultValue([NXApp appName], "screenSaver");

	if (!ptr || !strcmp(ptr,"Off")) [self setScreenSaver:NO andRemember:NO];
	else [self setScreenSaver:YES andRemember:NO];
	
	return self;
}

- changeScreenSaverSetting:sender
{
	[self setScreenSaver:([screenSaver state])andRemember:YES];
	return self;
}

- setScreenSaver:(BOOL)val andRemember:(BOOL)rem
{
	[screenSaver setState:val];
	screenSaverVal = val;
	
	if (val)
	{
		// turn it on...
		[self calcDimTime];
		if (rem) NXWriteDefault([NXApp appName], "screenSaver", "On");
	}
	else
	{
		// turn it off...
		if (rem) NXRemoveDefault([NXApp appName], "screenSaver");
	}
	
	return self;
}

- calcDimTime
{
	double dimTime;
	[self getDimTime :&dimTime];
	
	if (dimTime < 0) dimTime = .1;
	
	if (screenSaverVal && !doingSaver)
	{
		// printf("BackSpace calcDimTime: dims in %f seconds\n",dimTime);
	
		[self perform:@selector(maybeDoScreenSaver:)
			with:self
			afterDelay:SEC2MS(dimTime)
			cancelPrevious:YES];
	}
			
	return self;
}

- maybeDoScreenSaver:sender
{
	NXEvent anEvent;
	BOOL autoDimmed;

	// in case timed entry fires but user has killed screen saver
	if (!screenSaverVal || doingSaver) return self;
	
	autoDimmed = NXAutoDimState(evs);
	if (!autoDimmed)
	{
		[self calcDimTime];
		return self;
	}

	// The perform:afterDelay: method starts a timed entry to
	// invoke maybeDoScreenSaver, so we are in a timed entry
	// right now.  If we just jumped into doScreenSaver:, we
	// would interrupt the doDistributorLoop method while
	// it's still focused on the spaceView.  By posting an
	// event, we force that loop to bail out so we can jump
	// into the screen saver cleanly.
	
	keepLooping = NO;	// There was a bug related to this at one point.
						// I don't think it's necessary anymore.
	anEvent.type = NX_APPDEFINED;
	anEvent.data.compound.subtype = BSDOSAVER;
	anEvent.ctxt = [NXApp context];
	DPSPostEvent(&anEvent,0);
	
	return self;
}

- applicationDefined:(NXEvent *)theEvent
{
	switch (theEvent->data.compound.subtype)
	{
	case BSDOSAVER:
		[self doScreenSaver:self];
		[self calcDimTime];			// reset to fire again
		break;
	case BSOPENFILE:
		[self doDelayedOpenFile];
		break;
	default:
		break;
	}
	return self;
}

- showFakeScreenSaverAfterPause:sender
{
	usleep(250000);
	return [self showFakeScreenSaver:sender];
}

- showFakeScreenSaver:sender
{
	[self screenSaverMode];
	NXSetAutoDimState(evs, YES);
	[self doScreenSaver:self];
	NXSetAutoDimState(evs, NO);
	[self normalMode];			//usually not necessary
	
	// reset to fire again
	[self calcDimTime];
		
	return self;
}


- doScreenSaver:sender
{
	BOOL autoDimmed;
	int oldWindowType;
	BOOL mouseOK, oldTimerValid;
	BOOL ignoreMouseMovement = NO;
	BOOL isHidden;
	NXRect trackingRect;
	NXPoint mouseLoc;
	BOOL passwordOK;
	BOOL stoleActivation = NO;
	int oldActiveApp = 0;
	int iterationCount = 0;
		
	// must be sure we don't enter on timed entry after faking saver
	doingSaver = YES;
	
	isHidden = [NXApp isHidden];
	if (isHidden)
	{
		[NXApp unhideWithoutActivation:self];
	}

	// force activation here so we get keystrokes if someone
	// just types his password.
	if ([password isLocked])
	{
		oldActiveApp = [NXApp activateSelf:YES];
		stoleActivation = YES;
	}
	
	[self setVirtualViewIndexAndIncrement:YES];

	//save old window state
	oldWindowType = [windMatrix selectedRow];

	globalTier = SAVERTIER;

	[self blackOutAllScreens];
	
	//background window on screen
	[windMatrix selectCellAt:BACKWINDOW :0];
	[self changeWindowTypeAndRemember:NO];
	
	//nuke timer so timed entry doesn't fire
	oldTimerValid = timerValid;
	[self removeTimer];


	//set background window tier to SAVERTIER
	if ([self backingTypeForView:spaceView] == NX_BUFFERED)
	{
		// make sure the one shot buffer really exists
		//[spaceWindow display];	//xxx
		if ([spaceWindow windowNum] <= 0) [spaceWindow display];
		PSsetwindowlevel(SAVERTIER, [spaceWindow windowNum]);
	}
	else 
	{
		PSsetwindowlevel(SAVERTIER, [spaceWindow windowNum]);
		[spaceView fillBoundsWithBlack];
		[spaceView display];
	}

	NXPing();
	[self screenSaverMode];

	PSsetwaitcursorenabled(NO);

	if ([spaceView respondsTo:@selector(enteredScreenSaverMode)])
		[spaceView enteredScreenSaverMode];

	do {
		//obscure cursor
		PSobscurecursor();
	
		[spaceView lockFocus];
		if ([spaceView respondsTo:@selector(didLockFocus)])
			[spaceView didLockFocus];


		if ([spaceView respondsTo:@selector(ignoreMouseMovement)])
			ignoreMouseMovement = [spaceView ignoreMouseMovement];

		[spaceWindow getMouseLocation:&mouseLoc];
		trackingRect.origin.x = mouseLoc.x - 100;
		trackingRect.origin.y = mouseLoc.y - 100;
		trackingRect.size.width = trackingRect.size.height = 200;
	
		do {
			if ((++iterationCount & 0x7f)==0) PSobscurecursor();

			[spaceView oneStep];
			[spaceWindow flushWindow];
			NXPing();	// Synchronize postscript for smoother animation

			// note: window and view coordinates the same!
			// so I don't have to convert to view coord system
			if (ignoreMouseMovement) mouseOK = YES;
			else
			{
				[spaceWindow getMouseLocation:&mouseLoc];
				mouseOK = [spaceView mouse:&mouseLoc inRect:&trackingRect];
			}
		
			[spaceView oneStep];
			[spaceWindow flushWindow];
			NXPing();	// Synchronize postscript for smoother animation

			autoDimmed = NXAutoDimState(evs);
		} while (autoDimmed && mouseOK);
	
		[spaceView unlockFocus];

		// force activation here in case anyone changed it out
		// from under me.  (workspace does this, dog gone it!)
		if ([password isLocked])
		{
			[NXApp activateSelf:YES];
		}

		passwordOK = [password checkPassword: 
			NXLocalString("Screen is locked.  Enter password to unlock:",0,0) 
			randomPos:YES checkLock:YES withView:spaceView];

		if (!passwordOK) NXSetAutoDimState(evs, YES);

	} while (!passwordOK);

	if ([spaceView respondsTo:@selector(willExitScreenSaverMode)])
		[spaceView willExitScreenSaverMode];

	NXSetAutoDimState(evs, NO);

	PSsetwaitcursorenabled(YES);

	[self normalMode];

	//background window tier to BACKGROUNDTIER
	PSsetwindowlevel(BACKGROUNDTIER, [spaceWindow windowNum]);
	globalTier = BACKGROUNDTIER;
	
	if (([self backingTypeForView:spaceView] != NX_BUFFERED) &&
			oldWindowType == BACKWINDOW)
		// this justs fixes a display bug for really lazy nonretained windows
	{
		[spaceView fillBoundsWithBlack];
		[spaceView display];
	}

	if (oldTimerValid)	[self createTimer];

	[self unBlackOutAllScreens];
	
	//restore old window state
	[windMatrix selectCellAt:oldWindowType :0];
	[self changeWindowTypeAndRemember:NO];

	if (stoleActivation) 
	{
		if (oldActiveApp) [NXApp activate:oldActiveApp];
		else [NXApp deactivateSelf];
	}

	if (isHidden)
	{
		[NXApp hide:self];
	}

	doingSaver = NO;

	return self;
}

- appWillTerminate:sender
{
	[self normalMode];
	return self;
}

- appDidBecomeActive:sender
{
	id theMatrix;

	theMatrix = [viewSelectionBrowser matrixInColumn:0];
	[theMatrix scrollCellToVisible:realViewIndex :0];
	return self;
}

- app:sender powerOffIn:(int)ms andSave:(int)aFlag
{
	return [NXApp terminate:self];
}

- getPrioritySetting
{
	const char *ptr;
	int val;

	[mySlider setMinValue: 0];
	[mySlider setMaxValue: 10];
	
	ptr = NXGetDefaultValue([NXApp appName], "priority");
	if (ptr)
	{
		sscanf(ptr,"%d",&val);
		if (val >= 0 && val <= 10) priority = val;
		else priority = 4;
	}
	else priority = 4;
	
	[[mySlider cell] setIntValue:priority];
	[[priorityLevel cell] setIntValue:priority];

//	use mach call rather than unix - mach lets me increase priority!
//	setpriority(PRIO_PROCESS, 0, priority);
	cthread_priority(cthread_self(), priority, FALSE);

	return self;
}

- changeSliderValue:sender
{
	priority = [[mySlider cell] intValue];
	[[priorityLevel cell] setIntValue:priority];
	return self;
}

- saveSliderValue
{
	char str[50];
//	setpriority(PRIO_PROCESS, 0, priority);
	cthread_priority(cthread_self(), priority, FALSE);

	sprintf(str,"%d", priority);
	NXWriteDefault([NXApp appName], "priority", str);
	return self;
}

- windowWillResize:sender toSize:(NXSize *)frameSize
{
	if (frameSize->width < 100) frameSize->width = 100;
	if (frameSize->height < 100) frameSize->height = 100;
	return self;
}

- windowWillClose:sender
{
	[windMatrix selectCellAt:NOWINDOW :0];
	[self perform:@selector(changeWindowType:) with:self
		afterDelay:1 cancelPrevious:YES];
	return nil;
}

BStimeval currentTimeInMs()
{
    struct timeval curTime;
    gettimeofday (&curTime, NULL);
    return (curTime.tv_sec) * 1000 + curTime.tv_usec / 1000;
}

//
//  Additional methods to handle a common image object for views.
//  Lennart Lovstrad, Rank Xerox EuroPARC, August 1991.
//

- setImageFromFile: (const char *) filename
{
	[image free];

    image = [[NXImage alloc] initFromFile: filename];
    if (image == nil)
	{
		NXRunAlertPanel([NXApp appName], NXLocalString("Could not open %s",0,0),
				NULL, NULL, NULL, filename);
		image = nil;
		//return nil;	//can't return, image is invalid
    }
	
	return [self commonImageInit];
}

- setImageFromName: (const char *) name
{
	[image free];
    image = [[NXImage alloc] initFromSection: name];
	
	return [self commonImageInit];
}

- commonImageInit
{
    [imageView setImage: image];
    [imageView display];

    if ([spaceView respondsTo:@selector(setImage:)])
		[spaceView setImage: image];

	if ([self backingTypeForView:spaceView] != NX_BUFFERED)
	{
		[spaceView fillBoundsWithBlack];
		[spaceView display];
	}

    return self;
}

- getImageFile
{
    const char *filename;

    filename = NXGetDefaultValue([NXApp appName], "imageFile");
    if (filename)
		[self setImageFromFile: filename];
	else [self setImageFromName: "defaultImage"];

    return self;
}

- setImageFileFrom: sender
{
    id openPanel = [OpenPanel new];
    const char *fileTypes[] = {"tiff", "eps", NULL};
    
    if ([openPanel runModalForTypes: fileTypes])
	{
		[self setImageFromFile: [openPanel filename]];
		NXWriteDefault([NXApp appName], "imageFile", [openPanel filename]);
    }

	[spaceView display];	//don't know why this is necessary...

    return self;
}

// This should return a float between 0 and 1
float frandom()
{
	float val = (random() & 0x7fffffff);
	val /= 0x7fffffff;
	return val;
}

float randBetween(float a, float b)
{
	float val, scale, t;

	if (a > b)
	{	t = a; a = b; b = t;
	}
	
	scale = (b-a);
	val = scale * frandom();
	return (a + val);
}

@end
