/*
 * help.c  -  help file processing
 *
 * Copyright (C) 2004-2007 Gero Kuhlmann <gero@gkminix.han.de>
 *
 *  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 2 of the License, or
 *  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.
 *
 * $Id: help.c,v 1.7 2007/01/06 18:31:38 gkminix Exp $
 */

#define NEED_DIR 1
#include <common.h>
#include <nblib.h>
#include "privlib.h"



/*
 * Size of chunks for help file line buffer allocation
 */
#define HELP_CHUNK_SIZE	80



/*
 * Minimum size of read buffer
 */
#define MIN_BUF_SIZE	16



/*
 * Position of help topic within help file
 */
struct helptopic {
	int               id;		/* ID number of help topic */
	long              fpos;		/* position of topic in help file */
	struct helptopic *next;
};



/*
 * Variables used by the help file system
 */
static struct helptopic *helplist = NULL;	/* list of help topics */
static struct helptopic *curtopic;		/* current help topic */
static int readpos;				/* current read position */
static int isinit = FALSE;			/* initialization flag */
static int helpbufsize;				/* help read buffer size */
static char *helpbuf = NULL;			/* help read buffer */
static FILE *helpfile = NULL;			/* help file */




/*
 **************************************************************************
 *
 *			Private routines
 *
 **************************************************************************
 */

/*
 * Add a new character to the help read buffer
 */
static void addhelpbuf __F((c), char c)
{
  static int curpos;

  /* Create help read buffer if necessary */
  if (helpbuf == NULL) {
	helpbufsize = HELP_CHUNK_SIZE;
	helpbuf = (char *)nbmalloc(helpbufsize);
	curpos = 0;
  }

  /* Check if at end of line */
  if (c == '\0') {
	helpbuf[curpos] = '\0';
	curpos = 0;
	return;
  }

  /* Put next character into line buffer */
  if (curpos >= (helpbufsize - 1)) {
	helpbufsize += HELP_CHUNK_SIZE;
	helpbuf = (char *)nbrealloc(helpbuf, helpbufsize);
  }
  helpbuf[curpos++] = c;
}



/*
 * Read a line from the help file. Returns FALSE on EOF or error.
 */
static int readhline __F_NOARGS
{
  int c;

  /* Current read status */
  enum {
	start,			/* start reading */
	eolchar,		/* read eol character */
	readeol,		/* read until eol */
	readnormal,		/* read until special character */
	skipnormal,		/* skip rest of line */
	skipstart		/* skip rest of line */
  } status;

  /* Reset error code */
  nberror(0, NULL);

  /* Check if at end of file */
  if (helpfile == NULL || feof(helpfile))
	return(FALSE);

  /* Read the next line from the file */
  status = start;
  while (TRUE) {
	/* Get next character and check for error and EOF */
	if ((c = fgetc(helpfile)) == EOF) {
		if (!feof(helpfile)) {
			nberror(EXIT_HELP, "error reading from help file");
			return(FALSE);
		}
		break;
	}

	/* Check if previous character was end-of-line */
	if (status == eolchar || status == start) {
		if (c == '#') {
			if (status == start)
				status = skipstart;
			else
				status = skipnormal;
		} else if (c == '-' || c == '%' || c == '.') {
			if (status == start) {
				addhelpbuf(c);
				status = readeol;
			} else {
				ungetc(c, helpfile);
				break;
			}
		} else if (c != '\n') {
			if (status == eolchar)
				addhelpbuf(' ');
			if (c != ' ' && c != '\t')
				addhelpbuf(c);
			status = readnormal;
		}
		continue;
	}

	/* Skip until end-of-line */
	if (status == skipstart || status == skipnormal) {
		if (c == '\n') {
			if (status == skipstart)
				status = start;
			else
				status = eolchar;
		}
		continue;
	}

	/* Check for end-of-line */
	if (c == '\n') {
		if (status == readeol)
			break;
		status = eolchar;
		continue;
	}

	/* Put next character into line buffer */
	addhelpbuf(c);
  }

  /* Terminate the current input line */
  addhelpbuf('\0');
  return(TRUE);
}



/*
 * Close help file and cleanup
 */
static void cleanhelp __F_NOARGS
{
  struct helptopic *hp;

  if (helpfile != NULL) {
	(void)fclose(helpfile);
	helpfile = NULL;
  }
  if (helpbuf != NULL) {
	free(helpbuf);
	helpbuf = NULL;
  }
  while (helplist != NULL) {
	hp = helplist;
	helplist = hp->next;
	free(hp);
  }
}



/*
 * Initialize module
 */
static inline int initmodule __F_NOARGS
{
  if (!isinit) {
	if (nbatexit(&cleanhelp) != 0) {
		nberror(EXIT_INTERNAL, "unable to set help exit function");
		return(FALSE);
	}
	isinit = TRUE;
  }
  return(TRUE);
}




/*
 **************************************************************************
 *
 *			Public routines
 *
 **************************************************************************
 */


/*
 * Return next line of help topic. It returns -1 on error, 0 on EOF
 * and otherwise the buffer size including the terminating zero.
 */
int helpget __F((buf, bufsize), char *buf AND int bufsize)
{
  static int insert = 0;
  int cursize, oldinsert, len;
  char *cp;

  /* Check if topic is available */
  if (curtopic == NULL || bufsize < MIN_BUF_SIZE) {
	insert = 0;
	return(0);
  }

  /* Copy the read buffer contents into the output buffer */
  cursize = 0;
  while (cursize < (bufsize - 1)) {

	/* Insert leading spaces if requested */
	while (cursize < insert && cursize < (bufsize - 1)) {
		buf[cursize] = ' ';
		cursize++;
	}

	/* Get next line and process any special commands */
	if (readpos <= 0) {
		if (readpos < 0 || helpbuf == NULL || helpbuf[0] == '\0') {
			/* Empty line, get next one */
			if (!readhline()) {
				/* Check for error */
				if (nberrnum > 0)
					return(-1);
				/* No -> EOF */
				curtopic = NULL;
				readpos = -1;
				break;
			}
			readpos = 0;
			continue;
		}
		if (helpbuf[0] == '%') {
			/* End of topic */
			curtopic = NULL;
			readpos = -1;
			break;
		}
		if (helpbuf[0] == '-') {
			/* Paragraph break */
			if (cursize <= insert)
				readpos = -1;
			break;
		}
		if (helpbuf[0] == '.') {
			if (helpbuf[1] == 'P') {
				/* Paragraph break */
				if (cursize <= insert)
					readpos = -1;
				break;
			}
			oldinsert = insert;
			if (helpbuf[1] == 'I') {
				/* Insert leading spaces */
				if (insert + 8 + MIN_BUF_SIZE <= (bufsize - 1))
					insert += 8;
			}
			if (helpbuf[1] == 'E') {
				/* Remove leading spaces */
				if (insert >= 8)
					insert -= 8;
			}
			/* All special commands cause a line break */
			readpos = -1;
			if (cursize <= oldinsert) {
				if (cursize > insert)
					cursize = insert;
				continue;
			}
			break;
		}
	}

	/* Check if at end of line */
	if (helpbuf[readpos] == '\0') {
		readpos = -1;
		break;
	}

	/* Insert next tab character into output buffer */
	if (helpbuf[readpos] == '\t') {
		if (cursize > insert) {
			buf[cursize] = helpbuf[readpos];
			cursize++;
			readpos++;
		}
		continue;
	}

	/* Shrink multiple space characters into one */
	if (helpbuf[readpos] == ' ') {
		while (helpbuf[readpos] == ' ')
			readpos++;
		if (helpbuf[readpos] == '\0') {
			readpos = -1;
			break;
		}
		if (cursize > insert) {
			buf[cursize] = ' ';
			cursize++;
		}
		continue;
	}

	/* Insert next word into output buffer */
	cp = &(helpbuf[readpos]);
	while (*cp && *cp != ' ' && *cp != '\t')
		cp++;
	len = (int)(cp - &(helpbuf[readpos]));
	if ((cursize + len) > (bufsize - 1))
		break;
	while (&(helpbuf[readpos]) < cp) {
		buf[cursize] = helpbuf[readpos];
		cursize++;
		readpos++;
	}
  }
  if (cursize <= insert)
	cursize = 0;
  buf[cursize] = '\0';
  return(cursize + 1);
}



/*
 * Set help topic. Returns the topic ID, 0 if topic not found
 * or -1 in case of error.
 */
int helptopic __F((topic), int topic)
{
  struct helptopic *hp;

  /* Check if anything to do */
  curtopic = NULL;
  readpos = -1;
  if (helpfile == NULL)
	return(0);

  /* Find topic in list */
  hp = helplist;
  while (hp != NULL && hp->id != topic)
	hp = hp->next;

  /* Search topic in help file */
  if (hp != NULL) {
	if (fseek(helpfile, hp->fpos, 0) < 0) {
		nberror(EXIT_HELP, "error seeking in help file");
		return(-1);
	}
	curtopic = hp;
	return(hp->id);
  }
  return(0);
}



/*
 * Initialize help file. Returns FALSE in case of error.
 */
int helpinit __F((fname), const char *fname)
{
  struct helptopic *hp;
  char *line;
  int id, linenum;

  /* Initialize module */
  if (!initmodule())
	return(FALSE);

  /* Reset current help topic */
  curtopic = NULL;
  readpos = -1;

  /* Check if help file name specified, otherwise close everything */
  if (fname == NULL) {
	cleanhelp();
	return(TRUE);
  }

  /* Open help file for reading */
  assert(helpfile == NULL);
  if ((helpfile = fopen(fname, "r")) == NULL) {
	nberror(EXIT_HELP, "error opening help file %s", fname);
	return(FALSE);
  }

  /* Scan help file contents */
  linenum = 0;
  while (readhline()) {
	linenum++;
	line = helpbuf;
	if (*line == '%') {
		line++;
		while (*line == ' ' || *line == '\t')
			line++;
		if (sscanf(line, "%d", &id) != 1) {
			nberror(EXIT_HELP,
				"syntax error in help file at line %d",
				linenum);
			goto helpend;
		}
		if (id < 0) {
			nberror(EXIT_HELP,
				"invalid topic ID in help file at line %d",
				linenum);
			goto helpend;
		}
		if (id == 0)
			continue;
		hp = (struct helptopic *)nbmalloc(sizeof(struct helptopic));
		hp->id = id;
		hp->fpos = ftell(helpfile);
		hp->next = helplist;
		helplist = hp;
	} else if (*line == '.') {
		line++;
		switch (*line) {
			case 'B':
			case 'P':
			case 'I':
			case 'E':
				break;
			default:
				nberror(EXIT_HELP,
					"syntax error in help file at line %d",
					linenum);
				goto helpend;
		}
	}
  }

  /* Check for errors */
  if (nberrnum > 0) {
helpend:
	(void)fclose(helpfile);
	helpfile = NULL;
	return(FALSE);
  }

  /* Rewind the help file after reading */
  rewind(helpfile);
  return(TRUE);
}

