#import "PAThumbWheelCell.h"
#import "PAThumbWheelCellDrawing.h"
#import "PAThumbWheel.h"

/******************************************************************************
	PAThumbWheelCell
	
PAThumbWheel offers the functionality of Slider plus the features that you would expect from a real thumbwheel (including 2 3/4 D Graphics!).

PAThumbWheel has a linear display mode and a radial display mode and offers the ability to assign a value to the visible region of the control as well as an absolute value that the ThumbWheel will either ignore, bound to or wrap around.

PAThumbWheel can also return relative values via its -relativeIntValue & -relativeFloatValue methods. A snap back option allows mouse loops to start from and return to a base value.

Copyright 1992, Jeff Martin. (jmartin@next.com 415-780-3833)
******************************************************************************/
#define PI			3.141592654
#define HALF_PI		1.570796327
#define DEG_TO_RAD 0.0174532925199433		//  PI/180.0
#define SIN(x) (sin(DEG_TO_RAD*(x)))
#define COS(x) (cos(DEG_TO_RAD*(x)))
#define TAN(x) (tan(DEG_TO_RAD*(x)))

#define RAD_TO_DEG 57.295779513082320876	//  180.0/PI
#define ASIN(x) (asin(x)*RAD_TO_DEG)
#define ACOS(x) (acos(x)*RAD_TO_DEG)
#define ATAN(x) (atan(x)*RAD_TO_DEG)
// A mod function for floating values
#define MOD(x,y) ((x) - (y)*(int)((float)(x)/(y)))
// Keep 'a' between x and y
#define CLAMP(a,x,y) (MAX((x), MIN((y), (a))))
// Keep 'a' between 'x' and 'y' by wrapping it to the other side
#define CLAMP_WITH_WRAP(a,x,y)  											\
( ((a) < (x)) ? ((y) - MOD(((x)-(a)),((y)-(x)))) : 						\
( ((a) > (y)) ? ((x) + MOD(((a)-(y)),((y)-(x)))) : (a) ) )
// Is a between x and y
#define ISBETWEEN(a,x,y) (((a)>=(x))&&((a)<=(y)))
#define EQUAL(a,b) (ABS((a)-(b))<0.00001)

@implementation PAThumbWheelCell

// Set reasonable defaults
- init
{
	[super init];
	[self setType:NX_TEXTCELL];
	[self setHorizontal];

	[self setLinear]; [self setUnbounded];
	[self setVisibleMax:1.0]; [self setVisibleMin:-1.0];
	[self setAbsoluteMax:2.0]; [self setAbsoluteMin:-2.0];
	[self setSnapsBack:YES]; [self setSnapBackValue:0.0];
	[self setDashInterval:10]; [self setShowMainDash:YES];
	[self setColor:NX_COLORLTGRAY];
	
	[self sendActionOn:NX_MOUSEDOWNMASK|NX_MOUSEDRAGGEDMASK|NX_MOUSEUPMASK];
	return self;
}

// Override trackMouse to provide an infinitely large area(continuous tracking)
- (BOOL)trackMouse:(NXEvent *)event inRect:(const NXRect *)rect ofView:view
{ cellFrame = *rect; return [super trackMouse:event inRect:NULL ofView:view]; }

// Override to return YES (always track mouse)
- (BOOL)startTrackingAt:(const NXPoint *)startPoint inView:aView { return YES;}

// Override from cell to track mouse
- (BOOL)continueTracking:(const NXPoint *)lastPoint 
    at:(const NXPoint *)currPoint inView:view
{
	float currPointVal =[self floatValueAtPoint:*currPoint forFrame:cellFrame];
	float lastPointVal =[self floatValueAtPoint:*lastPoint forFrame:cellFrame];
	float mouseScale = ([NXApp currentEvent]->flags&NX_ALTERNATEMASK)? .1 : 1;
	
	// If TW is bounded and at absolute bound, only track when mouse comes back
	if([self isBounded]) {
		// Return if we are already at absoluteMax and currPoint is greater
		if(EQUAL([self floatValue], [self absoluteMax])) {
			if(currPointVal > [self absoluteMax]) return YES;
			else lastPointVal = [self absoluteMax];
		}
		// Return if we are already at absoluteMin and currPoint is less
		if(EQUAL([self floatValue], [self absoluteMin])) {
			if(currPointVal < [self absoluteMin]) return YES;
			else lastPointVal = [self absoluteMin];
		}
	}
	
	// Set the float value relative to last point
	[self setFloatValue:[self floatValue] + 
		(currPointVal - lastPointVal)*mouseScale];
	
	// Redraw control
	[view display];
	return YES;	// Always continue to track mouse
}


/******************************************************************************
	floatValueAtPoint:forFrame:
	
	This method gives the value that corresponds to a point with respect to the given frame and the visible range. When in radial mode, the point on the thumbwheel is approximated with a power series for arcCos to get legal values for points outside of the frame.
******************************************************************************/
- (float)floatValueAtPoint:(NXPoint)point forFrame:(NXRect)frame
{
	float pos = [self isVertical] ? point.y : point.x;
	float base = [self isVertical] ? NX_Y(&frame) : NX_X(&frame);
	float width = [self isVertical] ? NX_HEIGHT(&frame) : NX_WIDTH(&frame);
	float value;
	
	if([self isLinear])
		value = [self visibleMin] + (pos - base)/width*[self visibleRange];
	else {
		float radius = width/2;
		float midP = base + radius;
		float x = (midP - pos)/radius;
		// Get degrees by pwr series approximation of ArcCos (Pi/2 - x - x^3/6)
		float alpha = (HALF_PI - x - x*x*x/6);
		// Convert degrees to TW coords
		value = [self visibleMin] + alpha/PI*[self visibleRange];
	}
	return value;
}
	
// Override from cell to snapback if neccessary
- stopTracking:(const NXPoint *)lastPoint at:(const NXPoint *)stopPoint
    inView:view mouseIsUp:(BOOL)flag
{
	// If the ThumbWheel is in snap back mode, snap it back and redraw
	if([self snapsBack]) {
		[self setFloatValue:[self snapBackValue]];
		[self resetRelativeValue];
		[view display];
	}
	return [super stopTracking:lastPoint at:stopPoint inView:view 
		mouseIsUp:flag];
}

// Direction is either DIRECTION_HORIZONTAL or DIRECTION_VERTICAL
- (int)direction { return direction; }
- setDirection:(int)dir
{ direction = dir; [image free]; image = NULL; return self; }
- (BOOL)isVertical { return direction == DIRECTION_VERTICAL; }
- setVertical { direction = DIRECTION_VERTICAL; return self; }
- (BOOL)isHorizontal { return direction == DIRECTION_HORIZONTAL; }
- setHorizontal { direction = DIRECTION_HORIZONTAL; return self; }

/******************************************************************************
	displayMode, setDisplayMode
	
	The displayMode is either DISPLAY_MODE_LINEAR or DISPLAY_MODE_RADIAL. Linear displays a flat ruler type control whereas radial displays a 3D thumbwheel that actually looks curved .
******************************************************************************/
- (int)displayMode { return displayMode; }
- setDisplayMode:(int)mode { displayMode = mode; return self; }
- (BOOL)isRadial { return displayMode == DISPLAY_MODE_RADIAL; }
- setRadial { return [self setDisplayMode:DISPLAY_MODE_RADIAL]; }
- (BOOL)isLinear { return displayMode == DISPLAY_MODE_LINEAR; }
- setLinear { return [self setDisplayMode:DISPLAY_MODE_LINEAR]; }

/******************************************************************************
	intValue, setIntValue, floatValue, setFloatValue
	
	These methods are overridden to allow us to calculate relative values and to constrain the value with respect to the absolute mode and absolute values.
******************************************************************************/
- (int)intValue { return (int)[self floatValue]; }
- setIntValue:(int)val { [self setFloatValue:val]; return self; }
- (float)floatValue { return floatValue; }
- setFloatValue:(float)val
{ 
	// Clamp or Wrap newValue wrt the absoluteMode
	if(!ISBETWEEN(val, [self absoluteMin], [self absoluteMax])) {
		if([self isBounded]) val = 
			CLAMP(val, [self absoluteMin], [self absoluteMax]);
		else if([self isWrapped]) val = 
			CLAMP_WITH_WRAP(val, [self absoluteMin], [self absoluteMax]);
	}

	// Store last float value and set float value
	lastFloatValue = floatValue; floatValue = val;
	return self;
}
- (float)lastFloatValue { return lastFloatValue; }
- setLastFloatValue:(float)value { lastFloatValue=value; return self;}

/******************************************************************************
	visibleMax, visibleMin, visibleRange, middleValue
	
	VisibleMax and visibleMin are the values of the thumbwheel at either end; max value is at right/top, min is at left/bottom. middleValue is the value of the TW at the center (wrt visibleMin and visibleMax).
******************************************************************************/
- (float)visibleMax { return visMax; }
- setVisibleMax:(float)max { visMax = max; return self; }
- (float)visibleMin { return visMin; }
- setVisibleMin:(float)min { visMin = min; return self; }
- (float)visibleRange { return [self visibleMax] - [self visibleMin]; }
- (float)middleValue { return [self visibleMin] + [self visibleRange]/2; }

/******************************************************************************
	absoluteMode, absoluteMax, absoluteMin
	
	The absolute mode refers to ThumbWheel values that exceed the visible range. ABSOLUTE_UNBOUNDED means that the TW can be dragged as high or low as desired. ABSOLUTE_BOUNDED means that the TW will be clamped to some arbitrarily large value. ABSOLUTE_WRAPPED means that the TW will wrap from the absoluteMax to the absoluteMin (and vise-versa) when applicable. AbsoluteMax is the value off to the right and up. AbsoluteMin us the value off to the left and down.
******************************************************************************/
- (int)absoluteMode		{ return absMode; }
- setAbsoluteMode:(int)mode { absMode = mode; return self; }
- (BOOL)isUnbounded		{ return absMode == ABSOLUTE_UNBOUNDED;  }
- setUnbounded			{ return [self setAbsoluteMode:ABSOLUTE_UNBOUNDED]; }
- (BOOL)isBounded 		{ return absMode == ABSOLUTE_BOUNDED; }
- setBounded			{ return [self setAbsoluteMode:ABSOLUTE_BOUNDED]; }
- (BOOL)isWrapped		{ return absMode == ABSOLUTE_WRAPPED; }
- setWrapped			{ return [self setAbsoluteMode:ABSOLUTE_WRAPPED]; }
- (float)absoluteMax	{ return absMax; }
- setAbsoluteMax:(float)value { absMax = value; return self; }
- (float)absoluteMin 	{ return absMin; }
- setAbsoluteMin:(float)value { absMin = value; return self; }
- (float)absoluteRange	{ return [self absoluteMax] - [self absoluteMin]; }

/******************************************************************************
	relativeIntValue, relativeFloatValue, resetRelativeValue
	
	These two methods return the change of the value since the last iteration. This is useful for a relative method call ( rotateBy: as opposed to rotateTo:). resetRelativeValue sets the relative change to zero (typically only called internally when snapping back).
******************************************************************************/
- (int)relativeIntValue
{ return [self intValue] - (int)[self lastFloatValue];}
- (float)relativeFloatValue
{ return [self floatValue] - [self lastFloatValue]; }
- resetRelativeValue { [self setLastFloatValue:[self floatValue]];return self;}

/******************************************************************************
	snapsBack, snapBackValue
	
	It is sometimes useful to have the thumbwheel snap back to some value (zero by default) so that it can be used for relative modification of values (rotate by as opposed to rotateTo:).
******************************************************************************/
- (BOOL)snapsBack				{ return snapsBack; }
- setSnapsBack:(BOOL)flag		{ snapsBack = flag; return self; }
- (float)snapBackValue			{return snapBackValue; }
- setSnapBackValue:(float)value	{ snapBackValue = value;return self;}

/******************************************************************************
	dashInterval, setDashInterval, setDashIntervalEqual

	 The dash interval is either in degrees (DISPLAY_MODE_RADIAL) or PostScript 
points (DISPLAY_MODE_LINEAR). The default is 10 of each.
******************************************************************************/
- (float)dashInterval			{ return dashInterval; }
- setDashInterval:(float)val	{ dashInterval = val; return self; }

/******************************************************************************
	showMainDash, setShowMainDash

	 The main dash is the dash in the center of the control and gives feedback as to the absolute value of the control. This should be set to NO for TW that only provide relative values.
******************************************************************************/
- (BOOL)showMainDash 			{return showMainDash;}
- setShowMainDash:(BOOL)flag	{ showMainDash = flag; return self; }

/******************************************************************************
	color, setColor

	 These methods manipulate the predominant color of the ThumbWheel. Radial ThumbWheel are of course shades of these. The default is NX_COLORLTGRAY. 
******************************************************************************/
- (NXColor)color			{return color;}
- setColor:(NXColor)c		{ color = c; return self; }

/******************************************************************************
	shift
	
	 The shift is how much the dashes are shifted by to achieve the animation of motion it is in points. It is calculated from the visibleRange and the physicalRange (frame).
******************************************************************************/
- (int)shift:(const NXRect *)frame
{	
	if([self isLinear]) {
		if([self isHorizontal])
			return (int)(([self floatValue] - [self visibleMin]) / 
				[self visibleRange]*NX_WIDTH(frame) + .5);
		else
			return (int)(([self floatValue] - [self visibleMin]) / 
				[self visibleRange]*NX_HEIGHT(frame) +.5);
	}
	else return (int)(([self floatValue] - [self visibleMin])/ 
		[self visibleRange]*180 + .5);
}

// Override highlight:inView:lit: so that it does nothing
- highlight:(const NXRect *)frame inView:view lit:(BOOL)flag { return self; }

// Override this so that we track mouse whether or not it is on top of us.
+ (BOOL)prefersTrackingUntilMouseUp { return YES; }



/******************************************************************************

	 The Read and Write Methods are for archival.
******************************************************************************/
- write:(NXTypedStream *)stream
{
	[super write:stream];

	NXWriteType(stream, "i", &displayMode);
	NXWriteType(stream, "i", &direction);

	NXWriteType(stream, "f", &floatValue);
	NXWriteType(stream, "f", &lastFloatValue);

	NXWriteType(stream, "f", &visMax);
	NXWriteType(stream, "f", &visMin);

	NXWriteType(stream, "i", &absMode);
	NXWriteType(stream, "f", &absMax);
	NXWriteType(stream, "f", &absMin);

	NXWriteType(stream, "c", &snapsBack);
	NXWriteType(stream, "f", &snapBackValue);

	NXWriteType(stream, "i", &dashInterval);
	NXWriteType(stream, "c", &showMainDash);

	NXWriteColor(stream, color);
	return self;
}

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

	NXReadType(stream, "i", &displayMode);
	NXReadType(stream, "i", &direction);


	NXReadType(stream, "f", &floatValue);
	NXReadType(stream, "f", &lastFloatValue);

	NXReadType(stream, "f", &visMax);
	NXReadType(stream, "f", &visMin);

	NXReadType(stream, "i", &absMode);
	NXReadType(stream, "f", &absMax);
	NXReadType(stream, "f", &absMin);

	NXReadType(stream, "c", &snapsBack);
	NXReadType(stream, "f", &snapBackValue);

	NXReadType(stream, "i", &dashInterval);
	NXReadType(stream, "c", &showMainDash);

	color = NXReadColor(stream);
	return self;
}

- free
{
	[image free];
	return [super free];
}
@end
