/*
 *
 * fowc.c -- find online words with crude regexps
 *
 * This code is copyright 1995 by James A. Cherry, jac@doe.carleton.ca.
 * It is not to be redistributed or modified without his permission.
 *
 */

#include "funddefs.h"
#include "globals.h"
#include "socket.h"
#include "botutil.h"
#include "fowc.h"
#include "proto.h"
#include "critique.h"

int ParseInputArgs( p1, pe )
    char *p1, *pe;
/* Parse the input arguments. */
{
    char *ps;
    token_t token, prevtoken;
    pair_t pair;
    int bracketlevel;
    
    WArgC = 0;
    Tokens = 0;
    prevtoken = -1;
    bracketlevel = 0;
    while( p1 < pe ) {
	if( Tokens == MAXTOKENS ) {
	    OPrint( "Error: too many tokens." );
	    return( -1 );
	}
	while( *p1 == ' ' && p1 < pe ) p1++;
	if( *p1 == '(' ) token = TOKEN_OPEN;
	else if( *p1 == '|' ) token = TOKEN_OR;
	else if( *p1 == ')' ) token = TOKEN_CLOSE;
	else if( *p1 == '!' ) token = TOKEN_NOT;
	else token = TOKEN_ARG;
	if( prevtoken == -1 ) {
	    if( token == TOKEN_OR ) {
		OPrint( "Error: `|' at start." );
		return( -1 );
	    }
	    pair = PAIR_OK;
	} else
	    pair = TokenPairs[prevtoken][token];
	if( pair == PAIR_SYN ) {
	    OPrint( "Error: `%c' followed by `%c'.", TokenChars[prevtoken], TokenChars[token] );
	    return( -1 );
	}
	if( pair == PAIR_AND )
	    TokenType[Tokens++] = TOKEN_AND;
	TokenType[Tokens] = token;
	if( token == TOKEN_OPEN ) {
	    bracketlevel++;
	    p1++;
	} else if( token == TOKEN_CLOSE ) {
	    bracketlevel--;
	    if( bracketlevel < 0 ) {
		OPrint( "Error: too many closing brackets." );
		return( -1 );
	    }
	    p1++;
	} else if( token == TOKEN_NOT || token == TOKEN_OR ) {
	    p1++;
	} else {
	    for( ps = p1; *p1 != ' ' && *p1 != '(' && *p1 != ')'
			       && *p1 != '|' && p1 < pe; p1++ ) {
		if( *p1 == '!' && *(p1 - 1) != '[' ) break;
	    }
	    if( WArgC < 10 && p1 - ps > 0 ) {
		TokenArg[Tokens] = WArgC;
		WArgV[WArgC] = (char *) malloc( p1 - ps + 1 );
		strncpy( WArgV[WArgC], ps, p1 - ps );
		WArgV[WArgC++][p1 - ps] = '\0';
	    } else if( WArgC == 10 ) {
		OPrint( "Error: too many arguments to `words' command." );
		return( -1 );
	    }
	}
	prevtoken = token;
	Tokens++;
    }
    if( bracketlevel > 0 ) {
	OPrint( "Error: too many opening brackets." );
	return( -1 );
    }
    if( prevtoken == TOKEN_OR ) {
	OPrint( "Error: `|' at end." );
	return( -1 );
    }
    if( WArgC == 0 && SType != WORDS_BINGO ) {
	OPrint( "Error: no arguments to `words' command." );
	return( -1 );
    }
    return( 0 );
}

void FreeArgv()
/* Free argument list. */
{
    int i;
    
    for( i = 0; i < WArgC; i++ ) free( (char *) WArgV[i] );
}

int BuildExpressionTree()
/* Build a binary tree for the search expression. */
{
    int arg, leftarg, opleaf;
    args_t negsense;
    token_t tok;
    
    leftarg = -1;
    opleaf = -1;
    negsense = SENSE_POS;
  build_top:
    if( CTok == Tokens )
	tok = TOKEN_CLOSE;
    else
	tok = TokenType[CTok];
    if( tok == TOKEN_ARG ) {
	arg = FreeEntry++;
	WTree[arg].type = TOKEN_ARG;
	WTree[arg].arg = TokenArg[CTok];
	WTree[arg].left = -1;
	WTree[arg].right = -1;
	WTree[arg].negsense = negsense;
	negsense = SENSE_POS;
	if( leftarg == -1 ) {
	    leftarg = arg;
	} else {
	    WTree[opleaf].right = arg;
	    leftarg = opleaf;
	}
    } else if( tok == TOKEN_AND || tok == TOKEN_OR ) {
	opleaf = FreeEntry++;
	WTree[opleaf].type = tok;
	WTree[opleaf].arg = -1;
	WTree[opleaf].left = leftarg;
    } else if( tok == TOKEN_NOT ) {
	if( negsense == SENSE_POS )
	    negsense = SENSE_NEG;
	else
	    negsense = SENSE_POS;
    } else if( tok == TOKEN_OPEN ) {
	CTok++;
	arg = BuildExpressionTree();
	if( WTree[arg].type == TOKEN_ARG ) {
	    if( negsense == SENSE_NEG ) {
		if( WTree[arg].negsense == SENSE_POS ) {
		    WTree[arg].negsense = SENSE_NEG;
		} else {
		    WTree[arg].negsense = SENSE_POS;
		}
	    }
	} else {
	    WTree[arg].negsense = negsense;
	}
	negsense = SENSE_POS;
	if( leftarg == -1 ) {
	    leftarg = arg;
	} else {
	    WTree[opleaf].right = arg;
	    leftarg = opleaf;
	}
    } else if( tok == TOKEN_CLOSE ) {
	return( leftarg );
    }
    CTok++;
    goto build_top;
}

void bad_char_error( c, str, arg )
    char c;
    char *str;
    int arg;
/* Display error message for a bad character. */
{
    OPrint( "Error: invalid character `%c' in %s `%s'.", c, str, WArgV[arg] );
}

int process_letters( arg )
    int arg;
/* Process letter list after `>' or `<' argument. */
{
    int i;
    char c;
    
    for( i = 0; i < 27; i++ ) LetAr[arg][i] = 0;
    for( i = 1; i < WArgL[arg]; i++ ) {
	c = WArgV[arg][i];
	if( c == '?' ) LetAr[arg][26]++;
	else if( c >= 'a' && c <= 'z' ) LetAr[arg][c - 'a']++;
	else if( c >= 'A' && c <= 'Z' ) LetAr[arg][c - 'A']++;
	else {
	    bad_char_error( c, "letter set", arg );
	    return( -1 );
	}
    }
    if( WArgV[arg][0] == '<' ) {
	for( i = 2; i < WArgL[arg]; i++ )
	    VLen[arg][i] = LENGTH_POSSIBLE;
	for( i = WArgL[arg]; i < MAX_WLEN + 1; i++ )
	    VLen[arg][i] = LENGTH_IMPOSSIBLE;
    } else {
	for( i = 2; i < WArgL[arg] - 1; i++ )
	    VLen[arg][i] = LENGTH_IMPOSSIBLE;
	for( i = WArgL[arg] - 1; i < MAX_WLEN; i++ )
	    VLen[arg][i] = LENGTH_POSSIBLE;
    }
    return( 0 );
}

int process_length( arg )
    int arg;
/* Process lengths (`2' or `3-5', for example). */
{
    int i;
    int len1, len2;
    
    len1 = 0;
    for( i = 0; i < WArgL[arg] && WArgV[arg][i] >= '0' && WArgV[arg][i] <= '9';
		len1 = len1 * 10 + WArgV[arg][i++] - '0' );
    if( len1 < 2 || len1 > MAX_WLEN ) {
	OPrint( "Error: lengths must be between 2 and %d.", MAX_WLEN );
	return( -1 );
    }
    len2 = -1;
    if( i < WArgL[arg] ) {
	if( WArgV[arg][i] != '-' ) {
	    bad_char_error( WArgV[arg][i], "length", arg );
	    return( -1 );
	}
	i++;
	if( i == WArgL[arg] ) {
	    OPrint( "Error: missing length in `%s'.", WArgV[arg] );
	    return( -1 );
	}
	len2 = 0;
	for( ; i < WArgL[arg] && WArgV[arg][i] >= '0' && WArgV[arg][i] <= '9';
	       len2 = len2 * 10 + WArgV[arg][i++] - '0' );
	if( i < WArgL[arg] ) {
	    bad_char_error( WArgV[arg][i], "length", arg );
	    return( -1 );
	}
	if( len2 < 2 || len2 > MAX_WLEN ) {
	    OPrint( "Error: lengths must be between 2 and %d.", MAX_WLEN );
	    return( -1 );
	}
    }
    for( i = 2; i < MAX_WLEN; i++ )
	VLen[arg][i] = LENGTH_IMPOSSIBLE;
    if( len2 == -1 )
	VLen[arg][len1] = LENGTH_DEFINITE;
    else if( len2 > len1 ) {
	for( i = len1; i <= len2; i++ )
	    VLen[arg][i] = LENGTH_DEFINITE;
    } else {
	for( i = len2; i <= len1; i++ )
	    VLen[arg][i] = LENGTH_DEFINITE;
    }
    return( 0 );
}

int process_regexp( arg )
    int arg;
/* Process pattern argument from `/'. */
{
    int i, j, k, slash;
    int len;
    int negsq, range, fc, ch;
    char c;
    
    slash = 0;
    if( WArgV[arg][0] == '/' ) slash = 1;
    REStart[arg] = 0;
    if( WArgV[arg][slash] == '^' ) {
	WArgL[arg]--;
	REStart[arg] = 1;
	strncpy( WArgV[arg] + slash, WArgV[arg] + slash + 1, WArgL[arg] );
	WArgV[arg][WArgL[arg]] = '\0';
    }
    REEnd[arg] = 0;
    if( WArgV[arg][WArgL[arg] - 1] == '$' ) {
	REEnd[arg] = 1;
	WArgL[arg]--;
	WArgV[arg][WArgL[arg]] = '\0';
    }
    
    len = 0;
    for( i = slash; i < WArgL[arg] && len < MAX_WLEN; i++ ) {
	for( j = 0; j < 26; j++ )
	    REValidL[arg][len][j] = 0;
	c = WArgV[arg][i];
	if( c == '^' ) {
	    OPrint( "Error: `^' may only appear at the start of a pattern." );
	    return( -1 );
	} else if( c == '$' ) {
	    OPrint( "Error: `$' may only appear at the end of a pattern." );
	    return( -1 );
	}
	if( c == '.' ) {
	    for( j = 0; j < 26; j++ )
		REValidL[arg][len][j] = 1;
	} else if( c >= 'a' && c <= 'z' ) {
	    REValidL[arg][len][c - 'a'] = 1;
	} else if( c >= 'A' && c <= 'Z' ) {
	    REValidL[arg][len][c - 'A'] = 1;
	} else if( c == '[' ) {
	    i++;
	    negsq = 0;
	    if( WArgV[arg][i] == '^' || WArgV[arg][i] == '!' ) {
		i++;
		negsq = 1;
	    }
	    for( j = i; WArgV[arg][j] != ']' && j < WArgL[arg]; j++ );
	    if( j == WArgL[arg] || WArgV[arg][j] != ']' ) {
		OPrint( "Error: missing `]' in pattern `%s'.", WArgV[arg] );
		return( -1 );
	    } else if( j == i ) {
		OPrint( "Error: empty `[]' in pattern `%s'.", WArgV[arg] );
		return( -1 );
	    }
	    fc = -1;
	    ch = -1;
	    range = 0;
	    while( i < j ) {
		c = WArgV[arg][i];
		if( c == '-' ) {
		    if( fc == -1 || range == 1 ) {
			OPrint( "Error: bad `[]' range expression in pattern `%s'.", WArgV[arg] );
			return( -1 );
		    }
		    range = 1;
		} else if( ( c >= 'A' && c <= 'Z' ) || ( c >= 'a' && c <= 'z' ) ) {
		    if( c >= 'A' && c <= 'Z' ) ch = c - 'A';
		    else ch = c - 'a';
		    if( range == 1 ) {
			if( fc < ch ) {
			    for( k = fc; k <= ch; k++ )
				REValidL[arg][len][k] = 1;
			} else {
			    for( k = ch; k <= fc; k++ )
				REValidL[arg][len][k] = 1;
			}
			fc = -1;
			range = 0;
		    } else {
			fc = ch;
			REValidL[arg][len][ch] = 1;
		    }
		} else {
		    bad_char_error( c, "pattern", arg );
		    return( -1 );
		}
		i++;
	    }
	    if( range == 1 ) {
		OPrint( "Error: bad `[]' range expression in pattern `%s'.", WArgV[arg] );
		return( -1 );
	    }
	    if( negsq == 1 ) {
		for( j = 0; j < 26; j++ )
		    REValidL[arg][len][j] = 1 - REValidL[arg][len][j];
	    }
	} else {
	    bad_char_error( c, "pattern", arg );
	    return( -1 );
	}
	len++;
    }
    
    if( len == MAX_WLEN && i < WArgL[arg] ) {
	OPrint( "Error: pattern `%s' too long.", WArgV[arg] );
	return( -1 );
    }
    for( i = 2; i < MAX_WLEN + 1; i++ )
	VLen[arg][i] = LENGTH_IMPOSSIBLE;
    if( REStart[arg] == 1 && REEnd[arg] == 1 )
	VLen[arg][len] = LENGTH_POSSIBLE;
    else {
	for( i = len; i < MAX_WLEN + 1; i++ )
	    VLen[arg][i] = LENGTH_POSSIBLE;
    }
    RELen[arg] = len;
    return( 0 );
}

int InterpretArgs()
/* Figure out what kind of argument each is, and process it. */
{
    int i, rc;
    char c;
    
    rc = 0;
    for( i = 0; i < WArgC; i++ ) {
	c = WArgV[i][0];
	WArgL[i] = strlen( WArgV[i] );
	if( c == '<' ) {
	    WArgT[i] = ARG_MAY_ONLY_CONTAIN;
	    rc = process_letters( i );
	} else if( c == '>' ) {
	    WArgT[i] = ARG_MUST_CONTAIN;
	    rc = process_letters( i );
	} else if( c >= '0' && c <= '9' ) {
	    WArgT[i] = ARG_LENGTH;
	    rc = process_length( i );
	} else if( c == '/' ) {
	    WArgT[i] = ARG_REGEXP;
	    rc = process_regexp( i );
	} else {
	    OPrint( "Error: unknown argument `%s'.\n", WArgV[i] );
	    rc = -1;
	}
	if( rc == -1 ) break;
    }
    return( rc );
}

int iptr;

int say_node( leaf )
    int leaf;
/* Output particular node of search tree. */
{
    int hl, hr;
    int arg;
    
    if( WTree[leaf].negsense == SENSE_NEG )
	AddToStr( "!", &iptr );
    if( WTree[leaf].type == TOKEN_AND || WTree[leaf].type == TOKEN_OR ) {
	if( leaf != TreeHead || WTree[leaf].negsense == SENSE_NEG )
	    AddToStr( "(", &iptr );
	hl = say_node( WTree[leaf].left );
	if( WTree[leaf].type == TOKEN_AND ) AddToStr( " ", &iptr );
	else AddToStr( " | ", &iptr );
	hr = say_node( WTree[leaf].right );
	if( leaf != TreeHead || WTree[leaf].negsense == SENSE_NEG )
	    AddToStr( ")", &iptr );
	WTree[leaf].llower = hl;
	WTree[leaf].rlower = hr;
	if( hl > hr ) return( hl + 1 );
	else return( hr + 1 );
    } else {
	arg = WTree[leaf].arg;
	if( WArgT[arg] == ARG_REGEXP )
	    AddToStr( "/", &iptr );
	if( WArgT[arg] == ARG_REGEXP && REStart[arg] == 1 )
	    AddToStr( "^", &iptr );
	if( WArgT[arg] == ARG_REGEXP && WArgV[WTree[leaf].arg][0] == '/' )
	    AddToStr( WArgV[WTree[leaf].arg] + 1, &iptr );
	else
	    AddToStr( WArgV[WTree[leaf].arg], &iptr );
	if( WArgT[arg] == ARG_REGEXP && REEnd[arg] == 1 )
	    AddToStr( "$", &iptr );
	WTree[leaf].llower = 1;
	WTree[leaf].rlower = 1;
	return( 1 );
    }
}

void SayExpressionTree()
/* Output entire expression tree. */
{
    iptr = 0;
    if( SType == WORDS_ROBOT ) {
	AddToStr( "-", &iptr );
	AddToStr( RName, &iptr );
	AddToStr( " ", &iptr );
    }
    AddToStr( "Searching using `", &iptr );
    TreeDepth = say_node( TreeHead );
    AddToStr( "'", &iptr );
    if( SType == WORDS_ROBOT ) {
	TermStr( iptr, 0 );
    } else if( SType == WORDS_CRITIQUE || SType == WORDS_BINGO ) {
	OPrint( "%s\n", WriteBuf );
    }
}

void ApplyDemorgan( leaf )
    int leaf;
/* Apply Demorgan's rule: change all `!(a b)' to `!a | !b' and `!(a | b)' to
   `!a !b'. */
{
    int p;
    
    if( WTree[leaf].type == TOKEN_ARG ) {
	WArgS[WTree[leaf].arg] = WTree[leaf].negsense;
    } else {
	if( WTree[leaf].negsense == SENSE_NEG ) {
	    if( WTree[leaf].type == TOKEN_AND )
		WTree[leaf].type = TOKEN_OR;
	    else
		WTree[leaf].type = TOKEN_AND;
	    p = WTree[leaf].left;
	    if( WTree[p].negsense == SENSE_NEG )
		WTree[p].negsense = SENSE_POS;
	    else
		WTree[p].negsense = SENSE_NEG;
	    p = WTree[leaf].right;
	    if( WTree[p].negsense == SENSE_NEG )
		WTree[p].negsense = SENSE_POS;
	    else
		WTree[p].negsense = SENSE_NEG;
	}
	ApplyDemorgan( WTree[leaf].left );
	ApplyDemorgan( WTree[leaf].right );
    }
}

int check_lengths( leaf, length )
    int leaf, length;
/* See if all lengths below a particular leaf satisfy a given length.  This
   routine is meant to shortcut certain impossible cases.  It is recursive,
   and is called with the pointer to the head of the tree.  For example, for
   a `w >retina 7' command, clearly no words of any length other than 7 can
   match this; this routine will return 0 when called with `length' other than
   7 and 1 when called with `length' of 7. */
{
    int arg, i;
    int rc;
    
    if( WTree[leaf].type == TOKEN_ARG ) {
	arg = WTree[leaf].arg;
	i = VLen[arg][length];
	if( ( i == LENGTH_IMPOSSIBLE && WArgS[arg] == SENSE_POS )
	    || ( i == LENGTH_DEFINITE && WArgS[arg] == SENSE_NEG ) )
	    return( 0 );
	else
	    return( 1 );
    } else {
	if( WTree[leaf].llower < WTree[leaf].rlower )
	    rc = check_lengths( WTree[leaf].left, length );
	else
	    rc = check_lengths( WTree[leaf].right, length );
	if( WTree[leaf].type == TOKEN_AND && rc == 0 )
	    return( 0 );
	if( WTree[leaf].type == TOKEN_OR && rc == 1 )
	    return( 1 );
	if( WTree[leaf].llower < WTree[leaf].rlower )
	    return( check_lengths( WTree[leaf].right, length ) );
	else
	    return( check_lengths( WTree[leaf].left, length ) );
    }
}

int check_may_only_contain( arg, tword )
    int arg;
    char *tword;
/* See if a word satisfies a `<' argument. */
{
    int i, tletar[27];
    int j;
    
    for( i = 0; i < 27; i++ ) tletar[i] = LetAr[arg][i];
    for( i = 0; i < LenW; i++ ) {
	j = tword[i] - 'A';
	if( tletar[j] > 0 ) tletar[j]--;
	else if( tletar[26] > 0 ) tletar[26]--;
	else break;
    }
    if( ( i != LenW && WArgS[arg] == SENSE_POS )
	|| ( i == LenW && WArgS[arg] == SENSE_NEG ) )
	return( 0 );
    else
	return( 1 );
}

int check_must_contain( arg, tword )
    int arg;
    char *tword;
/* See if a word satisfies a `>' argument. */
{
    int i, tletar[27];
    int j;
    
    for( i = 0; i < 27; i++ ) tletar[i] = LetAr[arg][i];
    for( i = 0; i < LenW; i++ ) {
	j = tword[i] - 'A';
	if( tletar[j] > 0 ) tletar[j]--;
	else if( tletar[26] > 0 ) tletar[26]--;
    }
    for( i = 0; i < 27; i++ )
	if( tletar[i] > 0 ) break;
    if( ( i != 27 && WArgS[arg] == SENSE_POS )
	|| ( i == 27 && WArgS[arg] == SENSE_NEG ) )
	return( 0 );
    else
	return( 1 );
}

int check_regexp( arg, tword )
    int arg;
    char *tword;
/* See if a word satisfies a `/' argument. */
{
    int resl, reel;
    int i, j, k;
    
    resl = 0;
    reel = LenW - RELen[arg];
    if( REStart[arg] == 1 ) reel = resl;
    if( REEnd[arg] == 1 ) resl = reel;
    for( i = resl; i < reel + 1; i++ ) {
	for( j = 0; j < RELen[arg]; j++ ) {
	    k = tword[i + j] - 'A';
	    if( REValidL[arg][j][k] == 0 ) break;
	}
	if( j == RELen[arg] ) break;
    }
    if( ( i == reel + 1 && WArgS[arg] == SENSE_POS )
	|| ( i != reel + 1 && WArgS[arg] == SENSE_NEG ) )
	return( 0 );
    else
	return( 1 );
}

int check_conditions( leaf, tword )
    int leaf;
    char *tword;
/* Check recursively that all conditions below a leaf are satisfied.  Is
   called with a pointer to the head of the tree. */
{
    int arg;
    int rc;
    
    if( WTree[leaf].type == TOKEN_ARG ) {
	arg = WTree[leaf].arg;
	if( WArgT[arg] == ARG_MAY_ONLY_CONTAIN )
	    return( check_may_only_contain( arg, tword ) );
	else if( WArgT[arg] == ARG_MUST_CONTAIN )
	    return( check_must_contain( arg, tword ) );
	else if( WArgT[arg] == ARG_REGEXP )
	    return( check_regexp( arg, tword ) );
	else {
	    if( VLen[arg][LenW] == LENGTH_DEFINITE && WArgS[arg] == SENSE_POS )
		return( 1 );
	    if( VLen[arg][LenW] == LENGTH_IMPOSSIBLE && WArgS[arg] == SENSE_NEG )
		return( 1 );
	    return( 0 );
	}
    } else {
	if( WTree[leaf].llower < WTree[leaf].rlower )
	    rc = check_conditions( WTree[leaf].left, tword );
	else
	    rc = check_conditions( WTree[leaf].right, tword );
	if( WTree[leaf].type == TOKEN_AND && rc == 0 )
	    return( 0 );
	if( WTree[leaf].type == TOKEN_OR && rc == 1 )
	    return( 1 );
	if( WTree[leaf].llower < WTree[leaf].rlower )
	    return( check_conditions( WTree[leaf].right, tword ) );
	else
	    return( check_conditions( WTree[leaf].left, tword ) );
    }
}

void clear_mwgrid()
/* Erase grid of matching words. */
{
    int i;

    for( i = 0; i < MWYSize; i++ ) MWGrid[i][0] = '\0';
}

void show_mwgrid()
/* Show grid of matching words. */
{
    int i;
    char foundm[80], dispm[80];

    if( SType == WORDS_BINGO && BatchMode == BATCH_YES ) return;
    foundm[0] = '\0';
    if( FirstSearch == 1 || ( LineInd == 0 && LLLen <= MWYSize ) ) {
	if( WMatches == 1 ) {
	    sprintf( foundm, "Found one match.\n" );
	} else if( WMatches > 1 && WMatches < 10 ) {
	    sprintf( foundm, "Found %s matches.\n", NumNames[WMatches] );
	} else {
	    sprintf( foundm, "Found %d matches.\n", WMatches );
	}
    }
    dispm[0] = '\0';
    if( LineInd != 0 || LLLen > MWYSize ) {
	if( LineInd == 0 ) {
	    i = LineList[MWYSize];
	    if( i == 2 ) {
		sprintf( dispm, "Displaying first of %d matches.\n", WMatches );
	    } else if( i > 2 && i < 11 ) {
		sprintf( dispm, "Displaying first %s of %d matches.\n",
			 NumNames[i - 1], WMatches );
	    } else {
		sprintf( dispm, "Displaying first %d of %d matches.\n",
			 i - 1, WMatches );
	    }
	} else if( LineInd + MWYSize >= LLLen ) {
	    i = WMatches - LineList[LineInd];
	    if( i == 0 ) {
		sprintf( dispm, "Displaying last of %d matches.\n", WMatches );
	    } else if( i > 0 && i < 9 ) {
		sprintf( dispm, "Displaying last %s of %d matches.\n",
			 NumNames[i + 1], WMatches );
	    } else {
		sprintf( dispm, "Displaying last %d of %d matches.\n",
			 i + 1, WMatches );
	    }
	} else {
	    sprintf( dispm, "Displaying matches %d to %d of %d.\n",
		     LineList[LineInd], LineList[LineInd + MWYSize] - 1,
		     WMatches );
	}
    }
    if( foundm[0] != '\0' && SType != WORDS_BINGO ) OPrint( foundm );
    if( SType == WORDS_ROBOT ) {
	if( dispm[0] != '\0' ) OPrint( "%s", dispm );
	for( i = 0; i < MWYSize && MWGrid[i][0] != '\0'; i++ ) {
	    sprintf( WriteBuf, "\"%s\n", MWGrid[i] );
	    QueueCommand( WriteBuf, 0 );
	}
    } else if( SType == WORDS_CRITIQUE || SType == WORDS_BINGO ) {
	CritGridShow();
    }
}

void lower_blanks( tword )
    char *tword;
/* Figure out which letters represent blank tiles for critiquer `bi' command
   and make them lower case. */
{
    int i, tletar[27];
    int j;
    
    for( i = 0; i < 27; i++ ) tletar[i] = LetAr[0][i];
    for( i = 0; i < LenW; i++ ) {
	j = tword[i] - 'A';
	if( tletar[j] > 0 ) tletar[j]--;
	else tword[i] = tword[i] - 'A' + 'a';
    }
}

void find_page()
/* Find a page of matching words. */
{
    int teind, i;
    long index;
    char tword[MAX_WLEN + 1];
    int cmatch;

    clear_mwgrid();
    cmatch = 0;
    for( LenW = 2; LenW < MAX_WLEN + 1; LenW++ ) {
	if( check_lengths( TreeHead, LenW ) == 0 ) continue;
	teind = WLen[LenW][26];
	for( index = WLen[LenW][0]; index < teind; index++ ) {
	    strcpy( tword, &(Words[WPtr[index]]) );
	    if( check_conditions( TreeHead, tword ) == 0 ) continue;
	    cmatch++;
	    if( cmatch >= LineList[LineInd] ) {
		for( i = 0; cmatch >= LineList[LineInd + i]; i++ );
		i--;
		if( SType == WORDS_BINGO ) lower_blanks( tword );
		if( SType == WORDS_BINGO && BatchMode == BATCH_YES ) {
		    if( cmatch == LineList[LineInd + i] && i != 0 )
			printf( "\n" );
		    printf( "%s ", tword );
		} else {
		    sprintf( MWGrid[i] + strlen( MWGrid[i] ), "%s ", tword );
		    FMLen = LenW; FMInd = index; FMNum = LMNum + 1;
		}
	    }
	    if( SType == WORDS_BINGO && BatchMode == BATCH_YES ) continue;
	    if( cmatch == WMatches
		|| ( LineInd + MWYSize < LLLen
		     && cmatch == LineList[LineInd + MWYSize] ) ) break;
	}
	if( index < teind ) break;
    }
    if( SType == WORDS_BINGO && BatchMode == BATCH_YES
	&& WMatches != 0 ) printf( "\n" );
    for( i = 0; i < MWYSize; i++ )
	if( MWGrid[i][0] != '\0' )
	    MWGrid[i][strlen( MWGrid[i] ) - 1] = '\n';
}

void print_starting_line()
/* Print starting line for batch mode. */
{
    int i, j;

    printf( "Number of bingos in " );
    for( i = 0; i < LetAr[0][26]; i++ ) printf( "?" );
    for( i = 0; i < 26; i++ )
	for( j = 0; j < LetAr[0][i]; j++ )
	    printf( "%c", i + 'A' );
    printf( ": %d\n", WMatches );
}

void PerformSearch( cmtype, pagearg )
    int cmtype;
    int pagearg;
/* Perform a `words' command search. */
{
    int stind;
    long index, linelen;
    char estr[80];

    FirstSearch = 0;
    if( cmtype == WC_WORDS ) {
	FirstSearch = 1;
	if( LineList != NULL ) free( (int *) LineList );
	WMatches = 0; LLLen = 1; linelen = 0;
	for( LenW = 2; LenW < MAX_WLEN + 1; LenW++ ) {
	    if( check_lengths( TreeHead, LenW ) == 0 ) continue;
	    stind = WLen[LenW][26];
	    for( index = WLen[LenW][0]; index < stind; index++ ) {
		if( check_conditions( TreeHead, &(Words[WPtr[index]]) ) != 0 ) {
		    WMatches++;
		    linelen += LenW + 1;
		    if( linelen > MWXSize ) {
			LLLen++;
			linelen = LenW + 1;
		    }
		}
	    }
	}
	FreeArgv();
	if( SType == WORDS_BINGO && BatchMode == BATCH_YES ) {
	    print_starting_line();
	} else if( WMatches == 0 ) {
	    if( ( SType == WORDS_CRITIQUE || SType == WORDS_BINGO )
		&& FullScreen == FS_YES ) {
		werase( Wind[W_WORDS] );
		WHeaderLine();
		wrefresh( Wind[W_WORDS] );
	    }
	    if( SType != WORDS_BINGO ) OPrint( "No matches found." );
	    else OPrint( "No bingos found." );
	    return;
	}
	LineList = (int *) malloc( ( LLLen + 1 ) * sizeof( int ) );
	LineList[0] = 1;
	WMatches = 0; linelen = 0; LLLen = 0;
	for( LenW = 2; LenW < MAX_WLEN + 1; LenW++ ) {
	    if( check_lengths( TreeHead, LenW ) == 0 ) continue;
	    stind = WLen[LenW][26];
	    for( index = WLen[LenW][0]; index < stind; index++ ) {
		if( check_conditions( TreeHead, &(Words[WPtr[index]]) ) != 0 ) {
		    WMatches++;
		    linelen += LenW + 1;
		    if( linelen > MWXSize ) {
			LineList[++LLLen] = WMatches;
			linelen = LenW + 1;
		    }
		}
	    }
	}
	LineList[++LLLen] = WMatches + 1;
	LineInd = 0;
    } else if( cmtype == WC_GOTO ) {
	if( pagearg > WMatches ) {
	    if( WMatches == 0 )
		sprintf( estr, "there are no matches." );
	    else if( WMatches == 1 )
		sprintf( estr, "there is only one match." );
	    else if( WMatches > 1 && WMatches < 10 )
		sprintf( estr, "there are only %s matches.",
			 NumNames[WMatches] );
	    else
		sprintf( estr, "there are only %d matches.", WMatches );
	    OPrint( "Error: %s", estr );
	    return;
	} else if( pagearg < 1 ) {
	    sprintf( estr, "you can't go to a match before the first." );
	    OPrint( "Error: %s", estr );
	    return;
	} else {
	    for( LineInd = 0; LineList[LineInd] <= pagearg; LineInd++ );
	    LineInd--;
	}
    } else if( cmtype == WC_PAGE ) {
	if( LineInd == 0 && pagearg < 0 ) {
	    OPrint( "No more matches above." );
	    return;
	} else if( LineInd + MWYSize >= LLLen && pagearg > 0 ) {
	    OPrint( "No more matches below.\n" );
	    return;
	}
	LineInd += pagearg * MWYSize;
	if( LineInd < 0 ) LineInd = 0;
	if( LineInd >= LLLen ) LineInd = LLLen - 1;
    }
    find_page();
    show_mwgrid();
}
