/*
 * CheckGuide.c, A tool to check the consistency of AmigaGuide databases
 *
 * Copyright © 1997 by Olaf Barthel <olsen@sourcery.han.de>
 * All Rights Reserved
 *
 * :ts=4
 */

#include <exec/lists.h>
#include <exec/nodes.h>

#include <dos/dosextens.h>
#include <dos/rdargs.h>

#include <clib/exec_protos.h>
#include <clib/dos_protos.h>
#include <clib/alib_protos.h>

#ifdef __SASC
#include <pragmas/exec_sysbase_pragmas.h>
#include <pragmas/dos_pragmas.h>
#endif /* __SASC */

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

/****************************************************************************/

#define SAME	(0)
#define NOT		!
#define CANNOT	!

/****************************************************************************/

void FreeList(struct List *list);
struct Node *FindIName(const struct List *list, const char *name);
struct EntryNode *CreateNode(const char *name);
BOOL IsNumber(const char *s);
VOID CollectNodes(const char *file, struct List *list);
struct DatabaseNode *RegisterFile(struct List *list, BPTR lock);
struct DatabaseNode *LocateNode(const char *parentFileName, const char *thisFileName, int line, const char *linkFileName);
void CheckGuide(const char *file, const char *parentFile, FILE *in);
int main(int argc, char **argv);

/****************************************************************************/

STRPTR version = "$VER: CheckGuide 1.8 (21.8.97)";

/****************************************************************************/

extern struct Library *SysBase;
extern struct Library *DOSBase;

/****************************************************************************/

struct EntryNode
{
	struct Node			Node;
	long				line;
	long				references;
	struct EntryNode *	within;
};

struct DatabaseNode
{
	struct Node			Node;
	struct List			nodes;
	BOOL				collected;
	BOOL				checked;
};

/****************************************************************************/

#define LINK		"@{"
#define OPEN_BRACE	'{'
#define CLOSE_BRACE	'}'

/****************************************************************************/

const char *keywords[][2] =
{
	"@database",		"1NAME/A",
	"@author",			"1NAME/F",
	"@(c)",				"1NAME/F",
	"@$VER: ",			"1NAME/F",
	"@master",			"1NAME/A",
	"@font",			"1NAME/A,2SIZE/N/A",
	"@index",			"1NAME/A",
	"@help",			"1NAME/A",
	"@onopen",			"1NAME/F",
	"@onclose",			"1NAME/F",
	"@proportional",	NULL,
	"@wordwrap",		NULL,
	"@smartwrap",		NULL,
	"@node",			"1NAME/A,2TITLE",
	"@dnode",			"1NAME/A,2TITLE",
	"@remark",			NULL,
	"@rem",				NULL,
	"@title",			"1TITLE/A/F",
	"@toc",				"1NAME/A",
	"@macro",			"1NAME/A,2NAME/A",
	"@prev",			"1NAME/A",
	"@xref",			"1NAME/A",
	"@next",			"1NAME/A",
	"@width",			"1NAME/A/N",
	"@height",			"1NAME/A/N",
	"@tab",				"1NAME/A/N",
	"@keywords",		"1KEYWORDS/A/M",
	"@endnode",			NULL,
	LINK,				"1LABEL/A,2COMMAND/A/F",
	NULL
};

/****************************************************************************/

struct List CheckedFileList;
BOOL Recursive = FALSE;
BOOL OnlyGuideFiles = FALSE;
BOOL Warnings = TRUE;

/****************************************************************************/

void
FreeList(struct List *list)
{
	struct Node *node;

	while((node = RemHead(list)) != NULL)
		free(node);
}

struct Node *
FindIName(
	const struct List *	list,
	const char *		name)
{
	struct Node *node;

	for(node = list->lh_Head ; node->ln_Succ ; node = node->ln_Succ)
	{
		if(stricmp(node->ln_Name,name) == SAME)
			return(node);
	}

	return(NULL);
}

struct EntryNode *
CreateNode(const char *name)
{
	struct EntryNode * node;

	node = (struct EntryNode *)malloc(strlen(name)+1 + sizeof(*node));
	if(node != NULL)
	{
		memset(node,0,strlen(name)+1 + sizeof(*node));
		strcpy(node->Node.ln_Name = (char *)(node + 1),name);
	}
	else
	{
		perror("CheckGuide");
		exit(RETURN_FAIL);
	}

	return(node);
}

/****************************************************************************/

BOOL
IsNumber(const char *s)
{
	int len = strlen(s);

	if(len == 0)
	{
		return(FALSE);
	}
	else
	{
		int i;

		for(i = 0 ; i < len ; i++)
		{
			if(s[i] < '0' || s[i] > '9')
				return(FALSE);
		}

		return(TRUE);
	}
}

/****************************************************************************/

VOID
CollectNodes(
	const char *	file,
	struct List *	list)
{
	FILE *in;

	/* This file *must* exist. */
	in = fopen(file,"r");
	if(in != NULL)
	{
		static char buffer[1024];
		int i,len,line;

		line = 0;

		/* Two characters less to allow for line termination characters. */
		while(fgets(buffer,sizeof(buffer)-2,in) != NULL)
		{
			/* Strip trailing line feed. */
			len = strlen(buffer);
			if(len > 0 && buffer[len - 1] == '\n')
				buffer[--len] = '\0';

			/* If this is the first line, it must have the
			 * @database keyword in it (if this is an AmigaGuide
			 * database, that is).
			 */
			line++;
			if(line == 1 && strnicmp(buffer,"@database",9))
			{
				const char *main = "main";
				struct Node *node;

				/* A "main" node is always available, so to speak. */
				node = (struct Node *)malloc(sizeof(*node) + strlen(main)+1);
				if(node != NULL)
				{
					strcpy(node->ln_Name = (char *)(node + 1),main);
					AddTail(list,node);
				}
				else
				{
					perror("CheckGuide");
					exit(RETURN_FAIL);
				}

				break;
			}

			/* If this line carries a @node keyword, analyze
			 * it. We want to keep the node name.
			 */
			if(strnicmp(buffer,"@node",5) == SAME)
			{
				int quote = 0;
				BOOL gotNodeName = FALSE;
				char *skip = &buffer[5];

				while(*skip == ' ' || *skip == '\t')
					skip++;

				len = strlen(skip);

				/* Flatten all escape characters. */
				for(i = 0 ; i < len ; i++)
				{
					if(skip[i] == '\\' && i < len-1)
					{
						skip[i] = '-';
						skip[i+1] = '-';
					}
				}

				/* So the following search will always terminate. */
				strcat(skip," ");
				len++;

				/* Look for the node name. */
				for(i = 0 ; i < len ; i++)
				{
					if(skip[i] == '\"')
						quote ^= 1;

					/* The search stops at a blank space. */
					if(skip[i] == ' ' || skip[i] == '\t')
					{
						if(quote == 0)
						{
							skip[i] = '\0';
							gotNodeName = TRUE;
							break;
						}
					}
				}

				/* If we got a node name, check if it contains
				 * space characters (it must not).
				 */
				if(gotNodeName)
				{
					len = strlen(skip);

					for(i = 0 ; i < len ; i++)
					{
						if(skip[i] == ' ' || skip[i] == '\t')
						{
							gotNodeName = FALSE;
							break;
						}
					}
				}

				/* If this is still a valid node name, strip
				 * any quote characters and add it to the list.
				 */
				if(gotNodeName)
				{
					struct Node *node;
					int j;

					for(i = 0, j = 0 ; i < len ; i++)
					{
						if(skip[i] != '\"')
							buffer[j++] = skip[i];
					}

					buffer[j] = '\0';

					node = (struct Node *)malloc(sizeof(*node) + strlen(buffer)+1);
					if(node != NULL)
					{
						strcpy(node->ln_Name = (char *)(node + 1),buffer);
						AddTail(list,node);
					}
					else
					{
						perror("CheckGuide");
						exit(RETURN_FAIL);
					}
				}
			}
		}

		fclose(in);
	}
}

struct DatabaseNode *
RegisterFile(
	struct List *	list,
	BPTR			lock)
{
	char localBuffer[400];
	struct DatabaseNode * result = NULL;

	if(NameFromLock(lock,localBuffer,sizeof(localBuffer)))
	{
		result = (struct DatabaseNode *)FindIName(list,localBuffer);
		if(result == NULL)
		{
			result = (struct DatabaseNode *)malloc(sizeof(*result) + strlen(localBuffer)+1);
			if(result != NULL)
			{
				strcpy(result->Node.ln_Name = (char *)(result + 1),localBuffer);
				NewList(&result->nodes);
				result->checked = FALSE;
				result->collected = FALSE;

				AddTail(list,(struct Node *)result);
			}
			else
			{
				perror("CheckGuide");
				exit(RETURN_FAIL);
			}
		}
	}

	return(result);
}

struct DatabaseNode *
LocateNode(
	const char *	parentFileName,
	const char *	thisFileName,
	int				line,
	const char *	linkFileName)
{
	char fileName[400];
	char *nodeName;
	struct DatabaseNode *result = NULL;

	nodeName = FilePart((STRPTR)linkFileName);
	if(nodeName != linkFileName)
	{
		struct Process *thisProcess = (struct Process *)FindTask(NULL);
		APTR oldPtr = thisProcess->pr_WindowPtr;
		BPTR lock;
		BOOL relativePath = FALSE;

		strcpy(fileName,linkFileName);
		*PathPart(fileName) = '\0';

		thisProcess->pr_WindowPtr = (APTR)-1;

		lock = Lock(fileName,SHARED_LOCK);
		if(lock == NULL)
		{
			strcpy(fileName,parentFileName);
			*PathPart(fileName) = '\0';
			if(AddPart(fileName,(STRPTR)linkFileName,sizeof(fileName)))
			{
				*PathPart(fileName) = '\0';

				lock = Lock(fileName,SHARED_LOCK);
				relativePath = TRUE;
			}
		}

		thisProcess->pr_WindowPtr = oldPtr;

		if(lock != NULL)
		{
			struct DatabaseNode *node;

			node = RegisterFile(&CheckedFileList,lock);
			result = node;

			UnLock(lock);

			if(relativePath && Warnings)
				printf("%s:%d:File found in relative path only \"%s\"\n",thisFileName,line,fileName);

			if(node != NULL)
			{
				if(NOT node->collected)
				{
					CollectNodes(node->Node.ln_Name,&node->nodes);
					node->collected = TRUE;
				}

				if(CANNOT FindIName(&node->nodes,nodeName))
				{
					result = NULL;
				}
			}
		}
	}

	return(result);
}

/****************************************************************************/

void
CheckGuide(
	const char *	file,
	const char *	parentFile,
	FILE *			in)
{
	static char buffer[1024];
	static char localBuffer[1000];
	int line,len;
	struct EntryNode *lastNode = NULL;
	struct EntryNode *node, *mainFound;
	char plainName[400];
	int i,j;
	char *suffix = "guide";
	struct List NodeList;
	struct List IndexList;
	struct List TocList;
	struct RDArgs *rdargs;
	int underlineCol = -1,underlineRow = -1;
	int boldCol = -1,boldRow = -1;
	int italicsCol = -1,italicsRow = -1;

	NewList(&NodeList);
	NewList(&IndexList);
	NewList(&TocList);

	strcpy(plainName,FilePart((STRPTR)file));
	for(j = strlen(plainName) - 1 ; j >= 0 ; j--)
	{
		if(stricmp(&plainName[j],".hyper") == SAME)
		{
			if(Warnings)
				printf("%s:%d:File uses obsolete .hyper name suffix\n",file,1);

			plainName[j] = '\0';
			suffix = &plainName[j+1];
			break;
		}

		if(stricmp(&plainName[j],".guide") == SAME)
		{
			plainName[j] = '\0';
			suffix = &plainName[j+1];
			break;
		}
	}

	line = 0;

	while(fgets(buffer,sizeof(buffer)-1,in) != NULL)
	{
		line++;

		len = strlen(buffer);
		if(len > 0 && buffer[len - 1] == '\n')
			buffer[--len] = '\0';

		if(line == 1)
		{
			if(strnicmp(buffer,"@database",9))
			{
				if(OnlyGuideFiles && Recursive)
					return;

				printf("%s:%d:First line must contain @database keyword\n",file,line);
			}
		}

		for(i = 0 ; i < len ; i++)
		{
			if(buffer[i] == '@' && buffer[i+1] == '@')
			{
				buffer[i] = buffer[i+1] = ' ';
			}

			if(buffer[i] == '\\' && i < len-1)
			{
				buffer[i] = buffer[i+1] = '-';
			}
		}

		if(buffer[0] == '@')
		{
			char const *found = NULL;

			for(i = 0 ; keywords[i][0] != NULL ; i++)
			{
				if(strnicmp(buffer,keywords[i][0],strlen(keywords[i][0])) == SAME)
				{
					found = keywords[i][0];
					break;
				}
			}

			if(NOT found)
			{
				char key[40];

				strncpy(key,buffer,19);
				key[19] = '\0';
				printf("%s:%d:Unknown keyword \"%s\"\n",file,line,key);
			}
			else
			{
				if(keywords[i][1] != NULL)
				{
					long args[2] = { NULL,NULL };

					signal(SIGINT,SIG_IGN);

					rdargs = AllocDosObjectTagList(DOS_RDARGS,NULL);
					if(rdargs != NULL)
					{
						char *rest;
						int k;

						strcat(buffer,"\n");

						rest = buffer + strlen(keywords[i][0]);

						while(*rest == ' ')
							rest++;

						rdargs->RDA_Source.CS_Buffer	= rest;
						rdargs->RDA_Source.CS_Length	= strlen(rdargs->RDA_Source.CS_Buffer);
						rdargs->RDA_Source.CS_CurChr	= 0;
						rdargs->RDA_Flags				= RDAF_NOPROMPT;

						for(k = 0 ; k < strlen(rest) ; k++)
						{
							if(rest[k] == '*')	/* This confuses the ReadArgs() parser. */
								rest[k] = '+';
						}

						if(ReadArgs((char *)keywords[i][1],args,rdargs))
						{
							if(strcmp(found,"@node") == SAME || strcmp(found,"@dnode") == SAME)
							{
								if(lastNode != NULL)
								{
									printf("%s:%d:Unbalanced @node\n",file,lastNode->line);
								}

								lastNode = CreateNode((char *)args[0]);
								if(lastNode != NULL)
								{
									struct Node *node;

									lastNode->line = line;

									for(node = NodeList.lh_Head ; node->ln_Succ ; node = node->ln_Succ)
									{
										if(stricmp(node->ln_Name,(char *)args[0]) == SAME)
										{
											printf("%s:%d:Duplicate @node \"%s\" (see line %d)\n",file,line,node->ln_Name,((struct EntryNode *)lastNode)->line);
											break;
										}
									}

									AddTail(&NodeList,(struct Node *)lastNode);
								}
							}

							if(strcmp(found,"@database") == SAME)
							{
								char otherPlainName[400];
								char *otherSuffix = "guide";
								int j;

								strcpy(otherPlainName,FilePart((char *)args[0]));
								for(j = strlen(otherPlainName) - 1 ; j >= 0 ; j--)
								{
									if(stricmp(&otherPlainName[j],".guide") == SAME)
									{
										otherPlainName[j] = '\0';
										otherSuffix = &otherPlainName[j+1];
										break;
									}

									if(stricmp(&otherPlainName[j],".hyper") == SAME)
									{
										otherPlainName[j] = '\0';
										otherSuffix = &otherPlainName[j+1];

										if(Warnings)
											printf("%s:%d:Database name uses obsolete .hyper suffix\n",file,line,found);

										break;
									}
								}

								if(Warnings)
								{
									if(stricmp(suffix,otherSuffix))
										printf("%s:%d:Database \"%s\" and file name \"%s\" suffixes do not match\n",file,line,suffix,otherSuffix);
									else
									{
										if(stricmp(otherPlainName,plainName))
											printf("%s:%d:Database \"%s\" and file name \"%s\" do not match\n",file,line,otherPlainName,plainName);
									}

									if(FilePart((STRPTR)args[0]) != (STRPTR)args[0])
										printf("%s:%d:Database name \"%s\" contains path specifiers\n",file,line,args[0]);
								}
							}

							if(strcmp(found,"@toc") == SAME)
							{
								struct EntryNode *node;

								node = CreateNode((char *)args[0]);
								if(node != NULL)
								{
									node->line = line;
									node->within = lastNode;

									AddTail(&TocList,(struct Node *)node);
								}
							}

							if(strcmp(found,"@index") == SAME)
							{
								struct EntryNode *node;

								node = CreateNode((char *)args[0]);
								if(node != NULL)
								{
									node->line = line;
									node->within = lastNode;

									AddTail(&IndexList,(struct Node *)node);
								}
							}
						}
						else
						{
							if(strcmp(found,LINK))
							{
								char errorBuffer[400];

								Fault(IoErr(),NULL,errorBuffer,sizeof(errorBuffer));
								printf("%s:%d:%s: %s\n",file,line,found,errorBuffer);
							}
						}

						FreeArgs(rdargs);
						FreeDosObject(DOS_RDARGS,rdargs);
					}

					signal(SIGINT,SIG_DFL);
				}

				if(strcmp(found,"@endnode") == SAME)
				{
					if(lastNode != NULL)
					{
						lastNode = NULL;
					}
					else
					{
						printf("%s:%d:Unbalanced @endnode\n",file,line);
					}
				}

				if(strcmp(found,LINK) == SAME)
				{
					int i,quote = 0,brace = 1;

					for(i = 2 ; i < len ; i++)
					{
						if(buffer[i] == '\"')
						{
							quote ^= 1;
						}
						else
						{
							if(quote == 0)
							{
								if(buffer[i] == OPEN_BRACE)	/* open brace */
									brace++;

								if(buffer[i] == CLOSE_BRACE)	/* closing brace */
									brace--;

								if(brace == 0)
									break;
							}
						}
					}

					if(brace > 0)
						printf("%s:%d:Missing closing brace in button declaration\n",file,line);

					if(brace < 0)
						printf("%s:%d:Unbalanced open brace in button declaration\n",file,line);

					if(quote > 0)
						printf("%s:%d:Missing quote in button declaration\n",file,line);
				}
			}
		}
	}

	fseek(in,0,SEEK_SET);

	line = 0;

	while(fgets(buffer,sizeof(buffer)-1,in) != NULL)
	{
		line++;

		len = strlen(buffer);
		if(len > 0 && buffer[len - 1] == '\n')
			buffer[--len] = '\0';

		if(len > 0)
		{
			int i,quote = 0;

			for(i = 0 ; i < len ; i++)
			{
				if(buffer[i] == '@' && buffer[i+1] == '@')
				{
					if(Warnings)
						printf("%s:%d:\"%c\" character escaped in column %d\n",file,line,buffer[i],i+1);

					buffer[i] = buffer[i+1] = ' ';
				}

				if(buffer[i] == '\\' && i < len-1)
				{
					if(Warnings)
						printf("%s:%d:\"\\%c\" escape sequence in column %d\n",file,line,buffer[i+1],i+1);

					buffer[i] = buffer[i+1] = '-';
				}
			}

			for(i = 0 ; i < len ; )
			{
/*				if(buffer[i] == '\"')*/
/*					quote ^= 1;*/

				if(!quote && buffer[i] == '@' && buffer[i+1] == OPEN_BRACE)
				{
					int j,quote = 0,brace = 1,total;

					buffer[i] = ' ';

					for(j = i+2, total = 2 ; j < len ; j++, total++)
					{
						if(buffer[j] == '\"')
						{
							quote ^= 1;
						}
						else
						{
							if(quote == 0)
							{
								if(buffer[j] == OPEN_BRACE)
									brace++;

								if(buffer[j] == CLOSE_BRACE)
									brace--;

								if(brace == 0)
								{
									total++;
									break;
								}
							}
						}
					}

					if(brace == 0 && quote == 0)
					{
						static char *style[] =
						{
							"amigaguide",
							"b",
							"body",
							"cleartabs",
							"code",
							"fontn",
							"heading",
							"i",
							"jcenter",
							"jleft",
							"jright",
							"lindent",
							"line",
							"mono",
							"par",
							"pard",
							"pari",
							"plain",
							"prop",
							"settabs",
							"tab",
							"u",
							"ub",
							"ui",
							"uu",
							NULL
						};
						BOOL known = FALSE;
						int k,len;
						char *skip;

						skip = &buffer[i+2];
						len = total-3;

						while(*skip == ' ' && len > 0)
						{
							skip++;
							len--;
						}

						if(len > 0)
							strncpy(localBuffer,skip,len);
						localBuffer[len] = '\0';

						for(k = 0 ; style[k] != NULL ; k++)
						{
							if(stricmp(localBuffer,style[k]) == SAME)
							{
								if(stricmp(style[k],"u") == SAME)
								{
									if(underlineCol != -1)
										printf("%s:%d:Unbalanced @{%s} in column %d\n",file,underlineRow,style[k],underlineCol);

									underlineCol = i+1;
									underlineRow = line;
								}

								if(stricmp(style[k],"uu") == SAME)
								{
									if(underlineCol == -1)
										printf("%s:%d:Command @{%s} in column %d has no effect\n",file,line,style[k],i+1);

									underlineCol = -1;
									underlineRow = -1;
								}

								if(stricmp(style[k],"b") == SAME)
								{
									if(boldCol != -1)
										printf("%s:%d:Unbalanced @{%s} in column %d\n",file,boldRow,style[k],boldCol);

									boldCol = i+1;
									boldRow = line;
								}

								if(stricmp(style[k],"ub") == SAME)
								{
									if(boldCol == -1)
										printf("%s:%d:Command @{%s} in column %d has no effect\n",file,line,style[k],i+1);

									boldCol = -1;
									boldRow = -1;
								}

								if(stricmp(style[k],"i") == SAME)
								{
									if(italicsCol != -1)
										printf("%s:%d:Unbalanced @{%s} in column %d\n",file,italicsRow,style[k],italicsCol);

									italicsCol = i+1;
									italicsRow = line;
								}

								if(stricmp(style[k],"ui") == SAME)
								{
									if(italicsCol == -1)
										printf("%s:%d:Command @{%s} in column %d has no effect\n",file,line,style[k],i+1);

									italicsCol = -1;
									italicsRow = -1;
								}

								known = TRUE;
								break;
							}
						}

						if(NOT known)
						{
							static char *colourControl[] =
							{
								"fg",
								"apen",
								"bg",
								"bpen",
								NULL
							};

							char *rest = NULL;

							for(k = 0 ; colourControl[k] != NULL ; k++)
							{
								if(strnicmp(localBuffer,colourControl[k],strlen(colourControl[k])) == SAME)
								{
									rest = &localBuffer[strlen(colourControl[k])];
									break;
								}
							}

							if(rest != NULL)
							{
								known = TRUE;

								if(rest[0] != ' ')
									printf("%s:%d:Invalid colour control string \"%s\"\n",file,line,rest);
								else
								{
									static char *pens[] =
									{
										"text",
										"shine",
										"shadow",
										"fill",
										"filltext",
										"back",
										"background",
										"highlight",
										NULL
									};

									BOOL found = FALSE;

									while(*rest == ' ')
										rest++;

									for(k = 0 ; pens[k] != NULL ; k++)
									{
										if(stricmp(rest,pens[k]) == SAME)
										{
											found = TRUE;
											break;
										}
									}

									if(NOT found)
										printf("%s:%d:Invalid colour \"%s\"\n",file,line,rest);
								}
							}
						}

						signal(SIGINT,SIG_IGN);

						if(NOT known)
							rdargs = AllocDosObjectTagList(DOS_RDARGS,NULL);
						else
							rdargs = NULL;

						if(rdargs != NULL)
						{
							long args[4] = { NULL,NULL,NULL,NULL };
							int k;

							strcat(localBuffer,"\n");

							rdargs->RDA_Source.CS_Buffer	= localBuffer;
							rdargs->RDA_Source.CS_Length	= strlen(rdargs->RDA_Source.CS_Buffer);
							rdargs->RDA_Source.CS_CurChr	= 0;
							rdargs->RDA_Flags				= RDAF_NOPROMPT;

							for(k = 0 ; k < strlen(localBuffer) ; k++)
							{
								if(localBuffer[k] == '*')	/* This confuses the ReadArgs() parser. */
									localBuffer[k] = '+';
							}

							if(ReadArgs("1TEXT/A,2COMMAND/A,3ARG,4ARG",args,rdargs))
							{
								char *name = (char *)args[1];
								char *arg1 = (char *)args[2];
								char *arg2 = (char *)args[3];

								if(localBuffer[0] != '\"' || localBuffer[1 + strlen((char *)args[0])] != '\"')
									printf("%s:%d:Button label \"%s\" must be enclosed in quotes\n",file,line,args[0]);

								if(stricmp(name,"alink") == SAME)
								{
									if(arg1 == NULL || (arg1 != NULL && *arg1 == '\0'))
										printf("%s:%d:ALINK node name required\n",file,line);
									else
									{
										struct EntryNode *found;

										found = (struct EntryNode *)FindIName(&NodeList,arg1);

										if(found)
										{
											found->references++;
										}
										else
										{
											struct DatabaseNode *otherNode;

											otherNode = LocateNode(parentFile,file,line,(char *)arg1);
											if(otherNode != NULL && Recursive && !otherNode->checked)
											{
												FILE *in;

												in = fopen(otherNode->Node.ln_Name,"r");
												if(in != NULL)
												{
													otherNode->checked = TRUE;
													CheckGuide(otherNode->Node.ln_Name,parentFile,in);

													fclose(in);
												}
												else
												{
													perror(otherNode->Node.ln_Name);
												}
											}

											if(otherNode == NULL)
												printf("%s:%d:Reference to undefined node \"%s\"\n",file,line,arg1);
										}
									}

									if(arg2 != NULL)
									{
										if(!IsNumber(arg2))
											printf("%s:%d:ALINK line number invalid\n",file,line);
									}
								}
								else if (!stricmp(name,"close"))
								{
									if(arg1 != NULL && arg2 != NULL)
										printf("%s:%d:Too many arguments for CLOSE command\n",file,line);
								}
								else if (!stricmp(name,"link"))
								{
									if(arg1 == NULL || (arg1 != NULL && *arg1 == '\0'))
										printf("%s:%d:LINK node name required\n",file,line);
									else
									{
										struct EntryNode *found;

										found = (struct EntryNode *)FindIName(&NodeList,arg1);

										if(found)
										{
											found->references++;
										}
										else
										{
											struct DatabaseNode *otherNode;

											otherNode = LocateNode(parentFile,file,line,(char *)arg1);
											if(otherNode != NULL && Recursive && !otherNode->checked)
											{
												FILE *in;

												in = fopen(otherNode->Node.ln_Name,"r");
												if(in != NULL)
												{
													otherNode->checked = TRUE;
													CheckGuide(otherNode->Node.ln_Name,parentFile,in);

													fclose(in);
												}
												else
												{
													perror(otherNode->Node.ln_Name);
												}
											}

											if(otherNode == NULL)
												printf("%s:%d:Reference to undefined node \"%s\"\n",file,line,arg1);
										}
									}

									if(arg2 != NULL)
									{
										if(!IsNumber(arg2))
											printf("%s:%d:LINK line number invalid\n",file,line);
									}
								}
								else if (!stricmp(name,"rx"))
								{
									if(arg1 == NULL || (arg1 != NULL && *arg1 == '\0'))
										printf("%s:%d:RX command required\n",file,line);
								}
								else if (!stricmp(name,"rxs"))
								{
									if(arg1 == NULL || (arg1 != NULL && *arg1 == '\0'))
										printf("%s:%d:RXS command required\n",file,line);
								}
								else if (!stricmp(name,"system"))
								{
									if(arg1 == NULL || (arg1 != NULL && *arg1 == '\0'))
										printf("%s:%d:SYSTEM command required\n",file,line);
								}
								else if (!stricmp(name,"quit"))
								{
									if(arg1 != NULL && arg2 != NULL)
										printf("%s:%d:Too many arguments for QUIT command\n",file,line);
								}
								else
								{
									printf("%s:%d:Unknown link command \"%s\"\n",file,line,name);
								}
							}
							else
							{
								char errorBuffer[400];

								Fault(IoErr(),NULL,errorBuffer,sizeof(errorBuffer));
								printf("%s:%d:link: %s\n",file,line,errorBuffer);
							}

							FreeArgs(rdargs);
							FreeDosObject(DOS_RDARGS,rdargs);
						}

						signal(SIGINT,SIG_DFL);
					}

					if(quote > 0)
					{
						printf("%s:%d:Unbalanced quote in button declaration at column %d\n",file,line,i+1);
					}

					if(brace > 0)
					{
						printf("%s:%d:Unbalanced brace in button declaration at column %d\n",file,line,i+1);
					}

					i += total;
				}
				else
				{
					if(!quote && buffer[i] == '@')
					{
						int j;
						char const *found = NULL;

						for(j = 0 ; keywords[j][0] != NULL ; j++)
						{
							if(strnicmp(&buffer[i],keywords[j][0],strlen(keywords[j][0])) == SAME)
							{
								found = keywords[j][0];
								break;
							}
						}

						if(found)
							buffer[i] = ' ';

						if(found && i > 0)
						{
							printf("%s:%d:Command \"%s\" has no effect\n",file,line,found);
						}

						if(found && i == 0)
						{
							if(strcmp(found,"@next") == SAME || strcmp(found,"@prev") == SAME)
							{
								STRPTR nodeName = NULL;
								char *skip;

								skip = &buffer[strlen(found)];
								while(*skip == ' ')
									skip++;

								signal(SIGINT,SIG_IGN);

								rdargs = AllocDosObjectTagList(DOS_RDARGS,NULL);
								if(rdargs != NULL)
								{
									int k;

									strcat(skip,"\n");

									rdargs->RDA_Source.CS_Buffer	= skip;
									rdargs->RDA_Source.CS_Length	= strlen(rdargs->RDA_Source.CS_Buffer);
									rdargs->RDA_Source.CS_CurChr	= 0;
									rdargs->RDA_Flags				= RDAF_NOPROMPT;

									for(k = 0 ; k < strlen(skip) ; k++)
									{
										if(skip[k] == '*')	/* This confuses the ReadArgs() parser. */
											skip[k] = '+';
									}

									if(ReadArgs("1NODE/A",(LONG *)&nodeName,rdargs))
									{
										struct EntryNode *node;

										node = (struct EntryNode *)FindIName(&NodeList,nodeName);
										if(node != NULL)
										{
											node->references++;
										}
										else
										{
											struct DatabaseNode *otherNode;

											otherNode = LocateNode(parentFile,file,line,nodeName);
											if(otherNode != NULL && Recursive && !otherNode->checked)
											{
												FILE *in;

												in = fopen(otherNode->Node.ln_Name,"r");
												if(in != NULL)
												{
													otherNode->checked = TRUE;
													CheckGuide(otherNode->Node.ln_Name,parentFile,in);

													fclose(in);
												}
												else
												{
													perror(otherNode->Node.ln_Name);
												}
											}

											if(otherNode == NULL)
												printf("%s:%d:%s references undefined node \"%s\"\n",file,line,found,nodeName);
										}
									}
									else
									{
										char errorBuffer[400];

										Fault(IoErr(),NULL,errorBuffer,sizeof(errorBuffer));
										printf("%s:%d:%s: %s\n",file,line,found,errorBuffer);
									}

									FreeArgs(rdargs);
									FreeDosObject(DOS_RDARGS,rdargs);
								}

								signal(SIGINT,SIG_DFL);
							}
						}

						if(NOT found && buffer[i+1] == ' ')
						{
							char *rest = &buffer[i+1];

							while(*rest == ' ')
								rest++;

							if(*rest == OPEN_BRACE)
								printf("%s:%d:Incomplete/invalid link declaration\n",file,line);
						}
					}

					i++;
				}
			}

			for(i = 0 ; i < len ; i++)
			{
				if(buffer[i] == '@' || buffer[i] == '\\')
					printf("%s:%d:Single \"%c\" character in column %d\n",file,line,buffer[i],i + 1);
			}
		}
	}

	if(underlineCol != -1)
		printf("%s:%d:Unbalanced @{%s} in column %d\n",file,underlineRow,"u",underlineCol);

	if(boldCol != -1)
		printf("%s:%d:Unbalanced @{%s} in column %d\n",file,boldRow,"b",boldCol);

	if(italicsCol != -1)
		printf("%s:%d:Unbalanced @{%s} in column %d\n",file,italicsRow,"i",italicsCol);

	mainFound = (struct EntryNode *)FindIName(&NodeList,"main");
	if(mainFound)
		mainFound->references++;
	else
		printf("%s:%d:MAIN node missing\n",file,1);

	if(NOT IsListEmpty(&TocList))
	{
		struct EntryNode *found;
		int mainContext = 0;

		for(node = (struct EntryNode *)TocList.lh_Head ; node->Node.ln_Succ ; node = (struct EntryNode *)node->Node.ln_Succ)
		{
			found = (struct EntryNode *)FindIName(&NodeList,node->Node.ln_Name);
			if(found)
				found->references++;
			else
			{
				struct DatabaseNode *otherNode;

				otherNode = LocateNode(parentFile,file,node->line,(char *)node->Node.ln_Name);
				if(otherNode != NULL && Recursive && !otherNode->checked)
				{
					FILE *in;

					in = fopen(otherNode->Node.ln_Name,"r");
					if(in != NULL)
					{
						otherNode->checked = TRUE;
						CheckGuide(otherNode->Node.ln_Name,parentFile,in);

						fclose(in);
					}
					else
					{
						perror(otherNode->Node.ln_Name);
					}
				}

				if(otherNode == NULL)
					printf("%s:%d:TOC node \"%s\" missing\n",file,node->line,node->Node.ln_Name);
			}

			if(node->within == NULL)
			{
				if(mainContext++ > 0)
					printf("%s:%d:More than one @TOC at top level\n",file,node->line);
			}
		}

		FreeList(&TocList);
	}

	if(NOT IsListEmpty(&IndexList))
	{
		struct EntryNode *found;
		int mainContext = 0;

		for(node = (struct EntryNode *)IndexList.lh_Head ; node->Node.ln_Succ ; node = (struct EntryNode *)node->Node.ln_Succ)
		{
			found = (struct EntryNode *)FindIName(&NodeList,node->Node.ln_Name);
			if(found)
				found->references++;
			else
			{
				struct DatabaseNode *otherNode;

				otherNode = LocateNode(parentFile,file,node->line,(char *)node->Node.ln_Name);
				if(otherNode != NULL && Recursive && !otherNode->checked)
				{
					FILE *in;

					in = fopen(otherNode->Node.ln_Name,"r");
					if(in != NULL)
					{
						otherNode->checked = TRUE;
						CheckGuide(otherNode->Node.ln_Name,parentFile,in);

						fclose(in);
					}
					else
					{
						perror(otherNode->Node.ln_Name);
					}
				}

				if(otherNode == NULL)
					printf("%s:%d:INDEX node \"%s\" missing\n",file,node->line,node->Node.ln_Name);
			}

			if(node->within == NULL)
			{
				if(mainContext++ > 0)
					printf("%s:%d:More than one @INDEX at top level\n",file,node->line);
			}
		}

		FreeList(&IndexList);
	}

	for(node = (struct EntryNode *)NodeList.lh_Head ; node->Node.ln_Succ ; node = (struct EntryNode *)node->Node.ln_Succ)
	{
		if(node->references == 0)
			printf("%s:%d:Unused @node \"%s\"\n",file,node->line,node->Node.ln_Name);
	}

	FreeList(&NodeList);
}

/****************************************************************************/

int
main(int argc,char **argv)
{
	if(DOSBase->lib_Version >= 36)
	{
		if(argc > 1 && !(argc == 2 && argv[1][0] == '?'))
		{
			FILE * in;
			int i;

			NewList(&CheckedFileList);

			for(i = 1 ; i < argc ; i++)
			{
				if(stricmp(argv[i],"-r") == SAME || stricmp(argv[i],"--recursive") == SAME)
				{
					Recursive = TRUE;
					continue;
				}

				if(stricmp(argv[i],"-o") == SAME || stricmp(argv[i],"--onlyguidefiles") == SAME)
				{
					OnlyGuideFiles = TRUE;
					continue;
				}

				if(stricmp(argv[i],"-w") == SAME || stricmp(argv[i],"--nowarnings") == SAME)
				{
					Warnings = FALSE;
					continue;
				}

				in = fopen(argv[i],"r");
				if(in != NULL)
				{
					BPTR lock;

					lock = Lock(argv[i],SHARED_LOCK);
					if(lock != NULL)
					{
						struct DatabaseNode *node;

						node = RegisterFile(&CheckedFileList,lock);

						UnLock(lock);

						if(node != NULL)
						{
							node->checked = TRUE;
							CollectNodes(node->Node.ln_Name,&node->nodes);
						}
					}

					CheckGuide(argv[i],argv[i],in);
	
					fclose(in);
				}
				else
				{
					perror(argv[i]);
				}
			}

			FreeList(&CheckedFileList);
		}
		else
		{
			printf("Usage: checkguide [-r,--recursive] [-o,--onlyguidefiles] [-w,--nowarnings]\n");
			printf("                  <files>\n");
		}
	}

	return(0);
}
