// By Judy D. Halchin, Educational Computing Services, Allegheny College.
// You may freely copy, distribute and reuse this code. 
// Allegheny College and the author disclaim any warranty of any kind, 
// expressed or implied, as to its fitness for any particular use.
// This work was partially supported by a Teacher Preparation grant from the 
// National Science Foundation.

#import "NumberLine.h"
#import "drawLine.h"
#import <appkit/NXImage.h>
#import <appkit/graphics.h>
#import <dpsclient/wraps.h>
#import <appkit/nextstd.h>

@implementation NumberLine

float mantissa(float floatValue);
int   orderOfMag(float floatValue);
int   round(float floatValue);

- initFrame:(NXRect *)frameRect
{
	[super initFrame:frameRect];
	
	//initialize all instance variables
	  linePosition = 30;
      if (bounds.size.width >= bounds.size.height)
	  	  orientation = HORIZONTAL;
	  else
	  	  orientation = VERTICAL;
	  labelPosition = BELOWLEFT;
	  
	  backgroundGray = NX_WHITE;
	  lineGray = NX_BLACK;
	  labelGray = NX_BLACK;
	
	  borderWidth = 0;
	  endBars = NO;
	  
	//make an NXImage to hold the number line
	  numberLineNXImage = [[NXImage allocFromZone:[self zone]] 	
	  											initSize:&bounds.size];
	  
	//if orientation is vertical, rotate to match the number line 
	  if (orientation == VERTICAL)
	  {
	  	  [self rotate:90];
		  [self translate:0 :-bounds.size.height];
	  }

	//scale and translate                        
	  [self scale:bounds.size.width/20 :1];
	  [self translate:10 :linePosition];

	//calculate the tick marks and draw the line in the NXImage
	  [self calculateTicks];
	  [self drawIntoImage];
	
	return self;
}

- drawSelf:(const NXRect *)rects :(int)rectCount
{
    NXPoint compositePoint;
	
	compositePoint.x = bounds.origin.x;
	if (orientation == HORIZONTAL)
		compositePoint.y = bounds.origin.y;
	else
		compositePoint.y = bounds.origin.y + bounds.size.height;
	PSsetgray(1.0);
	NXRectFill(&bounds);
	[numberLineNXImage composite:NX_COPY toPoint:&compositePoint];
	return self;
}

- drawIntoImage
{
	NXSize imageSize;
	float scaleFactor, unscaleFactor, length;
	
	[numberLineNXImage getSize:&imageSize];
	length = MAX(imageSize.width, imageSize.height);
	scaleFactor = length / bounds.size.width;
	unscaleFactor = bounds.size.width / length;
	
	[numberLineNXImage lockFocus];

	drawBorders(imageSize.width, imageSize.height, linePosition, borderWidth,
				backgroundGray, lineGray, orientation, endBars);
						
 	drawLine(bounds.origin.x, bounds.origin.y, bounds.size.width, 
				bounds.size.height, scaleFactor, orientation, lineGray);
				
	drawTicks(bounds.origin.x, bounds.origin.y, bounds.size.height, 
				scaleFactor, orientation, lineGray, tickInfo.firstTick, 
				tickInfo.tickSpacing, tickInfo.firstLabel, 
				tickInfo.labelSpacing, tickInfo.numberOfTicks);
			  
	if (orientation == HORIZONTAL)
		drawHorzLabels(bounds.origin.x, bounds.origin.y, scaleFactor, 
					unscaleFactor, labelPosition, labelGray, 
					tickInfo.firstLabel, tickInfo.labelSpacing, 
					tickInfo.scientific, tickInfo.numberOfLabels, 
					tickInfo.labelString[0], 
					tickInfo.labelString[1], tickInfo.labelString[2], 
					tickInfo.labelString[3], tickInfo.labelString[4], 
					tickInfo.labelString[5], tickInfo.labelString[6], 
					tickInfo.labelString[7], tickInfo.labelString[8], 
					tickInfo.labelString[9], tickInfo.labelString[10], 
					tickInfo.expString[0], tickInfo.expString[1], 
					tickInfo.expString[2], tickInfo.expString[3], 
					tickInfo.expString[4], tickInfo.expString[5], 
					tickInfo.expString[6], tickInfo.expString[7], 
					tickInfo.expString[8], tickInfo.expString[9], 
					tickInfo.expString[10]);
 	else
		drawVertLabels(bounds.origin.x, bounds.origin.y, bounds.size.height, 
					scaleFactor, unscaleFactor, labelPosition, labelGray, 
					tickInfo.firstLabel, tickInfo.labelSpacing, 
					tickInfo.scientific, tickInfo.numberOfLabels, 
					tickInfo.labelString[0], 
					tickInfo.labelString[1], tickInfo.labelString[2], 
					tickInfo.labelString[3], tickInfo.labelString[4], 
					tickInfo.labelString[5], tickInfo.labelString[6], 
					tickInfo.labelString[7], tickInfo.labelString[8], 
					tickInfo.labelString[9], tickInfo.labelString[10], 
					tickInfo.expString[0], tickInfo.expString[1], 
					tickInfo.expString[2], tickInfo.expString[3], 
					tickInfo.expString[4], tickInfo.expString[5], 
					tickInfo.expString[6], tickInfo.expString[7], 
					tickInfo.expString[8], tickInfo.expString[9], 
					tickInfo.expString[10]);
	
	[numberLineNXImage unlockFocus];

	return self;
}

- numberLineNXImage
{
    return numberLineNXImage;
}
    
- (float)max
{
	return bounds.origin.x + bounds.size.width;
}

- (float)min
{
	return bounds.origin.x;
}

- setMin:(float)newMin max:(float)newMax
{
	//return if input is invalid
	  if (newMin >= newMax)
	      return self;

	//perform the translation and rotation
	  [super scale:bounds.size.width/(newMax-newMin) :1];
	  [super translate:bounds.origin.x - newMin :0];
	  	  
	//recalculate ticks and redraw into NXImage
	  [self calculateTicks];
	  [self drawIntoImage];
		
	//display if we're supposed to
	  if (!vFlags.disableAutodisplay)
	  	  [self display];
		  
	return self;
}

- zoomIn:(float)amount
{
	float center, halfRange;
	
  	if (amount > 0)
	{
		center = bounds.origin.x + bounds.size.width/2;
		halfRange = bounds.size.width/amount/2;
		[self setMin:center - halfRange max:center + halfRange];
	}
	return self;
}

- zoomOut:(float)amount
{
	float center, halfRange;
	
  	if (amount > 0)
	{
		center = bounds.origin.x + bounds.size.width/2;
		halfRange = bounds.size.width*amount/2;
		[self setMin:center - halfRange max:center + halfRange];
	}
	return self;
}

- (float)center
{
	return bounds.origin.x + bounds.size.width / 2;
}

- setCenter:(float)newCenter
{
	float translation;
	
	translation = newCenter - (bounds.origin.x + bounds.size.width / 2);
	[self setMin:bounds.origin.x + translation 
			 max:bounds.origin.x + bounds.size.width + translation];
	return self;
}

- slide:(float)translation
{
	[self setMin:bounds.origin.x + translation 
			 max:bounds.origin.x + bounds.size.width + translation];
	return self;
}

- (float)linePosition
{
	return linePosition;
}

- setLinePosition:(float)newPosition
{
	[self translate:0 :newPosition - linePosition];
	linePosition = newPosition;
	[self drawIntoImage];
	return self;
}

- setBackgroundGray:(float)gray
{
	backgroundGray = gray;
	[self drawIntoImage];
	return self;
}

- setLineGray:(float)gray
{
	lineGray = gray;
	[self drawIntoImage];
	return self;
}

- setLabelGray:(float)gray
{
	labelGray = gray;
	[self drawIntoImage];
	return self;
}

- (float)backgroundGray
{
	return backgroundGray;
}

- (float)lineGray
{
	return lineGray;
}

- (float)labelGray
{
	return labelGray;
}


- (int)orientation
{
	return orientation;
}

- (int)labelPosition
{
	return labelPosition;
}

- setLabelPosition:(int)newPosition
{
	labelPosition = newPosition;
	return self;
}

- setBordered:(BOOL)shouldHaveBorder width:(float)width
{
	if (shouldHaveBorder)
		borderWidth = width;
	else
		borderWidth = -1;
	[self drawIntoImage];
	return self;
}

- (BOOL)bordered
{
 	if (borderWidth == -1)
		return NO;
	else
		return YES;
}		
	
- setEndBars:(BOOL)shouldHaveEndBars
{
	endBars = shouldHaveEndBars;
	[self drawIntoImage];
	return self;
}
	
- (BOOL)hasEndBars
{
	return endBars;
}

- calculateTicks
{
	float rawValue, lastLabel, labelValue, tick, length;
	int orderSpacing, orderFirstLast, maxOrder;
	int labelsGoal, spaceAvail, digits, digitsRight=0, i, nonblank;
	char formatString[20], rawLabel[30], *ePointer;
	BOOL spacing25, labelsOK;
	NXSize imageSize;
	
	//calulate the ideal number of labels desired
	  [numberLineNXImage getSize:&imageSize];
	  length = MAX(imageSize.width, imageSize.height);
	  labelsGoal = (int)(length / 100);
	  if (labelsGoal < 3)
	  	labelsGoal = 3;
	
	//propose a label spacing
	  rawValue = mantissa(bounds.size.width) / labelsGoal;
	  
	  spacing25 = NO;
	  if (rawValue <= 0.128)
	  	  tickInfo.labelSpacing = pow(10, orderOfMag(bounds.size.width)-1);
	  else if ((0.128 < rawValue) && (rawValue <= 0.25))
	  	  tickInfo.labelSpacing = 2 * pow(10, orderOfMag(bounds.size.width)-1);
	  else if ((0.25 < rawValue) && (rawValue <= 0.35))
	  {
	  	  tickInfo.labelSpacing = 2.5 *pow(10,orderOfMag(bounds.size.width)-1);
		  spacing25 = YES;
	  }
	  else if ((0.35 < rawValue) && (rawValue <= 0.65))
	  	  tickInfo.labelSpacing = 5 * pow(10, orderOfMag(bounds.size.width)-1);
	  else if ((0.65 < rawValue) && (rawValue <= 1.23))
	  	  tickInfo.labelSpacing = pow(10, orderOfMag(bounds.size.width));
	  else if ((1.23 < rawValue) && (rawValue <= 2.5))
	  	  tickInfo.labelSpacing = 2 * pow(10, orderOfMag(bounds.size.width));
	  else if (2.5 < rawValue)
	  {
	  	  tickInfo.labelSpacing = 2.5 * pow(10, orderOfMag(bounds.size.width));
		  spacing25 = YES;
	  }
	  	 	
	//count the labels the proposed spacing will produce and adjust if nec.
	  labelsOK = NO;
	  while (!labelsOK)
	  {
	    //find the first and last labels that will be used and number of labels
	      //pick a tentative first label and last label
		    tickInfo.firstLabel = floor(bounds.origin.x/tickInfo.labelSpacing) 
										* tickInfo.labelSpacing;
		    if (tickInfo.firstLabel <= bounds.origin.x)
			    tickInfo.firstLabel = tickInfo.firstLabel + 
												tickInfo.labelSpacing;
		    for (lastLabel = tickInfo.firstLabel; 
				 lastLabel + tickInfo.labelSpacing < 
				 					bounds.origin.x + bounds.size.width; 
		  						lastLabel = lastLabel + tickInfo.labelSpacing);
  			if (orderOfMag(lastLabel) < orderOfMag(tickInfo.firstLabel) - 2) 
			{
				orderSpacing = orderOfMag(tickInfo.labelSpacing);
 				lastLabel = round(lastLabel * pow(10, -orderSpacing+2)) *
 								  			pow(10, orderSpacing-2);		
		    }						
	      //determine number of digits and whether to use scientific notation
		    orderSpacing = orderOfMag(tickInfo.labelSpacing);
		    if (abs(orderOfMag(tickInfo.firstLabel)) >= 	
												abs(orderOfMag(lastLabel)))
		  	    orderFirstLast = orderOfMag(tickInfo.firstLabel);
		    else
		  	    orderFirstLast = orderOfMag(lastLabel);
		  
		    if ((abs(orderSpacing) > 3) || (abs(orderFirstLast) > 3))
		  	   tickInfo.scientific = YES;
		    else
		    {
		  	    digits = orderFirstLast - orderSpacing;
			    if (spacing25)
			  	   digits = digits + 1;
			    if (digits > 3)
			  	    tickInfo.scientific = YES;
			    else
			  	    tickInfo.scientific = NO;
		    }
		  
		    if (tickInfo.scientific)
		    {
		  	    digits = orderFirstLast - orderSpacing;
			    if (spacing25)
			  	   digits = digits + 1;
		    }
		    else
		    {
		  	    maxOrder = MAX(abs(orderFirstLast), abs(orderSpacing));

			    if ((orderFirstLast > 0) && (orderSpacing < 0))
			    {
			  	    digits = orderFirstLast - orderSpacing;
				    digitsRight = - orderSpacing;
				    if (spacing25)
				    {
				  	    digits = digits + 1;
					  	digitsRight = digitsRight + 1;
				    }
			    }
			    else
			    {
					digits = maxOrder;
				    if ((orderSpacing <= 0) && spacing25)
				  	    digits = digits + 1;
				    if (orderFirstLast > 0)
						digitsRight = 0;
					else
						digitsRight = maxOrder;
				    if (spacing25 && (orderSpacing <= 0))
				  	    digitsRight = digitsRight + 1;
			    }
		    }

	      //adjust first and/or last label if there isn't enough space
	        spaceAvail = (tickInfo.firstLabel - bounds.origin.x) * length 
												/ bounds.size.width;
		    if (orientation == HORIZONTAL)
			{
				if ((tickInfo.scientific && (spaceAvail < (8+digits)*4)))
			    	tickInfo.firstLabel = tickInfo.firstLabel + 
												tickInfo.labelSpacing;
		    	if ((!tickInfo.scientific) && (spaceAvail < (2+digits)*4))
			    	tickInfo.firstLabel = tickInfo.firstLabel + 
												tickInfo.labelSpacing;
			}
			else
				if (spaceAvail < 6)
			    	tickInfo.firstLabel = tickInfo.firstLabel + 
												tickInfo.labelSpacing;
			
	        spaceAvail = (bounds.origin.x + bounds.size.width - lastLabel) 
												* length / bounds.size.width;

		    if (orientation == HORIZONTAL)
			{
		    	if (tickInfo.scientific && (spaceAvail < (8+digits)*4))
			    	lastLabel = lastLabel - tickInfo.labelSpacing;
		    	if ((!tickInfo.scientific) && (spaceAvail < (2+digits)*4))
			    	lastLabel = lastLabel - tickInfo.labelSpacing;
			}
			else
				if (spaceAvail < 10)
			    	lastLabel = lastLabel - tickInfo.labelSpacing;
	
		  //count the labels
		    tickInfo.numberOfLabels = round((lastLabel - 
							tickInfo.firstLabel)/tickInfo.labelSpacing) + 1;

	  //alter the spacing if the number of labels is not suitable
	    if (tickInfo.numberOfLabels >= 3)
	  	    labelsOK = YES;
	    else
	    {
	  	    if (fabs(mantissa(tickInfo.labelSpacing) - 1) < 0.01)
		       tickInfo.labelSpacing = 5 * 
			   					pow(10, orderOfMag(tickInfo.labelSpacing)-1);
	  	    else if (fabs(mantissa(tickInfo.labelSpacing) - 2) < 0.01)
		       tickInfo.labelSpacing = 
			   					pow(10, orderOfMag(tickInfo.labelSpacing));
	  	    else if (fabs(mantissa(tickInfo.labelSpacing) - 2.5) < 0.01)
		    {
		       tickInfo.labelSpacing = 2 * 
			   					pow(10, orderOfMag(tickInfo.labelSpacing));
			   spacing25 = NO;
		    }
	  	    else if (fabs(mantissa(tickInfo.labelSpacing) - 5) < 0.01)
		    {
		       tickInfo.labelSpacing = 2.5 * 
			   					pow(10, orderOfMag(tickInfo.labelSpacing));
			   spacing25 = YES;
		    }
	    }
	  }   // end of labelsOK loop
	      
	//generate the list of formatted labels

	  labelValue = tickInfo.firstLabel;
	  for (i = 0; i < tickInfo.numberOfLabels; i++)
	  {
		  //put the value into a string
		    if (fabs(labelValue) < tickInfo.labelSpacing * 0.01)
				labelValue = 0;
			if (tickInfo.scientific)
			{	
				//put value into string
				  sprintf(formatString, "%%25.%de", digits);
				  sprintf(rawLabel, formatString, labelValue);
				//delete e and put exponent into separate string
				  ePointer = strchr(rawLabel, 'e');
				  if (*(ePointer+1) == '+')
				  	  if (*(ePointer+2) == '0')
					  	  strcpy(tickInfo.expString[i], ePointer+3);
					  else
					  	  strcpy(tickInfo.expString[i], ePointer+2);
				  else
				  {	  
				  	  strcpy(tickInfo.expString[i], ePointer+1); 
				  	  if (*(ePointer+2) == '0')
					  	  strcpy(tickInfo.expString[i]+1, ePointer+3);
				  }
 				  *ePointer = '\0';
			}	  
			else
			{				 
			  	sprintf(formatString, "%%25.%df", digitsRight);
				sprintf(rawLabel, formatString, labelValue);
			} 
			  
		  //strip off any leading blanks
			nonblank = strspn(rawLabel, " ");
			strcpy(tickInfo.labelString[i], rawLabel + nonblank);
			
		  //don't use decimals with zero
			if (strspn(tickInfo.labelString[i], "0.") == 
											strlen(tickInfo.labelString[i]))
				strcpy(tickInfo.labelString[i], "0");
				
		  //increment value of the label
	  		labelValue = labelValue + tickInfo.labelSpacing;
	  }	
	
	//set the ticks
	  tickInfo.tickSpacing = tickInfo.labelSpacing / 5;
	  for (tickInfo.firstTick = tickInfo.firstLabel, 
	  					tickInfo.numberOfTicks = 0; 
	  			tickInfo.firstTick - tickInfo.tickSpacing > bounds.origin.x; 
	  			tickInfo.firstTick = tickInfo.firstTick - tickInfo.tickSpacing, 
										tickInfo.numberOfTicks++);	
	  tickInfo.numberOfTicks = tickInfo.numberOfTicks + 5 * 
	  										(tickInfo.numberOfLabels-1) + 1;
	  for (tick = lastLabel; 
	  		tick + tickInfo.tickSpacing < bounds.origin.x + bounds.size.width; 
	  		tick = tick + tickInfo.tickSpacing, tickInfo.numberOfTicks++);
															
	return self;
}

- setFrame:(const NXRect *)frameRect
{
	NXSize imageSize;
	NXRect saveBounds;
	
	saveBounds = bounds;
	[super setFrame:frameRect];
	
	[self scale:bounds.size.width/saveBounds.size.width :1];
	[self translate:bounds.origin.x - saveBounds.origin.x 
				   :bounds.origin.y - saveBounds.origin.y];
	if (orientation == HORIZONTAL)
	{
		if (frameRect->size.width < frameRect->size.height)
		{
			orientation = VERTICAL;
			[self rotate:90];
			[self translate:bounds.origin.x - saveBounds.origin.x
						   :bounds.origin.y + saveBounds.origin.y];
		}
	}
	else
		if (frameRect->size.width >= frameRect->size.height)
		{
			orientation = HORIZONTAL;
			[self rotate:-90];
		}
			
	printf("bounds %f %f %f %f\n", bounds.origin.x, bounds.origin.y,
				bounds.size.width, bounds.size.height);
					
	imageSize = bounds.size;
	[self convertSize:&imageSize toView:nil];
	
	[numberLineNXImage free];
	numberLineNXImage = [[NXImage allocFromZone:[self zone]] 
										initSize:&imageSize];
	[self calculateTicks];
	[self drawIntoImage];
	[self display];
		
	return self;
}

- read:(NXTypedStream *)typedStream
{
    [super read:typedStream];

    NXReadTypes(typedStream, "iifffff", &orientation, 
				&labelPosition, &linePosition, &backgroundGray, &lineGray, 
			    &labelGray, &borderWidth, &tickInfo); 
	numberLineNXImage =  NXReadObject(typedStream);
	
    return self;
}

- write:(NXTypedStream *)typedStream
{
   [super write:typedStream];

    NXWriteTypes(typedStream, "iifffff", &orientation, 
				&labelPosition, &linePosition, &backgroundGray, &lineGray, 
			    &labelGray, &borderWidth, &tickInfo); 
	NXWriteObject(typedStream, numberLineNXImage);

   return self;
}

- awake
{
	[super awake];
	[self calculateTicks];
	
	return self;
}

- (const char*)inspectorName
{
	return "NumberLineInspector";
}

float mantissa(float floatValue)
{
	return floatValue / pow(10, orderOfMag(floatValue));
}

int orderOfMag(float floatValue)
{
	char stringValue[30], *ePointer;
	
	sprintf(stringValue, "%e", floatValue);
	ePointer = strchr(stringValue, 'e');
	return atoi(ePointer + 1);
}

int round(float floatValue)  
{
	return (int)(floatValue + 0.5);
}

@end
