/*
 * MandelView.m, subclass of View, Jan.1992
 * written by R. Pajarola
 * E-mail: rpajarol@iiic.ethz.ch
 */

#import "MandelView.h"
#import <appkit/Application.h>
#import <dpsclient/wraps.h>
#import <appkit/appkit.h>
#import <appkit/Cell.h>
#import <appkit/TextField.h>
#import	<appkit/tiff.h>
#import <math.h>
#import <stdlib.h>

#define	MAXCOLOR 256
#define ESC '\x1B'

@implementation MandelView

double			last_x, last_y, last_s = -1, xmin, ymin, ymax, s, delta;
int				iter, width, height, count;
id				data, myWindow;
float			c = 3.14159265359/MAXCOLOR;
unsigned char	color, *pixels, palette[MAXCOLOR];
NXRect			bbox;
BOOL			stop;
NXEvent			*action;

void fillRect(int x1,int y1,int x2,int y2)
/* Filling a rectangle with "color".
 */
{
	int i;
	unsigned char *p, *p1, *p2;
	
	p1 = pixels + y1*width + x1;
	for(i=y1; i<y2; i++) {
		p2 = p1 + x2 - x1;
		for(p=p1; p<p2; *p++ = color);
		p1 += width;
	}
}

void setPixel(register int x, register int y, unsigned char *p)
/* Calculating the Mandelbrotset for the komplex point z=(zx, i*zy)
 * by the formula z = z^2 +c.
 * The color depends on the iterations done for this pixel.
 */
{
	register double	cx, cy, zx, zy, qx, qy;
	register int 	t;
	
	zx = cx = xmin + x*delta;
	zy = cy = ymax - y*delta;
	qx = zx*zx; qy = zy*zy;
	for (t=1; t<iter && qx+qy<4.0; t++) {
		zy = 2.0*zx*zy + cy;
		zx = qx - qy + cx;
		qx = zx*zx; qy = zy*zy;
	}
	if (t>=iter)
		color = 0;
	else {
		color = palette[t % MAXCOLOR];
	}
	*p = color;
}

void calcRect(int x1, int y1, int x2, int y2)
/* Calculating a rectangle with the corners x1,y1 (lower, left)
 * and x2,y2 (upper, right).
 */
{
	unsigned char	lastcolor, *p1, *p2;
	int		i,j,x,y;
	BOOL	equal = YES;
	
	/* Testing the keyboard for a <Esc>-key input.
	 */
	action = [NXApp getNextEvent:NX_KEYDOWNMASK
		waitFor:0.0000001 threshold:NX_MODALRESPTHRESHOLD];

	if (action != NULL)
		stop = (action->data.key.charSet == NX_ASCIISET)
			&& (action->data.key.charCode == ESC);
	
	/* If there was no <Esc>-key pressed, continue the calculations.
	 */
	if (!stop) {
		count++;
		/* First the edges of the rectangle are calculated.
		 * If all points have the same color, the rectangle will
		 * be filled by this color.
		 * Otherwise the rectangle will be split into four other
		 * rectangles and this procedure is called again.
		 * If the rectangle is too small it won't be split.
		 */
		p1 = pixels + y1*width + x1;
		setPixel(x1, y1, p1);
		lastcolor = color;
		p2 = pixels + (y2-1)*width + x1;
		setPixel(x1, y2-1, p2);
		equal = equal && (color == lastcolor);
		for (i = x1+1; i < x2; i++) {
			p1++;
			setPixel(i, y1, p1);
			equal = equal && (color == lastcolor);
			p2++;
			setPixel(i, y2-1, p2);
			equal = equal && (color == lastcolor);
		}
		p1 = pixels + y1*width + x1;
		p2 = pixels + y1*width + x2-1;
		for (j = y1+1; j < y2-1; j++) {
			p1 += width;
			setPixel(x1, j, p1);
			equal = equal && (color == lastcolor);
			p2 += width;
			setPixel(x2-1, j, p2);
			equal = equal && (color == lastcolor);
		}
		if (equal) {
			if (color) fillRect(x1+1, y1+1, x2-1, y2-1);
		}
		else {
			if (y2-y1 < 8) {
				p1 = pixels + y1*width + x1;
				for (j = y1+1; j < y2-1; j++) {
					p2 = p1 += width;
					for (i= x1+1; i < x2-1; i++) {
						p2++;
						setPixel(i, j, p2);
					}
				}
			}
			else {
				x = (x1+x2)/2; y = (y1+y2)/2;
				calcRect(x1+1, y1+1, x, y);
				if (!stop) calcRect(x, y1+1, x2-1, y);
				if (!stop) calcRect(x1+1, y, x, y2-1);
				if (!stop) calcRect(x, y, x2-1, y2-1);
			}
		}
		if (count == 3) {
			/* Rendering the calculated image on the screen.
			 */
			NXImageBitmap(&bbox, width, height, 8, 1, NX_MESHED,
				NX_MONOTONICMASK, pixels, NULL, NULL, NULL, NULL);
			[myWindow flushWindow];
		}
		count--;
	}
}

- initFrame:(NXRect *)rect
{
	int		i;
	
	[super initFrame:rect];
	[window useOptimizedDrawing:YES];
	[self setOpaque:YES];
	xmin = -2.1;
	ymin = -1.5;
	s = 3.0;
	
	/* Set colortable.
	 */
	for (i=0; i<MAXCOLOR; i++)
		palette[i] = sin(i*c)*254+1;

	return self;
}

- run:sender
{
	stop = NO;
	[self lockFocus];
	bbox = bounds;
	width = bbox.size.width;
	height = bbox.size.height;

	/* Getting or setting the values of the area that
	 * has to be calculated.
	 */
	iter = [depth intValue];
	if (iter==0) {
		[self setParameters: -2.1: -1.5: 3.0];
		[depth setIntValue: 0];
		[pixels_x setIntValue: width];
		[pixels_y setIntValue: height];
		
		[self unlockFocus];
		return self;
	}
	last_x = xmin;
	last_y = ymin;
	last_s = s;
	[self getParameters: &xmin: &ymin: &s];
	[pixels_x setIntValue: width];
	[pixels_y setIntValue: height];
	if (width <= height)
		delta = s/width;
	else
		delta = s/height;
	data = self;
	myWindow = window;

	/* Freeing and then newly allocating the memory for the bitmap data.
	 */
	free(pixels);
	pixels = (unsigned char *)calloc(width * height, sizeof(unsigned char));
	if (pixels) {
		/* Clear the wondow */
		NXImageBitmap(&bbox, width, height, 8, 1, NX_MESHED,
			NX_MONOTONICMASK, pixels, NULL, NULL, NULL, NULL);
		[window flushWindow];
		
		/* We need ymax instead of ymin because of the flippedness of the
		 * bitmap.
		 */
		ymax = ymin + height*delta;
		count = 0;
		calcRect(0.0, 0.0, width, height);
		/* Rendering the calculated image on the screen.
		 */
		NXImageBitmap(&bbox, width, height, 8, 1, NX_MESHED,
			NX_MONOTONICMASK, pixels, NULL, NULL, NULL, NULL);
		[window flushWindow];
	}
	[self unlockFocus];
	/* Signaling the user that the picture is completed.
	 */
	NXBeep();
	return self;
}

- setParameters: (float)x: (float)y: (float)s
{
	[x_min setDoubleValue: x];
	[y_min setDoubleValue: y];
	[scale setDoubleValue: s];
	return self;
}

- getParameters: (double *)x: (double *)y: (double *)s
{
	*x = [x_min doubleValue];
	*y = [y_min doubleValue];
	*s = [scale doubleValue];
	return self;
}

- setlastParameters:sender
{
	if (last_s >= 0) {
		[x_min setDoubleValue: last_x];
		[y_min setDoubleValue: last_y];
		[scale setDoubleValue: last_s];
	}
	return self;
}

- drawSelf :(const NXRect *)rects :(int)rectCount
{
	if (pixels)
		NXImageBitmap(&bbox, width, height, 8, 1, NX_MESHED,
				NX_MONOTONICMASK, pixels, NULL, NULL, NULL, NULL);

	return self;
}

- mouseDown:(NXEvent *)event
/*
 * Imported method from the Mandelbrot demonstrationprogramm.
 */
{
  int looping = YES;
  int oldMask;
  NXRect bbox;
  NXPoint startPoint, currPoint;
  float newX, newY, newDX;
  NXCoord aspectRatio;
  
  aspectRatio = bounds.size.height / bounds.size.width;

  oldMask =  [window addToEventMask:NX_MOUSEDRAGGEDMASK];
  startPoint = event->location;
  [self convertPoint:&startPoint fromView:nil];
  NXSetRect(&bbox,startPoint.x,startPoint.y,0.0,0.0);
  [self lockFocus];
  while (looping) {
    event=[NXApp getNextEvent:NX_MOUSEUPMASK | NX_MOUSEDRAGGEDMASK];
	currPoint = event->location;
    [self convertPoint:&currPoint fromView:nil];
    bbox.size.width = 2*(currPoint.x - startPoint.x);
    bbox.size.height = 2*(currPoint.y - startPoint.y);
    /* Normalize bbox to always have positive width and height */
    if (bbox.size.width < 0) bbox.size.width = -bbox.size.width;
    if (bbox.size.height < 0) bbox.size.height = -bbox.size.height;
    /*
     * constrain the box to have the aspect ratio of the view.  Choose
     * whichever dimension is closer to the desired result.
     */
    if ((bbox.size.height/bbox.size.width) > aspectRatio) {
      bbox.size.height = bbox.size.width * aspectRatio;
    } else {
      bbox.size.width = bbox.size.height / aspectRatio;
    }
    /* The startPoint is always at the center of the bbox */
    bbox.origin.x = startPoint.x - 0.5 * bbox.size.width;
    bbox.origin.y = startPoint.y - 0.5 * bbox.size.height;
    PSnewinstance();
    if (looping = (event->type == NX_MOUSEDRAGGED)) {
      PSsetinstance(YES);
      PSsetgray(NX_WHITE);
      NXFrameRect(&bbox);
      PSsetinstance(NO);
    }
  }
  [self unlockFocus];
  [window setEventMask:oldMask];
  if ((bbox.size.width > 0) && (bbox.size.height > 0)) {
    /*
     * At this point, bbox is in window coordinates.  Set new controller
     * parameters based on this view's coordinates and the bounding box.
     */
    newX = bbox.origin.x;
    newX = newX * delta + xmin - delta/2;
    newY = bbox.origin.y;
    newY = newY * delta + ymin - delta/2;
	if (bbox.size.width <= bbox.size.height)
	    newDX = (bbox.size.width*delta);
	else
		newDX = (bbox.size.height*delta);
    /*
     * Note that we only update the text fields in the mandelView -- we
     * don't update the internal variables.  If we were to update
     * the internal vs, then the user would only get one chance with the
     * mouse down method because subsequent mouse-downs would work in terms
     * of the new coordinate system.
     */
    [self setParameters:newX :newY :newDX];
  }
  return self;
}

- (int)getValues:(unsigned char **)d :(int *)w :(int *)h :(int *)bps
			:(int *)spp :(BOOL *)alpha :(BOOL *)config :(NXColorSpace *)space
/* Method for getting the bitmap and it's associated values.
 */
{
	*d = pixels;
	*w = width;
	*h = height;
	*bps = 8;
	*spp = 1;
	*alpha = NO;
	*config = NO;
	*space = NX_OneIsWhiteColorSpace;
	return (int)pixels;
}

@end
