/*---------------------------------------------------------------------------
HelpServer.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.
  
HelpServer displays the names of the "nextph" and "native" help topics.
Upon the user's request, read the help data from the server and display
it in a ScrollView.  There is one HelpServer object per server.
  
Rex Pruess <Rex-Pruess@uiowa.edu>
  
$Header: /rpruess/apps/Ph/info.subproj/RCS/HelpServer.m,v 3.0 93/06/26 07:59:53 rpruess Exp $
-----------------------------------------------------------------------------
$Log:	HelpServer.m,v $
Revision 3.0  93/06/26  07:59:53  rpruess
Brought Ph code up to NeXTSTEP 3.0 specifications.

Revision 2.1  91/11/21  09:34:26  rpruess
The scrollview is cleared of old data before sending the next help request.
This gives the user a clear indication that work is in progress.  I also
added code to handle "click-happy" users.  Multiple clicks on the same
row are ignored.  The currently selected row is captured promptly and
used throughout to prevent synchronization problems if the user should be
clicking rapidly among rows.

Revision 2.0  91/11/18  17:48:26  rpruess
Revision 2.0 is the initial production release of Ph.

-----------------------------------------------------------------------------*/

#define HELPSTR "Help - "
#define HELPNATIVE "help native "
#define HELPNEXTPH "help nextph "

#define MAXHELPNUM 2

#define MINVIEWHEIGHT 40.0

#define MINWINHEIGHT 125.0
#define MINWINWIDTH 200.0

/* Application class header files */
#import "HelpServer.h"
#import "../qiServers.h"
#import "../PhShare.h"

@implementation HelpServer

/*---------------------------------------------------------------------------
Initialize variables to safe values.
-----------------------------------------------------------------------------*/
- init
{
   id              aCell;
   NXSize          cSize;
   NXRect          mRect;

   [super init];

   amBusy = NO;
   curHelpNum = 1;
   lineNum = 0;
   nativeRow = -1;
   nRows = 0;
   readingTopics = YES;
   selRow = -1;
   stripRtf = YES;
   
   server = NULL;
   
   [NXApp loadNibSection:"HelpServer.nib" owner:self withNames:NO];
   [helpServerWindow setMiniwindowIcon:"app"];

   /*** Create the Help matrix.  This matrix will hold the names of the
        "nextph" & "native" help topics. */

   mRect.origin.x = 0;
   mRect.origin.y = 0;
   [matrixScrollView getContentSize:&mRect.size];
   
   helpMatrix = [[Matrix alloc] initFrame:&mRect
      mode:NX_RADIOMODE
      cellClass:[SelectionCell class]
      numRows:0
      numCols:1];

   [helpMatrix setEmptySelectionEnabled:YES];
   [helpMatrix setAutosizing:NX_WIDTHSIZABLE];
   [helpMatrix setAutosizeCells:YES];

   [helpMatrix setTarget:self];
   [helpMatrix setAction:@selector(singleClick)];

   [helpMatrix getCellSize:&cSize];
   
   cSize.width = mRect.size.width;
   [helpMatrix setCellSize:&cSize];

   [[matrixScrollView setDocView:helpMatrix] free];
   [matrixScrollView setDocCursor:NXArrow];

   [helpMatrix addRow];
   nRows++;

   /*** Create the cellMsgFont.  It is similar to the Digital Librian style. 
        Set the first cell to the initial separator-message string. */

   cellMsgFont = [Font newFont:"Helvetica-Bold" size:12 style:0 matrix:NX_FLIPPEDMATRIX];

   aCell = [helpMatrix cellAt:0 :0];
   [aCell setStringValue:"\xB7\xB7\xB7 NeXT-Specific Topics \xB7\xB7\xB7"];
   [aCell setFont:cellMsgFont];
   [aCell setEnabled:NO];

   /*** Create & set the font for the dataScrollView document. */

   [self createFont:self];

   [helpMatrix sizeToCells];
   [helpMatrix display];

   /*** Add the matrix and data views into the SplitView. */

   [splitView addSubview:matrixScrollView];
   [splitView addSubview:dataScrollView];

   return self;
}

/*---------------------------------------------------------------------------
Set the dataScrollView document font based on the defaults data base values.
-----------------------------------------------------------------------------*/
- createFont:sender
{
   const char     *fontName;
   float           fontSize;

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

   [[dataScrollView docView] setFont:[Font newFont:fontName size:fontSize style:0 matrix:NX_FLIPPEDMATRIX]];

   return self;
}

/*---------------------------------------------------------------------------
Save the server's name and the Qi ID for this server.  The site name is
used to construct the Help window's title.  Send the help command to the
Qi object to find the various help topics provided by the server.
-----------------------------------------------------------------------------*/
- initData:(const char *)aServer site:(const char *)aSite qi:aQi
{
   char           *title;

   /*** Save the Qi ID & server name for later */

   qi = aQi;
   
   server = malloc (strlen (aServer) + 1);
   strcpy (server, aServer);

   /*** Set the window's title */

   title = malloc (strlen (HELPSTR) + strlen (aSite) + 1);
   strcpy (title, HELPSTR);
   strcat (title, aSite);
   
   [helpServerWindow setTitle:title];

   free (title);

   /*** Send the help command to the server. */

   [qi qiSend:"help nextph\n" delegate:self];

   return self;
}

/*---------------------------------------------------------------------------
The user has selected a help topic.  Build the Help command based on whether
it is a "nextph" or "native" help topic request.  Send the help command to the
Qi object.
-----------------------------------------------------------------------------*/
- singleClick
{
   const char     *topic;
   char           *command;
   const char     *helpCom;
   int             curSelRow;

   /*** Grab the currently selected row as soon as possible.  The user might
        be "click happy" and jumping around.  In fact, it is possible to
        end up without any row being selected.  So, if the user selected
        the same row multiple times (eg., double-click) or if no row is
        currently selected, then quietly exit. */  

   curSelRow = [helpMatrix selectedRow];
   
   if (selRow == curSelRow || curSelRow < 0)
      return self;

   /*** If we're busy fetching data for another help topic, then tell the
        user that we're busy. */

   if (amBusy) {
      NXRunAlertPanel (NULL, "Busy processing another help request.  Please wait.", NULL, NULL, NULL);
      [[helpMatrix selectCellAt:selRow :0] display];
      return self;
   }

   /*** All's well.  Mark that we are busy and save the row (selRow) for
        checking against future user clicks." */

   amBusy = YES;
   selRow = curSelRow;
   
   /*** Build the Help command. */

   if (nativeRow < 0 || selRow < nativeRow)
      helpCom = HELPNEXTPH;
   else
      helpCom = HELPNATIVE;

   topic = [[helpMatrix cellAt:selRow :0] stringValue];

   command = malloc (strlen (helpCom) + strlen (topic) + strlen (".rtf\n") + 1);
   strcpy (command, helpCom);

   strcat (command, topic);

   if (selRow < MAXTOPICS && isRtf[selRow])
      strcat (command, ".rtf");
   
   strcat (command, "\n");

   /*** Set flags, clear the scrollview, open the memory stream, send the
        command, & free storage. */

   lineNum = 0;
   stripRtf = YES;

   [self clearData:self];
   helpStream = NXOpenMemory (NULL, 0, NX_READWRITE);

   [qi qiSend:command delegate:self];

   free (command);
   
   return self;
}

/*---------------------------------------------------------------------------
Process the Server's output.  Initially, the output will be the names of
the help topics.  Later, the output will be actual data.  This behavior is
controlled elsewhere by the setting of the "readingTopics" variable.
-----------------------------------------------------------------------------*/
- qiOutput:(char *)aBuf
{
   int             theCode;

   theCode = atoi (aBuf);
   if (theCode == LR_NUMRET)
      return self;

   if (theCode >= LR_OK) {
      [self qiOutputDone:self];
      return self;
   }

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

      if (readingTopics)
	 [self processHelpLine:aBuf];
      else
	 [self processDataLine:aBuf];

   }
   
   return self;
}

/*---------------------------------------------------------------------------
This method is called after all the data has been received from the server.
-----------------------------------------------------------------------------*/
- qiOutputDone:sender
{
   const char     *cellStr;
   int             i;
   char           *topic;
   
   [qi stopFd:self];		/* Tell Qi object to stop watching the FD. */

   /*** If we were reading the Help topic names, then check for "rtf"
        topics, re-size the matrix, and display the window. */

   if (readingTopics) {
      readingTopics = NO;
   
      if (nRows == 1) {
	 [helpMatrix removeRowAt:0 andFree:YES];
	 [self outputErr:"Sorry, this server does not support the 'help nextph' or 'help native' commands."];
      }

      /*** If any topic is "rich text format", then strip off the ".rtf"
           suffix.  Set the "isRtf" flag. */

      for (i = 1; i < MIN (nRows, MAXTOPICS); i++) {
	 cellStr = [[helpMatrix cellAt:i :0] stringValue];

	 if (strlen (cellStr) <= 4 || strcmp (cellStr + strlen (cellStr) - 4, ".rtf") != 0)
	    isRtf[i] = NO;
	 else {
	    isRtf[i] = YES;

	    topic = malloc (strlen (cellStr) + 1);
	    strcpy (topic, cellStr);

	    topic[strlen(topic) - 4] = '\0';
	    [[helpMatrix cellAt:i :0] setStringValue:topic];

	    free (topic);
	 }
      }

      [helpMatrix sizeToCells];
      [helpMatrix display];

      [self showHelpWindow:self];
   }      
   else {
      [self outputStream:self];
      amBusy = NO;
   }
   
   return self;
}

/*---------------------------------------------------------------------------
Servers return the topic names in a painful fashion.  It would be nice if
the server had an option to supply just the topic names.  But, alas, it
was written for humans and that makes this coding messy.  The assumption
is that some lines must be ignored since they are just informative lines
for humans.  All other lines are assumed to contain the names of topics,
separated by tabs.  The topic names are sorted, but once again this is
messy.  Numeric topics (error info) are presented after the alphabetic
topics.
-----------------------------------------------------------------------------*/
- processHelpLine:(char *)aBuf
{ 
   id              aCell;
   BOOL            alphaTopic;
   char           *aPtr;
   const char     *cellStr;
   int             ind;
   int             theHelpNum;
   char           *topic;

   if (strstr (aBuf, "help topics") != NULL || strstr (aBuf, "these topics") != NULL)
      return self;
   
   if ((aPtr = strtok (aBuf, ":")) == NULL) /* Code */
      return self;
   
   if ((aPtr = strtok (NULL, ":")) == NULL) /* Field number */
      return self;

   /*** The assumption is made that "nextph" help is number "1" and
        "native" help is number "2". */

   theHelpNum = atoi (aPtr);

   if (theHelpNum > MAXHELPNUM)	/* This should never happen! */
      return self;

   /*** If this is the "native" help topic, then insert a nice header line in
        the matrix to distinguish it from the "nextph" topic.  If there was
        no "nextph" help, then that header line is discarded. */

   if (curHelpNum != theHelpNum) {
      curHelpNum = theHelpNum;
      
      if (nRows == 1) {		/* Were there any "nextph" lines? */
	 [helpMatrix removeRowAt:0 andFree:YES];
         nRows = 0;
      }
      
      [helpMatrix addRow];

      aCell = [helpMatrix cellAt:nRows :0];
      [aCell setStringValue:"\xB7\xB7\xB7 General Topics \xB7\xB7\xB7"];
      [aCell setFont:cellMsgFont];
      [aCell setEnabled:NO];

      nRows++;
      nativeRow = nRows;	/* Save row # where native help begins */
   }      

   /*** Extract each help topic from the line.  Numeric topics are stored
        after the alphabetic topics. */

   topic = strtok (NULL, " \t\n");
      
   while ( topic != NULL) {

      if (topic[0] >= '0' && topic[0] <= '9')
	 alphaTopic = NO;
      else
	 alphaTopic = YES;

      /*** If this entry is a numeric topic, then skip the alpha ones before
           inserting into the matrix. */

      ind = MAX (1, nativeRow);

      if (!alphaTopic) {
	 for (; ind < nRows; ind++) {
	    cellStr = [[helpMatrix cellAt:ind :0] stringValue];

	    if (cellStr[0] >= '0' && cellStr[1] <= '9')
	       break;
	 }
      }

      /*** Finally, insert the topic in sorted order from "ind". */

      for (; ind < nRows; ind++) {
	 cellStr = [[helpMatrix cellAt:ind :0] stringValue];
	 
	 if (alphaTopic && cellStr[0] >= '0' && cellStr[0] <= '9')
	    break;

         if (strcmp (topic, cellStr) < 0)
	    break;
      }

      [helpMatrix insertRowAt:ind];
      nRows++;
      
      [[helpMatrix cellAt:ind :0] setStringValue:topic];

      topic = strtok (NULL, " \t\n");
   }

   return self;
}  

/*---------------------------------------------------------------------------
The first line of the help data returned by the Qi server probably contains
the help topic name.  This line is discarded since it is obvious from the NeXT
Ph implementation what topic the user requested.  RTF data may a have blank
prepended to each line.  If so, strip them. ASCI data must be parsed to
discard any backspace characters & then display the data line.
-----------------------------------------------------------------------------*/
- processDataLine:(char *)aBuf
{
   char           *line;	/* Points to beginning of line */
   char           *nc;		/* Pointer to "new" location for char */
   char           *oc;		/* Pointer to "old" location of char */

   lineNum++;
   
   /*** Set pointers to the actual data. */

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

   if (line == NULL)		/* Should never happen */
      return self;
   
   line++;			/* Skip the ":" */

   /*** The first line is probably the file name.  As a very rough check,
        verify the selected cell's name is somewhere in the line.  If so,
        throw the line away.  Obviously, this is not a very thorough check. */

   if (lineNum == 1 && strstr (line, [[helpMatrix cellAt:selRow :0] stringValue]) != NULL)
      return self;

   /*** Most Qi servers (but not all) like to prepend each line with a blank.
        This really messes up RTF files.  So, if this topic is a RTF file,
        check the second line (which is the first data line) for a blank in
        column one.  If true, set the stripRtf flag to YES so the first
        character is removed from every line.  This is not a very robust
        check, but this is the best I can come up with for dealing with
        Qi servers which don't all seem to behave the same way. */

   if (isRtf[selRow]) {

      if (lineNum == 2) {
	 if (*line == ' ')
	    stripRtf = YES;
	 else
	    stripRtf = NO;
      }
      
      if (stripRtf)
	 line++;		/* Skip the first character (blank). */
   }
   else				
      if (index (line, '\b') != 0) {

	 /*** Strip out the backspaces & associated characters. */

	 nc = line;
	 for (oc = nc; *oc != '\0'; oc++) {
	    if (*oc == '\b') {
	       if (nc != line)
		  --nc;
	       continue;
	    }
	    
	    *nc++ = *oc;
	 }
      
	 *nc = '\0';
      }

   /*** Write the line to the memory stream. */

   NXPrintf (helpStream, "%s", line);
   
   return self;
}

/*---------------------------------------------------------------------------
Display an error line of output in the scrollview.
-----------------------------------------------------------------------------*/
- outputErr:(char *)aString
{
   int             length;
   id              dataText;

   dataText = [dataScrollView docView];

   length = [dataText textLength];

   [dataText setSel:length :length];
   [dataText replaceSel:aString];

   [dataText display];
   
   return self;
}

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

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

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

   [helpServerWindow disableFlushWindow];

   dataText = [dataScrollView docView];

   /*** Display the helpStream data to the user.  Display it as rich
        text if the help topic had the ".rtf" suffix. */

   if (selRow < MAXTOPICS && isRtf[selRow])
      [dataText readRichText:helpStream];
   else
      [dataText readText:helpStream];

   [dataText scrollPoint:&origin];

   [[helpServerWindow reenableFlushWindow] flushWindow];

   /*** Gotta free that memory. */

   NXCloseMemory (helpStream, NX_FREEBUFFER);

   return self;
}

/*---------------------------------------------------------------------------
Clear the buffer.  This is necessary for a couple of reasons.  First, it
gives the user a clear indication that work is under way for the new request.
Also, the "readText" method overwrites the buffer, but it does not erase old
attributes (eg., bold).  Failure to clear the buffer results in those
attributes carrying over when non-RTF files are displayed.
-----------------------------------------------------------------------------*/
- clearData:sender
{
   id              dataText;

   [helpServerWindow disableFlushWindow];

   dataText = [dataScrollView docView];
   [dataText setSel:0 :[dataText textLength]];
   [dataText replaceSel:""];
   
   [[helpServerWindow reenableFlushWindow] flushWindow];

   return self;
}

/*---------------------------------------------------------------------------
Show the help window.
-----------------------------------------------------------------------------*/
- showHelpWindow:sender
{
   [helpServerWindow makeKeyAndOrderFront:self];
   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 = MINVIEWHEIGHT;

   return self;
}

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

   NXRect          dvRect;
   NXRect          mvRect;
   NXRect          splitRect;

   /*** There should be a matrixScrollView and a dataScrollView in this splitView. */

   subViews = [sender subviews];

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

   /*** mvInd will contain the index of the matrixScrollView object. */

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

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

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

   [[subViews objectAt:mvInd] getFrame:&mvRect];

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

   [[subViews objectAt:mvInd] setFrame:&mvRect];

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

   [dataScrollView getFrame:&dvRect];

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

   [dataScrollView setFrame:&dvRect];

   return self;
}

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

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

   return self;
}

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

@end
