#import "NSTableView+util.h"
#import "SwappingTableView.h"
#import "SortingDelegate.h"
#import "AnimatedTableView.h"
#import "Appcontroller.h"

#import "SortingDelegate.h"

// ----------------------------------------------------------------------------------------
// Sorting Support
// ----------------------------------------------------------------------------------------

typedef struct { NSString *key; BOOL descending; } SortContext;

// Sort array of itemNums, by looking up the itemNum in the dictionary of dictionaries.
// based on code of Ondra Cada <ocs@ocs.cz> on cocoa-dev list

int ORDER_BY_CONTEXT(id left, id right, void *ctxt)
{
	SortContext *context = (SortContext*)ctxt;
	int order = 0;
	id key = context->key;
	if (0 != key)
	{
		id first,second;	// the actual objects to compare

		if (context->descending)
		{
			first  = [right objectForKey:key];
			second = [left  objectForKey:key];
		}
		else
		{
			first  = [left  objectForKey:key];
			second = [right objectForKey:key];
		}

		if ([first respondsToSelector:@selector(caseInsensitiveCompare:)])
		{
			order = [first caseInsensitiveCompare:second];
		}
		else	// sort numbers or dates
		{
			order = [(NSNumber *)first compare:second];
		}
	}
	return order;
}

// ----------------------------------------------------------------------------------------
// Animated Sorting Support
// ----------------------------------------------------------------------------------------

// Many concepts and methods here are based on Apple's "AnimatedSlider" sample code.

// The "frame rate" for animating the table sort.
const float kFrameRate = 1.0/30;

// Duration of the animation, probably shouldn't be more than a second.
const float kAnimationTime = 0.75; 	

// How "curvy" the movement should be. 0.2 or 0.3 is a nice value; 0.5 is funky!
const float kCurve = 0.3; 

/*"	This function is used to de-center t (from 0 to 1.0) by a power p (a reasonable range is 0.8 to 1.2).  In conjunction with easeFunction, it will make the animated reordering a little more interesting to watch.

	You can paste y=(1-(1-x)^n)^(1/n) into Pacific Tech's "Graphing Calculator" (included on pre-X systems) to play with this function.
"*/

float curveFunction (float t, float p)
{
	return pow( 1 - pow((1-t),p) , 1/p );
}

/*"	This function implements a sinusoidal ease-in/ease-out for t = 0 to 1.0.  T is scaled to represent the interval of one full period of the sine function, and transposed to lie above the X axis.

	y=(sin((pi*x^n-pi/2))+1)/2
"*/

float easeFunction(float t)
{
	return (sin((t * M_PI) - M_PI_2) + 1.0 ) / 2.0;
}

@interface NSArray (findPositions)
- (NSArray *) findPositionsInUnsortedArray:(NSArray *)fromArray;
@end

@implementation NSArray (findPositions)

/*"	Build an array of NSNumbers, representing the position each item of an array used to be before it was sorted.
"*/

- (NSArray *) findPositionsInUnsortedArray:(NSArray *)fromArray
{
	NSMutableArray *result = [NSMutableArray arrayWithCapacity:[self count]];
	NSEnumerator *theEnum = [self objectEnumerator];
	id object;

	while (nil != (object = [theEnum nextObject]) )
	{
		int indexInOld = [fromArray indexOfObject:object];
		[result addObject:[NSNumber numberWithInt:indexInOld]];
	}
	return result;
}

@end

@implementation SortingDelegate

// ----------------------------------------------------------------------------------------
// Memory
// ----------------------------------------------------------------------------------------

- (void)dealloc
{
	[mSortingColumn release];
	[mSortingKey release];
	[mTimer invalidate];
	[mOldPositionsArray release];
	[super dealloc];
}

// ----------------------------------------------------------------------------------------
// Accessors
// ----------------------------------------------------------------------------------------

- (void) setSortingColumn:(NSTableColumn *)inNewValue
{
	[inNewValue retain];
	[mSortingColumn release];
	mSortingColumn = inNewValue;
}

- (void) setSortingKey:(NSString *)inNewValue
{
	[inNewValue retain];
	[mSortingKey release];
	mSortingKey = inNewValue;
}

// ----------------------------------------------------------------------------------------
// Sorting Animation
// ----------------------------------------------------------------------------------------

/*"	Set the array that holds the old positions of the items before they were sorted.
"*/
- (void) setOldPositionsArray:(NSArray *)inNewValue
{
	[inNewValue retain];
	[mOldPositionsArray release];
	mOldPositionsArray = inNewValue;
}

/*"	Remove any animating NSTimer.  Also clears mAnimationPosition to indicate that we are no longer animating.
"*/
- (void) stopAnimating
{
	[mTimer invalidate];
	mTimer = nil;
	mAnimationPosition = 0;
}

/*"	Set the animation timer, replacing any existing one.
"*/
- (void) setTimer:(NSTimer *)inNewValue
{
	[inNewValue retain];
	[mTimer release];
	[self stopAnimating];
	mTimer = inNewValue;
}

/*"	Invoked by AnimatedTableview.  If we're currently animating, we just return the entire range of all rows, so that all rows get drawn no matter where they are.  (They will still be clipped properly, but if we don't override this, then some rows may not be drawn.)  There are probably ways to make this more efficient, calclating which rows need to be displayed, but as long as our table isn't too big, this should be fine.
"*/
- (NSRange)tableView:(AnimatedTableView *)inTableView rowsInRect:(NSRect)inRect defaultRange:(NSRange)inDefaultRange
{
	if (mAnimationPosition > 0)		// are we currently animating?
	{
		return NSMakeRange(0, [oData count]);
	}
	else
	{
		return inDefaultRange;
	}
}

/*"	Invoked by AnimatedTableview.  Returns a rectangle for the given row in the table.  If we are animating, we calculate a rectangle to be be some percentage of the way between the row's old rectangle and its new rectangle.
"*/

- (NSRect)tableView:(AnimatedTableView *)inTableView rectOfRow:(int)inRowIndex defaultRect:(NSRect)inDefaultRect
{
	if (mAnimationPosition > 0)	// are we currently animating?
	{
		// Get the rectangles of where the row originally was, and where it will end up.
		int oldPosition = [[mOldPositionsArray objectAtIndex:inRowIndex] intValue];
		NSRect oldR = [inTableView unanimatedRectOfRow:oldPosition];
		NSRect newR = [inTableView unanimatedRectOfRow:inRowIndex];

		// t will be our fraction between 0 and 1 of how far along the row should be.
		float t = mAnimationPosition;		// start with linear position based on time

		// Adjust t so that the animation is asymmetrical; it will look like it's curved.
		// If viewing the top half of the table, flip it so our rows don't go out of view.
		float rowPos = ((float) inRowIndex / [oData count]);	// fractional position of row in table
		float rowPosAdjusted = mViewingTop ? (1.0 - rowPos) : rowPos;
		float p = rowPosAdjusted * (kCurve*2.0) + 1.0 - kCurve; // 0 -> 0.8; n/2 -> 1.0; n -> 1.2
		t = curveFunction(t, p);	// comment this out to "straighten" the sort

		// "Ease" the value sinusoidally so it will accelerate and decelerate.
		t = easeFunction(t);  // comment this out to make it linear acceleration

		// Calculate a rectangle between the original and the final rectangles.
		NSRect newRect = NSMakeRect(
			NSMinX(oldR) + (t * (NSMinX(newR) - NSMinX(oldR))),
			NSMinY(oldR) + (t * (NSMinY(newR) - NSMinY(oldR))),
			NSWidth(newR), NSHeight(newR) );
		return newRect;
	}
	else
	{
		return inDefaultRect;	// not animating, just return the standard value.
	}
}

/*"	Invoked by the NSTimer multiple times to handle the animation.  Calculate the position we are based on the current time, and then either force a display of the table, or stop the animation.
"*/

- (void) animateStep:(NSTimer *)inTimer
{
	NSDate *start = [inTimer userInfo];
	NSTimeInterval elapsed = fabs([start timeIntervalSinceNow]);
	mAnimationPosition = MIN(1.0 , elapsed / kAnimationTime);

	// Done yet?  Allow a bit of "servo jitter" to allow for floating-point messiness
	if (fabs(mAnimationPosition - 1.0) <= 0.01)  
	{
		[self stopAnimating];
		[oTable display];		// force a display of the table in its normal state
	}
	else
	{
		[oTable display];		// force a display of the table in its new state.
	}
}

/*"	Initiate an animated sort.  We pass in the old 
"*/

- (void)tableView:(NSTableView *)inTableView animateSortFromArray:(NSArray *)fromArray toArray:(NSArray *)toArray
{
	NSTimer *timer;
	NSRange visibleRows;

	[self stopAnimating];  // Stop any existing sort animation
	[self setOldPositionsArray:[toArray findPositionsInUnsortedArray:fromArray]];

	// Determine if the table is scrolled to approximately the top part of the table.
	// This will affect the "center point" for a sort, so we get more visible movement,
	// so the user won't see the rows disappear from view during the sort.
	// We just see if the topmost visible row is less than #rows * 0.6.
	visibleRows = [inTableView rowsInRect:[inTableView visibleRect]];
	mViewingTop = NSMaxRange(visibleRows) < 0.6 * [fromArray count];

	timer =
		[NSTimer scheduledTimerWithTimeInterval:kFrameRate
			target:self
			selector:@selector(animateStep:)
			userInfo: [NSDate date]	// store the starting date as user info
			repeats:YES];
	[self setTimer:timer];	// store the timer so we can stop it when done.
}


// ----------------------------------------------------------------------------------------
// Selection save/restore
// ----------------------------------------------------------------------------------------

- (NSSet *) saveSelectionFromTable:(NSTableView *)inTableView
{
	NSMutableSet *result = [NSMutableSet set];
	NSEnumerator *theEnum = [inTableView selectedRowEnumerator];
	NSNumber *rowNum;

	while (nil != (rowNum = [theEnum nextObject]) )
	{
		id item = [oData objectAtIndex:[rowNum intValue]];
		[result addObject:item];
	}
	return result;
}

/*"	Restore the selection to the given set of row numbers.  If inRefresh is YES, the display is scrolled to visible and marked as needing refresh.  If NO, the display is marked as not needing to be redisplayed; this is for animation support.
"*/
- (void) restoreSelection:(NSSet *)inSelectedItemNums toTable:(NSTableView *)inTableView refresh:(BOOL)inRefresh
{
	NSEnumerator *theEnum = [inSelectedItemNums objectEnumerator];
	id item;
	int savedLastRow;

	[inTableView deselectAll:nil];

	while (nil != (item = [theEnum nextObject]) )
	{
		int row = [oData indexOfObjectIdenticalTo:item];	// look for an exact match, which is OK here.
		if (NSNotFound != row)
		{
			[inTableView selectRow:row byExtendingSelection:YES];
			savedLastRow = row;
		}
	}
	if (inRefresh)
	{
		[inTableView scrollRowToVisible:savedLastRow];
	}
	else
	{
		[inTableView setNeedsDisplay:NO];
	}
}


- (void) sortData
{
	SortContext ctxt={ mSortingKey, mSortDescending };
	NSSet *oldSelection = [self saveSelectionFromTable:oTable];

	// sort the NSMutableArray

	if ([[NSApp delegate] animateSorts])
	{
		NSArray *originalOrderArray = [oData copy];
		[oData sortUsingFunction:ORDER_BY_CONTEXT context:&ctxt];
		[self restoreSelection:oldSelection toTable:oTable refresh:NO];

		[self tableView:oTable animateSortFromArray:originalOrderArray toArray:oData];
		// When done, the data should be displayed correctly.
	}
	else
	{
		[oData sortUsingFunction:ORDER_BY_CONTEXT context:&ctxt];
		[oTable reloadData];
		[self restoreSelection:oldSelection toTable:oTable refresh:YES];
	}
}

- (void)sortByColumn:(NSTableColumn *)inTableColumn
{
    if (mSortingColumn == inTableColumn)
	{
        // User clicked same column, change sort order
        mSortDescending = !mSortDescending;
		// Possible optimization: Don't actually re-sort if you just change the sorting direction;
		// instead, just display either the nth item or the (count-1-n)th item depending on ascending/descending.)
    }
	else
	{
        // User clicked new column, change old/new column headers,
        // save new sorting column, and re-sort the array.
        mSortDescending = NO;
        if (nil != mSortingColumn)
		{
            [oTable setIndicatorImage: nil inTableColumn: mSortingColumn];
        }
		[self setSortingKey:[inTableColumn identifier]];
		[self setSortingColumn:inTableColumn];
		[oTable setHighlightedTableColumn:inTableColumn];
    }
    [oTable setIndicatorImage: (mSortDescending ?
							  [NSTableView descendingSortIndicator] :
							  [NSTableView ascendingSortIndicator])
								   inTableColumn: inTableColumn];
	// Actually sort the data
    [self sortData];
}

- (void)awakeFromNib
{
	// Set up initial sorting.  This could actually read from a preference...
	// Perform this sort in the immediate future since things aren't quite ready yet at wakeup.
	[self performSelector:@selector(sortByColumn:) withObject:[[oTable tableColumns] objectAtIndex:0] afterDelay:0.0];
}


/*"	Sort by whatever column was clicked upon
"*/
- (void)tableView:(NSTableView*)inTableView
	didClickTableColumn:(NSTableColumn *)inTableColumn
{
	[[inTableView window] makeFirstResponder:inTableView];		// help make this tableView be first responder
	[self sortByColumn:inTableColumn];
}

// ----------------------------------------------------------------------------------------
// Alphabetic Type Ahead
// ----------------------------------------------------------------------------------------

- (void) typeAheadString:(NSString *)inString inTableView:(NSTableView *)inTableView
{
	// This general sample looks for a highlighted column, presuming that is that column we are sorted by, and uses that as the lookup key.
	NSTableColumn *col = [inTableView highlightedTableColumn];
	if (nil != col)
	{
		NSString *key = [col identifier];
		int i;
		for ( i = 0 ; i < [oData count] ; i++ )
		{
			NSDictionary *rowDict = [oData objectAtIndex:i];
			NSString *compareTo = [rowDict objectForKey:key];
			NSComparisonResult order = [inString caseInsensitiveCompare:compareTo];
			if (order != NSOrderedDescending)
			{
				break;
			}
		}
		// Make sure we're not overflowing the row count.
		if (i >= [oData count])
		{
			i = [oData count] - 1;
		}
		// Now select row i -- either the one we found, or the last row if not found.
		[inTableView selectRow:i byExtendingSelection:NO];
		[inTableView scrollRowToVisible:i];
	}
}

@end

