/*===============================
Copyright © 1998-2002 Brian Hall

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*===============================*/
#include "BlugsPrivate.h"

#ifndef __FP__
	#include <fp.h>
#endif

#pragma mark ¥¥¥Module Data¥¥¥
// All this stuff is unneeded under Carbon.
#if !TARGET_API_MAC_CARBON
enum
{
	// Misc scroll bar constants
	/*blLargeScrollArrowWidth = blLargeScrollBarWidth,
	blSmallScrollArrowWidth = blSmallScrollBarWidth,
	blLargeScrollThumbWidth = blLargeScrollBarWidth,
	blSmallScrollThumbWidth = blSmallScrollBarWidth,
	blLargeTotalWidthAdjust = (blLargeScrollArrowWidth << 1) + blLargeScrollThumbWidth,
	blSmallTotalWidthAdjust = (blSmallScrollArrowWidth << 1) + blSmallScrollThumbWidth,*/
	blThumbTrackWidthSlop = 25,
	blThumbTrackLengthSlop = 113
};



static const Rect	gNullRect = {0,0,0,0};

typedef struct
{
	BlugsRef				list;
	ControlRef		control;
	SInt32			valueSlop;
	SInt32			saveValue;
	SInt32			startValue;
	RgnHandle		saveClip;
	Rect			controlRect;
	Boolean			horizontal;
	UInt8			totalWidthAdjust;
} BLLiveScrollStuff;


static ControlRef		gControl;
static SInt32			gValueSlop;
static SInt32			gSaveValue;
static SInt32			gStartValue;
static RgnHandle		gSaveClip;
static Boolean			gHorizontal;
static UInt8			gTotalWidthAdjust;
static BlugsRef				gList;
static Rect				gControlRect;

static SInt32		local_CalcValueFromPoint( ControlRef inControl, Point inPoint );

#endif // !TARGET_API_MAC_CARBON

static void			local_BLScrollWorld( SInt16 inDX, SInt16 inDY, BlugsRef inList );

#define EnableDrawing		if (gSaveClip) SetClip( gSaveClip );
#define DisableDrawing		ClipRect( &gNullRect );

#pragma mark -
#pragma mark ¥¥¥Scrolling External Interface¥¥¥
//===============================
BL_PROC OSErr BLMakeVisible( BLCell inCell, BlugsRef inList )
{
	OSErr	err;
	
	_CheckHandlerFlag(inList)
	err = _BLValidateList( inList );
	if (!err)
	{
		err = _BLValidateCell( inCell, inList );
		if (!err) _BLMakeVisible( inCell, inList );
	}
	return err;
}


//===============================
// Pass 0 to use default
BL_PROC OSErr BLSetScrollDistance( UInt8 inDistance, Boolean inVertical, BlugsRef inList )
{
	OSErr	err;
	
	err = _BLValidateList( inList );
	if (!err)
	{
		if (!inDistance) inDistance = blDefaultScrollDistance;
		if (inVertical) (*inList)->vScrollDistance = inDistance;
		else (*inList)->hScrollDistance = inDistance;
	}
	return err;
}

//===============================
/* The page up and page down methods are vastly simplified and rather crude.
The original theory was great, but if you go page up, page down, page up, you will
find that you scroll a different amount each time since it was always trying
to align either a top cell or a bottom cell with an edge. Very hard to backtrack.*/
BL_PROC void BLPageUp( BlugsRef inList )
{
	SInt32	dy;
	UInt32	vScrollLoc;
	OSErr	err;
	
	_CheckHandlerFlag(inList)
	err = _BLValidateList( inList );
	if (!err)
	{
		vScrollLoc = bl_VScrollLoc(inList);
		// Scroll entire page minus an arbitrary 12 pixels (make this customizable?)
		dy = _BLViewHeight( inList ) - 12;
		if (dy > vScrollLoc) dy = vScrollLoc;
		if (dy) _BLScroll( blScrollAdjustThumb, 0, -dy, nil, nil, inList );
	}
}

//===============================
BL_PROC void BLPageDown( BlugsRef inList )
{
	SInt32	dy, belowHt;
	UInt16	viewHt;
	UInt32	vScrollLoc, totalHt;
	OSErr	err;
	
	_CheckHandlerFlag(inList)
	err = _BLValidateList( inList );
	if (!err)
	{
		viewHt = _BLViewHeight( inList );
		totalHt = bl_TotalHt(inList);
		vScrollLoc = bl_VScrollLoc(inList);
		dy = viewHt - 12;
		belowHt = totalHt - vScrollLoc - viewHt;
		if (dy > belowHt) dy = belowHt;
		if (dy > 0) _BLScroll( blScrollAdjustThumb, 0, dy, nil, nil, inList );
	}
}


#pragma mark -
#pragma mark ¥¥¥Regular Scrolling¥¥¥
//===============================
// Scrolling down or right, dx/y is positive
// If we are tracking thumb, OS has already set controlValue for us so adjustThumb = false
// You can pass nil for the outActualDX and outActualDY parameters if you don't need them.
void _BLScroll( UInt8 inFlags, SInt32 inDX, SInt32 inDY, SInt32* outActualDX,
				SInt32* outActualDY, BlugsRef inList )
{
	SInt32			actualDX = 0, actualDY = 0;
	UInt16			viewHt, viewWd;
	// bottomLoc and rightLoc are the bottom-most and rightmost visible pixel offsets. 
	UInt32			absoluteBottom, absoluteRight, vLoc, hLoc, bottomLoc, rightLoc;
	Rect			viewRect;
	
	vLoc = bl_VScrollLoc(inList);
	hLoc = bl_HScrollLoc(inList);
	viewRect = _BLViewRect( inList );
	viewHt = bl_Height(viewRect);
	viewWd = bl_Width(viewRect);
	absoluteBottom = bl_TotalHt(inList);
	absoluteRight = bl_TotalWd(inList);
	bottomLoc = vLoc + viewHt;
	rightLoc = hLoc + viewWd;
	if (inDY)
	{
		if (inDY > 0) // scroll down the distance, or as far as possible
		{
			if (absoluteBottom > bottomLoc + inDY) actualDY = inDY;
			else actualDY = (absoluteBottom > bottomLoc) ? absoluteBottom - bottomLoc : 0;
		}
		else // scroll up
		{
			if (vLoc > bl_Abs(inDY)) actualDY = inDY;
			else actualDY = -(vLoc);
		}
		bl_VScrollLoc(inList) += actualDY;
		// Only adjust the scroll bar if movement occurred
	}
	if (inDX)
	{
		if (inDX > 0) // scroll right the distance, or as far as possible
		{
			if (absoluteRight > rightLoc + inDX) actualDX = inDX;
			else actualDX = (absoluteRight > rightLoc) ? absoluteRight - rightLoc : 0;
		}
		else // scroll left
		{
			if (hLoc > bl_Abs(inDX)) actualDX = inDX;
			else actualDX = -(hLoc);
		}
		bl_HScrollLoc(inList) += actualDX;
	}
	if (actualDX || actualDY)
	{
		BLRecalcOptions	options = blRecalcVisCells;
		
		if (bl_Test(inFlags,blScrollAdjustThumb))
		{
			bl_Set(options,blRecalcThumb);
			if (actualDX) bl_Set(options,blRecalcHScroll);
			if (actualDY) bl_Set(options,blRecalcVScroll);
		}
		_BLRecalc( options, 0, inList );
		// Just draw all the new cells since we're scrolling an entire page in some direction
		if ((bl_Abs(actualDY) >= viewHt) || (bl_Abs(actualDX) >= viewWd))
			_BLUpdateWorld( blUpdateBlit, nil, inList );
		else local_BLScrollWorld( actualDX, actualDY, inList );
	}
	if (outActualDX) *outActualDX = actualDX;
	if (outActualDY) *outActualDY = actualDY;
}


//===============================
void _BLScrollTo( BLScrollTarget inTarget, BlugsRef inList )
{
	BLRecalcOptions	options = blRecalcVisCells | blRecalcThumb;
	
	if (bl_Test(inTarget,blScrollTopOrBottomBit))
	{
		if (bl_Test(inTarget,blScrollToTopBit)) bl_VScrollLoc(inList) = 0;
		else
		{
			UInt16			viewHt = _BLViewHeight( inList );
			BLPixelOffset	totalHt = bl_TotalHt(inList);
			
			if (totalHt > viewHt) bl_VScrollLoc(inList) = totalHt - viewHt;
		}
		bl_Set(options,blRecalcVScroll);
	}
	if (bl_Test(inTarget,blScrollLeftOrRightBit))
	{
		if (bl_Test(inTarget,blScrollToLeftBit)) bl_HScrollLoc(inList) = 0;
		else
		{
			UInt16			viewWd = _BLViewWidth( inList );
			BLPixelOffset	totalWd = bl_TotalWd(inList);
			
			if (totalWd > viewWd) bl_HScrollLoc(inList) = totalWd - viewWd;
		}
		bl_Set(options,blRecalcHScroll);
	}
	_BLRecalc( options, 0, inList );
	_BLUpdateWorld( blUpdateBlit | blUpdateBlitAll, nil, inList );
}


//===============================
// scrolling down or right dx/y is positive
pascal void _BLVScrollActionProc( ControlRef inControl, ControlPartCode inPart )
{
	BlugsRef		list;
	UInt8	scrollDistance;
	SInt32	value;
	
	list = (BlugsRef)bl_HiBitClear(GetControlReference( inControl ));
	_AssertStr(list != nil, "Internal error: no list in control reference.");
	if (list)
	{
		scrollDistance = bl_VScrollDist(list);
		switch (inPart)
		{
			case kControlUpButtonPart:
			_BLScroll( blScrollAdjustThumb, 0, -scrollDistance, nil, nil, list );
			break;
			
			case kControlDownButtonPart:
			_BLScroll( blScrollAdjustThumb, 0, scrollDistance, nil, nil, list );
			break;
			
			case kControlPageUpPart:
			BLPageUp( list );
			break;
			
			case kControlPageDownPart:
			BLPageDown( list );
			break;
			
			case kControlIndicatorPart:
			value = (bl_ControlMgr2) ? GetControl32BitValue( inControl ) : GetControlValue( inControl );
			_BLIndicatorMoved( value, false, list );
			break;
			
			default:
			break;
		}
	}
}

//===============================
pascal void _BLHScrollActionProc( ControlRef inControl, ControlPartCode inPart )
{
	BlugsRef		list;
	SInt32	dx, rightWd, value;
	UInt32	hScrollLoc, totalWd;
	UInt16	viewWd;
	
	list = (BlugsRef)bl_HiBitClear(GetControlReference( inControl ));
	_AssertStr(list != nil, "Internal error: no list in control reference.");
	if (list)
	{
		hScrollLoc = bl_HScrollLoc(list);
		viewWd = _BLViewWidth( list );
		totalWd = bl_TotalWd(list);
		switch (inPart)
		{
			case kControlUpButtonPart:
			_BLScroll( blScrollAdjustThumb, -bl_HScrollDist(list), 0, nil, nil, list );
			break;
			
			case kControlDownButtonPart:
			_BLScroll( blScrollAdjustThumb, bl_HScrollDist(list), 0, nil, nil, list );
			break;
			
			case kControlPageUpPart: // Scroll left page size minus 12
			dx = viewWd - 12;
			if (dx > hScrollLoc) dx = hScrollLoc;
			if (dx) _BLScroll( blScrollAdjustThumb, -dx, 0, nil, nil, list );
			break;
			
			case kControlPageDownPart:
			dx = viewWd - 12;
			rightWd = totalWd - hScrollLoc - viewWd;
			if (dx > rightWd) dx = rightWd;
			if (dx > 0) _BLScroll( blScrollAdjustThumb, dx, 0, nil, nil, list );
			break;
			
			case kControlIndicatorPart:
			value = (bl_ControlMgr2) ? GetControl32BitValue( inControl ) : GetControlValue( inControl );
			_BLIndicatorMoved( value, true, list );
			break;
			
			default:
			break;
		}
	}
}


//===============================
// Takes the new control value for a scroll bar
// Assumes control is set to maximize and minimize control maxima and minima
void _BLIndicatorMoved( SInt32 inNewValue, Boolean inHorizontal, BlugsRef inList )
{
	UInt32		newScrollLoc;
	SInt32		hDelta = 0, vDelta = 0;
	
	// Determine delta, based on the ratio of value to control range
	//  times the total height or width minus visible
	if (bl_ControlMgr2) newScrollLoc = inNewValue << 1;
	else
	{
		Rect		viewRect;
		UInt32		range;
		double_t	ratio, temp1;
		
		viewRect = _BLViewRect( inList );
		temp1 = inNewValue + 32768;
		_AssertStr(temp1 >= 0.0, "Internal error: control value < -32768?");
		ratio = temp1/65535.0;
		if (inHorizontal) range = bl_TotalWd(inList) - bl_Width(viewRect);
		else range = bl_TotalHt(inList) - bl_Height(viewRect);
		newScrollLoc = range * ratio;
	}
	if (inHorizontal) hDelta = newScrollLoc - bl_HScrollLoc(inList);
	else vDelta = newScrollLoc - bl_VScrollLoc(inList);
	_BLScroll( 0, hDelta, vDelta, nil, nil, inList );
}


//===============================
// The rect could be the list rect, maybe a title bar rect
// if noH, it will not scroll left or right; if noV it will not scroll up or down
// if constant speed, will scroll by the list's ScrollDistance
// You can pass nil for the limit to ignore it.
void _BLAutoscroll( Point inMouse, Rect inRect, UInt8 inFlags, SInt32* outActualDX,
					SInt32* outActualDY, /*BLScrollLimitPtr inLimit,*/ BlugsRef inList )
{
	SInt16	dx = 0, dy = 0;
	
	if (!PtInRect( inMouse, &inRect ))
	{
		Boolean	constantSpeed;
		
		constantSpeed = bl_Test(inFlags,blAutoscrollConstSpeed);
		if (!bl_Test(inFlags,blAutoscrollNoH))
		{
			if (inMouse.h < inRect.left)	// Scroll left (negative dx)
			{
				if (constantSpeed) dx = -(bl_HScrollDist(inList));
				else dx = (inMouse.h - inRect.left) >> 1;
				/*if (inLimit)
				{
					if (inLimit->left > dx) dx = inLimit->left;
				}*/
			}
			else if (inMouse.h > inRect.right)	// Scroll right (positive dx)
			{
				if (constantSpeed) dx = bl_HScrollDist(inList);
				else dx = (inMouse.h - inRect.right) >> 1;
				/*if (inLimit)
				{
					if (inLimit->right < dx) dx = inLimit->right;
				}*/
			}
		} // Horizontal scrolling allowed
		if (!bl_Test(inFlags,blAutoscrollNoV))
		{
			if (inMouse.v < inRect.top)	// Scroll up (negative dy)
			{
				if (constantSpeed) dy = -(bl_VScrollDist(inList));
				else dy = (inMouse.v - inRect.top) >> 1;
				/*if (inLimit)
				{
					if (inLimit->up > dy) dy = inLimit->up;
				}*/
			}
			else if (inMouse.v > inRect.bottom)	// Scroll down (positive dy)
			{
				if (constantSpeed) dy = bl_VScrollDist(inList);
				else dy = (inMouse.v - inRect.bottom) >> 1;
				/*if (inLimit)
				{
					if (inLimit->down < dy) dy = inLimit->down;
				}*/
			}
		} // Vertical autoscroll allowed
	} // Point outside list rect
	if (dx || dy)
		_BLScroll( blScrollAdjustThumb, dx, dy, outActualDX, outActualDY, inList );
	else
	{
		if (outActualDX) *outActualDX = dx;
		if (outActualDY) *outActualDY = dy;
	}
}


//===============================
// We want to do *animated* scroll for partially visible cells (like Resorcerer does)
// But it's distracting for other kinds of navigation, so just jump to the row/col.
// Nope, it's not so distracting. Better to see where you're jumping.
void _BLMakeVisible( BLCell inCell, BlugsRef inList )
{
	Rect	cellRect;
	OSErr	err;
	
	err = _BLCellRect( inCell, &cellRect, inList );
	if (!err && !EmptyRect(&cellRect))
	{
		Rect	viewRect;
		SInt16	dx = 0, dy = 0;
		
		viewRect = _BLViewRect( inList );
		if (cellRect.top < viewRect.top) dy = cellRect.top - viewRect.top; // scroll up
		else if (cellRect.bottom > viewRect.bottom) dy = cellRect.bottom - viewRect.bottom; // scroll down
		if (cellRect.left < viewRect.left) dx = cellRect.left - viewRect.left; // scroll left
		else if (cellRect.right > viewRect.right) dx = cellRect.right - viewRect.right; // scroll right
		// Left boundary should take priority over right boundary, I guess, because
		//	most content will be left or top -aligned. Not all, though. Hmmm.
		if (cellRect.top == viewRect.top) dy = 0;
		if (cellRect.left == viewRect.left) dx = 0;
		if (dx || dy)
		{
			//if (_BLCellIsVisible(inCell, inList))
			//{
				UInt16	absDX, absDY;
				UInt32	xIncrement = 1, yIncrement = 1, lastX = 1, lastY = 1;
				Boolean	xAccelerating = true, yAccelerating = true;
				
				absDX = bl_Abs(dx);
				absDY = bl_Abs(dy);
				if (!absDX)
				{
					xIncrement = 0;
					lastX = 0;
				}
				if (!absDY)
				{
					yIncrement = 0;
					lastY = 0;
				}
				while (absDX > 0 || absDY > 0)
				{
					UInt32	temp;
					
					_BLScroll( blScrollAdjustThumb, (dx > 0) ? xIncrement : -xIncrement,
								(dy > 0) ? yIncrement : -yIncrement, nil, nil, inList );
					//_AssertStr(xIncrement <= absDX,"Internal error: trying to scroll horz. too far.");
					//_AssertStr(yIncrement <= absDY,"Internal error: trying to scroll vert. too far.");
					if (absDX) absDX -= xIncrement;
					if (absDY) absDY -= yIncrement;
					if (xAccelerating && absDX <= xIncrement + lastX + 1) xAccelerating = false;
					if (yAccelerating && absDY <= yIncrement + lastY + 1) yAccelerating = false;
					if (!absDX) xIncrement = 0;
					else
					{
						if (!xAccelerating)
						{
							if (absDX <= 6) xIncrement = 1;
							else xIncrement = absDX / 2;
						}
						else
						{
							temp = xIncrement;
							xIncrement += lastX;
							lastX = temp;
						}
					}
					if (!absDY) yIncrement = 0;
					else
					{
						if (!yAccelerating)
						{
							if (absDY <= 6) yIncrement = 1;
							else yIncrement = absDY / 2;
						}
						else
						{
							temp = yIncrement;
							yIncrement += lastY;
							lastY = temp;
						}
					}
				}
			//}
			//else _BLScroll( blScrollAdjustThumb, dx, dy, nil, nil, inList );
		}
	}
}

//===============================
static void local_BLScrollWorld( SInt16 inDX, SInt16 inDY, BlugsRef inList )
{	
	GWorldPtr		world;
	
	world = bl_World(inList);
	if (world)
	{
		Boolean			restore;
		RgnHandle		hUpdateRgn = NewRgn(), vUpdateRgn = NewRgn(), updateRgn = NewRgn();
		GWorldPtr		saveWorld;
		PixMapHandle	pm;
		
		restore = _BLGotoWorld( &saveWorld, inList );
		pm = GetGWorldPixMap( world );
		if (LockPixels( pm ))
		{
			Rect			worldRect;
			
			worldRect = _BLOffscreenRect( inList );
			// First scroll vertically. Exclude horizontal title bar.
			if (inDY)
			{
				Rect			dySourceRect;
				BLTitleBarHdl	theHBar;
				
				dySourceRect = worldRect;
				// If we are scrolling down (dy positive) we set the destination top so as not
				//	to draw over the title bar.
				theHBar = bl_HTitle(inList);
				if (theHBar) dySourceRect.top += (*theHBar)->thickness;
				ScrollRect( &dySourceRect, 0, -inDY, vUpdateRgn );
			}
			// Now scroll horizontally
			// This time, exclude the vertical scroll bar but include the horizontal
			// Only makes sense to copy the material that was copied before, plus title bar
			if (inDX)
			{
				Rect			dxSourceRect;
				BLTitleBarHdl	theVBar;
				
				dxSourceRect = worldRect;
				theVBar = bl_VTitle(inList);
				if (theVBar) dxSourceRect.left += (*theVBar)->thickness;
				ScrollRect( &dxSourceRect, -inDX, 0, hUpdateRgn );
			}
			UnlockPixels( pm );
		}
		// Merge the regions into one
		UnionRgn( vUpdateRgn, hUpdateRgn, updateRgn );
		DisposeRgn( vUpdateRgn );
		DisposeRgn( hUpdateRgn );
		if (restore) SetGWorld( saveWorld, nil );
		_BLUpdateWorld( blUpdateBlit | blUpdateBlitAll, updateRgn, inList );
		DisposeRgn( updateRgn );
	}
}


#pragma mark -
#pragma mark ¥¥¥Live Scrolling¥¥¥

#if !__BL_USE_CARBON_API__
//===============================
void _BeginThumbTracking( ControlRef inControl, BlugsRef inList )
{
	Point			thePoint;
	SInt16			val;
	
	gSaveClip = NewRgn();
	GetClip( gSaveClip );
	gList = inList;
	gHorizontal = (inControl == bl_HScroll(inList));
	gTotalWidthAdjust = _BLScrollSize( inList ) * 3;
	gControl = inControl;	
	GetControlBounds( inControl, &gControlRect );
	InsetRect( &gControlRect, (gHorizontal) ? -blThumbTrackLengthSlop : -blThumbTrackWidthSlop,
				(gHorizontal) ? -blThumbTrackWidthSlop : -blThumbTrackLengthSlop );
	val = GetControlValue( inControl );
	gStartValue = gSaveValue = val;
	GetMouse( &thePoint );
	gValueSlop = val - local_CalcValueFromPoint( inControl, thePoint );
	HiliteControl( inControl, kControlIndicatorPart );
	DisableDrawing;
}

//===============================
void _EndThumbTracking( void )
{
	EnableDrawing;
	DisposeRgn( gSaveClip );
	SetControlValue( gControl, gSaveValue );
	HiliteControl( gControl, kControlNoPart );
	Draw1Control( gControl );
}

//===============================
pascal void _ScrollThumbActionProc( void )
{
	SInt16		value, controlValue;
	Point		mouse;
    
	GetMouse( &mouse );
	if (PtInRect( mouse, &gControlRect )) value = local_CalcValueFromPoint( gControl, mouse );
	else value = gStartValue;
	controlValue = GetControlValue( gControl );
	if (value != controlValue)
	{
		EnableDrawing;
		gSaveValue = value;
		_BLIndicatorMoved( value, gHorizontal, gList );
	 	SetControlValue( gControl, value );
	 	DisableDrawing;
	}
}

//===============================
static SInt32 local_CalcValueFromPoint( ControlRef inControl, Point inPoint )
{
	SInt32			value, range, distance;
	Rect			rect;
	SInt16			max, min, pinHere;
	
	max = GetControlMaximum( inControl );
	min = GetControlMinimum( inControl );
	GetControlBounds( inControl, &rect );
	range = max - min;
	if (!gHorizontal)
	{
		// Scroll distance adjusted for scroll arrows and the thumb
		distance = bl_Height(rect) - gTotalWidthAdjust;
		// Pin thePoint to the middle of the thumb
		inPoint.v -= rect.top + (gTotalWidthAdjust / 2);
		pinHere = inPoint.v;
	}
	else
	{
		distance = bl_Width(rect) - gTotalWidthAdjust;
		inPoint.h -= rect.left + (gTotalWidthAdjust / 2);
		pinHere = inPoint.h;
	}
	value = gValueSlop + min + (pinHere * range) / distance;
	if (value < min) value = min;
	else if (value > max) value = max;
	return value;
}

#endif // !__BL_USE_CARBON_API__

