/*
 *		CHECKGUIDE.C											vi:ts=4
 *
 *		by Eddy Carroll, August 1994
 *
 *		Reads in an AmigaGuide file and identifies potential problems
 *		with it, including:
 *
 *			- Nodes not referenced by anything
 *			- References to non-existent nodes
 *			- Superfluous text between an @ENDNODE and @NODE
 *		
 *		This is intended to make it easier for a developer to catch
 *		problems with a Guide file before releasing it to the public.
 *
 *		Usage: CheckGuide [options] [filename]
 *
 *		Valid options are:
 *
 *			ALL    		 - Perform all checks
 *			EXTRATEXT	 - Check for extra text between @ENDNODE and @NODE
 *			NODES		 - Check for nodes that aren't referenced
 *			REFS		 - Check for unresolved references
 *			BRACES		 - Check for braces not prefixed by '@'
 *			LINK         - Check every node reference for LINK keyword
 *			VERBOSE		 - List maximum amount of detail
 *
 *		If no options are given, ALL is assumed
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char Version[] = "$VER: CheckGuide 1.0 (31.8.94)";

char Usage[] =
"CheckGuide 1.0 © Eddy Carroll, Aug 1994. Checks AGuide files for common bugs.\n"
"\n"
"    Usage: CheckGuide filename.guide [Options]\n"
"\n"
"Valid options are:\n"
"\n"
"    EXTRATEXT  Check for extra text between @ENDNODE and @NODE\n"
"        NODES  Check for nodes that are not referenced anywhere\n"
"         REFS  Check for references to non-existant nodes\n"
"       BRACES  Check for braces that aren't preceded by @\n"
"         LINK  Check that every node reference includes LINK keyword\n"
"          ALL  Perform all the above checks (default)\n"
"      VERBOSE  Display lines with errors for EXTRATEXT and BRACES\n"
"\n"
"Each option can be prefixed with NO to negate its effect.\n";

int Verbose			= 0;
int CheckExtraText	= 0;
int CheckNodes		= 0;
int CheckRefs		= 0;
int CheckBraces		= 0;
int CheckLink       = 0;

int FoundNode       = 0;	/* If true, we found at least one node			*/

/*
 *		Scanning states, used to identify which part of the guide file
 *		we are currently scanning
 */
typedef enum {
	START_SCAN,
	OUTSIDE_NODE,
	INSIDE_NODE
} ScanStates;

typedef struct RefList {
	struct RefList	*next;		/* Link to next reference on the list		*/
	int				linenum;	/* A line number							*/
} RefList;

typedef struct GuideNode {
	struct GuideNode *next;		/* Link to next node on the list			*/
	char	*name;				/* Name of this AmigaGuide node				*/
	int		linenum;			/* Line number it's defined at (or zero)	*/
	RefList *reflist;			/* List of line numbers referencing it		*/
	int		numrefs;			/* Number of times node was referenced		*/
} GuideNode;

GuideNode *NodeList;

/*
 *		FindGuideNode(char *nodename)
 *
 *		Tries to locate the specified AmigaGuide node on the node list.
 *		If the node can't be found, a new node is created and initialised
 *		to zero.
 */
GuideNode *FindGuideNode(char *nodename)
{
	GuideNode **pnode;
	GuideNode *node;
	int len;

	for (pnode = &NodeList; *pnode; pnode = &(*pnode)->next) {
		int val = stricmp((*pnode)->name, nodename);

		if (val == 0)
			return (*pnode);

		if (val > 0)
			break;
	}
	/*
	 *		Okay, need to create a new node at the point indicated here
	 */
	len  = strlen(nodename) + 1;
	node = calloc(sizeof(GuideNode) + len, 1);
	if (!node) {
		printf("Fatal error -- out of memory!\n");
		exit(10);
	}
	node->next  = *pnode;
	*pnode	    = node;
	node->name	= (char *)(node+1);
	strcpy(node->name, nodename);
	return (node);
}

/*
 *		AddGuideNode(char *nodename, int linenum)
 *
 *		Adds a new node with the specified name at the given linenum.
 *		If the node already exists, prints an error message. If the
 *		node has outstanding references, deletes them.
 */
void AddGuideNode(char *nodename, int linenum)
{
	GuideNode *node = FindGuideNode(nodename);

	if (node->linenum) {
		/*
		 *		Redefining an existing node so print error message
		 */
		printf("%d: Redefinition of node %s (see line %d)\n",
			linenum, nodename, node->linenum);
		return;
	}
	if (node->reflist) {
		/*
		 *		We can delete the existing reference list now since
		 *		it's not needed any more
		 */
		node->reflist = NULL;
	}
	node->linenum = linenum;
}

/*
 *		AddGuideRef(char *linkname, int linenum)
 *
 *		Adds a reference to the specified node name at the given linenum.
 *		If the node already exists, then no action is taken. If the node
 *		doesn't exist, then the line number is added to the list of
 *		outstanding references.
 */
void AddGuideRef(char *nodename, int linenum)
{
	GuideNode *node = FindGuideNode(nodename);
	RefList **pref;
	RefList *ref;
	
	node->numrefs++;

	if (node->linenum) {
		/*
		 *		The node already exists so no need to remember the line
		 *		number
		 */
		return;
	}
	/*
	 *		Need to add another reference to the list of line numbers
	 */
	ref = malloc(sizeof(RefList));
	if (!ref) {
		printf("Fatal error -- out of memory!\n");
		exit(10);
	}
	ref->linenum = linenum;
	ref->next    = NULL;
	
	for (pref = &node->reflist; *pref; pref = &(*pref)->next)
		;
	*pref = ref;
}

/*
 *		DummyReference(name)
 *
 *		Creates a dummy reference for the specified node, so that
 *		if it was defined but not used, it doesn't cause a warning
 *		message (e.g. Index, Help, etc.)
 */
void DummyReference(char *nodename)
{
	GuideNode *node = FindGuideNode(nodename);

	if (node->linenum == 0)
		node->linenum = 1;
	node->numrefs++;
}

/*
 *		ScanGuide(fp)
 *
 *		Scans the given file, building a list of all the node definitions
 *		and references. If CheckExtraText is true, then text that falls
 *		between an @ENDNODE and @NODE causes a warning message to be
 *		displayed. If Verbose is true, then the actual text is printed
 *		too.
 */
void ScanGuide(FILE *fp)
{
	static char linebuf[1000];
	static char lastnode[100];
	char *nodename;
	char *linkname;
	int  printederror = 0;
	int  linenum 	  = 0;
	int  state		  = START_SCAN;

	while (fgets(linebuf, 999, fp) != NULL) {
		char *p;

		linenum++;
		for (p = linebuf; *p && *p == ' ' || *p == '\t' || *p == '\n'; p++)
			;
		if (!*p)
			continue;

		switch (state) {

		case START_SCAN:
			if (*linebuf != '@')
				continue;

			if (strnicmp(linebuf, "@NODE", 5) != 0)
				continue;

			FoundNode = 1;

found_node:
			/*
			 *		Get here if we were scanning for a new node. We
			 *		flip the state into INSIDE_NODE, and add the node
			 *		details to our list.
			 */
			p = linebuf + 5;
			while (*p == ' ' || *p == '\t')
				p++;
			if (*p == '\0' || *p == '\n') {
				printf("%d: @NODE keyword requires a node identifier.\n",
						linenum);
				state = INSIDE_NODE;
				continue;
			}
			if (*p == '\"') {
				/*
				 *		Copy node name in quotes
				 */
				p++;
				nodename = p;
				while (*p && *p != '\"' && *p != '\n')
					p++;
				*p = '\0';
			} else {
				/*
				 *		Copy node name
				 */
				nodename = p;
				while (*p && *p != ' ' && *p != '\t' && *p != '\n')
					p++;
				*p = '\0';
			}
			strcpy(lastnode, nodename);
			AddGuideNode(nodename, linenum);
			if (state == START_SCAN) {
				/*
				 *		The first node is always the root, so it doesn't
				 *		need to be referenced. We create a fake reference
				 *		to it, so that it won't be listed at the end.
				 */
				AddGuideRef(nodename, linenum);
			}
			state = INSIDE_NODE;
			continue;

		case OUTSIDE_NODE:
			/*
			 *		Searching for a new node. If we spot any text that
			 *		doesn't begin with an @, we signal an error. If we
			 *		spot an @ENDNODE, we also signal an error
			 */
			if (*linebuf != '@' && CheckExtraText) {
				/*
				 *		Got some extra text. If it's the first time,
				 *		then print a warning message. If verbose
				 *		is enabled, print the actual text too.
				 */
				if (!printederror) {
					printederror = 1;
					printf("%d: Extra text spotted between "
						   "@ENDNODE and @NODE.\n", linenum);
				}
				if (Verbose)
					printf("%d: %s", linenum, linebuf);
				continue;
			}
			if (strnicmp(linebuf, "@NODE", 5) == 0) {
				printederror = 0;	/* Reset flag */
				goto found_node;
			}
			if (strnicmp(linebuf, "@ENDNODE", 8) == 0) {
				printf("%d: @ENDNODE has no associated @NODE command.\n",
						linenum);
				continue;
			}
			break;
			 
		case INSIDE_NODE:
			/*
			 *		We're inside a node, looking for an @ENDNODE to
			 *		say we're done. If we spot any references to links,
			 *		then we need to resolve them.
			 */
			if (strnicmp(linebuf, "@ENDNODE", 8) == 0) {
				state = OUTSIDE_NODE;
				continue;
			}
			if (strnicmp(linebuf, "@NODE", 5) == 0) {
				printf("%d: @NODE found inside @NODE %s\n", linenum, lastnode);
				goto found_node;
			}
			if (*linebuf) {
				for (p = linebuf+1; *p; p++) {
					if (*p == '{') {
						/*
						 *		Got some kind of AmigaGuide sequence
						 */
						char linkbuf[100];

						if (p[-1] != '@') {
							/*
							 *		Could be an error so possibly print an
							 *		error message, else skip over it
							 */
							if (CheckBraces) {
								printf("%d: '{' was not prefixed by '@'\n",
										linenum);
								if (Verbose)
									printf("%d: %s", linenum, linebuf);
							}
							continue;
						}

						/*
						 *		Got an AmigaGuide sequence. Now look for the
						 *		link keyword, skipping over text in quotes)
						 *		If there is no text in quotes, then it is
						 *		some other AmigaGuide sequence and we can
						 *		ignore it.
						 */
						p++;
						while (*p == ' ' || *p == '\t')
							p++;
						if (*p != '\"') {
							/*
							 *		Not a quoted string, so skip to
							 *		closing brace
							 */
							while (*p && *p != '}')
								p++;
							continue;
						}
						/*
						 *		Got a quoted string so now skip past it
						 *		and onto the link
						 */
						p++;
						while (*p && *p != '\n' && *p != '\"')
							p++;
						if (*p)
							p++;
						linkname = NULL;
						while (*p && *p != '}' && *p != '\n') {
							if (strnicmp(p, "link", 4) == 0) {
								p += 4;
								while (*p == ' ' || *p == '\t')
									p++;
								linkname = linkbuf;
								if (*p == '\"') {
									p++;
									while (*p && *p != '\"' && *p != '\n')
										*linkname++ = *p++;
								} else {
									while (*p && *p != ' ' && *p != '\n' &&
										   *p != '\t' && *p != '}' &&
										   *p != '\"')
									{
										*linkname++ = *p++;
									}
								}
								*linkname = '\0';
								break;
							}
							p++;
						}
						if (linkname)
							AddGuideRef(linkbuf, linenum);
						else if (CheckLink) {
							printf("%d: Missing LINK command in node "
								   "reference\n", linenum);
							if (Verbose)
								printf("%d: %s", linenum, linebuf);
						}
						/*
						 *		Check for missing closing brace
						 */
						if (*p != '}') {
							while (*p == ' ' || *p == '\t' || *p == '\"')
								p++;
						}
						if (*p != '}') {
							printf("%d: Missing } in node reference.\n",
								   linenum);
							if (Verbose)
								printf("%d: %s", linenum, linebuf);
						}
					}
				}
			}
			break;
		}
	}
}

/*
 *		ShowNoNodeLines()
 *
 *		Displays all nodes that were referenced but not defined
 */
void ShowNoNodeLines()
{
	GuideNode *node;
	int nonode = 0;

	printf("\n");
	for (node = NodeList; node; node = node->next) {
		if (node->linenum == 0)
			nonode++;
	}
	if (nonode == 0) {
		printf("No undefined nodes were referenced.\n");
		return;
	}
	if (nonode > 1)
		printf("The following %d nodes were referenced but not defined:\n",
			nonode);
	else
		printf("The following node was referenced but not defined\n");

	for (node = NodeList; node; node = node->next) {
		if (node->linenum == 0) {
			int len = printf("  Node %-25s: ", node->name);
			int col = len;
			RefList *ref;

			for (ref = node->reflist; ref; ref = ref->next) {
				if (col > 72) {
					printf("\n%*s", len, "");
					col = len;
				}
				col += printf("%4d  ", ref->linenum);
			}
			printf("\n");
		}
	}
}

/*
 *		ShowNoRefLines()
 *
 *		Displays all nodes that were defined but not referenced
 */
void ShowNoRefLines(void)
{
	GuideNode *node;
	int norefs = 0;

	printf("\n");
	for (node = NodeList; node; node = node->next) {
		if (node->numrefs == 0)
			norefs++;
	}
	if (norefs == 0) {
		printf("All nodes defined were referenced at least once.\n");
		return;
	}
	if (norefs > 1)
		printf("The following %d nodes were defined but not referenced:\n",
			norefs);
	else
		printf("The following node was defined but not referenced\n");

	for (node = NodeList; node; node = node->next) {
		if (node->numrefs == 0)
			printf("    Line %4d: Node %s\n", node->linenum, node->name);
	}
}

/*
 *		Mainline
 */
int main(int argc, char **argv)
{
	char *filename = NULL;
	FILE *fp;
	int set = 0;

	if (argc < 2) {
		printf(Usage);
		exit(10);
	}
	/*
	 *		First, initialise all our settings to "uninitialised"
	 *		settings.
	 */
	CheckNodes	   = 2;
	CheckRefs	   = 2;
	CheckExtraText = 2;
	CheckBraces    = 2;
	CheckLink      = 2;

	/*
	 *		Now scan command line parsing options
	 */
	while (argc > 1) {
		char *opt = argv[1];
		int bool = 1;

		if (strnicmp(opt, "no", 2) == 0) {
			opt += 2;
			bool = 0;
		}
#define MATCHARG(s)	(stricmp(opt, s) == 0)

		if      MATCHARG("Nodes")  		CheckNodes	   = bool, set = bool;
		else if MATCHARG("Refs")  		CheckRefs	   = bool, set = bool;
		else if MATCHARG("ExtraText")	CheckExtraText = bool, set = bool;
		else if MATCHARG("Braces")    	CheckBraces    = bool, set = bool;
		else if MATCHARG("Link")        CheckLink      = bool, set = bool;
		else if MATCHARG("Verbose")	  	Verbose 	   = bool;
		else if MATCHARG("All")	{
			CheckNodes	   = bool;
			CheckRefs	   = bool;
			CheckExtraText = bool;
			CheckBraces    = bool;
			CheckLink      = bool;
		} else {
			if (filename) {
				printf("Either %s or %s is an unrecognised option.\n"
					   "Type CHECKGUIDE for help.\n", filename, opt);
				exit(10);
			}
			filename = argv[1];
		}
		argc--;
		argv++;
	}
	if (!filename) {
		printf(Usage);
		exit(10);
	}
	/*
	 *		Finally, update any options that weren't explictly set.
	 *		If the last main option set was negative, then all the
	 *		unset options default to ON, else they default to OFF.
	 */
	if (CheckExtraText == 2)	CheckExtraText = !set;
	if (CheckNodes     == 2)	CheckNodes     = !set;
	if (CheckRefs      == 2)	CheckRefs      = !set;
	if (CheckBraces    == 2)	CheckBraces    = !set;
	if (CheckLink      == 2)	CheckLink      = !set;

	fp = fopen(filename, "r");
	if (!fp) {
		static char newname[200];

		strcpy(newname, filename);
		strcat(newname, ".guide");
		fp = fopen(newname, "r");
		if (!fp) {
			printf("Can't open file %s for input\n", filename);
			exit(10);
		}
		filename = newname;
	}
	ScanGuide(fp);
	/*
	 *		Now ensure that an index node won't generate an
	 *		"unreferenced node" error, since it is accessed
	 *		directly by AmigaGuide.
	 */
	DummyReference("Main");
	DummyReference("Help");
	DummyReference("Index");

	/*
	 *		Now show output
	 */
	if (FoundNode) {
		if (CheckNodes)		ShowNoNodeLines();
		if (CheckRefs)		ShowNoRefLines();
	} else {
		printf("%s does not appear to be an amigaguide file.\n", filename);
	}
	fclose(fp);
	exit(0);
}
