#import <appkit/NXImage.h>
#import <ctype.h>
#import <dpsclient/wraps.h>
#import <math.h>
#import <appkit/graphics.h>
#import "TetMatrix.h"
#import "Piece.h"
#import "ScoreKeeper.h"
#import "TetApp.h"
#import "wraps.h"

// The number of pixels a block moves every step
// Make it a divisor of the block size
static const float ANM_DELTA  = 8.0;

const float GAMEOVER_BMAP_H = 100.0;

extern long random();
extern void srandom(int seed);
extern int getpid();

@implementation TetMatrix

/*
 * Some things should be set differently for color machines for either
 * performances or aesthetic reasons.
 */
- initForColor
{
	const NXScreen *deepestScreen;

	deepestScreen=[NXApp colorScreen];

	if ( deepestScreen->depth  == NX_TwoBitGrayDepth ) {
		anm_gameover_delta = 1.0;
	} else {
		anm_gameover_delta = 2.0;
	}
	return self;
}


- initFrame:(const NXRect *)frameRect
{
	NXSize size, interCell;
	extern BOOL resize(NXSize *aSize);
	float viewWidth, viewHeight;

	// Set the # of rows and columns in the view
	[super initFrame:frameRect numRows:TETRIS_ROWS numCols:TETRIS_COLUMNS];
	[self setBackgroundGray:NX_BLACK];

	size.width = size.height = 6.0;
	[self setInset:&size];

	// Set the amount of space to leave b/w each block
	interCell.width = interCell.height = 1.0; // 1 pixel
	[self setIntercell:&interCell];

	pieceVisible = active = NO;
	thePiece = [[Piece alloc] init];

	[thePiece getBlockSize:&size];

	// Should only resize in Piece.m
	resize(&size);					  // Make sure block image isn't too large
	[super setElementSize:&size];

	// Size Game View. 
	// Default view size is (182, 438)
	// Getting (176, 416)
	viewWidth =  6. + TETRIS_COLUMNS * (size.width + interCell.width)  + 6.;
	viewHeight = 6. + (TETRIS_ROWS)* (size.height + interCell.height) + 6.;

#ifdef DEBUG
	fprintf(stderr, "Sizing the Tetris view to (%f,%f)\n", viewWidth, viewHeight);
#endif
	[self sizeTo: viewWidth :viewHeight];

	scoreKeeper = nil;

	anmBitmap = [[NXImage allocFromZone:[self zone]] initSize:&bounds.size];

	// Eventually want the background to be setable

//	anmBitmap = [NXImage initSize:&bounds.size];
//	[anmBitmap useFromFile:"Snow.tiff"];
	showNext = nil;
	[self initForColor];

	gameRunning = NO;
	srandom(getpid());			  // Seed random # generator

	return self;
}

- setupRandomFill:(int)max
{
	int    blockNum;
	int    row, col;

	if (max > TETRIS_ROWS) max = TETRIS_ROWS;
	if (max > 10) max = 10;
	// fprintf (stderr, "Start random fill ...\n");
	for (row = 0; row < max; row++) {
		for (col = 0; col < TETRIS_COLUMNS; col++) {
			if ((random () & 0x3) == 0) {   /* set 1/4 */
				blockNum = (random () & 0x3); // Rand num 0..3
				[super setBitmap: [thePiece getBlockImage:blockNum] at:row :col];
			}
		}
	}
	return self;
}


- (BOOL)acceptsFirstResponder
{
    return active;
}

- drawSelf:(const NXRect *)rects :(int)rectCount
{
	[super drawSelf:rects :rectCount];
	if (pieceVisible) {
		NXRectClip(&insetBounds);
    	[thePiece draw:self];
	}
	return self;
}


/*
 *  Act upon keyboard input from the user.
 *
 */
- keyDown:(NXEvent *)theEvent
{
	unsigned short charCode;

	charCode = theEvent->data.key.charCode;
	if (isupper(charCode))
	  charCode = tolower(charCode);

	switch (charCode) {
    case 'k':
    case '5':
    	if (charCode == 'k' || theEvent->flags & NX_NUMERICPADMASK)
		  [thePiece turn:self];
		break;
    case 'j':
    case '4':
    	if (charCode == 'j' || theEvent->flags & NX_NUMERICPADMASK)
		  [thePiece left:self];
		break;
		
    case 'l':
    case '6':
    	if (charCode == 'l' || theEvent->flags & NX_NUMERICPADMASK)
		  [thePiece right:self];
		break;

    case ' ':
    case '2':
    case '0':
    	if (charCode == ' ' || theEvent->flags & NX_NUMERICPADMASK) {
			[self stopTimedEntry];
			[thePiece drop:self];
			[self startTimedEntry];
		}
		break;

	 case 13:						  // Pause if the user hits return
		[NXApp pause:self];
	}
	return self;
}


- setPieceVisible:(BOOL)flag
{
	pieceVisible = flag;
	return self;
}

- setScoreKeeper:keeper
{
	scoreKeeper = keeper;
	return self;
}

/*
 *  Determine how fast a piece will drop.
 */
-(double) newDelay
{
	return 0.5 - 0.1 * sqrt(2.5 * level);
}

- newGame:(int)theLevel
{
	[[super setBitmap:nil] display];
	[window makeFirstResponder:self];
	[self setPieceVisible:YES];
	[thePiece reset:self
       piece:((showNext) ? [showNext pieceInfo] : PIECE_INFO_NULL)];

	[self setupRandomFill:[randomFields intValue]];
	[self display];
	level = (float)theLevel;
	teDelay = [self newDelay];
	[self startTimedEntry];
	active = YES;
	return self;
}

- pause:sender
{
	// Make TetApp the first responder
	[window makeFirstResponder:window];
	// Will making this object the first responder cause problems?
//	[window makeFirstResponder:self];
	active = NO;
	return [self stopTimedEntry];
}


- continue:sender
{
	active = YES;
	[window makeFirstResponder:self];
	return [self startTimedEntry];
}

- stop:sender
{
	active = NO;
	[window makeFirstResponder:window];
	return [self stopTimedEntry];
}

/*
 * Scroll "Game Over" down the screen after the game is finished.
 */
- animateGameOver
{
	NXSize tmpSize;
	id anmGameOver;
	NXRect destRect, unionRect;
	
	tmpSize.height = GAMEOVER_BMAP_H;
	tmpSize.width = insetBounds.size.width;
	anmGameOver = [[NXImage alloc] initSize:&tmpSize];

	// Composite "Game Over" to a window
	[anmGameOver lockFocus];
	PSsetalpha(0.0);
	PSrectfill(0.0, 0.0, insetBounds.size.width, GAMEOVER_BMAP_H);
	PSsetalpha(1.0);
	PSgameover(insetBounds.size.width);
	[anmGameOver unlockFocus];
	
	[super getRect:&destRect for:(int)(TETRIS_ROWS / 2) :0];
	[super getRect:&unionRect for:TETRIS_ROWS - 1 :TETRIS_COLUMNS - 1];
	NXUnionRect(&unionRect, &destRect);

	[anmBitmap lockFocus];
	[self drawSelf:&destRect :1];
	[anmBitmap unlockFocus];
	
	NXSetRect(&unionRect, destRect.origin.x, unionRect.origin.y,
				 destRect.size.width, GAMEOVER_BMAP_H);
	[self lockFocus];
	NXRectClip(&destRect);

	while (unionRect.origin.y - anm_gameover_delta >= destRect.origin.y) {
		unionRect.origin.y -= anm_gameover_delta;
		
		[anmGameOver composite:NX_SOVER toPoint:&unionRect.origin];
		[[self window] flushWindow];
		NXPing();
		// Redraw the background to erase the "Game Over" letters 
//		[anmBitmap composite:NX_COPY fromRect:&unionRect toPoint:&unionRect.origin];
		[anmBitmap composite:NX_SOVER fromRect:&unionRect toPoint:&unionRect.origin];
		NXPing();
	}
	[anmGameOver composite:NX_SOVER toPoint:&unionRect.origin];
	[self unlockFocus];
	
	[anmGameOver free];
	return self;
}

/* The following methods: 
 *   Animate a piece dropping.
 *   Dissolve filled rows.
 *   Removes filled rows from the game.
 */

- (BOOL) rowFilled:(int)row
{
    int xc;

    for (xc = 0; xc < TETRIS_COLUMNS; xc++)
    	if (![super bitmapAt:row :xc])
	    break;
    return (xc == TETRIS_COLUMNS);
}

- (BOOL) rowEmpty:(int) row
{
	int xc;

	for (xc = 0; xc < TETRIS_COLUMNS; xc++)
	  if ([super bitmapAt:row :xc])
	    break;
	return (xc == TETRIS_COLUMNS);
}

- fadeFilledRows:(int) from :(int) to
{
	float gc;
	NXRect destRect;
	NXRect unionRect;
	
	[super getRect:&destRect for:from :0];
	[super getRect:&unionRect for:to :TETRIS_COLUMNS - 1];
	NXUnionRect(&unionRect, &destRect);

	// Composite the rows we are about to fade to anmBitmap
	[anmBitmap lockFocus];
	[self drawSelf:&destRect :1];
	[anmBitmap unlockFocus];

	// Fade the blocks
	[self lockFocus];
	for (gc = 1.0; gc > 0.3; gc -= .03) {
    	PSsetgray(gc);
		PScompositerect(destRect.origin.x, destRect.origin.y, destRect.size.width,
							 destRect.size.height, NX_PLUSD);
		[[self window] flushWindow];
		// Now redraw the blocks so we can fade them a little more
//		[anmBitmap composite:NX_COPY fromRect:&destRect toPoint:&destRect.origin];
		[anmBitmap composite:NX_SOVER fromRect:&destRect toPoint:&destRect.origin];
		NXPing();
	}
	// Now make the blocks totally disappear.
	PSsetgray(NX_BLACK);
	PScompositerect(destRect.origin.x, destRect.origin.y, destRect.size.width,
						 destRect.size.height, NX_SOVER);
//						 destRect.size.height, NX_COPY);

	[self unlockFocus];

	return self;
}

/*
 * Move the pieces as they are moved down on the screen after one or more rows
 * has been cleared.
 */
- animateDrop:(int) firstFullRow :(int)firstNonFullRow :(int) lastNonEmptyRow
{
	NXPoint dPt;
	NXRect destRect;
	NXRect unionRect;

	[super point:&dPt for:firstFullRow :0]; // Place where to put falling blocks
	[super getRect:&destRect for:firstNonFullRow :0];	// First row to move down
	// Last row to move down
	[super getRect:&unionRect for:lastNonEmptyRow :TETRIS_COLUMNS - 1];

	// We want to move everything down from the last non-full row to
	// the last row with a piece in it.

	NXUnionRect(&unionRect, &destRect);
	unionRect = destRect;

	// Create the image of the blocks
	[anmBitmap lockFocus];
	[self drawSelf:&destRect :1];
	[anmBitmap unlockFocus];

	[self lockFocus];

	PSsetgray(NX_BLACK);			  // The destination Rect is Black
	PScompositerect(destRect.origin.x, destRect.origin.y, destRect.size.width,
						 destRect.size.height, NX_COPY);


	while (unionRect.origin.y - ANM_DELTA > dPt.y) {

		unionRect.origin.y -= ANM_DELTA;

		// First clear the window
		[anmBitmap composite:NX_COPY fromRect:&destRect toPoint:&unionRect.origin];
		[[self window] flushWindow];
		PScompositerect(unionRect.origin.x, unionRect.origin.y, unionRect.size.width,
							 unionRect.size.height, NX_COPY);
		NXPing();
	}
	[anmBitmap composite:NX_COPY fromRect:&destRect toPoint:&dPt];
	[[self window] flushWindow];

	//	NXPing();
	[self unlockFocus];
	return self;
}

/*
 *  Remove the filled rows after they have dissolved
 */
- removeFilledRows
{
	BOOL filled;
	int ycfrom, ycto, yctop;
	int xc, yc;
	int lastFilledRow, lastNonEmptyRow, firstFilled;
	int piecey = [thePiece getCurRow]; // Get the bottom-most row of the piece.

	do {
    	filled = NO;				  // This should only execute once?!?!

		// Work our way up from the current piece's row.
		for (ycfrom = piecey; ycfrom < piecey + MAX_SHAPE_SIZE; ycfrom++) {
			if ([self rowFilled: ycfrom]) {
				filled = YES;
				firstFilled = ycfrom;
				break;
			}
		}

		if (filled) {

			// Find the first row that isn't completely filled.
			lastFilledRow = firstFilled;	// Loop can execute 0 times  
			for (ycto = firstFilled + 1; ycto < firstFilled + MAX_SHAPE_SIZE; ycto++) {
				if ([self rowFilled: ycto]) {
					lastFilledRow = ycto;
				} else {
					break;
				}
			}
			// Find the topmost row in the program.  A row with at least one block.
			// Assert: Must enter loop at least once.
			for (yctop = lastFilledRow+1; yctop < TETRIS_ROWS; yctop++) {
				if (! [self rowEmpty: yctop]) {
					lastNonEmptyRow = yctop; // But do I have to execute this once?
				} else {
					break;
				}
			}
			[self fadeFilledRows: firstFilled :lastFilledRow];

			// Animate the all of the pieces as the move down.
			[self animateDrop:firstFilled :lastFilledRow+1 :lastNonEmptyRow];


			// Move the rows above the faded rows down to take their place.

			for (yc = lastFilledRow+1; yc <=lastNonEmptyRow; yc++) {
				for (xc = 0; xc < TETRIS_COLUMNS; xc++) // For each block in the row.
				  [super setBitmap:[self bitmapAt:yc :xc] at:firstFilled :xc];
				firstFilled++;
			}

			// Clear the rows at the very top that have been moved down
			// and which have nothing to take their place.

			for (yc = firstFilled; yc <=lastNonEmptyRow; yc++) {
#ifdef DEBUG
				printf("Removing row %d\n", yc); 
#endif
				for (xc = 0; xc < TETRIS_COLUMNS; xc++)
				  [super setBitmap:nil at:yc :xc];
			}
			piecey++;
		}
	} while (filled);
	return self;
}


/* The main animation routines follow.  A timed entry routine is setup
 * so that it calls teHandler at a regular interval (depending on the
 * level)
 */

/*
 *  The timed entry handler that gets called every teDelay seconds.
 *
 */
- step
{
	static int downWait = 0;

	// Move the piece down.  If it can't move down make it stick,
	// remove filled rows, and generate a new piece.

	if (![thePiece down:self]) {

		// After the piece sticks to the bottom, wait for a while before
		// we send the next piece.  The wait depends on the current level.

    	if (++downWait >= level) {	// Don't commit the piece immediately
			downWait = 0;
			[self stopTimedEntry];
			[self setPieceVisible:NO];
			[thePiece stick:self]; // Make the piece stick
			[self removeFilledRows];
			[self setPieceVisible:YES];
			if (scoreKeeper) {
				[scoreKeeper addScore:[thePiece points]];
				level += (MAX_LEVEL - level) / 400.0;
				teDelay = [self newDelay];
			}
			// show the next piece.
			if ([thePiece reset:self piece:((showNext) ?
									  [showNext pieceInfo] : PIECE_INFO_NULL)])
			  [self startTimedEntry];
			else {
				[self animateGameOver];
				[[self window] makeFirstResponder:[self window]];
				active = NO;
				[NXApp gameOver];
			}
		}

	} else {							  // The piece can't go down
		downWait = 0;				  // Set a delay before showing next piece
	}
	return self;
}

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

- startTimedEntry
{
	if (!gameRunning) {
		timedEntryNum = DPSAddTimedEntry(teDelay,&runOneStep,
													self, NX_BASETHRESHOLD);
		gameRunning = YES;
	}
	return self;
}

- stopTimedEntry
{
	if (gameRunning) {
		DPSRemoveTimedEntry(timedEntryNum);
		gameRunning = NO;
	}
	return self;
}

/*
 * Clean up.
 */
- free
{
    [thePiece free];
    [anmBitmap free];
    return [super free];
}

@end
