//
//  KFTypeSelectTableView.m
//  KFTypeSelectTableView v1.0.4
//
//  ------------------------------------------------------------------------
//  Copyright (c) 2005, Ken Ferry All rights reserved.
//
//  Redistribution and use in source and binary forms, with or without
//  modification, are permitted provided that the following conditions are
//  met:
//
//  (1) Redistributions of source code must retain the above copyright notice,
//      this list of conditions and the following disclaimer.
//
//  (2) Redistributions in binary form must reproduce the above copyright
//      notice, this list of conditions and the following disclaimer in the
//      documentation and/or other materials provided with the distribution.
//      
//  (3) Neither Ken Ferry's name nor the names of other contributors
//      may be used to endorse or promote products derived from this software
//      without specific prior written permission.
//
//
//  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
//  IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
//  TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
//  PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
//  OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
//  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
//  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
//  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
//  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
//  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
//  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
//  ------------------------------------------------------------------------


#import <KFTypeSelectTableView.h>
#import <CoreServices/CoreServices.h>
#include <mach/mach.h>
#include <mach/mach_time.h>

static uint64_t SecondsToMachAbsolute(double seconds);

NSString *KFTypeSelectTableViewPatternDidChangeNotification = @"KFTypeSelectTableViewPatternDidChange";

/* NOTE - because of behavior detailed at cocoadev.com/index.pl?PosingWithCategoriesSuperGotcha, 
 * it's important that private methods be _implemented_ in the main implementation of the class,
 * not in a category.  It's okay for the declarations to be in a category, as below.
 * (Summary of link:  messages to super act like messages to self in categories on a posing class
 * in system 10.3)
 */
@interface KFTypeSelectTableView (Private)

// responding to events
static BOOL KFKeyEventIsBeginFindEvent(NSEvent *keyEvent);
static BOOL KFKeyEventIsExtendFindEvent(NSEvent *keyEvent);
static BOOL KFKeyEventIsFindNextEvent(NSEvent *keyEvent);
static BOOL KFKeyEventIsFindPreviousEvent(NSEvent *keyEvent);
static BOOL KFKeyEventIsDeleteEvent(NSEvent *keyEvent);
static BOOL KFKeyEventIsCancelEvent(NSEvent *keyEvent);

// finding strings
- (void)kfFindPattern:(NSString *)pattern 
           initialRow:(int)initialRow 
          topToBottom:(BOOL)topToBottom
       allowExtension:(BOOL)allowPatternExtension;
- (BOOL)kfWorkUnitGetMatch:(NSString **)match
                     range:(NSRange *)matchRange
           lastSearchedRow:(int *)lastSearchedRow
                forPattern:(NSString *)pattern
              matchOptions:(unsigned)patternMatchOptions
                initialRow:(int)initialRow
               boundaryRow:(int)boundaryRow
              rowIncrement:(int)rowIncrement
             searchColumns:(NSArray *)searchColumns
                   timeout:(uint64_t)timeout;
- (BOOL)kfShouldAcceptMatch:(NSString *)match 
                      range:(NSRange)matchedRange 
                      inRow:(int)row;
- (BOOL)kfCanPerformTypeSelect;
- (BOOL)kfSelectionShouldChange;
- (BOOL)kfCanGetTableData;
- (NSString *)kfStringValueForTableColumn:(NSTableColumn *)column row:(int)row;
- (NSArray *)kfSearchColumns;
- (BOOL)kfSearchTopToBottom;
- (int)kfInitialRowForNewSearch;

// taking action
- (void)kfPatternDidChange:(id)sender;
- (void)kfDidFindMatch:(NSString *)match 
                 range:(NSRange)matchedRange 
                 inRow:(int)row;
- (void)kfDidFailToFindMatchSearchingToRow:(int)row;
- (void)kfResetSearch;
- (void)kfConfigureDelegateIfNeeded;

// utility
- (BOOL)kfRowIsVisible:(int)row;
- (void)kfScrollRectToCenter:(NSRect)aRect vertical:(BOOL)scrollVertical horizontal:(BOOL)scrollHorizontal;

// simulated ivars infrastructure
- (NSMutableDictionary *)kfSimulatedIvars;
- (id)kfIdentifier;
- (void)kfSetUpSimulatedIvars;
- (void)kfTearDownSimulatedIvars;

// accessors
- (int)kfSavedRowForExtensionSearch;
- (void)setKfSavedRowForExtensionSearch:(int)row;
- (NSString *)kfLastSuccessfullyMatchedPattern;
- (void)setKfLastSuccessfullyMatchedPattern:(NSString *)string;
- (BOOL)kfCanExtendFind;
- (void)setKfCanExtendFind:(BOOL)flag;
- (id)kfLastConfiguredDelegate;
- (void)setKfLastConfiguredDelegate:(id)anObject;
- (NSInvocation *)kfTimeoutInvocation;
- (void)setKfTimeoutInvocation:(NSInvocation *)anInvocation;
- (void)setPattern:(NSString *)pattern;
@end


@implementation KFTypeSelectTableView 

#pragma mark -
#pragma mark SETUP/TEARDOWN
#pragma mark -


// Note: don't use init.  Won't receive it for preexisting objects when posing.

- (void)dealloc
{
    NSInvocation *timeoutInvocation = [self kfTimeoutInvocation];
    [[timeoutInvocation class] cancelPreviousPerformRequestsWithTarget:[self kfTimeoutInvocation]
                                                              selector:@selector(invoke) 
                                                                object:nil];
    [self kfTearDownSimulatedIvars];
    [super dealloc];
}

#pragma mark -
#pragma mark BODY
#pragma mark -

#pragma mark responding to events

- (void)keyDown:(NSEvent *)keyEvent
{
    // Will we drop this event to super? 
    BOOL eatEvent = NO;
    
    if ([self kfCanPerformTypeSelect] && ([[self window] firstResponder] == self))
    { 
        BOOL canExtendFind = [self kfCanExtendFind];

        if (canExtendFind && KFKeyEventIsExtendFindEvent(keyEvent))
        {
            NSText *fieldEditor = [[self window] fieldEditor:YES forObject:self];
            [fieldEditor interpretKeyEvents:[NSArray arrayWithObject:keyEvent]];

            [self kfFindPattern:[fieldEditor string]
                     initialRow:[self kfSavedRowForExtensionSearch]
                    topToBottom:[self kfSearchTopToBottom]
                 allowExtension:YES];            
            eatEvent = YES;
        }
        else if (KFKeyEventIsBeginFindEvent(keyEvent))
        {
            NSText *fieldEditor = [[self window] fieldEditor:YES forObject:self];
            [fieldEditor setString:@""];
            [fieldEditor interpretKeyEvents:[NSArray arrayWithObject:keyEvent]];
            
            NSString *newPattern = [fieldEditor string];
            
            if (![newPattern isEqualToString:@""])
            {
                [self kfFindPattern:[fieldEditor string]
                         initialRow:[self kfInitialRowForNewSearch]
                        topToBottom:[self kfSearchTopToBottom]
                     allowExtension:YES];
            }
            
            eatEvent = YES;
        }
        else if (canExtendFind && KFKeyEventIsDeleteEvent(keyEvent))
        {
            // User might expect us to knock a character off the pattern - that'd be dangerous.
            // If the user mistimed he could trigger a table view delete action.
            // Best to squelch the behavior by not doing anything useful.
            
            eatEvent = YES;
        }        
        else if (KFKeyEventIsFindNextEvent(keyEvent))
        {
            [self findNext:self];
            eatEvent = YES;
        }
        else if (KFKeyEventIsFindPreviousEvent(keyEvent))
        {
            [self findPrevious:self];
            eatEvent = YES;
        }
        else if (KFKeyEventIsCancelEvent(keyEvent))
        {
            // this is superfluous in 10.2 and 10.3, but may be useful on systems prior to
            // 10.2.  I haven't had a chance to find out.
            [self cancelOperation:self];
            eatEvent = YES;
			[super keyDown:keyEvent];
        }
    }
    
    if (!eatEvent)
    {
        // FIXME - hack  
        // I can't find a decent way to clear a hanging dead-key (i.e. option-e) in kfResetSearch.
        // Sending an event following a dead key event through interpretKeyEvents is the 
        // only thing I've found to do it, so we make sure that any keyEvent that we don't understand
        // goes through the field editor's interpretKeyEvents.  It won't cause any damage because the field
        // editor has no delegate and will be cleared before it's used again anyway.  
        // Without this workaround, entering "option-e, f" will stick this table in a state where all 
        // key-events start with character "«", which means type-select won't work. The state is only 
        // exited when a different control starts processing text.
        //
        // This workaround kills the above problem, but is suboptimal in that a dead-key never times out (besides just
        // being nasty).
        NSText *fieldEditor = [[self window] fieldEditor:YES forObject:self];
        [fieldEditor interpretKeyEvents:[NSArray arrayWithObject:keyEvent]];
        // end hack
        
        [self setKfCanExtendFind:NO];
        [super keyDown:keyEvent];
    }
}

- (void)cancelOperation:(id)sender
{
    [self kfResetSearch];
}

// 10.2 private version of cancelOperation
// I'm not sure how far back this will work.
- (void)_cancelKey:(id)sender
{
    [self cancelOperation:sender];
}


// an outline view catches control-down and control-up
// and uses them to select next and previous rows (same as plain up and down)
// We can catch the events by implementing moveDown and moveUp.

- (void)moveDown:(id)sender
{
    NSEvent *currentEvent = [NSApp currentEvent];
    if ([currentEvent type] == NSKeyDown && KFKeyEventIsFindNextEvent(currentEvent))
    {
        [self findNext:self];
    }
}

- (void)moveUp:(id)sender
{
    NSEvent *currentEvent = [NSApp currentEvent];
    if ([currentEvent type] == NSKeyDown && KFKeyEventIsFindPreviousEvent(currentEvent))
    {
        [self findPrevious:self];
    }    
}


- (BOOL)resignFirstResponder
{
    BOOL shouldResign = [super resignFirstResponder];
    if (shouldResign)
    {
        [self setKfCanExtendFind:NO];
    }
    
    return shouldResign;
}

static unsigned int modifierFlagsICareAboutMask = NSCommandKeyMask | NSShiftKeyMask | NSControlKeyMask | NSAlternateKeyMask | NSFunctionKeyMask;

// yes if every character in the event is alphanumeric and no command, control or function modifiers 
static BOOL KFKeyEventIsBeginFindEvent(NSEvent *keyEvent)
{
    unsigned int modifiers = [keyEvent modifierFlags] & modifierFlagsICareAboutMask;
    NSString *characters = [keyEvent characters];
    int numCharacters = [characters length];
    
    if ((modifiers & (NSCommandKeyMask | NSControlKeyMask | NSFunctionKeyMask)) != 0)
    {
        return NO;
    }
    
    NSMutableCharacterSet *beginFindCharacterSet = [[[NSCharacterSet alphanumericCharacterSet] mutableCopy] autorelease];
    [beginFindCharacterSet formUnionWithCharacterSet:[NSCharacterSet punctuationCharacterSet]];

    unichar character;
	int i;
    for (i = 0; i < numCharacters; i++)
    {
        character = [characters characterAtIndex:i];
        if (![beginFindCharacterSet characterIsMember:character])
        {
            return NO;
        }
    }
    
    return YES;    
}

// yes if every character in the event is alphanumeric, punctuation or a space, and no command, control or function modifiers 
static BOOL KFKeyEventIsExtendFindEvent(NSEvent *keyEvent)
{
    unsigned int modifiers = [keyEvent modifierFlags] & modifierFlagsICareAboutMask;
    NSString *characters = [keyEvent characters];
    int numCharacters = [characters length];
    
    if ((modifiers & (NSCommandKeyMask | NSControlKeyMask | NSFunctionKeyMask)) != 0)
    {
        return NO;
    }
    
    NSMutableCharacterSet *extendFindCharacterSet = [[[NSCharacterSet alphanumericCharacterSet] mutableCopy] autorelease];
    [extendFindCharacterSet formUnionWithCharacterSet:[NSCharacterSet punctuationCharacterSet]];
    [extendFindCharacterSet addCharactersInString:@" "];
    
    unichar character;
	int i;
    for (i = 0; i < numCharacters; i++)
    {
        character = [characters characterAtIndex:i];
        if (![extendFindCharacterSet characterIsMember:character])
        {
            return NO;
        }
    }
    
    return YES;    
}

static BOOL KFKeyEventIsFindNextEvent(NSEvent *keyEvent)
{
    unsigned int modifiers = [keyEvent modifierFlags] & modifierFlagsICareAboutMask;
    NSString *characters = [keyEvent characters];
    int numCharacters = [characters length];
    
    if (numCharacters == 1 && [characters characterAtIndex:0] == NSDownArrowFunctionKey && modifiers == (NSControlKeyMask | NSFunctionKeyMask))
    {
        return YES;
    }
    
    
    return NO;    
}

static BOOL KFKeyEventIsFindPreviousEvent(NSEvent *keyEvent)
{
    unsigned int modifiers = [keyEvent modifierFlags] & modifierFlagsICareAboutMask;
    NSString *characters = [keyEvent characters];
    int numCharacters = [characters length];
    
    if (numCharacters == 1 && [characters characterAtIndex:0] == NSUpArrowFunctionKey && modifiers == (NSControlKeyMask | NSFunctionKeyMask))
    {
        return YES;
    }
    
    return NO;    
}

static BOOL KFKeyEventIsDeleteEvent(NSEvent *keyEvent)
{
    unsigned int modifiers = [keyEvent modifierFlags] & modifierFlagsICareAboutMask;
    NSString *characters = [keyEvent characters];
    int numCharacters = [characters length];
    
    if (numCharacters == 1 && [characters characterAtIndex:0] == NSDeleteCharacter && modifiers == 0)
    {
        return YES;
    }
    if (numCharacters == 1 && [characters characterAtIndex:0] == NSBackspaceCharacter && modifiers == 0)
    {
        return YES;
    }    
    
    return NO;    
}

static BOOL KFKeyEventIsCancelEvent(NSEvent *keyEvent)
{
    unsigned int modifiers = [keyEvent modifierFlags] & modifierFlagsICareAboutMask;
    NSString *characters = [keyEvent characters];
    int numCharacters = [characters length];
    
    const unichar EscapeKeyCharacter = 0x1b;
    
    if ((modifiers == NSCommandKeyMask) && [characters isEqualToString:@"."])
    {
        return YES;
    }
    if (numCharacters == 1 && [characters characterAtIndex:0] == EscapeKeyCharacter && modifiers == 0)
    {
        return YES;
    }    

    return NO;    
}


#pragma mark finding patterns

- (void)findNext:(id)sender
{
    NSString *lastPattern = [self kfLastSuccessfullyMatchedPattern];
    
    if (lastPattern == nil || ![self kfCanPerformTypeSelect])
    {
        NSBeep();
    }
    else
    {
        [self kfFindPattern:lastPattern
                 initialRow:[self selectedRow] + 1 
                topToBottom:YES
             allowExtension:NO];        
    }
}

- (void)findPrevious:(id)sender
{
    NSString *lastPattern = [self kfLastSuccessfullyMatchedPattern];
    
    if (lastPattern == nil || ![self kfCanPerformTypeSelect])
    {
        NSBeep();
    }
    else
    {
        [self kfFindPattern:lastPattern
                 initialRow:[self selectedRow] - 1 
                topToBottom:NO
             allowExtension:NO];        
    }
}


- (void)kfFindPattern:(NSString *)pattern 
           initialRow:(int)initialRow 
          topToBottom:(BOOL)topToBottom
       allowExtension:(BOOL)allowPatternExtension
{
    NSArray *searchColumns = [self kfSearchColumns];
	
    BOOL shouldWrap = [self searchWraps];
    unsigned patternMatchOptions;
    NSDate *distantPast = [NSDate distantPast];
    NSMutableArray *suspendedEvents = [NSMutableArray array];
    const uint64_t eventCheckFrequency = SecondsToMachAbsolute(.01);
    NSString *match = nil;
    NSRange matchRange = {0,0};
    
    // we'll translate topToBottom into these parameters
    // so that we can use a single loop for both directions
    int rowIncrement, boundaryRow;

    if (topToBottom)
    {
        rowIncrement = 1;
        boundaryRow = [self numberOfRows];
        initialRow = (initialRow < boundaryRow) ? initialRow : boundaryRow;
        initialRow = (initialRow > 0) ? initialRow : 0;
        if (initialRow == 0)
            shouldWrap = NO;
    }
    else
    {
        rowIncrement = -1;
        boundaryRow = -1;
        initialRow = (initialRow > boundaryRow) ? initialRow : boundaryRow;
        initialRow = (initialRow < [self numberOfRows] - 1) ? initialRow : [self numberOfRows] - 1;
        if (initialRow == [self numberOfRows] - 1)
            shouldWrap = NO;
    }
    
    // keep ivars in sync
    [self setPattern:pattern];
    [self setKfCanExtendFind:allowPatternExtension];
    
    // set up pattern match options
    if ([self matchAlgorithm] == KFPrefixMatchAlgorithm)
    {
        patternMatchOptions = NSCaseInsensitiveSearch | NSAnchoredSearch;
    }
    else // substring match
    {
        patternMatchOptions = NSCaseInsensitiveSearch;
    }
    
    BOOL finished = NO;
    int row = initialRow;
    while (!finished)
    {
        // Mail generates 3MB in autoreleased objects in a search through 15000 rows
        // there's a noticable pause when they're deallocated.  We'll avoid it by using
        // our own autorelease pool. 
        // (Update - implementing typeSelectTableView:stringValueForTableColumn:row: in the mail
        // plugin dropped the number of allocations)
        //
        // Note: checking for new input no more often than 100 times a second drops time spent 
        // in -[NSApplication nextEventMatchingMask::::] from 30-60% of total function time to .2-.5% 
        // at a cost of < 1% for timing functions.
        NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
        finished = [self kfWorkUnitGetMatch:&match
                                      range:&matchRange
                            lastSearchedRow:&row
                                 forPattern:pattern
                               matchOptions:patternMatchOptions
                                 initialRow:row
                                boundaryRow:boundaryRow
                               rowIncrement:rowIncrement
                              searchColumns:searchColumns
                                    timeout:eventCheckFrequency];
        [match retain];
        [pool release];
        [match autorelease];
        
        if (!finished)
            row += rowIncrement;
        
        if (finished && match == nil && shouldWrap)
        {
            if (topToBottom)
                row = 0;
            else
                row = [self numberOfRows] - 1;
            
            boundaryRow = initialRow;
            shouldWrap = NO;
            finished = NO;
        }
        
        if (!finished)
        {
            NSEvent *keyEvent;
            while ((keyEvent = [NSApp nextEventMatchingMask:NSKeyDownMask
                                                  untilDate:distantPast // means grab events that have already occurred
                                                     inMode:NSEventTrackingRunLoopMode 
                                                    dequeue:YES]) != nil)
            {
                if (KFKeyEventIsCancelEvent(keyEvent))
                {
                    [self kfResetSearch];
                    // we intentionally dump the suspended events in this case
                    return;
                }
                else if (allowPatternExtension  && KFKeyEventIsExtendFindEvent(keyEvent))
                {
                    NSText *fieldEditor = [[self window] fieldEditor:YES forObject:self];
                    [fieldEditor interpretKeyEvents:[NSArray arrayWithObject:keyEvent]];
                    pattern = [fieldEditor string];
                    [self setPattern:pattern];
                }
                else if (KFKeyEventIsDeleteEvent(keyEvent))
                {
                    // eat the event, do nothing.
                    // User might expect us to knock a character off the pattern - that'd be dangerous.
                    // If the user mistimed he could trigger a table view delete action.  
                    // Best to squelch the behavior by not doing anything useful.
                }
                else
                {
                    [suspendedEvents addObject:keyEvent];
                }
            }            
        }
    }
        
    if (match != nil)
    {
        [self kfDidFindMatch:match
                       range:matchRange
                       inRow:row];
    }
    else
    {
        [self kfDidFailToFindMatchSearchingToRow:row];
    }
    
    int numSuspendedEvents, i;
    numSuspendedEvents = [suspendedEvents count];
    for (i = numSuspendedEvents-1; i >= 0; i--)
    {
        [NSApp postEvent:[suspendedEvents objectAtIndex:i] atStart:YES];
    }
}

- (BOOL)kfWorkUnitGetMatch:(NSString **)match
                     range:(NSRange *)matchRange
           lastSearchedRow:(int *)lastSearchedRow
                forPattern:(NSString *)pattern
              matchOptions:(unsigned)patternMatchOptions
                initialRow:(int)initialRow
               boundaryRow:(int)boundaryRow
              rowIncrement:(int)rowIncrement
             searchColumns:(NSArray *)searchColumns
                   timeout:(uint64_t)timeout // times are mach absolute times
{
    int row, col;
    int numCols = [searchColumns count];
    NSString *candidateMatch;
    NSRange rangeOfPattern;
    
    uint64_t stopTime = mach_absolute_time() + timeout;
    for (row = initialRow; row != boundaryRow; row += rowIncrement)
    {
        for (col = 0; col < numCols; col++)
        {
            candidateMatch = [self kfStringValueForTableColumn:[searchColumns objectAtIndex:col] row:row];

            rangeOfPattern = [candidateMatch rangeOfString:pattern options:patternMatchOptions];
            if (   (rangeOfPattern.location != NSNotFound)
                && [self kfShouldAcceptMatch:candidateMatch range:rangeOfPattern inRow:row])
            {
                *match = candidateMatch;
                *matchRange = rangeOfPattern;
                *lastSearchedRow = row;
                return YES;
            }
        }
        
        // think of this as part of the loop condition, but we want to make sure that
        // the loop completes at least one iteration
        if (mach_absolute_time() > stopTime) 
        {
            row += rowIncrement;
            break;
        }
    }
    
    *match = nil;
    *matchRange = NSMakeRange(NSNotFound, 0);
    *lastSearchedRow = row - rowIncrement;
    return row == boundaryRow;
}

- (BOOL)kfShouldAcceptMatch:(NSString *)match 
                      range:(NSRange)matchedRange 
                      inRow:(int)row
{
    id delegate = [self delegate];
    
    if (   [self isKindOfClass:[NSOutlineView class]] 
           && [delegate respondsToSelector:@selector(outlineView:shouldSelectItem:)])
    {
        return [delegate outlineView:(NSOutlineView *)self shouldSelectItem:[(NSOutlineView *)self itemAtRow:row]];
    }
    else if ([delegate respondsToSelector:@selector(tableView:shouldSelectRow:)])
    {
        return [delegate tableView:self shouldSelectRow:row];
    }
    else
    {
        return YES;
    }
}

- (NSTimeInterval)kfPatternTimeoutInterval
{
    // from Dan Wood's 'Table Techniques Taught Tastefully', as pointed out by someone
    // on cocoadev.com
    
    // Timeout is two times the key repeat rate "InitialKeyRepeat" user default.
    // (converted from sixtieths of a second to seconds), but no more than two seconds.
    // This behavior is determined based on Inside Macintosh documentation on the List Manager.
    
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    int keyThreshTicks = [defaults integerForKey:@"InitialKeyRepeat"]; // undocumented key.  Still valid in 10.3. 
    if (0 == keyThreshTicks)	// missing value in defaults?  Means user has never changed the default.
    {
        keyThreshTicks = 35;	// apparent default value. translates to 1.17 sec timeout.
    }
    
    return MIN(2.0/60.0*keyThreshTicks, 2.0);
}

- (BOOL)kfCanPerformTypeSelect
{
    return [self kfCanGetTableData] && [self kfSelectionShouldChange];
}

- (BOOL)kfSelectionShouldChange
{
    id delegate = [self delegate];
    
    if (   [self isKindOfClass:[NSOutlineView class]] 
           && [delegate respondsToSelector:@selector(selectionShouldChangeInOutlineView:)])
    {
        return [delegate selectionShouldChangeInOutlineView:(NSOutlineView *)self];
    }
    else if ([delegate respondsToSelector:@selector(selectionShouldChangeInTableView:)])
    {
        return [delegate selectionShouldChangeInTableView:self];
    }
    else
    {
        return YES;
    }    
}

- (BOOL)kfCanGetTableData
{    
    // First case:  datasource implements NSTableViewDataSource protocol.  Usually not true when 
    //              table view uses bindings or is actually an outline view.
    // Second case: self is an outline view and datasource implements NSOutlineViewDataSource protocol.  This could arise when using class posing.
    // Third case:  our delegate supplies the info we need.
    return ([[self dataSource] respondsToSelector:@selector(tableView:objectValueForTableColumn:row:)] || 
            ([self isKindOfClass:[NSOutlineView class]] && [[self dataSource] respondsToSelector:@selector(outlineView:objectValueForTableColumn:byItem:)]) ||
            [[self delegate] respondsToSelector:@selector(typeSelectTableView:stringValueForTableColumn:row:)]);
}

- (NSString *)kfStringValueForTableColumn:(NSTableColumn *)column row:(int)row
{
    // There are three ways we can get this information: (1) our delegate supplies it, (2) our datasource 
    // supplies it like an NSTableViewDataSource, (3) our datasource supplies it like an NSOutlineViewDataSource
    
    // could optimize by factoring into three separate methods and precomputing which one to call (from keyDown:).
    // current sharking indicates this wouldn't help much.
    
    id delegate = [self delegate];
    NSString *stringValue = nil;
    
    if ([delegate respondsToSelector:@selector(typeSelectTableView:stringValueForTableColumn:row:)])
    {
        stringValue = [delegate typeSelectTableView:self stringValueForTableColumn:column row:row];
    }
    else
    {
        id objectValue = nil;
        id dataSource = [self dataSource];
        
        // why do we check our own class?  outline view is not bindings enabled, so datasource could be
        // acting as a datasource for an outline while being a binding data source for us
        if ([self isKindOfClass:[NSOutlineView class]] 
            && [dataSource respondsToSelector:@selector(outlineView:objectValueForTableColumn:byItem:)])
        {
            objectValue = [dataSource outlineView:(NSOutlineView *)self 
                        objectValueForTableColumn:column 
                                           byItem:[(NSOutlineView *)self itemAtRow:row]];
        }
        else if ([dataSource respondsToSelector:@selector(tableView:objectValueForTableColumn:row:)])
        {
            objectValue = [dataSource tableView:self objectValueForTableColumn:column row:row];
        }
        
        NSCell *dataCell = [column dataCellForRow:row];
        [dataCell setObjectValue:objectValue];
        
        // sometimes the delegate changes the cell value in tableView:willDisplayCell:forTableColumn:row:
        if ([self isKindOfClass:[NSOutlineView class]] 
            && [delegate respondsToSelector:@selector(outlineView:willDisplayCell:forTableColumn:item:)])
        {
            [delegate outlineView:(NSOutlineView *)self 
                  willDisplayCell:dataCell
                   forTableColumn:column 
                             item:[(NSOutlineView *)self itemAtRow:row]];
        }
        else if ([delegate respondsToSelector:@selector(tableView:willDisplayCell:forTableColumn:row:)])
        {
            [delegate tableView:self 
                willDisplayCell:dataCell
                 forTableColumn:column 
                            row:row];
        }
        
        stringValue = [dataCell stringValue];
    }
    
    if (stringValue == nil)
    {
        stringValue = @"";
    }
    
    return stringValue;
}

- (NSArray *)kfSearchColumns
{
    NSArray *searchColumns;
    NSSet *searchColumnIdentifiers = [self searchColumnIdentifiers];
    
    if (searchColumnIdentifiers != nil)
    {
        NSMutableArray *partialSearchColumns;
        NSArray *candidateColumns = [self tableColumns];
        NSTableColumn *column;
        int numCols, col;
        
        partialSearchColumns = [NSMutableArray array];
        
        numCols = [candidateColumns count];
        for (col = 0; col < numCols; col++)
        {
            column = [candidateColumns objectAtIndex:col];
            if ([searchColumnIdentifiers containsObject:[column identifier]])
            {
                [partialSearchColumns addObject:column];
            }
        }
        
        searchColumns = partialSearchColumns;
    }
    else
    {
        searchColumns = [self tableColumns];
    }
    
    return searchColumns;
}


- (BOOL)kfSearchTopToBottom
{
    BOOL topToBottom = YES;
    
    id delegate = [self delegate];
    if ([delegate respondsToSelector:@selector(typeSelectTableViewSearchTopToBottom:)])
    {
        topToBottom = [delegate typeSelectTableViewSearchTopToBottom:self];
    }
    
    return topToBottom;
}

- (int)kfInitialRowForNewSearch
{
    int row;
    
    id delegate = [self delegate];
    if ([delegate respondsToSelector:@selector(typeSelectTableViewInitialSearchRow:)])
    {
        row = [delegate typeSelectTableViewInitialSearchRow:self];
    }
    else
    {
        if ([self kfSearchTopToBottom])
        {
            row = 0;
        }
        else
        {
            row = [self numberOfRows] - 1;
        }
    }
    
    return row;
}

#pragma mark taking action 

-(void)kfPatternDidChange:(id)sender 
{
    NSInvocation *timeoutInvocation = [self kfTimeoutInvocation];
    [[timeoutInvocation class] cancelPreviousPerformRequestsWithTarget:[self kfTimeoutInvocation]
                                                              selector:@selector(invoke) 
                                                                object:nil];
    
    id delegate = [self delegate];
    if ([delegate respondsToSelector:@selector(typeSelectTableViewPatternDidChange:)])
        [delegate typeSelectTableViewPatternDidChange:sender];
}

- (void)kfDidFindMatch:(NSString *)match 
                 range:(NSRange)matchedRange 
                 inRow:(int)row
{   
    NSString *pattern = [self pattern];
    
    // update ivars
    [self setKfLastSuccessfullyMatchedPattern:pattern];    
    if ([self kfCanExtendFind])
    {
        [self setKfSavedRowForExtensionSearch:row];
    }
    
    // select row
    [self selectRow:row byExtendingSelection:NO];
    if (![self kfRowIsVisible:row])
    {
        // this is what NSTextView does when it finds patterns, and it's what Mail does
        // when moving through message table with up and down arrows
        [self kfScrollRectToCenter:[self rectOfRow:row] vertical:YES horizontal:NO];
    }
    
    // start pattern timeout timer (see kfTimeoutInvocation for details)
    [[self kfTimeoutInvocation] performSelector:@selector(invoke)
                                     withObject:nil
                                     afterDelay:[self kfPatternTimeoutInterval] 
                                        inModes:[NSArray arrayWithObjects:NSDefaultRunLoopMode, NSModalPanelRunLoopMode, nil]];

    
    // inform the delegate
    id delegate = [self delegate];
    if ([delegate respondsToSelector:@selector(typeSelectTableView:didFindMatch:range:forPattern:)])
    {
        [delegate typeSelectTableView:self didFindMatch:match range:matchedRange forPattern:pattern];
    }
}

- (void)kfDidFailToFindMatchSearchingToRow:(int)row
{
    if ([self kfCanExtendFind])
    {
        [self setKfSavedRowForExtensionSearch:row];
    }
    
    // start pattern timeout timer (see kfTimeoutInvocation for details)
    [[self kfTimeoutInvocation] performSelector:@selector(invoke)
                                     withObject:nil
                                     afterDelay:[self kfPatternTimeoutInterval] 
                                        inModes:[NSArray arrayWithObjects:NSDefaultRunLoopMode, NSModalPanelRunLoopMode, nil]];

    
    id delegate = [self delegate];
    if ([delegate respondsToSelector:@selector(typeSelectTableView:didFailToFindMatchForPattern:)])
    {
        [delegate typeSelectTableView:self didFailToFindMatchForPattern:[self pattern]];
    }
    else
    {
        NSBeep();
    }
}

- (void)kfResetSearch
{
    // note - doesn't clear hanging dead key.  See keyDown for discussion and workaround.
    [self setPattern:@""];
    [self setKfCanExtendFind:NO];
}

// note: don't use setDelegate: to call this. NSOutlineView doesn't call through to 
// -[NSTableView setDelegate:], so it messes us up when posing.
- (void)kfConfigureDelegateIfNeeded
{
    id delegate = [self delegate];
    if (delegate != [self kfLastConfiguredDelegate])
    {
        // order is important here
        // We don't want to go into a recursion if the delegate tries to access a configurable value from 
        // configureTypeSelectTableView.  The delegate is interested in the pre-configuration values anyway.
        [self setKfLastConfiguredDelegate:delegate];
        if ([delegate respondsToSelector:@selector(configureTypeSelectTableView:)])
            [delegate configureTypeSelectTableView:self];
    }    
}
#pragma mark utility

- (BOOL)kfRowIsVisible:(int)row 
{
    NSScrollView *enclosingScrollView = [self enclosingScrollView];
    if (enclosingScrollView == nil)
    {
        return NO;
    }
    else
    {
        NSRect visibleRect = [enclosingScrollView documentVisibleRect];
        NSRect rowRect = [self rectOfRow:row];
        
        // only care about whether we're onscreen vertically
        return (   (NSMaxY(visibleRect) >= NSMaxY(rowRect))
                && (NSMinY(visibleRect) <= NSMinY(rowRect)));
    }
}

- (void)kfScrollRectToCenter:(NSRect)aRect vertical:(BOOL)scrollVertical horizontal:(BOOL)scrollHorizontal
{
    NSScrollView *scrollView = [self enclosingScrollView];
    
    if (scrollView != nil)
    {
        NSRect newVisibleRect = [scrollView documentVisibleRect];
        
        if (scrollVertical)
            newVisibleRect.origin.y += NSMidY(aRect) - NSMidY([scrollView documentVisibleRect]);
        if (scrollHorizontal)
            newVisibleRect.origin.x += NSMidX(aRect) - NSMidX([scrollView documentVisibleRect]);
        
        newVisibleRect = NSIntersectionRect(newVisibleRect,[self bounds]);
        
        [self scrollRectToVisible:newVisibleRect];
    }
}

#pragma mark -
#pragma mark ACCESSORS
#pragma mark -

#pragma mark simulated ivars setup

static NSMutableDictionary *idToSimulatedIvarsMap = nil; 

- (NSMutableDictionary *)kfSimulatedIvars
{
    NSMutableDictionary *simulatedIvars = [idToSimulatedIvarsMap objectForKey:[self kfIdentifier]];
    
    if (simulatedIvars == nil)
    {
        [self kfSetUpSimulatedIvars];
        simulatedIvars = [idToSimulatedIvarsMap objectForKey:[self kfIdentifier]];
    }
    
    return simulatedIvars;
}

// can avoid memory allocation if we use CFDictionary or NSMapTable and work with self directly
- (id)kfIdentifier
{
    return [NSValue valueWithPointer:self];
}

- (void)kfSetUpSimulatedIvars
{
    // prime idToSimulatedIvarsMap
    if (idToSimulatedIvarsMap == nil)
    {
        idToSimulatedIvarsMap = [[NSMutableDictionary alloc] init];
    }
        
    // if the simulatedIvars dict doesn't exist yet, create it
    NSMutableDictionary *simulatedIvars = [idToSimulatedIvarsMap objectForKey:[self kfIdentifier]];
    if (!simulatedIvars)
    {
        simulatedIvars = [NSMutableDictionary dictionary];
        [idToSimulatedIvarsMap setObject:simulatedIvars forKey:[self kfIdentifier]];
    }
}

- (void)kfTearDownSimulatedIvars
{
    [idToSimulatedIvarsMap removeObjectForKey:[self kfIdentifier]];
    
    if ([idToSimulatedIvarsMap count] == 0)
    {
        [idToSimulatedIvarsMap release];
        idToSimulatedIvarsMap = nil;
    }
}

#pragma mark private accessors

- (int)kfSavedRowForExtensionSearch
{
    int row;
    NSNumber *rowNumber = [[self kfSimulatedIvars] objectForKey:@"initialRowForExtensionSearch"];
    
    // default value
    if (rowNumber == nil)
        row = NSNotFound;
    else
    {
        row = [rowNumber intValue];
    }
    
    return row;
}

- (void)setKfSavedRowForExtensionSearch:(int)row
{
    [[self kfSimulatedIvars]  setObject:[NSNumber numberWithInt:row]
                                 forKey:@"initialRowForExtensionSearch"];
}

- (NSString *)kfLastSuccessfullyMatchedPattern
{
    NSString *string = [[self kfSimulatedIvars] objectForKey:@"lastPattern"];
    
    // defaults to nil
    
    return string;
}

- (void)setKfLastSuccessfullyMatchedPattern:(NSString *)string
{
    if (string == nil)
        [[self kfSimulatedIvars] removeObjectForKey:@"lastPattern"];
    else
        [[self kfSimulatedIvars] setObject:[[string copy] autorelease] forKey:@"lastPattern"];
}

-(BOOL)kfCanExtendFind
{
    NSNumber *canExtendFindNumber = [[self kfSimulatedIvars] objectForKey:@"canExtendFind"];
    
    // default value
    if (canExtendFindNumber == nil)
        return NO;
    else
        return [canExtendFindNumber boolValue];
}

-(void)setKfCanExtendFind:(BOOL)flag
{
    [[self kfSimulatedIvars] setObject:[NSNumber numberWithBool:flag]
                                forKey:@"canExtendFind"];
}

// keep track of the last delegate for which we tried to run configureTypeSelectTableView
- (id)kfLastConfiguredDelegate
{
    return [[[self kfSimulatedIvars] objectForKey:@"lastConfiguredDelegate"] nonretainedObjectValue];
}

- (void)setKfLastConfiguredDelegate:(id)anObject
{
    if (anObject == nil)
        [[self kfSimulatedIvars] removeObjectForKey:@"lastConfiguredDelegate"];
    else
        [[self kfSimulatedIvars] setObject:[NSValue valueWithNonretainedObject:anObject] forKey:@"lastConfiguredDelegate"];
}

//
//  the timeoutNotification encapsulates the message that we send to self when the timeout
//  (for clearing the input buffer) expires.  Invoke it with a delayed message send.
//
//  Why do we use an invocation instead of doing the delayed message send directly?   
//  -[NSObject performSelector:afterDelay:] retains the receiver until after the message send is
//  performed.  That can extend the life of the tableView past the life of the delegate, which is
//  bad mojo.  Yielded a crash in Adium.  By buffering with an invocation that doesn't retain its
//  target, we can avoid the problem.  Any pending delayed messages are cancelled when the table
//  table is dealloc'd.
//
- (NSInvocation *)kfTimeoutInvocation
{
    NSInvocation *invocation = [[self kfSimulatedIvars] objectForKey:@"timeoutInvocation"];
    
    // defaults to a message to kfResetSearch
    if (invocation == nil)
    {
        SEL selector = @selector(kfResetSearch);
        invocation = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:selector]];
        [invocation setTarget:self];
        [invocation setSelector:selector];
        
        [self setKfTimeoutInvocation:invocation];
    }
    
    return invocation;
}

- (void)setKfTimeoutInvocation:(NSInvocation *)anInvocation
{
    if (anInvocation == nil)
        [[self kfSimulatedIvars] removeObjectForKey:@"timeoutInvocation"];
    else
        [[self kfSimulatedIvars] setObject:anInvocation forKey:@"timeoutInvocation"];    
}


-(void)setPattern:(NSString *)pattern
{
    NSString *oldPattern = [self pattern];
    
    if (pattern == nil)
        pattern = @"";
    
    [[self kfSimulatedIvars] setObject:[[pattern copy] autorelease]
                                forKey:@"pattern"];
    
    NSNotification *patternChangedNotification = [NSNotification notificationWithName:KFTypeSelectTableViewPatternDidChangeNotification
                                                                              object:self
                                                                            userInfo:[NSDictionary dictionaryWithObject:oldPattern forKey:@"oldPattern"]];
    [self kfPatternDidChange:patternChangedNotification];
    [[NSNotificationCenter defaultCenter] postNotification:patternChangedNotification];
}

#pragma mark public accessors

-(NSString *)pattern
{
    NSString *pattern = [[self kfSimulatedIvars] objectForKey:@"pattern"];
    
    if (pattern == nil)
        pattern = @"";
    
    return [[pattern retain] autorelease];
}

static KFTypeSelectMatchAlgorithm defaultMatchAlgorith = KFPrefixMatchAlgorithm;
+ (KFTypeSelectMatchAlgorithm)defaultMatchAlgorithm
{
    return defaultMatchAlgorith;
}

+ (void)setDefaultMatchAlgorithm:(KFTypeSelectMatchAlgorithm)algorithm
{
    defaultMatchAlgorith = algorithm;
}


-(KFTypeSelectMatchAlgorithm)matchAlgorithm
{
    [self kfConfigureDelegateIfNeeded];
    
    NSNumber *algorithmNumber = [[self kfSimulatedIvars] objectForKey:@"matchAlgorithm"];

    if (algorithmNumber == nil)
        return defaultMatchAlgorith;
    else 
        return [algorithmNumber intValue];
}

-(void)setMatchAlgorithm:(KFTypeSelectMatchAlgorithm)algorithm
{
    [[self kfSimulatedIvars] setObject:[NSNumber numberWithInt:algorithm] forKey:@"matchAlgorithm"];
}

- (BOOL)searchWraps
{
    [self kfConfigureDelegateIfNeeded];

    NSNumber *searchWraphsNum = [[self kfSimulatedIvars] objectForKey:@"searchWraps"];

    // default value
    if (searchWraphsNum == nil)
        return NO;
    else
        return [searchWraphsNum boolValue];
}

-(void)setSearchWraps:(BOOL)flag
{
    [[self kfSimulatedIvars] setObject:[NSNumber numberWithBool:flag]
                                forKey:@"searchWraps"];
}


- (NSSet *)searchColumnIdentifiers
{
    [self kfConfigureDelegateIfNeeded];

    return [[self kfSimulatedIvars] objectForKey:@"searchColumnIdentifiers"];
}

- (void)setSearchColumnIdentifiers:(NSSet *)identifiers
{
    if (identifiers == nil)
        [[self kfSimulatedIvars] removeObjectForKey:@"searchColumnIdentifiers"];
    else
        [[self kfSimulatedIvars] setObject:identifiers forKey:@"searchColumnIdentifiers"];
}

@end

#pragma mark -
#pragma mark HELPER 
#pragma mark -

// need a time function, don't want it to be sensitive to time zone switches or
// clock syncs, would rather not require linking Carbon. We'll go with mach_absolute_time.
// Written with reference to <http://developer.apple.com/qa/qa2004/qa1398.html>.
static uint64_t SecondsToMachAbsolute(double seconds)
{
    double nanoseconds_d = seconds * 1000000000;
    Nanoseconds nanoseconds_n = UInt64ToUnsignedWide((uint64_t) nanoseconds_d);
    return UnsignedWideToUInt64(NanosecondsToAbsolute(nanoseconds_n));
}


