/* GNUPLOT - help.c */

/*[
 * Copyright 1986 - 1993, 1998, 2004   Thomas Williams, Colin Kelley
 *
 * Permission to use, copy, and distribute this software and its
 * documentation for any purpose with or without fee is hereby granted,
 * provided that the above copyright notice appear in all copies and
 * that both that copyright notice and this permission notice appear
 * in supporting documentation.
 *
 * Permission to modify the software is granted, but not the right to
 * distribute the complete modified source code.  Modifications are to
 * be distributed as patches to the released version.  Permission to
 * distribute binaries produced by compiling modified sources is granted,
 * provided you
 *   1. distribute the corresponding source modifications from the
 *    released version in the form of a patch file along with the binaries,
 *   2. add special version identification to distinguish your version
 *    in addition to the base release version number,
 *   3. provide your name and address as the primary contact for the
 *    support of your modified version, and
 *   4. retain our contact information in regard to use of the base
 *    software.
 * Permission to distribute the released version of the source code along
 * with corresponding source modifications in the form of a patch file is
 * granted with same provisions 2 through 4 for binary distributions.
 *
 * This software is provided "as is" without express or implied warranty
 * to the extent permitted by applicable law.
]*/

#include "help.h"

#include "alloc.h"
#include "util.h"

/*
 ** help -- help subsystem that understands defined keywords
 **
 ** Looks for the desired keyword in the help file at runtime, so you
 ** can give extra help or supply local customizations by merely editing
 ** the help file.
 **
 ** The original (single-file) idea and algorithm is by John D. Johnson,
 ** Hewlett-Packard Company.  Thanx and a tip of the Hatlo hat!
 **
 ** Much extension by David Kotz for use in gnutex, and then in gnuplot.
 ** Added output paging support, both unix and builtin. Rewrote completely
 ** to read helpfile into memory, avoiding reread of help file. 12/89.
 **
 ** Modified by Russell Lang to avoid reading completely into memory
 ** if DOS16 defined.  This uses much less memory.  6/91
 **
 ** The help file looks like this (the question marks are really in column 1):
 **
 **   ?topic
 **   This line is printed when the user wants help on "topic".
 **   ?keyword
 **   ?Keyword
 **   ?KEYWORD
 **   These lines will be printed on the screen if the user wanted
 **   help on "keyword", "Keyword", or "KEYWORD".  No casefolding is
 **   done on the keywords.
 **   ?subject
 **   ?alias
 **   This line is printed for help on "subject" and "alias".
 **   ?
 **   ??
 **   Since there is a null keyword for this line, this section
 **   is printed when the user wants general help (when a help
 **   keyword isn't given).  A command summary is usually here.
 **   Notice that the null keyword is equivalent to a "?" keyword
 **   here, because of the '?' and '??' topic lines above.
 **   If multiple keywords are given, the first is considered the
 **   'primary' keyword. This affects a listing of available topics.
 **   ?last-subject
 **   Note that help sections are terminated by the start of the next
 **   '?' entry or by EOF.  So you can't have a leading '?' on a line
 **   of any help section.  You can re-define the magic character to
 **   recognize in column 1, though, if '?' is too useful.  (Try ^A.)
 */

#define	KEYFLAG	'?'		/* leading char in help file topic lines */

/*
 ** Calling sequence:
 **   int result;             # 0 == success
 **   char *keyword;          # topic to give help on
 **   char *pathname;         # path of help file
 **   int subtopics;          # set to TRUE if only subtopics to be listed
 **                           # returns TRUE if subtopics were found
 **   result = help(keyword, pathname, &subtopics);
 ** Sample:
 **   cmd = "search\n";
 **   helpfile = "/usr/local/lib/program/program.help";
 **   subtopics = FALSE;
 **   if (help(cmd, helpfile, &subtopics) != H_FOUND)
 **           printf("Sorry, no help for %s", cmd);
 **
 **
 ** Speed this up by replacing the stdio calls with open/close/read/write.
 */
#ifdef WDLEN
# define PATHSIZE WDLEN
#else
# define PATHSIZE BUFSIZ
#endif

typedef struct line_s LINEBUF;
struct line_s {
    char *line;			/* the text of this line */
    LINEBUF *next;		/* the next line */
};

typedef struct linkey_s LINKEY;
struct linkey_s {
    char *key;			/* the name of this key */
    long pos;			/* ftell position */
    LINEBUF *text;		/* the text for this key */
    TBOOLEAN primary;		/* TRUE -> is a primary name for a text block */
    LINKEY *next;		/* the next key in linked list */
};

typedef struct key_s KEY;
struct key_s 
{
	char *key;			/* the name of this key */
	long pos;			/* ftell position */
	LINEBUF *text;		/* the text for this key */
	TBOOLEAN primary;		/* TRUE -> is a primary name for a text block */
};
static LINKEY *keylist = NULL;	/* linked list of keys */
static KEY *keys = NULL;	/* array of keys */
static int keycount = 0;	/* number of keys */
static FILE *helpfp = NULL;
static KEY empty_key = {NULL, 0, NULL, 0};

static int LoadHelp (char *path);
static void sortkeys (void);
int keycomp (SORTFUNC_ARGS a, SORTFUNC_ARGS b);
static LINEBUF *storeline (char *text);
static LINKEY *storekey (char *key);
static KEY *FindHelp (char *keyword);
static TBOOLEAN Ambiguous (struct key_s * key, size_t len);

/* Help output */
static void PrintHelp (struct key_s * key, TBOOLEAN *subtopics);
static void ShowSubtopics (struct key_s * key, TBOOLEAN *subtopics);

#if defined(PIPES)
static FILE *outfile;		/* for unix pager, if any */
#endif
static int pagelines;		/* count for builtin pager */
#define SCREENSIZE 24		/* lines on screen (most have at least 24) */

/* help:
 * print a help message
 * also print available subtopics, if subtopics is TRUE
 */
int
help(
    char *keyword,		/* on this topic */
    char *path,			/* from this file */
    TBOOLEAN *subtopics)	/* (in) - subtopics only? */
				/* (out) - are there subtopics? */
{
    static char oldpath[PATHSIZE] = "";		/* previous help file */
    int status;			/* result of LoadHelp */
    KEY *key;			/* key that matches keyword */

    /*
     ** Load the help file if necessary (say, first time we enter this routine,
     ** or if the help file changes from the last time we were called).
     ** Also may occur if in-memory copy was freed.
     ** Calling routine may access errno to determine cause of H_ERROR.
     */
    errno = 0;
    if (strncmp(oldpath, path, PATHSIZE) != 0)
	FreeHelp();
    if (keys == NULL) {
	status = LoadHelp(path);
	if (status == H_ERROR)
	    return (status);

	/* save the new path in oldpath */
	safe_strncpy(oldpath, path, PATHSIZE);
    }
    /* look for the keyword in the help file */
    key = FindHelp(keyword);
    if (key != NULL) {
	/* found the keyword: if help exists, print and return */
	if (key->text)
	    PrintHelp(key, subtopics);
	status = H_FOUND;
    } else {
	status = H_NOTFOUND;
    }

    return (status);
}

/* we only read the file once, into memory
 * except for DOS16 when we don't read all the file -
 * just the keys and location of the text
 */
static int
LoadHelp(char *path)
{
    LINKEY *key = 0;		/* this key */
    long pos = 0;		/* ftell location within help file */
    char buf[BUFSIZ];		/* line from help file */
    LINEBUF *head;		/* head of text list  */
    LINEBUF *firsthead = NULL;
    TBOOLEAN primary;		/* first ? line of a set is primary */
    TBOOLEAN flag;

    if ((helpfp = fopen(path, "r")) == NULL) {
	/* can't open help file, so error exit */
	return (H_ERROR);
    }
    /*
     ** The help file is open.  Look in there for the keyword.
     */
    if (!fgets(buf, BUFSIZ - 1, helpfp) || *buf != KEYFLAG)
	return (H_ERROR);	/* it is probably not the .gih file */

    while (!feof(helpfp)) {
	/*
	 ** Make an entry for each synonym keyword
	 */
	primary = TRUE;
	while (buf[0] == KEYFLAG) {
	    key = storekey(buf + 1);	/* store this key */
	    key->primary = primary;
	    key->text = NULL;	/* fill in with real value later */
	    key->pos = 0;	/* fill in with real value later */
	    primary = FALSE;
	    pos = ftell(helpfp);
	    if (fgets(buf, BUFSIZ - 1, helpfp) == (char *) NULL)
		break;
	}
	/*
	 ** Now store the text for this entry.
	 ** buf already contains the first line of text.
	 */
#ifndef DOS16
	firsthead = storeline(buf);
	head = firsthead;
#endif
	while ((fgets(buf, BUFSIZ - 1, helpfp) != (char *) NULL)
	       && (buf[0] != KEYFLAG)) {
#ifndef DOS16
	    /* save text line */
	    head->next = storeline(buf);
	    head = head->next;
#endif
	}
	/* make each synonym key point to the same text */
	do {
	    key->pos = pos;
	    key->text = firsthead;
	    flag = key->primary;
	    key = key->next;
	} while (flag != TRUE && key != NULL);
    }
#ifndef DOS16
    (void) fclose(helpfp);
#endif

    /* we sort the keys so we can use binary search later */
    sortkeys();
    return (H_FOUND);		/* ok */
}

/* make a new line buffer and save this string there */
static LINEBUF *storeline(char *text)
{
    LINEBUF *new_;

    new_ = (LINEBUF *) gp_alloc(sizeof(LINEBUF), "new line buffer");
    if (text)
	new_->line = gp_strdup(text);
    else
	new_->line = NULL;

    new_->next = NULL;

    return (new_);
}

/* Add this keyword to the keys list, with the given text */
static LINKEY *
storekey(char *key)
{
    LINKEY *new_;

    key[strlen(key) - 1] = NUL;	/* cut off \n  */

    new_ = (LINKEY *) gp_alloc(sizeof(LINKEY), "new key list");
    if (key)
	new_->key = gp_strdup(key);

    /* add to front of list */
    new_->next = keylist;
    keylist = new_;
    keycount++;
    return (new_);
}

/* we sort the keys so we can use binary search later */
/* We have a linked list of keys and the number.
 * to sort them we need an array, so we reform them into an array,
 * and then throw away the list.
 */
static void sortkeys()
{
    LINKEY *p, *n;		/* pointers to linked list */
    int i;			/* index into key array */

    /* allocate the array */
    keys = (KEY *) gp_alloc((keycount + 1) * sizeof(KEY), "key array");

    /* copy info from list to array, freeing list */
    for (p = keylist, i = 0; p != NULL; p = n, i++) {
	keys[i].key = p->key;
	keys[i].pos = p->pos;
	keys[i].text = p->text;
	keys[i].primary = p->primary;
	n = p->next;
	free((char *) p);
    }

    /* a null entry to terminate subtopic searches */
    keys[keycount].key = NULL;
    keys[keycount].pos = 0;
    keys[keycount].text = NULL;

    /* sort the array */
    /* note that it only moves objects of size (two pointers + long + int) */
    /* it moves no strings */
    /* HBB 20010720: removed superfluous, potentially dangerous casts */
    qsort(keys, keycount, sizeof(KEY), keycomp);
}

/* HBB 20010720: changed to make this match the prototype qsort()
 * really expects. Casting function pointers, as we did before, is
 * illegal! */
/* HBB 20010720: removed 'static' to avoid HP-sUX gcc bug */
int keycomp(SORTFUNC_ARGS arg1, SORTFUNC_ARGS arg2)
{
	const KEY *a = (const struct key_s *)arg1;
	const KEY *b = (const struct key_s *)arg2;
	
	return (strcmp(a->key, b->key));
}

/* Free the help file from memory. */
/* May be called externally if space is needed */
void FreeHelp()
{
    int i;			/* index into keys[] */
    LINEBUF *t, *next;

    if (keys == NULL)
	return;

    for (i = 0; i < keycount; i++) {
	free((char *) keys[i].key);
	if (keys[i].primary)	/* only try to release text once! */
	    for (t = keys[i].text; t != NULL; t = next) {
		free((char *) t->line);
		next = t->next;
		free((char *) t);
	    }
    }
    free((char *) keys);
    keys = NULL;
    keycount = 0;
#ifdef DOS16
    (void) fclose(helpfp);
#endif
}

/* FindHelp:
 *  Find the key that matches the keyword.
 *  The keys[] array is sorted by key.
 *  We could use a binary search, but a linear search will aid our
 *  attempt to allow abbreviations. We search for the first thing that
 *  matches all the text we're given. If not an exact match, then
 *  it is an abbreviated match, and there must be no other abbreviated
 *  matches -- for if there are, the abbreviation is ambiguous.
 *  We print the ambiguous matches in that case, and return not found.
 */
static KEY * /* NULL if not found */
FindHelp(char *keyword)		/* string we look for */
{
    KEY *key;
    size_t len = strcspn(keyword, " ");

    for (key = keys; key->key != NULL; key++) {
	if (!strncmp(keyword, key->key, len)) {  /* we have a match! */
	    if (!Ambiguous(key, len)) {
		size_t key_len = strlen(key->key);
		size_t keyword_len = strlen(keyword);

		if (key_len != len) {
		    /* Expand front portion of keyword */
		    int i, shift = key_len - len;

		    for (i=keyword_len+shift; i >= len; i--)
			keyword[i] = keyword[i-shift];
		    strncpy(keyword, key->key, key_len);  /* give back the full spelling */
		    len = key_len;
		    keyword_len += shift;
		}
		/* Check for another subword */
		if (keyword[len] == ' ') {
		    len = len + 1 + strcspn(keyword + len + 1, " ");
		    key--; /* start with current key */
		} else
		    return (key);  /* found!!  non-ambiguous abbreviation */
	    } else
		return (&empty_key);
	}
    }

    /* not found */
    return (NULL);
}

/* Ambiguous:
 * Check the key for ambiguity up to the given length.
 * It is ambiguous if it is not a complete string and there are other
 * keys following it with the same leading substring.
 */
static TBOOLEAN
Ambiguous(KEY *key, size_t len)
{
    char *first;
    char *prev;
    TBOOLEAN status = FALSE;	/* assume not ambiguous */
    int compare;
    size_t sublen;

    if (key->key[len] == NUL)
	return (FALSE);

    for (prev = first = key->key, compare = 0, key++;
	 key->key != NULL && compare == 0; key++) {
	compare = strncmp(first, key->key, len);
	if (compare == 0) {
	    /* So this key matches the first one, up to len.
	     * But is it different enough from the previous one
	     * to bother printing it as a separate choice?
	     */
	    sublen = strcspn(prev + len, " ");
	    if (strncmp(key->key, prev, len + sublen) != 0) {
		/* yup, this is different up to the next space */
		if (!status) {
		    /* first one we have printed is special */
		    fprintf(stderr,
			    "Ambiguous request '%.*s'; possible matches:\n",
			    (int)len, first);
		    fprintf(stderr, "\t%s\n", prev);
		    status = TRUE;
		}
		fprintf(stderr, "\t%s\n", key->key);
		prev = key->key;
	    }
	}
    }

    return (status);
}

/* PrintHelp:
 * print the text for key
 */
static void
PrintHelp(
    KEY *key,
    TBOOLEAN *subtopics)	/* (in) - subtopics only? */
				/* (out) - are there subtopics? */
{
    LINEBUF *t;
#ifdef DOS16
    char buf[BUFSIZ];		/* line from help file */
#endif

    StartOutput();

    if (subtopics == NULL || !*subtopics) {
#ifdef DOS16
	fseek(helpfp, key->pos, 0);
	while ((fgets(buf, BUFSIZ - 1, helpfp) != (char *) NULL)
	       && (buf[0] != KEYFLAG)) {
	    OutLine(buf);
	}
#else
	for (t = key->text; t != NULL; t = t->next)
	    OutLine(t->line);	/* print text line */
#endif
    }
    ShowSubtopics(key, subtopics);
    OutLine("\n");

    EndOutput();
}


/* ShowSubtopics:
 *  Print a list of subtopic names
 */
/* The maximum number of subtopics per line */
#define PER_LINE 4

static void
ShowSubtopics(
    KEY *key,			/* the topic */
    TBOOLEAN *subtopics)	/* (out) are there any subtopics */
{
    int subt = 0;		/* printed any subtopics yet? */
    KEY *subkey;		/* subtopic key */
    size_t len;			/* length of key name */
    char line[BUFSIZ];		/* subtopic output line */
    char *start;		/* position of subname in key name */
    size_t sublen;			/* length of subname */
    char *prev = NULL;		/* the last thing we put on the list */

#define MAXSTARTS 256
    int stopics = 0;		/* count of (and index to next) subtopic name */
    char *starts[MAXSTARTS];	/* saved positions of subnames */

    *line = NUL;
    len = strlen(key->key);

    for (subkey = key + 1; subkey->key != NULL; subkey++) {
	if (strncmp(subkey->key, key->key, len) == 0) {
	    /* find this subtopic name */
	    start = subkey->key + len;
	    if (len > 0) {
		if (*start == ' ')
		    start++;	/* skip space */
		else
		    break;	/* not the same topic after all  */
	    } else {
		/* here we are looking for main topics */
		if (!subkey->primary)
		    continue;	/* not a main topic */
	    }
	    sublen = strcspn(start, " ");
	    if (prev == NULL || strncmp(start, prev, sublen) != 0) {
		if (subt == 0) {
		    subt++;
		    if (len) {
			strcpy(line, "\nSubtopics available for ");
			strncat(line, key->key, BUFSIZ - 25 - 2 - 1);
			strcat(line, ":\n");
		    } else
			strcpy(line, "\nHelp topics available:\n");
		    OutLine(line);
		    *line = NUL;
		}
		starts[stopics++] = start;
		prev = start;
	    }
	} else {
	    /* new topic */
	    break;
	}
    }

/* The number of the first column for subtopic entries */
#define FIRSTCOL	4
/* Length of a subtopic entry; if COLLENGTH is exceeded,
 * the next column is skipped */
#define COLLENGTH	18

#ifndef COLUMN_HELP
    {
	/* sort subtopics by row - default */
	int subtopic;
	int spacelen = 0, ispacelen;
	int pos = 0;

	for (subtopic = 0; subtopic < stopics; subtopic++) {
	    start = starts[subtopic];
	    sublen = strcspn(start, " ");
	    if (pos == 0)
		spacelen = FIRSTCOL;
	    /* adapted by DvdSchaaf */
	    for (ispacelen = 0; ispacelen < spacelen; ispacelen++)
		(void) strcat(line, " ");
	    (void) strncat(line, start, sublen);

	    spacelen = COLLENGTH - sublen;
	    while (spacelen <= 0) {
		spacelen += COLLENGTH;
		pos++;
	    }

	    pos++;
	    if (pos >= PER_LINE) {
		(void) strcat(line, "\n");
		OutLine(line);
		*line = NUL;
		pos = 0;
	    }
	}

	/* put out the last line */
	if (subt > 0 && pos > 0) {
	    (void) strcat(line, "\n");
	    OutLine(line);
	}
    }
#else /* COLUMN_HELP */
    {
	/* sort subtopics by column */
	int subtopic, sublen;
	int spacelen = 0, ispacelen;
	int row, col;
	int rows = (int) (stopics / PER_LINE) + 1;

	for (row = 0; row < rows; row++) {
	    *line = NUL;
	    for (ispacelen = 0; ispacelen < FIRSTCOL; ispacelen++)
		(void) strcat(line, " ");

	    for (col = 0; col < PER_LINE; col++) {
		subtopic = row + rows * col;
		if (subtopic >= stopics) {
		    break;
		} else {
		    start = starts[subtopic];
		    sublen = strcspn(start, " ");
		    (void) strncat(line, start, sublen);
		    spacelen = COLLENGTH - sublen;
		    if (spacelen <= 0)
			spacelen = 1;
		    for (ispacelen = 0; ispacelen < spacelen; ispacelen++)
			(void) strcat(line, " ");
		}
	    }
	    (void) strcat(line, "\n");
	    OutLine(line);
	}
    }
#endif /* COLUMN_HELP */

    if (subtopics)
	*subtopics = (subt != 0);
}


/* StartOutput:
 * Open a file pointer to a pipe to user's $PAGER, if there is one,
 * otherwise use our own pager.
 */
void
StartOutput()
{
#if defined(PIPES)
    char *pager_name = getenv("PAGER");

    if (pager_name != NULL && *pager_name != NUL)
	if ((outfile = popen(pager_name, "w")) != (FILE *) NULL)
	    return;		/* success */
    outfile = stderr;
    /* fall through to built-in pager */
#endif

    /* built-in pager */
    pagelines = 0;
}


/* write a line of help output  */
/* line should contain only one \n, at the end */
void
OutLine(const char *line)
{
    int c;			/* dummy input char */
#if defined(PIPES)
    if (outfile != stderr) {
	fputs(line, outfile);
	return;
    }
#endif

    /* built-in dumb pager */
    /* leave room for prompt line */
    if (pagelines >= SCREENSIZE - 2) {
	fputs("Press return for more: ", stderr);
#if defined(ATARI) || defined(MTOS)
	do
	    c = tos_getch();
	while (c != '\x04' && c != '\r' && c != '\n');
#else
	do
	    c = getchar();
	while (c != EOF && c != '\n');
#endif
	pagelines = 0;
    }
    fputs(line, stderr);
    pagelines++;
}

void
EndOutput()
{
#if defined(PIPES)
    if (outfile != stderr)
	(void) pclose(outfile);
#endif
}
