/* 
 * EXTRACT.C
 * 
 * Documentation extractor.  Extracts tagged comment blocks from source
 * code, interprets and reformats the tag definitions, and outputs an
 * intermediate level 2 tag file, suitable for processing by a final
 * formatting tool to coerce the level 2 tags into something appropriate
 * for the presentation medium (paper, WinHelp RTF, Ventura, etc).
 * 
 */

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <assert.h>
#include "extract.h"
#include "tags.h"
#include "version.h"
#if MMWIN
#include <mmsysver.h>
#endif

/* Whether to do any output at all?  */
BOOL	fNoOutput	= False;
/*  The output file to use if not stdout */
PSTR	szOutputFile	= NULL;
/*  The actual output file pointer  */
FILE	*fpOutput;

/*
 *  File-private procedure templates
 */
void ProcessSourceFile( NPSourceFile sf );
void AppendLineToBuf(NPSourceFile sf, PSTR buf);
BOOL LookForCommentStart(NPSourceFile sf, PSTR buf, PSTR *nbuf);
BOOL IsTag(PSTR p);
BOOL PrepLine( NPSourceFile sf, PSTR buf, PSTR *nbuf );

/*
 *  User messages
 */
char msgStdin[] = "Using Standard Input for source text...\n";
char msgCurFile[] = "Processing file %s...\n";
char msgSyntaxCheck[] = "Syntax check only.\n";

char msgTypeMASM[] = "%s (%d): File is MASM source.\n";
char msgTypeC[] = "%s (%d): File is C source.\n";

char errOutputFile[] = "%s: Can not open output file\n";
char errInputFile[] = "%s: Can not open file.\n";
char errEOFinComment[] = "%s (%d): Premature end of file within comment block.\n";
char errRead[] = "%s (%d): Unable to read.\n";


/* 
 * @doc EXTRACT
 * 
 * @func int | main | This program extracts documentation information 
 * from the given input file and sends it to the standard output.  
 * Information is not sorted or formatted, but parsed from the
 * initial tag types to an intermediate tag output format that contains
 * full information as to tag placement within documentation/function
 * declarations.
 * 
 * @rdesc The return value is zero if there are no errors, otherwise the
 * return value is a non-zero error code.
 * 
 */
void main(argc, argv)
int argc;	/* Specifies the number of arguments. */
char *argv[];	/* Specifies an array of pointers to the arguments */
{
	SourceFile	sourceBuf;
	FileEntry	fileEntry;
	BOOL		fStdin = False;

	#define INITIAL_BUF	8192
	
#ifdef MMWIN
	/* announce our existance */
	fprintf(stderr, "%s\n", VERSIONNAME);
	fprintf(stderr, "Program Version %d.%d.%d\t%s\n", rmj, rmm, rup,
		MMSYSVERSIONSTR);
#ifdef DEBUG
	fprintf(stderr, "Compiled: %s %s by %s\n", __DATE__, __TIME__,
		szVerUser);
	fDebug = 1;
#endif
#endif

	ParseArgs(argc, argv);

	if (fNoOutput) {
	  fprintf(stderr, msgSyntaxCheck);
	  szOutputFile == NULL;
	}
	else {
	   /*  Open the output file, if one was specified.  If !szOutputFile,
	    *  then use stdout.
	    */
	   if (szOutputFile) {
	     fpOutput = fopen(szOutputFile, "w");
	     if (fpOutput == NULL) {
		fprintf(stderr, errOutputFile, szOutputFile);
		exit(1);
	     }
	   }
	   else {		/* Using stdout for output */
	     fpOutput = stdout;
	     szOutputFile = StringAlloc("stdout");
	   }
	   
	   OutputFileHeader(fpOutput);
	}

	/*  If no files were specified on command line, use stdin.
	 *  Fake a fileEntry structure for stdin.
	 */
	if (FilesToProcess == NULL) {
		/* No files specified, use stdin */
		fileEntry.filename = StringAlloc("stdin");
		fileEntry.next = NULL;
		fileEntry.type = SRC_UNKNOWN;
		FilesToProcess = &fileEntry;
		fStdin = True;
	}

	/*
	 *  Loop over all files specified on command line
	 */
	while (FilesToProcess) {
		/*
		 *  Setup the source file access buffer
		 */
		sourceBuf.fileEntry = FilesToProcess;	// get head of list.
	
		/*  Open the file, except when using stdin */
		if (fStdin) {
			sourceBuf.fp = stdin;
			fprintf(stderr, msgStdin);
		}
		else {	// deal with normal file, need to open it.
			sourceBuf.fp = fopen(FilesToProcess->filename, "r");
			/* couldn't open file */
			if (!sourceBuf.fp) {
			  fprintf(stderr, errInputFile,
				  FilesToProcess->filename);
			  /* Skip to next file in list */
			  FilesToProcess = FilesToProcess->next;
			  continue;
			}
			
			/* Send message telling current file */
			fprintf(stderr, msgCurFile, FilesToProcess->filename);
		}

		/* Reset line numbers of input files to zero */
		sourceBuf.wLineNo = 0;
		sourceBuf.wLineBuf = 0;
		/* Setup copy buffer */
		sourceBuf.lpbuf = NearMalloc(INITIAL_BUF, False);
		sourceBuf.pt = sourceBuf.mark = sourceBuf.lpbuf;
		sourceBuf.fHasTags = sourceBuf.fTag = False;
		sourceBuf.fExitAfter = FALSE;
		
		ProcessSourceFile( &sourceBuf );

		if (!fStdin)
			fclose(sourceBuf.fp);
		NearFree(sourceBuf.lpbuf);
		NearFree(FilesToProcess->filename);
		FilesToProcess = FilesToProcess->next;
		/*
		 * Bail out with non-zero exit if fatal error encountered
		 */
		if (sourceBuf.fExitAfter) {
			fcloseall();
			exit(1);
		}
	}

	/*
	 *  Close output file if not stdout.
	 */
	fcloseall();
	exit(0);
}



/* 
 * @doc	EXTRACT
 * @api	void | ProcessSourceFile | Process a given file, searching
 * for and extracting doc tagged comment blocks and processing and
 * outputting these comment blocks.
 * 
 * @parm	NPSourceFile | sf | Specifies the source file comment block.
 * It must have a valid file pointer, and a valid buffer (lpbuf field)
 * before calling this function.  The file pointer will be open upon
 * return. 
 * 
 * @comm	This proc sits in a loop reading lines until it finds a
 * comment.  Once inside a comment, the lines are stripped of fuzz
 * pretty printing characters and examined for being an autodoc tagged
 * line.  If a tag is found in the comment block, the following comment
 * lines are copied into the lpbuf buffer of <p sf>, and passed to the
 * <f TagProcessBuffer> function to parse and output the tags.
 * 
 */
#define LOCALBUF_SIZE	1024

void ProcessSourceFile( NPSourceFile sf )
{
  char	*buf;
  char	*pOrigBuf;
  char	*nBuf, *nBuf2;
  int	inComment;
  int	w;
  
  inComment = False;
  pOrigBuf = NearMalloc(LOCALBUF_SIZE, False);
  buf = pOrigBuf + 1;	// give one space of padding at beginning
  
  while (!feof(sf->fp)) {
    /*
     *  Grab the next line
     */
    #ifdef HEAPDEBUG
	NearHeapCheck();
    #endif
	    
    w = (int) fgets(buf, LOCALBUF_SIZE, sf->fp);

    #ifdef HEAPDEBUG
	NearHeapCheck();
    #endif
    
    /*  Handle error or EOF conditions  */
    if (w == 0) {
      /*  Am i at EOF?  */
      if (feof(sf->fp)) {
	      /* Message is EOF happened while in a comment block */
	      if (inComment) {
		      /* MASM comment blocks can end on EOF,
		       * so go handle it if in a masm file.
		       */
		      if (sf->fileEntry->type == SRC_MASM) {
			      if (sf->fTag)
				      /*  This is BOGUS!! */
				      TagProcessBuffer(sf);
		      }
		      else {	// premature eof otherwise
			      fprintf(stderr, errEOFinComment,
				      sf->fileEntry->filename, sf->wLineNo);
		      }
	      }
	      /* Cause the enclosing while loop to exit on EOF */
	      continue;
      }
      else {	// error condition, bail out!
	      fprintf(stderr, errRead, sf->fileEntry->filename, sf->wLineNo);
	      goto BailOut;
      }
    }
    else {
      /* 
       * Process this line - depending on current mode:
       *
       * -- CommentSearch mode:  inComment = False
       * Not currently in a comment, looking for comment begin
       * characters.  If commentBegin found, enter InsideComment
       * mode to look for end of comment and prep lines for
       * output processing.
       * 
       * -- InsideComment mode:  inComment = True
       * Inside a comment block, taking each line, stripping beginning
       * whitespace, and appending to global buffer for output
       * processing.  When end of comment is found, send the entire
       * buffer for tag processing.  (only if there was a tag
       * detected!).  Enter CommentSearch mode.
       * 
       */
      sf->wLineNo++;		// line count for file - now current line no.

      /* 
       * I'm in InsideComment mode, so process the next line as a comment
       * line.  The magic is in PrepLine(), which strips whitespace, sets the
       * fTag flag of the sourceBuf if a tag is detected, and returns TRUE
       * when end of comment is detected.
       * 
       */
      if (inComment) {
	w = PrepLine(sf, buf, &nBuf);
	AppendLineToBuf(sf, nBuf);
	if (w) {	// detected end of comment, exit in comment state
	  if (sf->fTag) {	// a tag was in the current buffer
		TagProcessBuffer(sf);
	  }
		
	  /* Go back to comment-search mode */
	  inComment = False;
	  
	}
      }
      /* 
       * Otherwise, I'm in CommentSearch mode, looking for a comment begin.
       * LookForCommentStart() returns TRUE when a comment start is detected.
       * It also fiddles <buf> so that the beginning of <buf> now points to
       * the character following the comment start.
       * 
       * Pass to PrepLine() to detect an immediate comment close, and then
       * add this initial line to the global buffer after reseting buffer
       * status.
       * 
       * Enter InsideComment mode.
       */
      else {		// not in a comment buffer
	if (LookForCommentStart(sf, buf, &nBuf)) {
	  // dprintf("Entering InsideComment mode, point is %d\n",
	  //	  (int) (sf->pt - sf->lpbuf));
		
	  /* Reset source file buffer status */
	  sf->fTag = sf->fHasTags = False;
	  sf->wLineBuf = sf->wLineNo;
	  sf->pt = sf->mark = sf->lpbuf;
	  
	  /* Check for immediate comment close */
	  if (PrepLine(sf, nBuf, &nBuf2)) {
		  assert(sf->fTag == False);
		  continue;		// detected immediate end comment
	  }

	  AppendLineToBuf(sf, nBuf2);

	  /*  Enter InsideComment mode  */
	  inComment = True;
	}
	/* else, no comment start found, continue scan */
      }  // endof CommentSearch mode stuff.
    }/* else not a string read error */
  } /* file-level while loop */

BailOut:

  NearFree(pOrigBuf);
	  
}


#define ISSPACE(c) ((c) == ' ' || (c) == '\t')

/* 
 * @doc	EXTRACT
 * @api	BOOL | PrepLine | Prepares an InsideComment mode line,
 * stripping off initial whitespace and fuzz characters, and detecting
 * end of comment conditions.
 * 
 * @parm	NPSourceFile | sf | Pointer to source file status buffer.
 * @parm	PSTR | buf | Pointer to beginning of source text line, as
 * read from the source file.
 * @parm	PSTR * | nbuf | Pointer to a char pointer, which is altered
 * to point the post-processed and stripped beginning of the line upon
 * procedure exit.
 * 
 * @rdesc	Returns TRUE when end of comment is encountered.  In this
 * case, the end of comment characters are not included in the return
 * string.  Returns FALSE when no end of comment is detected.
 * 
 * The char pointer pointed to by the <p nbuf> parameter is altered to
 * point to the new (post-processed and stripped) beginning of the line.
 * This new beginning is the beginning of the text of interest, having
 * had all comment leader characters and whitespace stripped off.  NULL
 * is an acceptable string to return, which will simply add nothing to
 * the tag buffer.  If a blank line is encountered, (ie simply a
 * newline), then the newline should be returned.
 * 
 * If a tag is detected on the line, then the <p sf->fTag> flag is set
 * to True to indicate that this is a valid tagged comment block.
 * 
 * @comm	This procedure does the stripping of language specific fuzz
 * characters into a simple text block.  The setting of <p sf->fTag> is
 * critical, and may be accomplished by calling the <f IsTag> procedure when
 * the tag should appear within the source line.
 * 
 */
BOOL PrepLine( NPSourceFile sf, PSTR buf, PSTR *nbuf )
{
  PSTR	chClose;
  PSTR	pend;
  
  /* Scan forward, removing initial whitespace */
  for (; *buf && ISSPACE(*buf); buf++);
  
  /* I never have to deal with begin comment processing, this is done
   * by the LookForCommentStart() proc.  In C, PrepLine() is invoked on
   * the char following the '/ *'.  In MASM, the ';' is left in.
   */
  
  switch (sf->fileEntry->type) {
    case SRC_MASM:

	/*  End of comment check:  If this first character (after whitespace
	 *  stripped out) is not a ';', then this is the end of the comment
	 *  block.  Return TRUE to indicate this.
	 */
	if (*buf && *buf != ';') {
		*buf = '\0';
		*nbuf = buf;
		return True;
	}

	/* strip contiguous ';' and '*', followed by whitespace */	    
	for (; *buf && (*buf == ';' || *buf == '*'); buf++);
	for (; *buf && ISSPACE(*buf); buf++);
	if (IsTag(buf)) {
		sf->fTag = True;
		*nbuf = buf;
	}
	else {
		/* HACK!
		 * If first char is a @ (and not a tag), pad with a space
		 */
		if (*buf == TAG) {
			*(--buf) = ' ';
		}
		*nbuf = buf;
	}
	
	/* Very hack way of kicking out extra comments */
	if ((buf = strstr(buf, "//")) != NULL)
		*buf = '\0';
	
	return False;

    case SRC_C:
	/* Remove leading stars */
	for (; *buf && *buf == '*'; buf++);
	/* Quick check for close comment - */
	if (*buf && *buf == '/') {
		*buf = '\0';
		*nbuf = buf;
		return True;
	}
	
	/* Otherwise, remove whitespace between the '*' and the text */
	for (; *buf && ISSPACE(*buf); buf++);
	/* Check for a tag here */
	if (IsTag(buf))
		sf->fTag = True;
	else {
		/*  If not tag but a @ on first char of line  */
		if (*buf == TAG) {
			buf--;	// can do this since buf is padded by one
			*buf = ' ';
		}
	}
	
	/* Implement the comment scheme of Rick's request */
	if ((pend = strstr(buf, "//")) != NULL)
		*pend = '\0';
	
	/* And if the line hasn't ended, search line for a close comment */
	chClose = strstr(buf, "*/");
	if (chClose) {
		/* found end of comment, NULL this spot, and return from func
		 * with TRUE, with nbuf pointing the beginning of non-white
		 * space text above
		 */
		*nbuf = buf;
		*chClose = '\0';
		return True;
	}
	
	/* Otherwise, found no end of comment on this line, so simply
	 * return whole line
	 */
	*nbuf = buf;
	return False;
	
    default:
	// dprintf("Invalid source type in PrepLine()!\n");
	assert(False);
	exit(5);
	
  }  /* switch */
	
}


/*
 * @doc	EXTRACT
 * @api	BOOL | IsTag | Perform a quick and dirty check to see if the
 * word pointed to by <p p> is a tag.
 * 
 * @parm	PSTR | p | Buffer, queued to the start of a word/tag.  If
 * this is a possible tag, then it must point to the initial '@'
 * character.
 * 
 * @rdesc	Returns TRUE if this is probably a tag, or FALSE otherwise.
 * 
 * @comm	This is a hack test, but works 99.9% of the time.
 * 
 */
BOOL IsTag(PSTR p)
{
  PSTR	pbegin;
  
  pbegin = p;
  
  if (*p != TAG)
	  return False;
  
  /*  For this procedure, allow newline as a whitespace delimeter */

  /* Skip to next whitespace */
  for (; *p && !(ISSPACE(*p) || *p == '\n'); p++);

  /* This is a test for a tag, but if the first char was
   * a '@' and there is a space following the word, then I'm going to
   * say it is a tag.
   */
  if (*p && (p > pbegin + 1) && (ISSPACE(*p) || *p == '\n'))
	  return True;

  return False;
}


/* 
 * @doc	EXTRACT
 * @api	BOOL | LookForCommentStart | Search a source line for comment
 * start characters.
 * 
 * @parm	NPSourceFile | sf | Pointer to the source file block
 * structure.
 * @parm	PSTR | buf | Pointer to beginning of source text file line to
 * examine.
 * @parm	PSTR * | nbuf | Pointer to a pointer that is modified to
 * indicate the beginning of the true source text line if a comment
 * block begin is found.
 * 
 * @rdesc	Returns False if no comment start characters are found.
 * Returns True if a comment start is found.  If True is returned,
 * <p *nbuf> will point to the start of the source text line as it
 * should be passed to <f AppendLineToBuf>.
 * 
 * This examination method for determining start of comment depends on
 * the source file type (as obtained from the fileEntry.type field of
 * <p sf>).  Unknown file types are examined and placed into one of the
 * other known source types as soon as distinguishing characters are
 * found.  (ie if '/ *' is found in an unknown, the file is marked as C
 * source file the remainder of file processing.  Note that this can
 * cause unknown file types to be incorrectly processed.)
 * 
 */
BOOL LookForCommentStart(NPSourceFile sf, PSTR buf, PSTR *nbuf)
{

  /*  Skip leading whitespace  */
  for (; *buf && ISSPACE(*buf); buf++);
  
  if (!*buf)
	  return False;
  
  switch (sf->fileEntry->type) {
    case SRC_C:
	if (!*(buf + 1))
		return False;
	if ((*buf == '/') && (*(buf+1) == '*')) {
		*nbuf = buf+2;
		return True;
	}
	break;

    case SRC_MASM:
	if (*buf == ';') {
		*nbuf = buf;
		return True;
	}
	break;
	
    /*
     *  The catch all.  This has serious potential for disaster!
     */
    case SRC_UNKNOWN:
	/* Try the MASM comment character */
	if (*buf == ';') {
		fprintf(stderr, msgTypeMASM, 
			sf->fileEntry->filename, sf->wLineNo);
		sf->fileEntry->type = SRC_MASM;
		*nbuf = buf;
		return True;
	}

	/* Otherwise, try the C-method */
	if (!*(buf + 1))
		return False;
	if ((*buf == '/') && (*(buf+1) == '*')) {
		fprintf(stderr, msgTypeC,
			sf->fileEntry->filename, sf->wLineNo);
		sf->fileEntry->type = SRC_C;
		*nbuf = buf+2;
		return True;
	}
	break;

    default:
	// dprintf("Unknown filetype identifier in sourceFile buffer.\n");
	assert(False);
	
  }
  
  return False;

}

/* 
 * @doc	EXTRACT
 * @api	void | AppendLineToBuf | Appends an stripped comment line the
 * comment buffer contained in <p sf>.
 * 
 * @parm	NPSourceFile | sf | Source file buffer block pointer.
 * Contains the buffer that is appended to.
 * @parm	PSTR | buf | Pointer to NULL terminated line to add to the
 * comment buffer.
 * 
 * @comm	Appends <p buf> to the comment buffer, contained in the lpbuf
 * field of <p sf>.  The current point in the comment buffer, (given by
 * the pt field of <p sf>) is advanced to the end of the appended
 * string. 
 * 
 */
void AppendLineToBuf(NPSourceFile sf, PSTR buf)
{
  int	size;
  PSTR	ch;
  PSTR	end;

  #define GROWSIZE	1024
  
  if (!sf->fHasTags)
	  /* If buffer doesn't yet have tags, check if one was just
	   * found, and the copy
	   */
	  if (sf->fTag) {
		  sf->fHasTags = True;
		  sf->wLineBuf = sf->wLineNo;
	  }
	  /*  Or no tags in buffer yet, return  */
	  else {
		  *sf->pt = '\0';
		  return;
	  }

  // dprintf("AppendLineToBuf:  %d\n", (int) (sf->pt - sf->lpbuf));
	  
  /*  Otherwise, the buffer has tags, so copy the new string  */
  end = (PSTR) (sf->lpbuf + (int) NearSize(sf->lpbuf));

  for (ch = buf; *ch && (sf->pt < end); *sf->pt++ = *ch++);
  /* Deal with possible buffer overrun */
  if (sf->pt >= end) {
	WORD	origPt;
	int	needSize;
	
/*	dprintf("AppendLine:  expanding buf %x, pt %x, end %x\n",
		sf->lpbuf, sf->pt, end);
*/
	origPt = (WORD) (sf->pt - sf->lpbuf);	// save current offset
	needSize = strlen(ch) + 1;		// grow by this much

	sf->lpbuf = NearRealloc(sf->lpbuf,
		       (WORD)(NearSize(sf->lpbuf) + max(needSize, GROWSIZE)));
	sf->pt = sf->lpbuf + origPt;
	
	/* Continue with the copy */
	for (; *ch; *sf->pt++ = *ch++);
  }
  
  /* make sure that final buffer is null terminated */
  *sf->pt = '\0';
}