/*---------------------------------------------------------------------------
Query.m -- Copyright (c) 1991 Rex Pruess
  
   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 1, or (at your option)
   any later version.
  
   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.
  
   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA, or send
   electronic mail to the the author.
  
A new Query object is created each time the user asks for a new query
window.  This code manages the query window and is the primary interface
to the Qi server for query requests.  The methods are grouped by function
and roughly in the order they are called by previous methods.
  
Rex Pruess <Rex-Pruess@uiowa.edu>
  
$Header: /rpruess/apps/Ph/query.subproj/RCS/Query.m,v 2.4 91/12/19 09:27:23 rpruess Exp $
-----------------------------------------------------------------------------
$Log:	Query.m,v $
Revision 2.4  91/12/19  09:27:23  rpruess
Fixed timing bug whereby "hide" preference option caused Ph to crash
in the situation where the socket had timed out before the user unhid
Ph.  Then, Ph would crash if the user selected the "fields" view.

Revision 2.3  91/12/11  08:48:27  rpruess
Added code to verify the search string was non-null when doing name
searches.  If it is null, an alert panel is displayed.  The user can
still beat this simple check by entering a blank search string.  Oh well.

Revision 2.2  91/12/10  16:30:51  rpruess
Moved the timer code from QueryManager to Query.  Each Query object is
now responsible for determining when it has received the server "fields"
data.  This code was added prior to the initial Ph Version 2.0 release.

Revision 2.1  91/11/20  08:47:36  rpruess
Added code to clear the ScrollView of old data just before a new search
is begun.  This provides a clear signal to the user as to when the search
has really finished.  Prior to this code, consecutive searches which
returned the same data did not provide as clear a signal as to when it
had finished a search.

Revision 2.0  91/11/19  08:19:29  rpruess
Revision 2.0 is the initial production release of Ph.

-----------------------------------------------------------------------------*/
#define FIELDBOXTITLE "Field Search"  /* Title in PopUpList of fields box */

#define ALLFIELDS 0		/* Returned Fields radio switch tags */
#define DEFFIELDS 1
#define SPCFIELDS 2

#define EMAILTO "email to"	/* Title to use for refomatted mail addrs */

#define MAILLEN	128		/* Maximum length of reformatted mail addr */

#define MINBOXHEIGHT 67.0	/* Name/Field box minimum size */

#define MINQUERYHEIGHT 200.0	/* Minimum sizes for Query window */
#define MINQUERYWIDTH  400.0

#define SEARCHING "Searching..."

#define TIMERSECS 0.5		/* Animator wakeup interval */

enum returnType {
   ALL, DEFAULT, SPECIFIC
};

/* Objective-C & Appkit header files */
#import <objc/List.h>
#import <appkit/Application.h>
#import <defaults/defaults.h>
#import <appkit/Font.h>
#import <appkit/Form.h>
#import <appkit/FormCell.h>
#import <appkit/nextstd.h>
#import <appkit/NXImage.h>
#import <appkit/NXSplitView.h>
#import <appkit/Panel.h>
#import <appkit/ScrollView.h>
#import <appkit/Text.h>
#import <appkit/Window.h>

/* Application class header files */
#import "Query.h"
#import "BoxViewSwitcher.h"
#import "QueryFieldView.h"
#import "Specify.h"
#import "../PhShare.h"
#import "../info.h"
#import "../qiServers.h"

@implementation Query

/*---------------------------------------------------------------------------
A new Query object must load its nib file, bring the BoxViewSwitcher to
life, and intialize several variables.
-----------------------------------------------------------------------------*/
- init
{
   [super init];
   [NXApp loadNibSection:"Query.nib" owner:self withNames:NO];
   [queryWindow setMiniwindowIcon:"app"];

   [self createBoxViewSwitcher:self];

   /*** This set of fields needed for the rewriting of email addresses */

   alias[0] = '\0';
   copiedEmail = NO;
   domain = NULL;
   email[0] = '\0';
   reformatEmail = NO;

   /*** Finish initializing variables to default values. */

   curPerson = 0;
   isQueryReady = NO;
   returnedFields = DEFAULT;

   qiObject = nil;
   myFieldForm = nil;
   myIconMatrix = nil;
   myQueryFieldView = nil;
   mySpecify = nil;
   queryAnimator = nil;
   theQiManager = nil;

   /*** Initialize the searching string & search button. */

   [self displaySearchingWord:NO];

   /*** Create the fonts, set the line wrapping, and initialize the
        hyphens/equals divider lines. */

   [self createFonts:self];
   [self initLineWrap:self];

   /*** Add the two subviews to the splitView. */

   [splitView addSubview:boxView];
   [splitView addSubview:phoneView];

   return self;
}

/*---------------------------------------------------------------------------
The BoxViewSwitcher must be brought to life.  It is responsible for switching
the name box with the fields box based on the user's popuplist selection.
-----------------------------------------------------------------------------*/
- createBoxViewSwitcher:sender
{
   myBVS = [[BoxViewSwitcher alloc] init];
   [myBVS setBoxView:boxView];
   [myBVS setBoxViewWindow:boxViewWindow];
   [myBVS setControlButton:searchTypeButton];
   [myBVS removeBoxTitles:self];
   [myBVS setDelegate:self];

   return self;
}

/*---------------------------------------------------------------------------
Initialize the font based on the defaults data base values.
-----------------------------------------------------------------------------*/
- createFonts:sender
{
   const char     *fontName;
   float           fontSize;
   id              defaultFont;

   fontName = NXGetDefaultValue ([NXApp appName], FONT);
   fontSize = atof (NXGetDefaultValue ([NXApp appName], FONTSIZE));

   defaultFont = [Font newFont:fontName size:fontSize style:0 matrix:NX_FLIPPEDMATRIX];
   [[phoneView docView] setFont:defaultFont];

   return self;
}

/*---------------------------------------------------------------------------
Set the docView line wrapping capability based on the defaults data base
setting.
-----------------------------------------------------------------------------*/
- initLineWrap:sender
{
   int             i;
   int             nChars;
   const char     *lineWrap;
   BOOL            wrapLines;

   lineWrap = NXGetDefaultValue ([NXApp appName], WRAPLINES);

   if (strcmp (lineWrap, "YES") == 0)
      wrapLines = YES;
   else {
      wrapLines = NO;
      [[phoneView docView] setNoWrap];
   }

   /*** Set width of separator lines based on line wrap value */

   if (wrapLines)
      nChars = WRAPWIDTH;
   else
      nChars = NWRAPWIDTH;

   /*** Set hyphens and equals separator lines */   

   hyphens[0] = '\0';
   for (i = 0; i < nChars - 3; i += 2)
      strcat (hyphens, "- ");

   strcat (hyphens, "\n");
   
   equals[0] = '\0';
   for (i = 0; i < nChars - 2; i++)
      strcat (equals, "=");

   strcat (equals, "\n");
   
   return self;
}

/*---------------------------------------------------------------------------
This method must be called before calling the openSession method.  This
method sets important variables.
-----------------------------------------------------------------------------*/
- setQueryVars:aQiManager domain:(const char *)aDomain
{
   theQiManager = aQiManager;

   if (aDomain == NULL)
      domain = NULL;
   else {
      domain = malloc (strlen (aDomain) + 1);
      strcpy (domain, aDomain);
   }

   return self;
}

/*---------------------------------------------------------------------------
This method should be called before calling the openSession method.  It
shifts each Query window a bit from the previous Query window.  This makes
it clear to the user that there are multiple Query windows.
-----------------------------------------------------------------------------*/
- setQueryWindow:(const char *)aTitle offset:(float)anOffset
{
   NXRect          aRect;

   [queryWindow setTitle:aTitle];

   [queryWindow getFrame:&aRect];
   [queryWindow moveTo:(aRect.origin.x + anOffset) :(aRect.origin.y - anOffset)];

   return self;
}

/*---------------------------------------------------------------------------
Try to form a connection with the Qi server.  If it can not make a connection,
it will return nil.  If it is successful, start the animator so it can wait
for all the "fields" to come back from the server.
-----------------------------------------------------------------------------*/
- (BOOL) openSession:(const char *)aServer
{
   if ((qiObject = [theQiManager open:aServer]) == nil)
      return NO;

   /*** If the Qi object was obtained, then create the nifty matrix view
        & establish views appropriately.  */

   [self createQueryFieldView:self];
   [[fieldScrollView setDocView:myQueryFieldView] free];

   /*** Start the animator.  It will wait for the "fields" to come back
        from the Qi server. */

   queryAnimator = [[Animator alloc] initChronon:TIMERSECS
      adaptation:0.0
      target:self
      action:@selector (queryTimeCheck)
      autoStart:YES
      eventMask:0];

   return YES;
}

/*---------------------------------------------------------------------------
The animator is responsible for periodically invoking this method.  This
code checks to see if the Query object has been initialized.  Query objects
need to get "fields" data from the Qi server so there is some delay before
they come to life.
-----------------------------------------------------------------------------*/
- queryTimeCheck
{
   /*** If the "fields" have not been read, then we must wait some more.
        Check the server socket to see if it has closed.  If so, stop the
        timer since we won't get the data anyway. */

   if ([qiObject hasQiFields] == NO) {
      
      if ([qiObject hasClosed])
	 [self stopTimer:self];

      return self;
   }

   /*** Success!  Stop the animator, fill the "fields" matrix, and show
        the window. */

   [self stopTimer:self];
   [self fillFieldMatrix:self];
   [self showWindow:self];

   return self;
}

/*---------------------------------------------------------------------------
The Query object has initialized itself.  Stop the animator & free it.
-----------------------------------------------------------------------------*/
- stopTimer:sender
{
   [queryAnimator stopEntry];
   free (queryAnimator);
   return self;
}


/*---------------------------------------------------------------------------
This method is called after the Qi object has been obtained (i.e., the
connection was successfully established with the server).  The purpose of
this method is to create the QueryFieldView object.  That object will contain
two matrixes.  One matrix will contain tiny cells capable of holding the
bullet icon.  The second matrix will be a form which will eventally hold all
the "fields" from the server.
-----------------------------------------------------------------------------*/
- createQueryFieldView:sender
{
   NXRect          mRect;

   mRect.origin.x = 0.0;
   mRect.origin.y = 0.0;

   [fieldScrollView getContentSize:&mRect.size];

   myQueryFieldView = [[QueryFieldView alloc] initFrame:&mRect];

   myIconMatrix = [myQueryFieldView iconMatrix];
   myFieldForm = [myQueryFieldView fieldForm];

   return self;
}

/*---------------------------------------------------------------------------
This method is invoked from within the queryTimeCheck method after all of the
"fields" have been retrieved from the server.  This code loops through the
list of fields to determine which ones are eligible for selection criteria and
which ones are indexed.  Indexed fields are marked with a bullet icon so the
user knows which fields are actually indexed.
-----------------------------------------------------------------------------*/
- fillFieldMatrix:sender
{
   id              aCell;
   id              aField;
   int             i;
   int             row;
   id              myFieldList;

   row = 0;

   myFieldList = [qiObject fieldList];

   for (i = 0; i < [myFieldList count]; i++) {
      aField = [myFieldList objectAt:i];

      if (strstr ([aField keywords], "Lookup") != NULL
	 && strstr ([aField keywords], "Public") != NULL) {

	 aCell = [myFieldForm insertEntry:[aField name] at:row];

	 [aCell setScrollable:YES];
	 [aCell setTarget:fieldSearchButton];
	 [aCell setAction:@selector (performClick:)];

	 [myIconMatrix addRow];

	 [[myIconMatrix cellAt:row :0] setBordered:NO];
	 [[myIconMatrix cellAt:row :0] setBezeled:NO];
	 [[myIconMatrix cellAt:row :0] setEnabled:NO];

	 if (strstr ([aField keywords], "Indexed") != NULL)
	    [[myIconMatrix cellAt:row :0] setIcon:"Bullet"];

	 row++;
      }

   }

   [myFieldForm sizeToCells];
   [myFieldForm display];

   [myIconMatrix sizeToCells];
   [myIconMatrix display];

   [myQueryFieldView adjHeight:self];
   [fieldScrollView display];

   isQueryReady = YES;
   
   return self;
}

/*---------------------------------------------------------------------------
When the user clicks on one of the radio buttons in the "return fields" box,
we go to work.  For the button ALL & DEFAULT, we discard the Specify window
(if any).  In the case of of SPECIFIC, we must display a window with all of
the returnable fields displayed as switches.
-----------------------------------------------------------------------------*/
- setReturnedFields:sender
{
   switch ([[sender selectedCell] tag]) {
   case ALLFIELDS:
      returnedFields = ALL;
      [self hideSpecify:self];
      break;

   case SPCFIELDS:
      returnedFields = SPECIFIC;
      [self showSpecify:self];
      break;

   default:
      returnedFields = DEFAULT;
      [self hideSpecify:self];
      break;
   }

   return self;
}

/*---------------------------------------------------------------------------
The first time the user requests that specific fields be returned, we must
do a lot of work.  The Specify object is created.  It creates a window with
a scrollview to hold the switches.  Specify is told how many items will
created as well as a good origin for the Specify window.  This presents a
consistent, pleasant interface to the user.  Once the window has been setup,
then we walk through all of the fields to determine which ones are valid
for return.
-----------------------------------------------------------------------------*/
- createSpecify:sender
{
   id              aField;
   int             i;
   int             nItems;
   id              myFieldList;
   float           xLoc;
   float           yLoc;

   nItems = 0;
   mySpecify = [[Specify alloc] init];
   myFieldList = [qiObject fieldList];

   /*** Find the number of items which will be placed into the Specify
        matrix.  This value is needed so we can tell the Specify object
        how many items there will be.  This allows Specify to create the
        proper sized window. */

   for (i = 0; i < [myFieldList count]; i++) {
      aField = [myFieldList objectAt:i];

      if (strstr ([aField keywords], "Lookup") != NULL && strstr ([aField keywords], "Public") != NULL)
	 nItems++;
   }

   /*** Go calculate the origin for the Specify window & then create it. */
   [self specOrigin:&xLoc:&yLoc];

   [mySpecify initSpecify:nItems x:xLoc y:yLoc title:[queryWindow title]];

   /*** Make another pass through the fields.  This time we add a switch to
        the Specify matrix for each field we wish to include. */

   for (i = 0; i < [myFieldList count]; i++) {
      aField = [myFieldList objectAt:i];

      if (strstr ([aField keywords], "Lookup") != NULL && strstr ([aField keywords], "Public") != NULL)
	 [mySpecify addSwitch:[aField name]];
   }

   [mySpecify updateDisplay:self];

   return self;
}

/*---------------------------------------------------------------------------
Calculate a good origin for the window created by Specify.  It should be
placed under the returnFieldsBox (radio matrix).
-----------------------------------------------------------------------------*/
- specOrigin:(float *)xLoc :(float *)yLoc
{
   NXRect          aFrame;

   [queryWindow getFrame:&aFrame];
   *xLoc = aFrame.origin.x;
   *yLoc = aFrame.origin.y;

   [returnFieldsBox getFrame:&aFrame];
   *xLoc += aFrame.origin.x;
   *yLoc += aFrame.origin.y;

   return self;
}

/*---------------------------------------------------------------------------
Create the Specify object & order it to show its window.
-----------------------------------------------------------------------------*/
- showSpecify:sender
{
   if (mySpecify == nil)
      [self createSpecify:self];

   [mySpecify showWindow:self];

   return self;
}

/*---------------------------------------------------------------------------
Order the specify window out.
-----------------------------------------------------------------------------*/
- hideSpecify:sender
{
   if (mySpecify != nil)
      [mySpecify orderWindowOut:self];

   return self;
}

/*---------------------------------------------------------------------------
This code is executed by another application using the Speaker interface.
It accepts a search command of the form "name=pruess department=weeg",
fills in the appropriate search fields, & then performs the search.
-----------------------------------------------------------------------------*/
- speakerSearch:(const char *)aCommand
{
   char           *command;
   char           *field;
   int             i;
   BOOL            foundOne;
   int             nCols;
   int             nRows;
   char           *string;

   if (aCommand == NULL || strlen (aCommand) <= 0)
      return self;

   /*** Switch to the field box view, blank out all field forms. */

   [queryWindow disableFlushWindow];

   [myBVS selectBoxViewTitle:FIELDBOXTITLE];
   
   [myFieldForm getNumRows:&nRows numCols:&nCols];

   for (i = 0; i < nRows; i++)
      [myFieldForm setStringValue:"" at:i];

   /*** Parse the query command and fill in the query fields appropriately. */

   foundOne = NO;
   
   command = malloc (strlen (aCommand) + 1);
   strcpy (command, aCommand);

   field = strtok (command, "=");
   
   while (field != NULL) {
      if ((string = strtok (NULL, "\t")) == NULL)
	 break;

      for (i = 0; i < nRows; i++) {

	 if (strcmp (field, [myFieldForm titleAt:i]) == 0) {
	    foundOne = YES;
	    [myFieldForm setStringValue:string at:i];
	    break;
	 }

      }
      
      field = strtok (NULL, "=");
   }
   
   free (command);

   [[queryWindow reenableFlushWindow] flushWindow];

   /*** Invoke the search method if at least one of the fields was valid. */

   if (foundOne)
      [self search:self];
   
   return self;
}

/*---------------------------------------------------------------------------
Typically, this method is invoked when the user has clicked on the search
button.  However, it can be invoked by the speakerSearch method due to
another application using the Speaker interface.  In any event, this method
is responsible for building the search command and sending it to the server.
-----------------------------------------------------------------------------*/
- search:sender
{
   char            command[COMSIZE];

   strcpy (command, "query");

   /*** Get search criteria based on the popuplist value */

   if (strstr ([searchTypeButton title], "Name") != NULL) {

      if (strlen ([nameForm stringValueAt:0]) <= 0) {
	 NXRunAlertPanel (NULL, "You must specify a search string.", NULL, NULL, NULL);
	 return self;
      }
      
      strcat (command, " ");
      strcat (command,[nameForm stringValueAt:0]);
   }
   else {
      if ([self getFieldForms:command] == NO)
	 return self;
   }

   /*** Specify which fields should be returned based on the user's request */

   switch (returnedFields) {
   case ALL:
      reformatEmail = NO;

      if ([self catData:command data:" return all"] == NO)
	 return self;
      
      break;

   case SPECIFIC:
      reformatEmail = NO;

      if ([self catData:command data:" return"] == NO)
	 return self;

      [mySpecify getSelectedFields:selFields];

      if ([self catData:command data:selFields] == NO)
	 return self;

      break;

   default:
      if (domain != NULL && strlen (domain) > 0)
	 reformatEmail = YES;

      break;
   }

   /*** Tack on a newline to the command.  Initialize the values for a
        new person.  Display the searching word. */

   if ([self catData:command data:"\n"] == NO)
      return self;

   [self newPerson:0];
   [self clearQueryView];
   [self displaySearchingWord:YES];

   /*** Go tell it to the server & then fetch the output from the server */

   queryStream = NXOpenMemory (NULL, 0, NX_READWRITE);
   [qiObject qiSend:command delegate:self];

   return self;
}

/*---------------------------------------------------------------------------
Verify that the command does not get too long.  Highly doubtful it ever will.
-----------------------------------------------------------------------------*/
- (BOOL) catData:(char *)aSource data:(const char *)aData
{
   if (strlen (aSource) + strlen (aData) + 1 > COMSIZE) {
      strcpy (errMsg, "The query command is too long.  Reduce your selection ");
      strcat (errMsg, "criteria or reduce the number of fields you wish to ");
      strcat (errMsg, "return.");

      NXRunAlertPanel (NULL, errMsg, NULL, NULL, NULL);

      return NO;
   }

   strcat (aSource, aData);

   return YES;
}

/*---------------------------------------------------------------------------
Go through all of the field forms and capture the user requested search
criteria.  At least one of the search fields must be indexed.
-----------------------------------------------------------------------------*/
- (BOOL) getFieldForms:(char *)aCommand
{
   BOOL            indexFlag;
   int             i;
   int             nCols;
   int             nRows;

   [myFieldForm getNumRows:&nRows numCols:&nCols];

   indexFlag = NO;
   for (i = 0; i < nRows; i++) {
      if (strlen ([myFieldForm stringValueAt:i]) > 0) {

	 if ([[myIconMatrix cellAt:i :0] icon] != NULL)
	    indexFlag = YES;

	 if ([self catData:aCommand data:" "] == NO ||
	    [self catData:aCommand data:[myFieldForm titleAt:i]] == NO ||
	    [self catData:aCommand data:"="] == NO ||
	    [self catData:aCommand data:[myFieldForm stringValueAt:i]] == NO)
	    return NO;
      }
   }

   if (indexFlag == NO) {
      strcpy (errMsg, "You must search by at least one indexed field.  ");
      strcat (errMsg, "(Indexed fields are marked with a bullet.)");

      NXRunAlertPanel (NULL, errMsg, NULL, NULL, NULL);

      return NO;
   }

   return YES;
}

/*---------------------------------------------------------------------------
Brand new person.  Reset the default values.
-----------------------------------------------------------------------------*/
- newPerson:(const int)aPerson
{
   alias[0] = '\0';
   email[0] = '\0';
   copiedEmail = NO;

   curPerson = aPerson;
   
   return self;
}

/*---------------------------------------------------------------------------
Process a single line of Qi server output.  Each line identifies the person
by number.  We rely on this number to determine when pretty separator lines
should be displayed.
-----------------------------------------------------------------------------*/
- qiOutput:(char *)aBuf
{
   int             theCode;
   int             thePerson;

   theCode = atoi (aBuf);

   /*** Display the number of matches. */

   if (theCode == LR_NUMRET) {
      [self outputString:(strchr (aBuf, ':') + 1)];
      return self;
   }

   /*** Upon receiving the final line, do some final processing for the
        entry.  This includes reformatting the email address, flushing
        the outputStream to the ScrollView, and telling the Qi object to
        stop watching the file descriptor. */

   if (theCode >= LR_OK) {
      if (theCode != LR_OK) {
	 if (theCode == LR_NOMATCH)
	    [self outputString:(strchr (aBuf, ':') + 1)];
	 else
	    [self outputString:aBuf];
      }

      if (reformatEmail)
	 [self displayEmail:self];
      
      [self outputString:equals];
      [self displaySearchingWord:NO];
      [self outputStream:self];
      
      [qiObject stopFd:self];

      return self;
   }

   /*** If this is a new person, finish off the previous person.  This
        includes reformatting the email address & displaying a pretty
        separator line.  The newPerson method is invoked to initialize
        variables for the new person. */

   if (theCode == -LR_OK || theCode == -LR_AINFO || theCode == -LR_ABSENT
      || theCode == -LR_ISCRYPT) {

      thePerson = atoi (index (aBuf, ':') + 1);

      if (curPerson != thePerson) {
	 if (curPerson != 0) {

	    if (reformatEmail)
	       	 [self displayEmail:self];

	    [self outputString:hyphens];
	 }
	 
	 [self newPerson:thePerson];
      }

      [self processLine:aBuf];
   }
   else
      [self outputString:aBuf];

   return self;
}

/*---------------------------------------------------------------------------
If we are in the "reformatMail" mode, then we must grab the "alias" and
"email" fields for later processing.  This code is based on Steve Dorner's
UNIX Ph code.  The code has been altered to fit into my implementation.
I've added very crude length checks; EMAILSIZE should be plenty large, but
I don't want to crash due to an array bounds problem.
-----------------------------------------------------------------------------*/
- processLine:(const char *)aBuf
{
   char           *cp;		/* Character pointer */
   char           *lp;		/* Line pointer */

   lp = (strchr (strchr (aBuf, ':') + 1, ':') + 1);

   if (reformatEmail) {

      cp = lp;
      while (*cp && *cp==' ')
	 cp++;

      if (!strncmp ("alias", cp, 5)) {
	 copiedEmail = NO;

	 strncpy (alias, lp, EMAILSIZE - 2);

	 if (strlen (alias) > EMAILSIZE - 2)
	    alias[strlen (alias)] = '\n';
	    
	 return self;
      }
      else 
	 if (!strncmp ("email", cp, 5)) {
	    strncpy (email, lp, EMAILSIZE - 2);

	    if (strlen (email) > EMAILSIZE - 2)
	       email[strlen (email)] = '\n';
	    
	    copiedEmail = YES;
	    return self;
	 }
	else if (*cp==':' && copiedEmail)
	  return self;
	else
	  copiedEmail = NO;
      }

   /*** Output the line */

   [self outputString:lp];

   return self;
}

/*---------------------------------------------------------------------------
Print the e-mail line in a nice, pretty format.  This code is based on Steve
Dorner's UNIX Ph code.  The code has been altered to fit into my
implementation.  I've added very crude length checks; EMAILSIZE should be
plenty large, but I don't want to crash due to an array bounds problem.
-----------------------------------------------------------------------------*/
- emailLine:sender
{
   char            scratch[EMAILSIZE];
   char           *emSpot;	/* Beginning of email account */
   char           *alSpot;	/* Beginning of nameserver alias */
  
   if (*alias) {
      alSpot = index (alias, ':') + 2;
      emSpot = index (email, ':') + 2;

      *index (alSpot, '\n') = 0;
      *index (emSpot, '\n') = 0;

      /*** Overwrite the email label. */

      if (strlen (alias) + strlen (domain) + 1 < EMAILSIZE &&  strlen (email) + 2 < EMAILSIZE ) {
	 strcpy (alSpot - 2 - strlen (EMAILTO), EMAILTO);
	 alSpot[-2] = ':';	/* strcpy clobbered the colon; repair */
	 sprintf (scratch, "@%s\n", domain);
	 strcat (alias, scratch);
	 [self outputString:alias];	 

	 strcpy (emSpot - 2 - strlen ("email"), "     ");
	 emSpot[-2] = ':';	/* strcpy clobbered the colon; repair */
	 sprintf (scratch, "(%s)\n", emSpot);
	 strcpy (emSpot, scratch);	/* Leave it in the "email" line */
      }
      
      [self outputString:email];
   }

   return self;
}

/*---------------------------------------------------------------------------
Print a "not registered" e-mail line in a nice, pretty format.  This code is
based on Steve Dorner's UNIX Ph code.  The code has been altered to fit into
my implementation.  I've added very crude length checks; EMAILSIZE should be
plenty large, but I don't want to crash due to an array bounds problem.
-----------------------------------------------------------------------------*/
- notRegisteredLine:sender
{
   char           *cp;

   if (*alias) {
      cp = index (alias, ':');
      strcpy (cp - strlen (EMAILTO), EMAILTO);
      *cp = ':';

      if (strlen (alias) + strlen (domain) + 30 < EMAILSIZE) {
	 *index (cp, '\n') = 0;

	 strcat (alias, "@");
	 strcat (alias, domain);
	 strcat (alias, " (no account registered)\n");
      }
   
      [self outputString:alias];
   }

   return self;
}

/*---------------------------------------------------------------------------
The user has clicked the Clear Fields menu item.  Only clear the fields if
the window is the key window.  
-----------------------------------------------------------------------------*/
- clearQueryFields
{
   int             i;
   int             nCols;
   int             nRows;

   if ([queryWindow isKeyWindow] == NO)
      return self;

   /*** Determine which boxView is visible and clear the fields in it. */

   if (strstr ([searchTypeButton title], "Name") != NULL)
      [nameForm setStringValue:"" at:0];
   else {
      [myFieldForm getNumRows:&nRows numCols:&nCols];

      for (i = 0; i < nRows; i++)
	 [myFieldForm setStringValue:"" at:i];
   }
   
   return self;
}

/*---------------------------------------------------------------------------
This method is called in a couple of ways.  It is called internally by the
search method so that the view is cleared just before searching begins.  It
is also invoked whenever the user clicks the Clear Buffer menu item.  The
view is cleared only if the window is the key window.  This is necessary so
that the Clear Buffer menu item will only clear the key window's view.  There
is probably a better way to do this.
-----------------------------------------------------------------------------*/
- clearQueryView
{
   id              phoneText;

   if ([queryWindow isKeyWindow] == NO)
      return self;

   [queryWindow disableFlushWindow];

   phoneText = [phoneView docView];
   [phoneText setSel:0 :[phoneText textLength]];
   [phoneText replaceSel:""];

   [[queryWindow reenableFlushWindow] flushWindow];

   return self;
}

/*---------------------------------------------------------------------------
Let the user know we're busy searching.
-----------------------------------------------------------------------------*/
- displaySearchingWord:(BOOL) searching
{
   BOOL enable;
   char *sWord;
   
   if (searching) {
      enable = NO;
      sWord = SEARCHING;
   }
   else {
      enable = YES;
      sWord = NULL;
      [self selCell:self];	/* Select a cell so it is highlighted. */
   }
   
   [fieldSearchButton setEnabled:enable];
   [nameSearchButton setEnabled:enable];

   [fieldSearchingWord setStringValue:sWord];
   [nameSearchingWord setStringValue:sWord];

   return self;
}

/*---------------------------------------------------------------------------
Display a normal line of output in the scrollview.
-----------------------------------------------------------------------------*/
- outputString:(char *)aString
{
   NXPrintf (queryStream, "%s", aString);
   return self;
}

/*---------------------------------------------------------------------------
We have received all of the data for this query.  Display the data.
-----------------------------------------------------------------------------*/
- outputStream:sender
{
   static NXPoint  origin = {0.0,0.0};
   id              phoneText;

   /*** Flush the buffer; reset pointer to beginning. */

   NXFlush (queryStream);
   NXSeek (queryStream, 0, NX_FROMSTART);

   /*** Display the queryStream data to the user. */

   [queryWindow disableFlushWindow];

   phoneText = [phoneView docView];
   [phoneText readText:queryStream];
   [phoneText scrollPoint:&origin];

   [[queryWindow reenableFlushWindow] flushWindow];

   /*** Gotta free that memory. */

   NXCloseMemory (queryStream, NX_FREEBUFFER);

   return self;
}

/*---------------------------------------------------------------------------
When there is a valid domain for the site and we are returning the "default"
fields, then we reformat the e-mail address to reflect the fancy format.
-----------------------------------------------------------------------------*/
- displayEmail:sender
{
   if (*alias && !*email)
      [self notRegisteredLine:sender];
   else
      if (*email) {
	 [self emailLine:sender];
	 email[0] = '\0';
      }
   
   copiedEmail = NO;
   
   return self;
}

/*---------------------------------------------------------------------------
When the box view switches, select an appropriate cell for the user
-----------------------------------------------------------------------------*/
- boxViewDidSwitch:sender
{
   [self selCell:self];
   return self;
}   

/*---------------------------------------------------------------------------
Select an appropriate cell for the user.  If the Name box is visibile
select the nameForm text.  Otherwise, select the the fieldForm which was
previously selected by the user.
-----------------------------------------------------------------------------*/
- selCell:sender
{
   id              aCell;

   if (strstr ([searchTypeButton title], "Name") != NULL)
      [nameForm selectTextAt:0];
   else {
      if ([myFieldForm cellCount] <= 0)
	 return self;

      aCell = [myFieldForm selectedCell];
      if (aCell == nil)
	 return self;

      [myFieldForm selectCell:aCell];
   }
   
   return self;
}

/*---------------------------------------------------------------------------
The user has grabbed the divider bar.  Enforce some sizing constraints.
-----------------------------------------------------------------------------*/
- splitView:sender getMinY:(NXCoord *) minY maxY:(NXCoord *) maxY ofSubviewAt:(int)offset
{
   *minY = MINBOXHEIGHT;

   return self;
}

/*---------------------------------------------------------------------------
If the splitView is going to resize, then try to do it in a sensible way.
The boxView width is allowed to change, but its height will remain constant.
The phoneView height & width will change to fit the remaining space.
-----------------------------------------------------------------------------*/
- splitView:sender resizeSubviews:(const NXSize *) oldSize
{
   int             bvInd;
   id              subViews;
   NXRect          bvRect;
   NXRect          pvRect;
   NXRect          splitRect;

   /*** There should be a boxView and a phoneView in this splitView. */

   subViews = [sender subviews];

   if ([subViews count] != 2) {
      fprintf (stderr, "%s: Number of Query SplitView subviews is %d.  Should be 2.\n",
	 [NXApp appName],[subViews count]);
      return self;
   }

   /*** bvInd will contain the index of the boxView object. */

   if ([subViews objectAt:1] == phoneView)
      bvInd = 0;
   else {
      if ([subViews objectAt:0] != phoneView) {
	 fprintf (stderr, "%s: Query SplitView does not have phoneView\n",
	    [NXApp appName],[subViews count]);
	 return self;
      }
      bvInd = 1;
   }

   [splitView getFrame:&splitRect];	/* Grab the new size of the splitView */

   /*** Set the boxView width to the same width as the splitView. */

   [[subViews objectAt:bvInd] getFrame:&bvRect];

   bvRect.origin.x = 0.0;
   bvRect.origin.y = 0.0;
   bvRect.size.width = splitRect.size.width;

   [[subViews objectAt:bvInd] setFrame:&bvRect];

   /*** Set the phoneView origin, height, & width so it fills the remainder
        of splitView. */

   [phoneView getFrame:&pvRect];

   pvRect.origin.x = 0.0;
   pvRect.origin.y = bvRect.size.height + [splitView dividerHeight];
   pvRect.size.width = splitRect.size.width;
   pvRect.size.height = splitRect.size.height - pvRect.origin.y;

   [phoneView setFrame:&pvRect];

   return self;
}

/*---------------------------------------------------------------------------
Each new Query window should be shifted a bit from the placement of the
previous Query window.  Otherwise, it is difficult to distinguish the new
window from previous ones.
-----------------------------------------------------------------------------*/
- shiftWindow:(float)anOffset
{
   return self;
}

/*---------------------------------------------------------------------------
Put the window on the screen.
-----------------------------------------------------------------------------*/
- showWindow:sender
{
   [nameForm selectTextAt:0];
   [queryWindow makeKeyAndOrderFront:self];

   return self;
}

/*---------------------------------------------------------------------------
Get rid of the Specify window if the query window goes away.
-----------------------------------------------------------------------------*/
- windowDidMiniaturize:sender
{
   [self hideSpecify:self];
   return self;
}

- windowWillClose:sender
{
   [self hideSpecify:self];
   return self;
}

/*---------------------------------------------------------------------------
Don't let the query window be too small.
-----------------------------------------------------------------------------*/
- windowWillResize:sender toSize:(NXSize *) frameSize
{
   if (frameSize -> width < MINQUERYWIDTH)
      frameSize -> width = MINQUERYWIDTH;

   if (frameSize -> height < MINQUERYHEIGHT)
      frameSize -> height = MINQUERYHEIGHT;

   return self;
}

/*---------------------------------------------------------------------------
Methods to return the requested data.
-----------------------------------------------------------------------------*/
- (const char *)getServerName
{
   return[qiObject server];
}

- (BOOL) isQueryReady
{
   return isQueryReady;
}

- qiObject
{
   return qiObject;
}

@end
