/*
 * Shane Artis  (shanega@athena.mit.edu)
 *
 * The guts of this method are straight from BreakApp, courtesy of Ali Ozer
 * at NeXT.  It should be useful to anyone writing a generic game, or
 * any type of animation loop which must be smooth and quick.
 *
 * TemplateView implements an interactive custom view that allows the user
 * to generate any single-image animation by placing it within the -step
 * method at the appropriate location.  Multi-image animation is also
 * straightforward.
 *
 * Hooks are provided for sound, game logic (# of lives, high scores, etc.),
 * and a screen background picture which is loaded by default and may be set
 * from a menu or button.
 *
 */

#import "TemplateView.h"
#import "SoundGenerator.h"

#import <libc.h>
#import <math.h>
#import <dpsclient/wraps.h>	// PSxxx functions
#import <appkit/color.h>	// For background colors of images
#import <appkit/defaults.h>	// For writing/reading high score
#import <appkit/Application.h>	// For NXApp
#import <appkit/Button.h>
#import <appkit/Control.h>
#import <appkit/NXImage.h>
#import <appkit/OpenPanel.h>
#import <appkit/Panel.h>
#import <appkit/Window.h>

#import <musickit/TuningSystem.h>
#import <musickit/pitches.h>

// Maximum amount of time that is allowed to pass between two calls to the
// step method. If the time is greater than MAXTIMEDIFFERENCE, then this
// value is used instead. MAXTIMEDIFFERENCE should be no greater
// Than the time it takes for any object to completely pass through
// another object (or else we can miss collisions between objects).

#define IMAGEHEIGHT   imageSize.height

#define MAXYV         fabs(imageYVel)

#define MAXTIMEDIFFERENCE (IMAGEHEIGHT * 0.8 / MAXYV)

// Game logic definitions

#define LIVES     5				// Number of lives per game

#define STOPGAMEAT (-10)			// Number of loops through the
						// game after all pieces die
#define LEVELBONUS 50				// Bonus at the end of a level

// Starting location definitions can go here...
						
// Score value definitions of various things can go here...

// Randomizers

extern void srandom();				// Hmm; not in libc.h
#define RANDINT(n) (random() % ((n)+1))		// Random integer 0..n
#define ONEIN(n)   ((random() % (n)) == 0)	// TRUE one in n times 
#define INITRAND   srandom(time(0))		// Randomizer

// Screen size

#define gameSize  bounds.size

// Now for some music stuff. We define a few notes.

#define GENERICNOTE   c4				// Choose some notes.
#define NUMGAMENOTES   8
static MKKeyNum gameNotes[NUMGAMENOTES] = {c6k,d6k,b6k,g6k,a6k,f6k,d7k,b5k};
#define GAMENOTE   (MKKeyNumToFreq (gameNotes[		\
			((level % 6) == 0) ?		\
			RANDINT(NUMGAMENOTES-1) :	\
			(level % NUMGAMENOTES)		\
		    ]))

// Restrict a value to the range -max .. max.  It's here, so I just left it.

inline float restrictValue(float val, float max)
{
    if (val > max) return max;
    else if (val < -max) return -max;
    else return val;
}

@implementation TemplateView

- initFrame:(const NXRect *)frm
{
    [super initFrame:frm];      // Initialize our frame.
    
    [self allocateGState];	// For faster lock/unlockFocus

    // Generic technique for getting bitmap image from our executable or
    // directory.  findImageNamed will check everywhere for us.  Just put
    // the image.(tiff/eps) file into the interface builder project and it will
    // be incorporated into the executable automagically.
    
    [(image = [NXImage findImageNamed:"Object.tiff"]) setScalable:NO];

    // Generic technique for getting a default value and setting a background

    [self setBackgroundFile:NXGetDefaultValue([NXApp appName], "BackGround") 
		andRemember:NO];

    // Generic method to get high score from a high score list in user defaults
    
    [self getHighScore];
    
    // Provision for an alternate or demo mode in the game logic.

    demoMode = NO;

    // Display initial values

    [levelView setIntValue:level];
    [scoreView setIntValue:score];
    [livesView setIntValue:lives];
    [hscoreView setIntValue:highScore];
    [statusView setStringValue:"Game Ready"];

    // Some default values for animation globals

    imageXVel = 0.1; imageYVel = 0.1;               // moving right and down

    [image getSize:&imageSize];
    imageX = bounds.origin.x + imageSize.width;
    imageY = bounds.size.height - 1.5 * imageSize.height;  // upper left

    imageYAccel = -.015;   // acclerating downwards

    INITRAND;
    
    return self;
}

// free simply gets rid of everything we created for TemplateView, including
// the instance of BreakView itself. This is how nice objects clean up.

- free
{
    if (gameRunning) {
	DPSRemoveTimedEntry (timer);
    }

    // Send free to any objects we have created

    [image free];
    [backGround free];
    return [super free];
}

// The following allows TemplateView to grab the mousedown event that activates
// the window. By default, the View's acceptsFirstMouse returns NO.

- (BOOL)acceptsFirstMouse
{
    return YES;
}

// Allows us to grab keyboard events

- (BOOL)acceptsFirstResponder
{
    return YES;
}

// This method allows changing the file used to paint the background of the
// playing field. Set fileName to NULL to revert to the default. Set
// remember to YES if you wish the write the value out in the defaults.

- setBackgroundFile:(const char *)fileName andRemember:(BOOL)remember
{
    [backGround free];
    backGround = [[NXImage allocFromZone:[self zone]] initSize:&gameSize];
    if (fileName) {
	[backGround useFromFile:fileName];
	if (remember) {
	    NXWriteDefault ([NXApp appName], "BackGround", fileName);
	}
    } else {
	[backGround useFromSection:"BackGround.tiff"];
	if (remember) {
	    NXRemoveDefault ([NXApp appName], "BackGround");
	}
    }
    [backGround setBackgroundColor:NX_COLORWHITE];
    [backGround setScalable:YES];
    [self display];

    return self;   
}

// The following two methods allow changing the background image from
// menu items or buttons.

- changeBackground:sender
{
    const char *const types[] = {"tiff", "eps", NULL};  

    if ([[OpenPanel new] runModalForTypes:types]) {
	[self setBackgroundFile:[[OpenPanel new] filename] andRemember:YES];
	[self display];
    }

    return self;
}

- revertBackground:sender
{
    [self setBackgroundFile:NULL andRemember:YES];
    [self display];
    return self;
}

// getHighScore reads the previous high score from the user's defaults file.
// If no such default is found, then the high score is set to zero.

- getHighScore
{
    const char *tmpstr;
    if (((tmpstr = NXGetDefaultValue ([NXApp appName], "HighScore")) &&
	(sscanf(tmpstr, "%d", &highScore) != 1))) highScore = 0;
    
    return self;
}

// setHighScore should be called when the user score for a game is above 
// the current high score. setHighScore sets the high score and 
// writes it out the defaults file so that it can be remembered for eternity.

- setHighScore:(int)hScore
{
    char str[10];
    [hscoreView setIntValue:(highScore = hScore)];
    sprintf (str, "%d\0", highScore);
    NXWriteDefault ([NXApp appName], "HighScore", str);
    return self;
}

- (int)score
{
    return score;
}

- (int)level
{
    return level;
}

- (int)lives
{
    return lives;
}

// A mousedown effectively allows pausing and unpausing the game by
// alternately calling one of the above two functions (stop/go).

- mouseDown:(NXEvent *)event
{
    if (gameRunning) {
        [self stop:self];
    } else if (lives) {
        [self go:self];
    }
    return self;
}


// gotoFirstLevel: sets everything up for a new game.

- gotoFirstLevel:sender
{
    score = 0;
    level = 0;
    lives = LIVES;
    return [self gotoNextLevel:sender];
}

// gotoNextLevel: sets everything up for the next level of the game

- gotoNextLevel:sender
{
    // We are at the next level... Stop the game and increment the level.
    
    [self stop:sender];
    
    level++;

    // setup and draw your new level here.
    
    [levelView setIntValue:level];
    [scoreView setIntValue:score];
    [livesView setIntValue:lives];
    [hscoreView setIntValue:highScore];
    [statusView setStringValue:"Game Ready"];
    
    [self display];			// Display the new arrangement
    
    [self go:sender];	// start rolling
    
    return (self);
}      

// setDemoMode allows the user to put the game in a demo mode.

- setDemoMode:sender
{
    if (demoMode = ([sender state] == 0 ? NO : YES)) {
	[self go:sender];
    } else {
	[self stop:sender];
    }
    return (self);
}


- go:sender
{
    void runOneStep ();
    if (lives && !gameRunning) {

	// Write initialization code here

	gameRunning = YES;
	timer = DPSAddTimedEntry(0.0, &runOneStep, self, NX_BASETHRESHOLD);
	[statusView setStringValue:"Running"];

	// This lets us capture keyDowns without a mouse down in the window.

	[[self window] makeFirstResponder:self];
    }
    return self;
}

- stop:sender
{
    if (gameRunning) {
	gameRunning = NO;
	DPSRemoveTimedEntry (timer);
	[statusView setStringValue:"Paused"]; 
	[soundGenerator shutUp];
    }
    return self;
}

// It may be that passing around images and bitmaps is slow, therefore
// These methods use global variables instead of taking args.
// This uses more space, but may take less time.  If the number of images
// used is few you can write a showImage method for each, but this one
// has the image passed in.

- showImage:(id)anImage
{
    NXRect tmpRect = {{floor(imageX), floor(imageY)},
			imageSize};
    [anImage composite:NX_SOVER toPoint:&tmpRect.origin];
    return self;
}

- eraseImage
{
    NXRect tmpRect = {{imageX, imageY},
			imageSize};
    return [self drawBackground:&tmpRect];
}

// drawBackground: just draws the specified piece of the background by
// compositing from the background image.

- drawBackground:(NXRect *)rect
{
    NXRect tmpRect = *rect;

    NX_X(&tmpRect) = floor(NX_X(&tmpRect));
    NX_Y(&tmpRect) = floor(NX_Y(&tmpRect));
    if (NXDrawingStatus == NX_DRAWING) {
	PSsetgray (NX_WHITE);
	PScompositerect (NX_X(&tmpRect), NX_Y(&tmpRect),
			 NX_WIDTH(&tmpRect), NX_HEIGHT(&tmpRect), NX_COPY);
    }
    [backGround composite:NX_COPY fromRect:&tmpRect toPoint:&tmpRect.origin];
    return self;
}

// drawSelf::, a method every decent View should have, redraws the game
// in its current state. This allows us to print the game very easily.

- drawSelf:(NXRect *)rects :(int)rectCount 
{

    [self drawBackground:&bounds];

    [self showImage:image];

    return self;
}

// incrementGameScore: adds the value of the argument to the score if the game
// is not in demo mode.

- incrementGameScore:(int)scoreIncrement
{
    if (demoMode == NO) {
	score += scoreIncrement;
    }
    return self;
}

- playNote:(double)note
{
    [soundGenerator playNoteAtFreq:note];
    return self;
}
    
- keyDown:(NXEvent *)myevent
{
  NXEvent* eptr = NULL;
  NXEvent e;

 do {
    if (myevent->data.key.charSet == NX_ASCIISET &&
        (myevent->flags&(NX_CONTROLMASK|NX_ALTERNATEMASK|NX_COMMANDMASK)) == 0)
      {	

	// A 'pause' key implementation

	if (myevent->data.key.charCode == 'p') {
	  if (gameRunning) { [self stop:self]; }
	  else { [self go:self]; }
	}
	if (myevent->data.key.charCode == 'a') {
	  imageYAccel *= 2;
	}
	if (myevent->data.key.charCode == 'd') {
	  imageYAccel /= 2;
	}
	if (myevent->data.key.charCode == 'r') {
	  imageXVel *= 2;
	}
	if (myevent->data.key.charCode == 'l') {
	  imageXVel /= 2;
	}	
      }
    else return [super keyDown:myevent];
  }
  while (eptr = [NXApp peekNextEvent:NX_KEYDOWNMASK into:&e]);
  return self;
}


unsigned currentTimeInMs()
{
    struct timeval curTime;
    gettimeofday (&curTime, NULL);
    return ((unsigned)curTime.tv_sec) * 1000 + curTime.tv_usec / 1000;
}
  
// The step method implements one step through the main game loop. Actually,
// for efficiency purposes, right before returning, this method checks to see
// if there are any pending events; if there are none, step will simply loop
// instead of returning. This avoids the overhead of a lock/unlockFocus and
// increases the frame rate. The distance between animation steps should be
// adjusted by the time between frames, so we should get the game on
// different processors.


- step
{
    NXEvent dummyEvent;
    NXPoint mouseLoc;
    float newX;
    unsigned int timeBefore = 0;
    NXRect aRect = {{imageX, imageY}, imageSize};


    [self lockFocus];
    
    do {

	unsigned int timeNow = currentTimeInMs();
	unsigned int timeDelta = MIN(MAXTIMEDIFFERENCE, timeNow-timeBefore);
	timeBefore = timeNow;
    
	// Clear screen of animated objects

	[self eraseImage];
	
	// Update image locations (Depends on time between frames
	// for consistent behaviour across platforms)
    
	imageX += imageXVel * timeDelta * gameSize.width / GAMEWIDTH; 
	imageY += imageYVel * timeDelta * gameSize.height / GAMEHEIGHT;
  
	if (gameRunning) {
	  
	    // Test for collisions, do game logic
	    
	    // EXAMPLE:  This is a trivial animation.  It bounces
            //           a cute picture around the background.
            //           This is everything right here.  All the rest
            //           is just setup and frills.

	    if (imageY < bounds.origin.y - 17.0 ||
		imageY > bounds.size.height - imageSize.height) {
	      imageY -= imageYVel * timeDelta * gameSize.height / GAMEHEIGHT;
	      imageYVel *= -1.0;
  	    }

	    if (imageX < bounds.origin.x - 14.0 ||
		imageX > bounds.size.width - imageSize.height) {
	      // we let the shadow go off screen so it's more convincing.
              imageX -= imageXVel * timeDelta * gameSize.width / GAMEWIDTH; 
	      imageXVel *= -1.0;	
            }

	   imageYVel += imageYAccel;
	}

	[self showImage:image];

    
	// Get the mouse location and convert from window to the view coords.
	// Not necessarily needed so commented out, but a useful thing to have.
    
	//	[[self window] getMouseLocation:&mouseLoc];
	//	[self convertPoint:&mouseLoc fromView:nil];
    
	// It may be that a flushwindow should be done for each image drawn.
	// This could be slower but smoother - I'm not really sure.
	// Find out for yourself...
	// BTW:  Make sure you make your animation FAST so us poor '030
	//       users can still play!!  - SGA
	
	[[self window] flushWindow];
    
	NXPing ();	// Synchronize postscript for smoother animation

    } while (gameRunning &&
	   ([NXApp peekNextEvent:NX_ALLEVENTS into:&dummyEvent] == NULL));

    [self unlockFocus];

    return self;
}

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

void runOneStep (DPSTimedEntry timedEntry, double timeNow, void *data)
{
    [(id)data step];
}

@end




  
