// BWDripScreenSaverView -- a reimplementation of a classic Apple ][ program
//                          that dripped water down on the low-res graphics 
//                          screen.

// Copyright (C) 2002 Borkware
// We use the BSD License.  Check out http://borkware.com/license


#import "BWDripScreenSaverView.h"
#import "BWDripController.h"

// access the memory directly.  No really *good* reason for using
// a macro like this, but it seemed like a fun thing to do at the time

#define blockAddress(pt) (blocks + ((pt.x) + ((pt.y) * width)))
#define blockAt(pt) *blockAddress(pt)
#define setBlockAt(pt, value) *(blockAddress(pt)) = value

// calculate the rectangle for a given point
#define rectFor(pt) \
    NSMakeRect(pt.x*rectSize, pt.y*rectSize, rectSize, rectSize)

@implementation BWDripScreenSaverView




// fill in the playfield with a random assortment of blocks

- (void) setUpPlayfield
{
    int x, y;
    
    // out with the old
    memset (blocks, 0x00, sizeof(int) * width * height);
    memset (fadeInPoints, 0x00, sizeof(IntPoint) * width * height);
    fadeInPointCount = 0;
    currentDrop = 0;

    // seed the random number generator
    srandom (time(NULL));

    // walk each block and generate a random number.  If the number is
    // below the 'density' setting, put a new block there

    for (x = 0; x < width; x++) {
	for (y = 0; y < height; y++) {
	    IntPoint point;
	    int urgle = random() % 100;
	    BOOL block = NO;

	    if (urgle < density) {
		block = YES;
	    }

	    point = MakeIntPoint (x, y);
	    setBlockAt (point, DripSpace);

	    if (y > 2 && block) {
		setBlockAt (point, DripBlock);

		// add the block to the list of points to fade in
		fadeInPoints[fadeInPointCount] = point;
		fadeInPointCount++;
	    } else {
		setBlockAt (point, DripSpace);
	    }
	}
    }

    // now randomize the fade in points.  Walk the point list
    // from beginning to end, swapping that point with a random point.
    // quickie, yet pretty good, to permute a collection

    for (x = 0; x < fadeInPointCount; x++) {
	IntPoint swap;
	int index = random() % fadeInPointCount;
	swap = fadeInPoints[x];
	fadeInPoints[x] = fadeInPoints[index];
	fadeInPoints[index] = swap;
    }

    // we're fading in now - so have the animator draw the blocks rather
    // than running the drip around
    fadeIn = YES;
    lastTimeNoMove = NO;

    drip = MakeIntPoint (-1, -1);

    [self setNeedsDisplay: YES];

} // setUpPlayfield



// populate a dictionary that holds our 'factory default' tweakable
// paramters (the block density, and the flow rate)

- (NSDictionary *) makeDefaultPreferenceValues
{
    NSDictionary *defaults;

    defaults = [NSDictionary dictionaryWithObjectsAndKeys:
			     [NSNumber numberWithFloat: 25.0], @"density",
			     [NSNumber numberWithFloat: 40.0], @"rate", 
			     nil];
    return (defaults);

} // makeDefaultPreferenceValues


- (void) setValuesFromPrefs
{
    ScreenSaverDefaults *userPrefs;
    userPrefs = [ScreenSaverDefaults defaultsForModuleWithName: @"BWDrip"];

    float dripRate;
    dripRate = [userPrefs floatForKey: @"rate"];
    [self setAnimationTimeInterval: 1/dripRate];

    density = [userPrefs floatForKey: @"density"];
    fadeDripColor = [userPrefs boolForKey: @"fadeColor"];

} // setValuesFromPrefs


// our first entry point.  Allocate the memory we're going to use,
// and get our playfield built

- (id)initWithFrame:(NSRect)frame isPreview:(BOOL)isPreview
{
    ScreenSaverDefaults *userPrefs;
    userPrefs = [ScreenSaverDefaults defaultsForModuleWithName: @"BWDrip"];

    if (self = [super initWithFrame: frame  isPreview: isPreview]) {
	// snarf the tweakable values from the user prefs
	[userPrefs registerDefaults: [self makeDefaultPreferenceValues]];
        [self setValuesFromPrefs];

	// draw smaller for the preview window.  It actually kind of looks
	// kind of cool in the preview
	if (isPreview) {
	    rectSize = 8;
	} else {
	    rectSize = 16;
	}

	// figure out how much area we have to play with, given a constant
	// rect size.
	width = frame.size.width / rectSize;
	height = frame.size.height / rectSize;

	// argh! since the preview window is a freaking *prime* number of
	// pixels across, bump up the rect count to not have a stupid looking
	// unfilled border.
	if (isPreview) {
	    width++;
	    height++;
	}

	// allocate a chunk of memory to hold the blocks, as well
	// as the set of points to fade in.  (yeah, we're allocating
	// a maximal amount of memory here for the fade-in list. we're
	// lazy)

	blocks = malloc (sizeof(int) * width * height);
	fadeInPoints = malloc (sizeof(IntPoint) * width * height);
	fadeInPointCount = 0;

	// finally!  set up the initial set of occupied blocks
	[self setUpPlayfield];

	// and start by falling down
	direction = DripDown;
    }

    return (self);

} // initWithFrame/isPreview



// clean up our mess and put away our toys.  We're done

- (void) dealloc
{
    free (blocks);
    free (fadeInPoints);

    [controller release];

    [super dealloc];

} // dealloc



// this is invoked by the preference sheet when the user says "OK".
// It re-reads the the density and drip rate from the user preferences
// and then rebuilds the playfield

- (void) restart
{
    [self setValuesFromPrefs];
    [self setUpPlayfield];

} // restart


//
// ------ The Hard-Core (yeah, right) logic for drippyness
//


// given a point, can we move our water blob into it?
// If the point is out of bounds, or if there is something already there
// like a block or more water, then the answer is "NO(e)"

- (BOOL) isValidMoveTo: (IntPoint) point
{
    // make sure it's in bounds
    if (point.x < 0 || point.y < 0 || point.x >= width || point.y >= height) {
	return (NO);

    } else {

	// make sure nothing's there
	if (blockAt(point) == DripSpace) {
	    return (YES);
	} else {
	    return (NO);
	}
    }

} // isValidMoveToXY



// draw the block type at the given point

- (void) fillBlock: (int) blockValue  at: (IntPoint) point
{
    NSRect rect;

    // note the highly sophisticated graphics employed

    if (blockValue == DripSpace) {
        [[NSColor blackColor] set];

    } else if (blockValue == DripBlock) {
        [[NSColor yellowColor] set];

    } else {
        NSColor *color;

        if (fadeDripColor) {
            // as time goes on, newer drops get closer to white
            // suggestion by Ian Krieg
            float scaledValue = (float)(blockValue) / (float)(width * height);
            
            color = [NSColor colorWithDeviceRed: scaledValue
                             green: scaledValue
                             blue: 1.0
                             alpha: 1.0];
        } else {
            color = [NSColor blueColor];
        }

        [color set];
    }

    rect = rectFor(point);

    [NSBezierPath fillRect: rect];

} // fillBlock



// move the drip from its old point to the new point.  Assumes that drawing
// is focused on us.

- (void) moveTo: (IntPoint) point
{
    // clear out the old
    setBlockAt (drip, DripSpace);
    [self fillBlock: DripSpace  at: drip];

    // fill in the new
    setBlockAt (point, currentDrop);
    [self fillBlock: currentDrop  at: point];

    drip = point;
    lastTimeNoMove = NO;

} // moveTo



// draw the whole playfield.  If we're still fading the blocks in, don't
// draw everything (kind of defeats the purpose of the fade-in)
// at any rate, fill in the background with black

- (void)drawRect:(NSRect)rect
{
    int x, y;

    [[NSColor blackColor] set];
    [NSBezierPath fillRect: rect];

    // necessary to have this for the preview pane

    if (!fadeIn) {
	for (x = 0; x < width; x++) {
	    for (y = 0; y < height; y++) {
		IntPoint point = MakeIntPoint(x, y);
		[self fillBlock: blockAt(point)  at: point];
	    }
	}
    }

} // drawRect



// we're still in fade-in mode.  Draw some of the blocks on each animate
// loop.  

- (void) fadeInOne
{
    int i;
    IntPoint currentPoint;

    // draw 6 blocks each time.  That's just a number that seems to have
    // a good balance between coolness of the fade-in effect, and not taking
    // for-ev-er

    for (i = 0; i < 6; i++) {
	currentPoint = fadeInPoints[fadeInPointCount-1];
	
	[self fillBlock: blockAt(currentPoint)  at: currentPoint];
	
	fadeInPointCount--;
	
	// yay!  we're done
	if (fadeInPointCount == 0) {
	    fadeIn = NO;
	    break;
	}
    }

} // fadeInOne


// done with the current drop.  Get ready for the next one

- (void) startOver
{
    currentDrop++;
    drip = MakeIntPoint (-1, -1);
    direction = DripDown;

} // startOver


// this gets called by The System for each time we need to animate 
// a frame

- (void) animateOneFrame
{
    IntPoint point = drip;

    // if we're fading in, draw them

    if (fadeIn) {
	[self fadeInOne];
	goto bailout;
    }

    // point.x/y of -1 means that we're starting over with a new drip.
    // start it in the middle and have it falling down

    if (point.x == -1 || point.y == -1) {
	drip.x = width / 2;

	drip.y = 0;
	setBlockAt (drip, currentDrop);
	direction = DripDown;

	goto bailout;
    }

    // try moving down first

    point.y++;
    if ([self isValidMoveTo: point]) {
	[self moveTo: point];
	direction = DripDown;
	goto bailout;
    }

    // if we got here, we can't move down.  rollback the previous move
    // down
    point.y--;

    // ok, we can't move down.  Try moving left or right

    if (direction == DripDown) {

	// pick a direction at random
	if (random() % 2) {
	    direction = DripLeft;
	    point.x--;
	} else {
	    direction = DripRight;
	    point.x++;
	}

	// try moving that direction.  If not, try the other direction.

	if ([self isValidMoveTo: point]) {
	    [self moveTo: point];
	    // keep moving same direction
	    goto bailout;
	}

	// not a valid direction.  try the other direction
	if (direction == DripLeft) {
	    direction = DripRight;
	    point.x += 2;  // 2 to compensate for the previous move
	} else {
	    direction = DripLeft;
	    point.x -= 2;  // ditto(e)x
	}

	if ([self isValidMoveTo: point]) {
	    // yay!
	    [self moveTo: point];
	    goto bailout;
	}
	
	// nope, can't move their either.  We're done with this drip.
	// see if we're done done for good (e.g. filled in the whole
	// field)

	if (lastTimeNoMove) {
	    [self setUpPlayfield];
	} else {
	    lastTimeNoMove = YES;
	}

        // time for a new drip
        [self startOver];

	goto bailout;
    }

    // ok, so we're not moving down.  If we're already moving left / right,
    // go that way

    if (direction == DripLeft) {
	point.x--;
    } else if (direction == DripRight) {
	point.x++;
    }

    // try moving there
    if ([self isValidMoveTo: point]) {
	[self moveTo: point];
	// and we'll keep moving same direction
	goto bailout;

    } else {
	// we can't move down, and we can't move any more in the
	// direction we were headed.  We're done with this drip.  start
	// over next time
        [self startOver];
	goto bailout;
    }

 bailout:
    return;

} // animateOneFrame



// yep.  we have a configure sheet

- (BOOL) hasConfigureSheet
{
    return (YES);
} // hasConfigureSheet



// and here it is

- (NSWindow *) configureSheet
{
    if (controller == nil) {
	controller = [[BWDripController alloc] initWithDripView: self];
    }

    return ([controller getConfigureSheet]);

} // configureSheet



// the graphic calculations are a little easier when we're flipped.

- (BOOL) isFlipped
{
    return (YES);
} // isFlipped



// necessary to get key events

- (BOOL) acceptsFirstResponder
{
    return (YES);

} // acceptsFirstResponder



// restart with new playfield on a tab
// suggestion by Ian Krieg

- (void) keyDown: (NSEvent *) event
{
    if ([[event characters] characterAtIndex: 0] == 9) {
        [self setUpPlayfield];
    } else {
        [super keyDown: event];
    }

} // keyDown


@end // BWDripScreenSaverViewGoodnessThisNameIsReallyLong


