/*---------------------------------------------------------------------------
Ph.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.
  
Ph (and associated routines) is a phones client interface to the query
interpreter server.  This routine starts everything on its way.  Most of
the Ph work is done by other routines.
  
Rex Pruess <Rex-Pruess@uiowa.edu>
  
$Header: /rpruess/apps/Ph/RCS/Ph.m,v 3.0 93/06/26 07:41:54 rpruess Exp $
-----------------------------------------------------------------------------
$Log:	Ph.m,v $
Revision 3.0  93/06/26  07:41:54  rpruess
Brought Ph code up to NeXTSTEP 3.0 specifications.

Revision 2.2  92/04/27  13:54:21  rpruess
Added code to provide Query Ph services to other applications.

Revision 2.1  91/12/10  16:17:25  rpruess
Added Speaker/Listener support prior to sending the first production
release to the archive sites.  The first Ph production release will
be 2.0.  So, the RCS revision numbers do NOT necessarily match the
Ph Version numbers.

Revision 2.0  91/11/18  17:40:13  rpruess
Revision 2.0 is the initial production release.

-----------------------------------------------------------------------------*/
#if 0				/* "1" for beta release; "0" for production */
#define BETA			/* Define BETA if this is a beta release */
#define BETAEXPIREDAY 336	/* Day of year when program expires */
#define BETAEXPIREYEAR 1991     /* Year when program expires */
#endif

#define TIMERSECS 0.5

#define MAXCOMLEN 4096
#define MAXSERVERLEN 128
#define MAXSITELEN 128

/* C header files */
#include <netdb.h>

/* Application class header files */
#import "Ph.h"
#import "PhShare.h"
#import "PhListener.h"

#import "info.h"
#import "qiServers.h"
#import "query.h"

@implementation Ph

/*---------------------------------------------------------------------------
Override the initialize method so the defaults can be registered.
-----------------------------------------------------------------------------*/
+ initialize
{
   static NXDefaultsVector PhDefaults =
   {
      {DAYOFYEAR, "0"},
      {DEBUG, "NO"},
      {DEFAULTSERVER, NULL},
      {FONT, "Ohlfs"},
      {FONTSIZE, "12"},
      {HIDELAUNCH, "NO"},
      {HIDEQUERY, "NO"},
      {WRAPLINES, "NO"},
      {NULL}
   };

   NXRegisterDefaults ([NXApp appName], PhDefaults);

   return self;
}

/*---------------------------------------------------------------------------
Initialize some key variables.
-----------------------------------------------------------------------------*/
- init
{
   infoPanel = nil;
   helpIntroPanel = nil;
   helpServerMgr = nil;
   phAnimator = nil;
   prefs = nil;
   speakerQueryId = nil;

   defaultServer = NULL;
   unhiddenOnce = NO;
   speakerCommand = NULL;
   speakerServer = NULL;
   speakerSite = NULL;

   return self;
}

/*---------------------------------------------------------------------------
Create Ph's listener object, set delegates, and check the listener in.
-----------------------------------------------------------------------------*/
- appWillInit:sender
{
   id              myListener;

   myListener = [[PhListener alloc] init];
   [[myListener setDelegate:self] setServicesDelegate:self];
   [myListener checkInAs:[NXApp appName]];
   [myListener addPort];

   return self;
}

/*---------------------------------------------------------------------------
The application is on its way to life.  Initialize variables, create the
manager objects which will be used by other objects, etc.
-----------------------------------------------------------------------------*/
- appDidInit:sender
{
   /*** Disable the Servers menu.  It will be enabled by the Servers object
        as soon as it has fetched the names of the other nameservers. */

   [serversMenuCell setEnabled:NO];

   /*** Verify the expiration date is still in the future. */

#ifdef BETA
      [self checkExpiration:self];
#endif

   /*** Begin initializing the various objects */

   qiManager = [[QiManager alloc] init];
   queryManager = [[QueryManager alloc] init];

   servers = [[Servers alloc] init];
   [servers initIDs:serversMenuCell queryManager:queryManager qiManager:qiManager];

   [queryManager initIDs:servers qiManager:qiManager];
   
   /*** If there is no default Server, then alert the user & fire up
        Preferences so one can be assigned. */

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

   if (defaultServer == NULL || strlen (defaultServer) == 0) {
      strcpy (errMsg, "You must provide the hostname of a CSO Nameserver.  ");
      strcat (errMsg, "For example, the University of Iowa's Nameserver is ");
      strcat (errMsg, "ns.uiowa.edu.  The Preferences window will be displayed ");
      strcat (errMsg, "so you can set the default Nameserver.");
      
      NXRunAlertPanel (NULL, errMsg, NULL, NULL, NULL);

      [self showPrefs:self];
      return self;
   }

   /*** Check the /etc/services file for a valid 'ns' entry.  This check is
        purposely done after checking for a default Nameserver.  If there
        is not a default Nameserver, then the user will not see this alert
        panel during their first session.  It is not wise to scare new users
        with too many alert panels on their maiden voyage. */

   if (getservbyname (NSSERVICE, "tcp") == NULL) {
      strcpy (errMsg, "There is no entry for 'ns' in your Netinfo services directory.  ");
      strcat (errMsg, "Please refer to Help for more information.  ");
      strcat (errMsg, "The %s default port of %d will be used.");

      NXRunAlertPanel (NULL, errMsg, NULL, NULL, NULL, [NXApp appName], DEFAULTPORT);
   }

   /*** If we get here, then we did find a default Nameserver.  Tell the
        Servers object to fetch the other server names.  The Servers object
        should fire up the default Query window. */

   [servers fetchServers:defaultServer];

   /*** If this is an auto-launch & the user has requested it to hide, then
        tell Ph to hide. */

   if (strcmp (NXGetDefaultValue ([NXApp appName], "NXAutoLaunch"), "YES") == 0 &&
       strcmp (NXGetDefaultValue ([NXApp appName], HIDELAUNCH), "YES") == 0)
      [NXApp hide:self];
   
   return self;
}

/*---------------------------------------------------------------------------
If Ph was auto launched from the dock, then we must be sure that it has
been initialized when the user brings it to life for real.
-----------------------------------------------------------------------------*/
- appDidUnhide:sender
{
   if (unhiddenOnce)
      return self;
   
   unhiddenOnce = YES;

   /*** We must take into account a few Preferences combinations.  If the
        user has asked that the Default Query window be hidden, then we
        just get out of here. */

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

   /*** If the user requested that Ph "hide on auto-launch", then display
        the Query window since Ph has unhidden for the first time. */

   if (strcmp (NXGetDefaultValue ([NXApp appName], "NXAutoLaunch"), "YES") == 0 &&
       strcmp (NXGetDefaultValue ([NXApp appName], HIDELAUNCH), "YES") == 0) {

      if (defaultServer != NULL)
	 [queryManager startQuery:defaultServer];

   }
   
   return self;
}

/*---------------------------------------------------------------------------
This section of code is used for beta releases of this software.  It
will exit if the expiration date has passed.  Otherwise, it displays an
alert panel once a week informing the user of how many days are left in
the beta test period.
-----------------------------------------------------------------------------*/
- checkExpiration:sender
{
#ifdef BETA
   int             doy;		/* Day of the year when Ph last run*/
   int             expire;	/* # of days before expiration */
   char            string[4];
   struct timeval  timep;
   struct timezone timezonep;
   struct tm      *tmp;

   /*** Get the current date and convert it to a useable form. */

   gettimeofday (&timep, &timezonep);
   tmp = localtime (&timep.tv_sec);

   /** Calculate number of days until expiration. */

   if (tmp -> tm_year + 1900 > BETAEXPIREYEAR)
       expire = -1;
   else
      expire = BETAEXPIREDAY - tmp -> tm_yday;

   /*** If expired, alert the user and exit. */

   if (expire <= 0) {
      strcpy (errMsg, "Sorry.  This beta release expired on day %d in the year %d.");
      NXRunAlertPanel (NULL, errMsg, NULL, NULL, NULL, BETAEXPIREDAY, BETAEXPIREYEAR);
      [NXApp terminate:self];
   }

   /*** If we haven't warned the user for a few days, then do so again. */

   doy = atoi (NXGetDefaultValue ([NXApp appName], DAYOFYEAR));

   if (tmp -> tm_yday - doy >= 7) {
      sprintf (string, "%3d", tmp -> tm_yday);
      
      NXWriteDefault ([NXApp appName], DAYOFYEAR, string);
      
      strcpy (errMsg, "This beta release expires in %d days.\n");
      strcat (errMsg, "Direct comments to Rex-Pruess@uiowa.edu.");
      NXRunAlertPanel (NULL, errMsg, NULL, NULL, NULL, BETAEXPIREDAY - tmp -> tm_yday);
   }
#endif      

   return self;
}

/*---------------------------------------------------------------------------
The user clicked on the clear buffer menu item.
-----------------------------------------------------------------------------*/
- clearBuffer:sender
{
   [queryManager clearQueryView:self];

   return self;
}

/*---------------------------------------------------------------------------
The user clicked on the clear fields menu item.
-----------------------------------------------------------------------------*/
- clearFields:sender
{
   [queryManager clearQueryFields:self];

   return self;
}

/*---------------------------------------------------------------------------
The user clicked on the Help>Server menu selection.
-----------------------------------------------------------------------------*/
- showHelpServer:sender
{
   const char     *serverName;
   const char     *winTitle;
   
   winTitle = [[NXApp keyWindow] title];
   serverName = [servers getServerName:winTitle];
   
   if (serverName == NULL) {
      strcpy (errMsg, "The key window is not a Query window.  Click ");
      strcat (errMsg, "in a Query window before clicking 'Server...' in the ");
      strcat (errMsg, "Help menu.");

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

      return self;
   }
   
   if (helpServerMgr == nil)
      helpServerMgr = [[HelpServerMgr alloc] init];

   [helpServerMgr showServerHelp:serverName site:winTitle qi:[qiManager open:serverName]];
   
   return self;
}

/*---------------------------------------------------------------------------
This method is invoked if there is no defaults Nameserver in the defaults
data base.  It is also invoked when the user clicked on the preferences menu
selection.
-----------------------------------------------------------------------------*/
- showPrefs:sender
{
   if (prefs == nil) {
      prefs = [[Prefs alloc] init];
      [prefs setServers:servers];
   }

   [prefs showPrefsWindow:self];

   return self;
}

/*---------------------------------------------------------------------------
The user clicked on the info menu selection so bring the info panel to life.
-----------------------------------------------------------------------------*/
- showInfo:sender
{
   if (infoPanel == nil)
      infoPanel = [[Info alloc] init];

   [infoPanel showInfoPanel:self];

   return self;
}

/*---------------------------------------------------------------------------
Show the Servers window.
-----------------------------------------------------------------------------*/
- showServers:sender
{
   [servers showServersWindow:self];
   
   return self;
}

/*---------------------------------------------------------------------------
Order all windows out; this is necessary to prevent the windows from
"blinking" when the application terminates.  (There must be a better way to
do this.)  Prior to returning, send a message to the qiManager telling it to
send a "quit" command to all of the servers.
-----------------------------------------------------------------------------*/
- appWillTerminate:sender
{
   int             i;
   id              wList;
   
   /*** To prevent blinking windows, order all windows out. */

   wList = [NXApp windowList];

   for (i = 0; i < [wList count]; i++)
      [[wList objectAt:i] orderOut:self];
   

   /*** The qiManager will send a "quit" to all of the servers. */

   [qiManager quitServers];

   return self;
}

/*---------------------------------------------------------------------------
This method processes "Services" requests.  The userData variable contains
the value of the "User Data:" field in the services file; it must agree
with the name of the default server's name.  The services file needs to
be tailored on a site-by-site basis.
-----------------------------------------------------------------------------*/
- phQueryService:(id)pb userData:(const char *)userData error:(char **)msg
{
   char           *data;
   int             length;
   int             okay;
   char           *phCmd;
   char *const    *s;
   const char *const *types;

   okay = TRUE;

   types = [pb types];
   
   for (s = types; *s; s++) {
      if (!strcmp (*s, NXAsciiPboardType)) 
	 break;
   }
   
   if (*s && [pb readType:NXAsciiPboardType data:&data length:&length]) {

      phCmd = malloc (strlen (userData) + 1 + length + 1);

      strcpy (phCmd, userData);
      strcat (phCmd, "=");
      strncat (phCmd, data, length);

      [self queryServer:(char *)defaultServer site:(char *)defaultServer command:phCmd ok:&okay];

      free (phCmd);
      [pb deallocatePasteboardData:data length:length];
   }
   
   return self;
}

/*---------------------------------------------------------------------------
These methods are used for support of the Speaker/Listener code.  They are
invoked by other applications wishing to hide or unhide Ph.
-----------------------------------------------------------------------------*/
- (int) hide
{
   [NXApp hide:self];
   return 0;
}

- (int) unhide
{
   [NXApp unhide:self];
   return 0;
}

/*---------------------------------------------------------------------------
This method is for support of the Speaker/Listener code.  It is invoked by
another application requesting to add a new server to the Servers menu.
-----------------------------------------------------------------------------*/
- (int) addServer:(char *)aServer site:(char *)aSite ok:(int *)flag
{
   /*** If Ph has not received the server names from the default nameserver,
        then exit.  Don't do a thing until that is complete. */

   *flag = NO;
   
   if (![serversMenuCell isEnabled])
      return 0;

   if (aServer == NULL || strlen (aServer) <= 0 || strlen (aServer) > MAXSERVERLEN)
      return 0;
   
   if (aSite == NULL || strlen (aSite) <= 0 || strlen (aSite) > MAXSITELEN)
      return 0;
   
   /*** Ph is alive & well.  Send the data back to the caller. */

   *flag = YES;
   [servers addServerSite:aServer site:aSite];
   
   return 0;
}

/*---------------------------------------------------------------------------
This method is for support of the Speaker/Listener code.  It is invoked by
another application to obtain the names of the Ph Nameservers.  Note: The
output character variables (serverNames & siteNames) must be a non-NULL
pointer or Ph will abort when the PhListener tries to resolve the return
addresses.  I fought with this glitch for a while!
-----------------------------------------------------------------------------*/
- (int) getServers:(char **)serverNames sites:(char **)siteNames ok:(int *)flag
{
   /*** If Ph has not received the server names from the default nameserver,
        then return to the caller.  The caller can decide how long to wait
	before trying again. */
   
   *flag = NO;

   if (![serversMenuCell isEnabled]) {
      *serverNames = *siteNames = malloc (1);
      *serverNames[0] = '\0';
      return 0;
   }

   /*** Ph is alive & well.  Send the data back to the caller. */

   *flag = YES;
   [servers getServersAndSites:serverNames sites:siteNames];
   
   return 0;
}

/*---------------------------------------------------------------------------
This method is for support of the Speaker/Listener code.  It is invoked by
another application which wishes to issue a query command for a specific
server.
-----------------------------------------------------------------------------*/
- (int) queryServer:(char *)aServer site:(char *)aSite command:(char *)aCommand ok:(int *)flag
{
   /*** Don't allow a new request if we're processing an old Speaker
        request.  The timer is only smart enough to handle one at a time.
	Do some primitive validity checks. */

   *flag = NO;
   
   if (speakerServer != NULL)
      return 0;

   if (aServer == NULL || strlen (aServer) <= 0 || strlen (aServer) > MAXSERVERLEN)
      return 0;

   /*** Accept the request.  This does not mean it will really work though. */

   *flag = YES;

   /*** Oh darn.  We gotta do some real work.  Ph could be in one of
        many states at this point.  In the worst case, Ph is just coming
        to life & is finding info from its default nameserver.  In the
        best case, it already has a Query object for this server.  In any
        event, save the speaker's server/site/command data because we're
        gonna have to use a timer.  The speakerQueryId is used to sync
        inside of the phTimeCheck code. */

   speakerQueryId = nil;
   
   speakerServer = malloc (strlen (aServer) + 1);
   strcpy (speakerServer, aServer);
   
   if (aSite == NULL || strlen (aSite) <= 0 || strlen (aSite) > MAXSITELEN)
      speakerSite = NULL;
   else {
      speakerSite = malloc (strlen (aSite) + 1);
      strcpy (speakerSite, aSite);
   }

   if (aCommand == NULL || strlen (aCommand) <= 0 || strlen (aCommand) > MAXCOMLEN)
      speakerCommand = NULL;
   else {
      speakerCommand = malloc (strlen (aCommand) + 1);
      strcpy (speakerCommand, aCommand);
   }

   if (phAnimator == nil)
      phAnimator = [[Animator alloc] initChronon:TIMERSECS
	 adaptation:0.0
         target:self
         action:@selector (phTimeCheck)
         autoStart:NO
         eventMask:0];

   [self startPhTimer:self];
   
   return 0;
}

/*---------------------------------------------------------------------------
This method is for support of the Speaker/Listener code.  It is invoked by
another application which wishes to fire up a Query window for a specific
server.  This is really a simplified case of the queryServer method so
defer to that method for the work.
-----------------------------------------------------------------------------*/
- (int) showServer:(char *)aServer site:(char *)aSite ok:(int *)flag
{
   [self queryServer:aServer site:aSite command:NULL ok:flag];
   return 0;
}

/*---------------------------------------------------------------------------
The timer routines are needed for the speaker/listener support.  Ph can
be in one of many states when the speaker shouts at it.  It might be
just coming to life or hiding or busy with another speaker request or ...
-----------------------------------------------------------------------------*/
- phTimeCheck
{
   /*** If Ph has not received the server names from the default nameserver,
        then exit.  Don't do a thing until that is complete. */

   if (![serversMenuCell isEnabled])
      return self;
   
   /*** If there is no site name for the Speaker's server request, then 
        add this server/site into Ph's list. */

   if ([servers getSiteName:speakerServer] == NULL)
      [servers addServerSite:speakerServer site:speakerSite];

   /*** Only bother the queryManager once for any given request.  Once the
        queryManager has received the request, it will then be able
        to return the Query ID associated with that server.  If it is
        nil, then we're in trouble so we just give up. */

   if (speakerQueryId == nil)
      [queryManager startQuery:speakerServer];
   
   speakerQueryId = [queryManager getQueryId:speakerServer];

   if (speakerQueryId == nil) {
      [self stopPhTimer:self];
      fprintf (stderr, "%s: speakerQuery ID is nil for %s.\n", [NXApp appName], speakerServer);
      return self;
   }

   if (speakerCommand == NULL) {
      [self stopPhTimer:self];
      return self;
   }
      
   /*** The Query ID is ready as soon as it has received all of its "fields'
        from its server & built its "field" matrix.  At that point, it is 
        ready to perform searches. */
        
   if ([speakerQueryId isQueryReady] == YES) {
      [speakerQueryId speakerSearch:speakerCommand];
      [self stopPhTimer:self];
   }
   
   return self;
}

- startPhTimer:sender
{
   [phAnimator startEntry];
   return self;
}

- stopPhTimer:sender
{
   [phAnimator stopEntry];

   /*** Reset old Speaker variables.  New Speaker requests check for a NULL
        speakerServer value so reset it last. */

   speakerQueryId = nil;
   
   if (speakerCommand != NULL) {
      free (speakerCommand);
      speakerCommand = NULL;
   }
   
   if (speakerSite != NULL) {
      free (speakerSite);
      speakerSite = NULL;
   }
   
   if (speakerServer != NULL) {
      free (speakerServer);
      speakerServer = NULL;
   }
   
   return self;
}
   
@end
