//=============================================================================
//
//		Copyright (C) 1996-1997 by Paul S. McCarthy and Eric Sunshine.
//				Written by Paul S. McCarthy and Eric Sunshine.
//							All Rights Reserved.
//
//		This notice may not be removed from this source code.
//
//		This object is included in the MiscKit by permission from the authors
//		and its use is governed by the MiscKit license, found in the file
//		"License.rtf" in the MiscKit distribution.	Please refer to that file
//		for a list of all applicable permissions and restrictions.
//
//=============================================================================
//-----------------------------------------------------------------------------
// MiscTableScrollIncSearch.M
//
//		Incremental searching methods for MiscTableScroll.
//
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
// $Id: MiscTableScrollISearch.M,v 1.7 97/06/10 05:38:45 sunshine Exp $
// $Log:		MiscTableScrollISearch.M,v $
//	Revision 1.7  97/06/10	05:38:45  sunshine
//	v127: Fixed bug: Incremental-search feedback wasn't being clipped, thus
//	drew outside of the MiscTableScroll's frame and was never erased.
//	
//	Revision 1.6  96/09/20	11:15:40  zarnuk
//	Moved -keyboardSelect: declaration to MiscTableScrollPrivate.h
//	Updated to new MiscTableBorder selection method names.
//	
//	Revision 1.5  96/08/30	15:26:57  sunshine
//	Uses new MiscDelegateFlags which caches information about which methods
//	the delegate implements in order to avoid having to query the delegate's
//	-respondsTo: method each time.
//	Fixed bug: Pressing Space to end/select during incremental-search was not
//	respecting selection mode (radio, list, highlight, etc.).
//	Fixed bug: Wasn't taking tracking-border into account when setting keyboard
//	cursor position.
//-----------------------------------------------------------------------------
#import <misckit/MiscTableScroll.h>
#import "MiscBorderCell.h"
#import "MiscTableBorder.h"
#import "MiscTableScrollPrivate.h"
#import "MiscTableView.h"

extern "Objective-C" {
#import <appkit/Application.h>
#import <appkit/Text.h>
#import <appkit/publicWraps.h>
#import <dpsclient/wraps.h>
}

extern "C" {
#import <ctype.h>
#import <strings.h>		// strncasecmp().
}

static float const ISEARCH_TIMEOUT = 2.5;		// seconds

enum MiscKeyAction
		{
		MISC_KEY_IGNORE,		// Ignore this event, get next.
		MISC_KEY_SPACE,			// Space character, not valid as first char.
		MISC_KEY_APPEND,		// Append a character to the template.
		MISC_KEY_EXPAND,		// Expand common-prefix.
		MISC_KEY_DELETE,		// Delete the last character from template.
		MISC_KEY_CLEAR,			// Clear (empty) the template.
		MISC_KEY_SELECT,		// Stop, Select the first entry.
		MISC_KEY_EXECUTE,		// Stop, Select and perform double action.
		MISC_KEY_STOP,			// Stop, don't select, consume keystroke.
		MISC_KEY_ABORT,			// Stop, don't select, don't consume.
		};


//-----------------------------------------------------------------------------
// Feedback
//-----------------------------------------------------------------------------
class MISC_IS_Feedback
	{
private:
	MiscTableScroll* scroll;
	BOOL drawn;
	MiscBorderCell* cell;
	NXRect drawRect;
	void init_cell();

public:
	MISC_IS_Feedback( MiscTableScroll* s ) : scroll(s),drawn(NO),cell(0) {}
	~MISC_IS_Feedback() { if (cell != 0) [cell free]; }
	void draw( char const* str );
	void erase();
	};

void MISC_IS_Feedback::init_cell()
	{
	if (cell == 0)
		{
		cell = [[MiscBorderCell allocFromZone:[scroll zone]]
						initTextCell:""];
		[cell setAlignment:NX_LEFTALIGNED];
		}
	}

void MISC_IS_Feedback::erase()
	{
	if (drawn)
		{
		drawn = NO;
		[scroll display:&drawRect:1];
		}
	}


void MISC_IS_Feedback::draw( char const* s )
	{
	init_cell();
	[cell setStringValue:s];

	NXSize size; [cell calcCellSize:&size];
	NXRect clipFrame; [scroll getDocClipFrame:&clipFrame];
	NXRect r; NXSetRect( &r, NX_X(&clipFrame),
				 NX_MAXY(&clipFrame) - size.height, size.width, size.height );
	NXIntersectionRect( &clipFrame, &r );

	[scroll lockFocus];
	[cell drawSelf:&r inView:scroll];
	[scroll unlockFocus];

	drawn = YES;
	drawRect = r;
	}


//-----------------------------------------------------------------------------
// classify_event
//-----------------------------------------------------------------------------
static MiscKeyAction classify_event( NXEvent const* p )
	{
	int const BAD_FLAGS = (NX_COMMANDMASK|NX_HELPMASK|NX_CONTROLMASK);
	MiscKeyAction rc = MISC_KEY_ABORT;
	unsigned char const K_DEL = '\x7f';
	unsigned char const K_ALT_SPACE = '\x80';

	if (p->type == NX_KEYDOWN)
		{
		int const flags = p->flags;
		if ((flags & BAD_FLAGS) == 0 && p->data.key.charSet == NX_ASCIISET)
			{
			unsigned char const ch = (unsigned char) p->data.key.charCode;
			if (ch == K_DEL || ch == '\b')
				rc = MISC_KEY_DELETE;
			else if (ch == '\t')
				rc = MISC_KEY_EXPAND;
			else if (ch > ' ' && ch != K_ALT_SPACE)
				rc = MISC_KEY_APPEND;
			else if (ch == ' ' || ch == K_ALT_SPACE)
				rc = MISC_KEY_SPACE;
			}
		}
	else if (p->type == NX_KEYUP || p->type == NX_FLAGSCHANGED)
		{
		rc = MISC_KEY_IGNORE;
		}

	return rc;
	}



//-----------------------------------------------------------------------------
// map
//		Conditionally apply a mapping vector to an index value.
//-----------------------------------------------------------------------------
inline static int map( int const* a, int x ) { return a ? a[x] : x; }


//-----------------------------------------------------------------------------
// DATA RETRIEVAL AND COMPARE SUPPORT
//-----------------------------------------------------------------------------

typedef char const* (*Datafunc)( id obj, SEL cmd,
								MiscCoord_P row, MiscCoord_P col );
typedef int (*Cmpfunc)( char const* s1, char const* s2, int len );

struct MiscSortTypeInfo
		{
		SEL const* data_sel;	// Selector to use to retrieve string.
		Cmpfunc cmp_func;		// Comparison function to compare strings.
		BOOL fold_case;			// Case insensitive compare?
		};

#define SEL_STRV &@selector(stringValueAt: :)
#define SEL_TITL &@selector(titleAt: :)

static MiscSortTypeInfo SORT_TYPE_INFO[ MISC_SORT_TYPE_MAX + 1 ] =
		{
		{ SEL_STRV, strncasecmp, YES }, // MISC_SORT_STRING_CASE_INSENSITIVE
		{ SEL_STRV, strncmp, NO },		// MISC_SORT_STRING_CASE_SENSITIVE
		{ 0, 0, NO },					// MISC_SORT_INT
		{ 0, 0, NO },					// MISC_SORT_UNSIGNED_INT
		{ 0, 0, NO },					// MISC_SORT_TAG
		{ 0, 0, NO },					// MISC_SORT_UNSIGNED_TAG
		{ 0, 0, NO },					// MISC_SORT_FLOAT
		{ 0, 0, NO },					// MISC_SORT_DOUBLE
		{ 0, 0, NO },					// MISC_SORT_SKIP
		{ SEL_TITL, strncasecmp, YES }, // MISC_SORT_TITLE_CASE_INSENSITIVE
		{ SEL_TITL, strncmp, NO },		// MISC_SORT_TITLE_CASE_SENSITIVE
		{ 0, 0, NO },					// MISC_SORT_STATE
		{ 0, 0, NO },					// MISC_SORT_UNSIGNED_STATE
		};

#undef SEL_STRV
#undef SEL_TITL


//-----------------------------------------------------------------------------
// bsearch
//-----------------------------------------------------------------------------
static int bsearch(
		MiscCoord_V lo, MiscCoord_V hi,
		BOOL upper_bound, BOOL descending,
		MiscTableScroll* scroll,
		char const* buff, int len,
		MiscCoord_P col, MiscCoord_P const* v2p,
		Cmpfunc cmp_func, Datafunc data_func, SEL data_sel )
	{
	while (lo <= hi)
		{
		MiscCoord_V const mid = (lo + hi) >> 1;
		MiscCoord_P const row = map( v2p, mid );
		char const* s = (*data_func)( scroll, data_sel, row, col );
		if (s == 0)
			s = "";
		int cmp = (*cmp_func)( s, buff, len );
		if (descending)
			cmp = -cmp;
		if (cmp < 0 || upper_bound && cmp == 0)
			lo = mid + 1;
		else
			hi = mid - 1;
		}
	return (upper_bound ? hi : lo);
	}


//-----------------------------------------------------------------------------
// peek_event
//-----------------------------------------------------------------------------
inline static NXEvent* peek_event( NXEvent* buff, float timeout )
	{
	return [NXApp peekNextEvent:NX_ALLEVENTS
						into:buff
						waitFor:timeout
						threshold:NX_BASETHRESHOLD];
	}


@implementation MiscTableScroll(IncrementalSearch)

//-----------------------------------------------------------------------------
// -doIncrementalSearch:col:
//-----------------------------------------------------------------------------
- (BOOL) doIncrementalSearch:(NXEvent*)p col:(int)col
	{
	BOOL handled = YES;			// Event was handled here.
	BOOL descending = NO;
	if (col < 0)
		{
		col = ~col;
		descending = YES;
		}
	if ([self border:MISC_COL_BORDER slotSortDirection:col] ==
				MISC_SORT_DESCENDING)
		descending = !descending;

	MiscSortType const type = [self border:MISC_COL_BORDER slotSortType:col];
	MiscSortTypeInfo const& INFO = SORT_TYPE_INFO[type];
	BOOL const fold_case = INFO.fold_case;
	Cmpfunc const cmp_func = INFO.cmp_func;

	if (cmp_func == 0 || [self border:MISC_COL_BORDER slotSortFunc:col] != 0)
		return NO;						// *** RETURN ***

	SEL const data_sel = *(INFO.data_sel);
	Datafunc const data_func = (Datafunc) [self methodFor:data_sel];
	MiscCoord_P const* const v2p = rowInfo.border->getV2PMap();

	MISC_IS_Feedback feedback( self );
	NXEvent event_buff;
	MiscKeyAction ka;
	int const MARGIN = 2;
	int const BUFF_MAX = 61;
	int buff_len = 0;
	char margin[ MARGIN + BUFF_MAX + 1 ];
	char* buff = margin + MARGIN;
	MiscCoord_V first_stk[ BUFF_MAX + 1 ], last_stk[ BUFF_MAX + 1 ];
	MiscCoord_V first = 0, last = [self numRows] - 1;
	MiscCoord_V prev_first = -1;
	BOOL update_cursor = ([self trackingBy] == MISC_ROW_BORDER);
	BOOL changed = NO;

	for (int i = 0; i < MARGIN; i++)
		margin[i] = ' ';

	float delay = 0.25;

	first_stk[0] = first;
	last_stk [0] = last;

	ka = classify_event( p );
	while (1)
		{
		PSobscurecursor();
		switch (ka)
			{
		case MISC_KEY_IGNORE:
				break;
		case MISC_KEY_SPACE:
		case MISC_KEY_APPEND:
				if (buff_len < BUFF_MAX && first <= last)
					{
					buff[ buff_len++ ] = char( p->data.key.charCode );
					buff[ buff_len ] = '\0';

					MiscCoord_V const new_first = bsearch( first, last, NO,
								descending, self, buff, buff_len, col,
								v2p, cmp_func, data_func, data_sel );

					MiscCoord_V const new_last = bsearch( new_first, last, YES,
								descending, self, buff, buff_len, col,
								v2p, cmp_func, data_func, data_sel );

					if (new_first <= new_last)
						{
						first = new_first;
						last  = new_last;
						first_stk[ buff_len ] = first;
						last_stk [ buff_len ] = last;
						changed = YES;
						}
					else				// Do not add "invalid" characters.
						{
						buff[ --buff_len ] = '\0';
						if (ka == MISC_KEY_SPACE)
							{
							ka = MISC_KEY_SELECT;
							goto finished;		// *** GOTO ***
							}
						else if (buff_len == 0)
							{					// Couldn't even add 1st char,
							handled = NO;		// so don't go modal.
							goto finished;		// *** GOTO ***
							}
						else
							NXBeep();
						}
					}
				else
					NXBeep();
				break;
		case MISC_KEY_EXPAND:
				{
				if (first <= last)
					{
					char const* s;
					s = (*data_func)(self,data_sel,map(v2p,first),col);
					strncpy( buff+buff_len, s+buff_len, BUFF_MAX-buff_len );
					buff[ BUFF_MAX ] = '\0';
					s = (*data_func)(self,data_sel,map(v2p,last),col);
					int i = buff_len;
					if (fold_case)
						{
						while (buff[i] != '\0' &&
							tolower((unsigned char)buff[i]) ==
							tolower((unsigned char)s[i]))
							i++;
						}
					else
						{
						while (buff[i] != '\0' && buff[i] == s[i])
							i++;
						}
					if (i > buff_len)
						{
						for (int j = buff_len + 1; j <= i; j++)
							{
							first_stk[j] = first;
							last_stk[j] = last;
							}
						buff_len = i;
						buff[ buff_len ] = '\0';
						changed = YES;
						}
					}
				}
				break;
		case MISC_KEY_DELETE:
				if (buff_len > 0)
					{
					buff[ --buff_len ] = '\0';
					first = first_stk[ buff_len ];
					last  = last_stk[ buff_len ];
					changed = YES;
					}
				else
					goto finished;		// *** GOTO ***
				break;
		case MISC_KEY_CLEAR:
		case MISC_KEY_SELECT:
		case MISC_KEY_EXECUTE:
		case MISC_KEY_STOP:
		case MISC_KEY_ABORT:
				goto finished;			// *** GOTO ***
			}

		if ((p = peek_event( &event_buff, delay )) == 0)
			{
			if (changed)
				{
				changed = NO;
				[window disableFlushWindow];
				feedback.erase();
				if (prev_first != first)
					{
					prev_first = first;
					MiscCoord_P const r = map( v2p, first );
					[self border:MISC_ROW_BORDER setFirstVisibleSlot:r];
					if (update_cursor)
						[self border:MISC_ROW_BORDER setCursor:r];
					}
				feedback.draw( margin );
				[window reenableFlushWindow];
				[window flushWindow];
				if (buff_len == 0)
					break;				// *** BREAK ***
				}

			if ((p = peek_event( &event_buff, ISEARCH_TIMEOUT )) == 0)
				break;					// *** BREAK *** inactivity timeout.
			}

		delay = 0.0;
		ka = classify_event( p );

		if (ka != MISC_KEY_ABORT)		// "Eat" the event.
			[NXApp getNextEvent:NX_ALLEVENTS
						waitFor:ISEARCH_TIMEOUT
						threshold:NX_BASETHRESHOLD];
		}

finished:
	feedback.erase();

	if (ka==MISC_KEY_SELECT || ka==MISC_KEY_EXECUTE)
		{
		MiscCoord_P const phys_first = map( v2p, first );
		[window disableFlushWindow];
		if (prev_first != first)
			{
			[self border:MISC_ROW_BORDER setFirstVisibleSlot:phys_first];
			if (update_cursor)
				[self border:MISC_ROW_BORDER setCursor:phys_first];
			}
		if (update_cursor)
			[self keyboardSelect:p];
		[[window reenableFlushWindow] flushWindow];
		if ([self isEnabled])
			{
			[self sendAction];
			if (ka == MISC_KEY_EXECUTE)
				[self sendDoubleAction];
			}
		}

	return handled;
	}


//-----------------------------------------------------------------------------
// -doGetISearchCol:
//		Default implementation.	 If auto-sort-rows is on, and the first
//		non-skip column is string-based, use that column, else fail.
//-----------------------------------------------------------------------------
- (BOOL) doGetISearchCol:(int*)col
	{
	if ([self autoSortRows])
		{
		int const lim = colInfo.border->count();
		int const* v2p = colInfo.border->getV2PMap();
		for (int i = 0; i < lim; i++)
			{
			int const j = map( v2p, i );
			if ([self border:MISC_COL_BORDER slotSortFunc:j] != 0)
				return NO;
			MiscSortType const t = [self border:MISC_COL_BORDER slotSortType:j];
			if (t != MISC_SORT_SKIP)
				{
				*col = j;
				return (SORT_TYPE_INFO[t].cmp_func != 0);
				}
			}
		}
	return NO;
	}


//-----------------------------------------------------------------------------
// -getISearchCol:
//-----------------------------------------------------------------------------
- (BOOL) getISearchCol:(int*)col
	{
	id del = [self responsibleDelegate:MiscDelegateFlags::DEL_GET_ISEARCH_COL];
	if (del != 0)
		return [del tableScroll:self getISearchCol:col];
	return [self doGetISearchCol:col];
	}


//-----------------------------------------------------------------------------
// -incrementalSearch:
//-----------------------------------------------------------------------------
- (BOOL) incrementalSearch:(NXEvent*)p
	{
	if ([self numRows] > 0 && [self numCols] > 0)
		{
		MiscKeyAction const ka = classify_event( p );
		if (ka == MISC_KEY_APPEND)
			{
			int col;
			if ([self getISearchCol:&col])
				return [self doIncrementalSearch:p col:col];
			}
		}
	return NO;	// Event was not handled here.
	}

@end
