/*---------------------------------------------------------------------------
Qi.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.
  
There is one Qi object for each unique server.  Each Qi object keeps
track of the from & to file pointers used to talk to the server.  It
is also responsible for getting the "field" values from the server &
setting up QiField objects (one for each field).
  
Rex Pruess <Rex-Pruess@uiowa.edu>
  
$Header: /rpruess/apps/Ph/qiServers.subproj/RCS/Qi.m,v 3.0 93/06/26 07:44:45 rpruess Exp $
-----------------------------------------------------------------------------
$Log:	Qi.m,v $
Revision 3.0  93/06/26  07:44:45  rpruess
Brought Ph code up to NeXTSTEP 3.0 specifications.

Revision 2.2  91/12/15  15:52:33  rpruess
Added timer code to timeout inactive sockets.  The socket is automatically
re-created if the user makes a subsequent query request.

Revision 2.1  91/12/10  16:22:46  rpruess
Added code to suppress the display of the status panel for the default
query in certain situations.  If the user has requested the "hide default
query" preference, then the panel is not displayed.  Also, if Ph is
auto-launched & the user has requested "hide on auto-launch", the status
panel is not displayed.

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

-----------------------------------------------------------------------------*/
#define QUITSTR "quit\n"	/* Command to close the Qi connection */
#define TIMERMINS 15 		/* Animator wakeup interval (in minutes) */

/* C header files */
#include <netdb.h>
#include <sys/socket.h>
#include <netinet/in.h>

/* Application class header files */
#import "Qi.h"
#import "QiField.h"
#import "../PhShare.h"
#import "../info.h"

/* Qi Server header file (Qi return codes) */
#import "QiReplies.h"

/*---------------------------------------------------------------------------
The Qi private section is based somewhat on the Subprocess code found in the
/NextDeveloper/Examples directory.  This code handles all output from a Qi
server.  Each buffer of output is separated into lines before being
distributed to the delegate process.
-----------------------------------------------------------------------------*/
@implementation Qi (Private)
- fdHandler:(int)theFd
{
   int             bufferCount;	/* Number of bytes read */
   int             len;		/* Line length */
   char           *nl;		/* Pointer to '\n' in outputBuffer */
   char           *ob;		/* Pointer which moves thru outputBuffer */

   /*** If a socket error occurs, it will most likely first be detected
        here.  If so, close the files & send a dummy error line to the
        delegate process so it knows something is awry.  Otherwise, the
        delegate will wait forever for data that is never coming. */
   
   if (((bufferCount = read (theFd, outputBuffer, MAXSIZE - 1)) <= 0)) {

      strcpy (errMsg, "%s socket read error.  Try request again if you wish ");
      strcat (errMsg, "to re-establish the link.\n");
      strcat (errMsg, strerror (errno));

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

      [self closeFiles:self];

      strcpy (lineBuffer, "9999: Socket closed.  Try again if you wish to re-open it.\n");

      if (delegate && [delegate respondsTo:@selector (qiOutput:)])
	 [delegate perform:@selector (qiOutput:)
	    with :(void *)&lineBuffer];

      lineBuffer[0] = '\0';
      
      return self;
   }

   /* *** Normal processing passes through here.  Set the socketIO variable
          to indicate we've seen I/O. Then, terminate the output buffer
          properly. */

   socketIO = YES;
   outputBuffer[bufferCount] = '\0';
   ob = outputBuffer;

   /*** This loop moves through the output buffer and sends each line it
        finds to the delegate process. */

   while ((nl = index (ob, '\n')) != 0) {
      len = nl - ob + 1;
      strncat (lineBuffer, ob, MIN (len, MAXSIZE - strlen (lineBuffer) - 1));

      if (debug)
	 [self debugOut:lineBuffer toQi:NO];

      if (delegate && [delegate respondsTo:@selector (qiOutput:)])
	 [delegate perform:@selector (qiOutput:)
	    with :(void *)&lineBuffer];

      lineBuffer[0] = '\0';
      ob += len;
   }

   /*** The output buffer may still have a partial line in it.  If so,
        assign it to the lineBuffer so we don't lose it next time through. */

   if (strlen (ob) == 0)
      lineBuffer[0] = '\0';
   else
      strcpy (lineBuffer, ob);

   return self;
}

static void fdHandler (int theFd, id self)
{
   [self fdHandler:theFd];
}

@end

@implementation Qi

/*---------------------------------------------------------------------------
Initialize variables to safe values.
-----------------------------------------------------------------------------*/
- init
{
   [super init];
   [NXApp loadNibSection:"Qi.nib" owner:self withNames:NO];

   delegate = nil;
   hasQiFields = NO;
   hasClosed = NO;
   isTimerRunning = NO;
   nDefaults = 0;
   server = NULL;
   socketIO = NO;
   watchingFd = NO;

   sock = -1;

   lineBuffer[0] = '\0';
   outputBuffer[0] = '\0';

   fieldList = [[List alloc] init];

   if (strcmp (NXGetDefaultValue ([NXApp appName], DEBUG), "YES") == 0)
      debug = YES;
   else
      debug = NO;

   qiAnimator = [[Animator alloc] initChronon:TIMERMINS * 60
      adaptation:0.0
      target:self
      action:@selector (qiTimeCheck)
      autoStart:NO
      eventMask:0];

   return self;
}

/*---------------------------------------------------------------------------
Save the server name, try to open a socket, and send the "fields" command
to the server.
-----------------------------------------------------------------------------*/
- (BOOL) connectTo:(const char *)aServer
{
   server = malloc (strlen (aServer) + 1);
   strcpy (server, aServer);

   [self showStatusPanel:"Connecting to:" server:server];

   if ([self openSocket] == NO) {
      [self hideStatusPanel:self];
      return NO;
   }

   /*** Send the "fields" command to the server. */

   [self showStatusPanel:"Fetching fields from:" server:aServer];
   [self qiSend:"fields\n" delegate:self];

   return YES;
}

/*---------------------------------------------------------------------------
Set up the socket and try to connect to the server.
-----------------------------------------------------------------------------*/
- (BOOL)openSocket
{
   struct hostent *hp, *gethostbyname ();
   struct sockaddr_in qiSock;
   struct servent *theNs;

   /*** Create socket */

   sock = socket (PF_INET, SOCK_STREAM, 0);
   if (sock < 0) {
      strcpy (errMsg, "Trouble opening socket.\n");
      strcat (errMsg, strerror (errno));

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

      return NO;
   }

   /*** Find the proper port */

   qiSock.sin_family = AF_INET;

   if ((theNs = getservbyname (NSSERVICE, "tcp")) != NULL)
      qiSock.sin_port = theNs -> s_port;
   else
      qiSock.sin_port = htons (DEFAULTPORT);

   /*** Connect socket to Qi server */

   hp = gethostbyname (server);
   if (hp == 0) {
      strcpy (errMsg, "Unable to get network host entry for '%s'.\n");
      strcat (errMsg, strerror (errno));

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

      return NO;
   }
   bcopy (hp -> h_addr, &qiSock.sin_addr, hp -> h_length);

   if (connect (sock, (struct sockaddr *) & qiSock, sizeof (qiSock)) < 0) {
      strcpy (errMsg, "Trouble connecting to socket.\n");
      strcat (errMsg, strerror (errno));

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

      return NO;
   }

   return YES;
}

/*---------------------------------------------------------------------------
This method is set to be Qi's delegate.  It processes one line of Qi output
at a time.  This output is the result of the "fields" inquiry.  Upon end
of output, it is responsible for telling Qi to stop.
-----------------------------------------------------------------------------*/
- qiOutput:(char *)aBuf
{
   char           *aPtr;
   int             fieldNum;
   char           *fieldName;
   int             ind;
   int             theCode;
   id              theQiField;

   theQiField = nil;

   theCode = atoi (aBuf);

   if (theCode >= LR_OK) {

      [self hideStatusPanel:self];
      [self stopFd:self];

      if (theCode == LR_OK || theCode == LR_RONLY) {
	 hasQiFields = YES;
	 return self;
      }

      strcpy (errMsg, "%s server error.\n");
      strncat (errMsg, aBuf, MIN (strlen (aBuf), sizeof (errMsg) - strlen (errMsg) - 1));
      
      NXRunAlertPanel (NULL, errMsg, NULL, NULL, NULL, server);

      [self stopFd:self];
      [self closeFiles:self];

      return self;
   }

   if (theCode != -LR_OK)
      return self;

   if ((aPtr = strtok (aBuf, ":")) == NULL)	/* Code */
      return self;

   if ((aPtr = strtok (NULL, ":")) == NULL)	/* Field number */
      return self;

   fieldNum = atoi (aPtr);

   if ((fieldName = strtok (NULL, ":")) == NULL)	/* Field name */
      return self;

   if ((aPtr = strtok (NULL, ":")) == NULL)	/* keywords or description */
      return self;

   /*** If we have this field in the fieldList, then this line must be 
        its description. */

   for (ind = 0; ind < [fieldList count]; ind++) {

      if ([[fieldList objectAt:ind] number] == fieldNum) {

	 [[fieldList objectAt:ind] setDescription:aPtr];

	 return self;
	 }

   }

   /*** If we get here, it is a brand new field.  If it is a default field,
        then it is inserted after the last default entry.  Non-default fields
        are inserted alphabetically after the last default field (per Steve
        Dorner's recommendation.) */

   theQiField = [[QiField alloc] init];
   [theQiField setNumNameKeys:fieldNum name:fieldName keys:aPtr];

   if (strstr (aPtr, "Default") != NULL)
      ind = nDefaults++;
   else {		
      for (ind = nDefaults; ind < [fieldList count]; ind++) {
	 if (strcmp (fieldName,[[fieldList objectAt:ind] name]) < 0)
	    break;
      }
   }
   
   [fieldList insertObject:theQiField at:ind];

   return self;
} 

/*---------------------------------------------------------------------------
Send a command to the Qi server.
-----------------------------------------------------------------------------*/
- qiSend:(const char *)aCommand delegate:aDelegate
{
   const char     *aPtr;
   int             bytes;
   int             i;
   BOOL            isQuitCommand;

   /*** Set the "isQuitCommand" flag appropriately. */

   if (strcmp (aCommand, QUITSTR) == 0)
      isQuitCommand = YES;
   else
      isQuitCommand = NO;
   
   /*** If the socket has been closed, then we will silently ('cept for
        debugging fprint message) try to rebind it. */

   if (hasClosed) {
      if (isQuitCommand)
	 return self;
      
      if (debug)
	 printf ("%s: Binding new socket with %s.\n", [NXApp appName], server);

      if ([self openSocket] == NO)
	 return self;

      hasClosed = NO;
      [self stopFd:self];
   }

   /*** If we are waiting on output for some delegate, then abort this request. */

   if (watchingFd && aDelegate != nil) {
      strcpy (errMsg, "Output is pending from a previous command.  Please try later.");
      NXRunAlertPanel (NULL, errMsg, NULL, NULL, NULL);
      return self;
   }

   /*** All's well so far.  If debugging is enabled, show the command to
        the user too. */

   if (debug)
      [self debugOut:aCommand toQi:YES];

   /*** Send the command to the server.  If the write fails, try to
        re-bind the socket and send it again.  */

   if ((bytes = write (sock, aCommand, strlen (aCommand))) < 0) {

      if (isQuitCommand)
	 return self;

      [self closeFiles:self];

      fprintf (stderr, "%s: Re-establishing socket with %s.\n", [NXApp appName], server);

      if ([self openSocket] == NO)
	 return self;

      hasClosed = NO;

      if ((bytes = write (sock, aCommand, strlen (aCommand))) < 0) {
	 strcpy (errMsg, "%s socket write error.  Aborting write to server.\n");
	 strcat (errMsg, strerror (errno));
	 
	 NXRunAlertPanel (NULL, errMsg, NULL, NULL, NULL, server);
	 return self;
      }
   }

   /*** Start watching the file descriptor (except when sending the
        "quit" command). */

   if (!isQuitCommand)
      [self startFd:aDelegate];

   /*** I doubt the following "if" code will ever be executed.  It is possible
        the "write" will not send all of the command.  In such a case, it
        is necessary to loop until all of the command has been sent.  Quit
        when all data has been sent, if an error is encountered, or after
        a few iterations. This is not elegant, but should suffice. */

   if (bytes < strlen (aCommand)) {

      aPtr = aCommand + bytes;

      for (i = 1; i <= 5; i++) {
	 if ((bytes = write (sock, aPtr, strlen (aPtr))) < 0)
	     break;

	 if (bytes >= strlen (aPtr))
	    break;

	 aPtr += bytes;
	 sleep (1);
      }
      
   }

   /*** If the timer is not running, then start it (except when sending
        the "quit" command).  The timer will periodically check to see if
        the connection should be closed due to inactivity.  socketIO is
        the boolean flag used to indicate there has been activity. */

   if (!isTimerRunning && !isQuitCommand)
      [self startQiTimer:self];

   socketIO = YES;
   
   return self;
}

/*---------------------------------------------------------------------------
Print a debugging line of output.
-----------------------------------------------------------------------------*/
- debugOut:(const char *)aLine toQi:(BOOL) aCommand
{
   printf ("%s: Server=%s, %s=%s",[NXApp appName], server,
	   (aCommand ? "Command" : "Data"), aLine);

   fflush (stdout);

   return self;
}

/*---------------------------------------------------------------------------
This method is closed during normal shutdown or when a socket error occurs.
-----------------------------------------------------------------------------*/
- closeFiles:sender
{
   close (sock);
   hasClosed = YES;

   if (isTimerRunning)
      [self stopQiTimer:self];
   
   return self;
}

/*---------------------------------------------------------------------------
Start watching the file descriptor.
-----------------------------------------------------------------------------*/
- startFd:aDelegate
{
   delegate = aDelegate;

   if (!watchingFd)
      DPSAddFD (sock, (DPSFDProc) fdHandler, (id) self, NX_MODALRESPTHRESHOLD + 1);

   watchingFd = YES;
   
   return self;
}

/*---------------------------------------------------------------------------
Stop watching the file descriptor.  Setting the delegate to nil is necessary
to allow any future sends.
-----------------------------------------------------------------------------*/
- stopFd:sender
{
   delegate = nil;

   if (watchingFd)
      DPSRemoveFD (sock);

   watchingFd = NO;
   
   return self;
}

/*---------------------------------------------------------------------------
Tell the server to quit.  Stop watching the file descriptor, close files,
and stop the timer.
-----------------------------------------------------------------------------*/
- quit
{
   [self qiSend:QUITSTR delegate:nil];
   
   [self stopFd:self];
   [self closeFiles:self];
   
   return self;
}

/*---------------------------------------------------------------------------
Periodically, check to see if there has been any activity on the socket.
If not, close the connection.
-----------------------------------------------------------------------------*/
- qiTimeCheck
{
   if (socketIO)
      socketIO = NO;
   else
      [self quit];
   
   return self;
}

/*---------------------------------------------------------------------------
Start the timer.
-----------------------------------------------------------------------------*/
- startQiTimer:sender
{
   isTimerRunning = YES;
   [qiAnimator startEntry];

   return self;
}

/*---------------------------------------------------------------------------
Stop the timer.
-----------------------------------------------------------------------------*/
- stopQiTimer:sender
{
   isTimerRunning = NO;
   [qiAnimator stopEntry];
   
   return self;
}

/*---------------------------------------------------------------------------
Hide the status window.
-----------------------------------------------------------------------------*/
- hideStatusPanel:sender
{
   [statusPanel orderOut:self];
   return self;
}

/*---------------------------------------------------------------------------
Insert the name of the server in the text field & display the panel.
The status panel is not displayed for the default nameserver if Ph has
been auto-launched or if the user has requested the default Query window
be hidden.
-----------------------------------------------------------------------------*/
- showStatusPanel:(const char *)aString server:(const char *)aServer
{
   const char     *defServer;   /* Default CSO Nameserver */

   /*** Get the default nameserver name & see if we should display the panel. */

   defServer = NXGetDefaultValue ([NXApp appName], DEFAULTSERVER);

   if (defServer != NULL && strcmp (defServer, aServer) == 0) {

      if (strcmp (NXGetDefaultValue ([NXApp appName], "NXAutoLaunch"), "YES") == 0)
	 return self;

      if (strcmp (NXGetDefaultValue ([NXApp appName], HIDEQUERY), "YES") == 0)
	 return self;
      
   }

   /*** Guess, we gotta display the panel. */

   [msgTextField setStringValue:aString];
   [serverTextField setStringValue:aServer];

   [statusPanel makeKeyAndOrderFront:self];

   return self;
}

/*---------------------------------------------------------------------------
Methods to return the requested data.
-----------------------------------------------------------------------------*/
- fieldList
{
   return fieldList;
}

- (BOOL) hasQiFields
{
   return hasQiFields;
}

- (BOOL) hasClosed
{
   return hasClosed;
}

- (const char *)server
{
   return server;
}

@end
