/*
 * COMMANDS.C
 *
 * This module contains all the command parsing stuff used to parse the
 * commands in the script file.
 *
 */

#ifndef LATTICE_50
#include "system.h"
#endif

#include "bbsindex.h"

/*
 *		The list of command names and associated functions
 */

void com_append(), com_close(), com_config(), com_database(), com_echo(),
	 com_exec(), com_format(), com_list(), com_macro(), com_msg(),
	 com_open(), com_reset(), com_scan(), com_trace(), com_ignore();

struct {
	char *name;
	void (*proc)();
} com[] = {

	{	"APPEND",		com_append		},
	{	"CHECKFILES",	com_checkfiles	},
	{	"CLOSE",		com_close		},
	{	"CONFIG",		com_config		},
	{	"DATABASE",		com_database	},
	{	"ECHO",			com_echo		},
	{	"EXEC",			com_exec		},
	{	"FOREIGN",		com_foreign		},
	{	"FORMAT",		com_format		},
	{	"IGNORE",		com_ignore		},
	{	"LIST",			com_list		},
	{	"MACRO",		com_macro		},
	{	"MSG",			com_msg			},
	{	"NOREQUEST",	com_norequest	},
	{	"OPEN",			com_open		},
	{	"RESET",		com_reset		},
	{	"SCAN",			com_scan		},
	{	"SELECT",		com_select		},
	{	"SORT",			com_sort		},
	{	"TRACE",		com_trace		},
	{	NULL,			NULL			} 
};

struct constval {
	struct constval *next;
	char *name;
	char *value;
};
typedef struct constval CONST;

CONST *firstconst = NULL;		/* Pointer to first constant on list	*/

static char line[MAXCOM];	/* Temporary line buffer */

/*
 *		scripterror()
 *		-------------
 *		Prints an error message for the current script command; the filename
 *		and linenumber are automatically output, followed by the user
 *		specified message. The message should be terminated with a NL,
 *		unless the caller intends outputting any more info afterwards.
 */
void scripterror(s)
char *s;
{
	print3("===> ", scriptname, " (");
	print3(itoa(linenum), "): ",s);
}

/*
 *		addconst()
 *		--------
 *		Creates a new constant entry, and links it into the list of 
 *		existing constant definitions. The name of the constant is
 *		initialised appropriately. A pointer to the constant is returned.
 */
CONST *addconst(name)
char *name;
{
	CONST *tv;

	tv			= mymalloc(sizeof(CONST));
	tv->next	= firstconst;
	firstconst	= tv;
	tv->name	= mymalloc(strlen(name)+1);
	strcpy(tv->name, name);
	return (tv);
}

/*
 *		findconst()
 *		---------
 *		Searches constant table for specified constant, and returns a
 *		pointer to it if found, or NULL if not found.
 */
CONST *findconst(name)
char *name;
{
	CONST *tv;

	for (tv = firstconst; tv && stricmp(name, tv->name); tv = tv->next)
		;
	return (tv);
}


/*
 *		readcommand()
 *		-------------
 *		This function reads a command from the script buffer into a
 *		specified command buffer. The following modifications are made
 *		to the original script text:
 *
 *		- Anything after a # to an end of line is ignored
 *		- Any line ending with \ is continued on the next line
 *		- Everything outside quotes is capitalised.
 *		- Any white space outside quotes gets reduced to a single space
 *		- White space surrounding command lines is removed.
 *		- Any commas on the line are removed, and replaced by spaces
 *
 *		The script command is terminated by either a semicolon or a newline.
 *		The command buffer is null-terminated on return. When the end of the
 *		script is reached, a 0 is returned. The maximum size of the command
 *		line is specified in max - if this is exceeded, then an error message
 *		is generated. Normally, the length of the command line read in is
 *		returned.
 *
 *		For the technically minded, a mini state machine is setup, to handle
 *		the different requirements.
 */

#define STATE_START		1	/* Skip over space at start of command			*/
#define STATE_SPACE		2	/* Replace multiple white space by single space	*/
#define STATE_COPY		3	/* Copy normal text, capitalising				*/
#define STATE_IGNORE	4	/* If next char is newline, then skip it		*/
#define STATE_QUOTE		5	/* Copy text up until next quote				*/
#define STATE_IGQUOTE	6	/* Like STATE_IGNORE, but between quotes		*/
#define STATE_COMMENT	7	/* Ignore everything until the next newline		*/
#define STATE_CONST_ST	8	/* Starting to expand a constant $(..)			*/
#define STATE_CONST_CP  9	/* Copying constant name, and expanding it		*/

/*
 *		Retrieve next character from script buffer, updating line counter
 *		and checking for end of buffer.
 */
#define nextchar(ch) 									\
	(	(ch) = script[scriptpos++],				 		\
	 	((ch) == CHAR_NL ? linenum++ : 0),				\
	 	((scriptpos > scriptsize) ? (loop = 0) : 0) 	\
	)

/*
 *		Add character to command line, checking for end of buffer and
 *		aborting if the buffer is overrun.
 */
#define addchar(ch)										\
	(	buf[pos++] = (ch),								\
		((pos > max) ? (scripterror("line too long\n"), Cleanup(10)) : 0)	\
	)

int readcommand(buf,max)
char *buf;
int max;
{
	int pos = 0;
	int state = STATE_START;
	int laststate;
	int loop = 1;
	int ch;
	CONST *var;
	char varname[MAXCONST], *p;
	char openquote;
	int varpos;

	if (scriptpos >= scriptsize)
		return 0;

	nextchar(ch);
	while (loop) {
		switch (state) {

			case STATE_START:
				switch (ch) {

					case CHAR_SPACE:
					case CHAR_TAB:
					case CHAR_SEMI:
					case CHAR_NL:
					case CHAR_COMMA:
						nextchar(ch);
						break;

					case CHAR_HASH:
						state = STATE_COMMENT;
						break;

					default:
						state = STATE_COPY;
						break;
				}
				break;

			case STATE_SPACE:
				switch (ch) {

					case CHAR_SPACE:
					case CHAR_TAB:
					case CHAR_COMMA:
						nextchar(ch);
						break;

					case CHAR_NL:
						linenum--;
						/* Drop through */
					case CHAR_SEMI:
						scriptpos--;
						loop = 0;
						break;

					default:
						addchar(CHAR_SPACE);
						state = STATE_COPY;
						break;
				}
				break;

			case STATE_COPY:
				switch (ch) {

					/*
					 * If we get a # after a command, then we immediately
					 * stop, so that the comment will get eaten the NEXT
					 * time we call readcommand(). If we went into
					 * STATE_COMMENT, then the following command would end
					 * up getting read into the buffer as well.
					 *
					 */
					case CHAR_HASH:
						scriptpos--;
						loop = 0;
						break;

					case CHAR_QUOTE:
					case CHAR_QUOTES:
						addchar(ch);
						openquote = ch;
						state = STATE_QUOTE;
						nextchar(ch);
						break;

					case CHAR_DOLLAR:
						laststate = state;	/* Save return state */
						state = STATE_CONST_ST;
						nextchar(ch);
						break;

					case CHAR_NL:
						linenum--;
						/* Drop through */

					case CHAR_SEMI:
						loop = 0;
						scriptpos--;
						break;

					case CHAR_ESC:
						nextchar(ch);
						state = STATE_IGNORE;
						break;

					case CHAR_SPACE:
					case CHAR_TAB:
					case CHAR_COMMA:
						state = STATE_SPACE;
						break;

					default:
						addchar(toupper(ch));
						nextchar(ch);
						break;
				}
				break;

			case STATE_IGNORE:
				if (ch == CHAR_NL)
					ch = CHAR_SPACE;
				addchar(ch);
				nextchar(ch);
				state = STATE_COPY;
				break;

			case STATE_QUOTE:
				switch (ch) {

					case CHAR_NL:
						scriptpos--;
						linenum--;
						loop = 0;
						break;

					case CHAR_ESC:
						state = STATE_IGQUOTE;
						nextchar(ch);
						break;

					case CHAR_DOLLAR:
						laststate = state;
						state = STATE_CONST_ST;
						nextchar(ch);
						break;

					case CHAR_QUOTE:
					case CHAR_QUOTES:
						if (openquote == ch)
							state = STATE_COPY;
						/* Deliberate fall through to next switch */

					default:
						addchar(ch);
						nextchar(ch);
						break;
				}
				break;

			case STATE_IGQUOTE:
				if (ch != CHAR_NL) {
					addchar(CHAR_ESC);
					addchar(ch);
				}
				nextchar(ch);
				state = STATE_QUOTE;
				break;

			case STATE_COMMENT:
				if (ch == CHAR_NL)
					state = STATE_START;
				nextchar(ch);
				break;

			case STATE_CONST_ST:
				if (ch != '(') { /* If not a constant usage, copy unchanged */
					addchar(CHAR_DOLLAR);
					state = laststate;
					break;
				}
				varpos = 0;
				nextchar(ch);
				state = STATE_CONST_CP;
				break;

			case STATE_CONST_CP:
				if (ch == ')') {
					/*
					 *		Variable name has been fully entered, so now
					 *		expand it.
					 */
					varname[varpos] = CHAR_NULL;
					var = findconst(varname);
					if (!var) {
						scripterror("unknown constant ");
						print2(varname, ".\n");
						Cleanup(10);
					}
					for (p = var->value; *p; p++)
						addchar(*p);
					state = laststate;
				} else {
					/*
					 *		Else still gathering constant name, so keep
					 *		copying into array.
					 */
					if (varpos >= MAXCONST) {
						scripterror("constant name too long.\n");
						Cleanup(10);
					}
					varname[varpos++] = ch;
				}
				nextchar(ch);
				break;
		}
	}
	addchar(CHAR_NULL);	/* Null terminate command string */
	compos = 0;
	comlen = strlen(buf);
	return (comlen);
}

/*
 *		findmacro()
 *		-----------
 *		Searches the macro table for the specified macro. If found, returns
 *		pointer to the macro definition, else returns -1.
 */
int findmacro(name)
char *name;
{
	int i;

	for (i = 0; i < nummacros; i++)
		if (!strcmp(macros[i]->name, name))
			return (i);
	return (-1);
}

/*
 *		getstring()
 *		-----------
 *		This function scans the command buffer starting at position
 *		'compos', and returns a pointer to the next identifier/string in
 *		the buffer. The string is null-terminated, and if it was enclosed
 *		in quotes, these are removed. compos is left pointing to just after
 *		the string.
 */
char *getstring()
{
	char *s = combuf + compos;
	char *p, openquote;

	if (compos > comlen) {
		scripterror("missing parameter\n");
		Cleanup(10);
	}
	if (*s == CHAR_SPACE)
		s++;

	if (*s == CHAR_QUOTE || *s == CHAR_QUOTES) {
		openquote = *s++;
		for (p = s; *p && *p != openquote; p++) {
			if (*p == CHAR_ESC)
				p++;	/* Skip over possible escaped quotes */
		}
		*p = CHAR_NULL;
		compos = (p - combuf) + 1;
		return (s);
	}

	for (p = s; *p && *p != CHAR_SPACE; p++)
		;
	*p = CHAR_NULL;
	compos = (p - combuf) + 1;
	return (s);
}


/*
 *		execline()
 *		----------
 *		This function executes the current line in the command buffer,
 *		handling macro expansion as necessary.
 */
void execline()
{
	char *cmd;
	char *p, *s;
	char *equals;
	MACRO *curmac;
	CONST *var;
	int macronum;
	char *ps[10];
	PARAM *par;
	int length;
	int i;

	chkabort();
	if (tracemode) {
		if (nestlevel > 0) {
			int i;
			for (i = 0; i < nestlevel; i++)
				print("    ");
			print2(combuf, "\n");
		} else {
			print3(itoa(linenum), ":", scriptname);
			print3(": ", combuf, "\n");
		}
	}
	cmd = getstring();
	for (i = 0; com[i].name && strcmp(cmd, com[i].name); i++)
		;
	if (com[i].name) {
		/*
		 *		A valid command was found, so execute it
		 */
		com[i].proc();
	} else if ((macronum = findmacro(cmd)) == -1) {
		/*
		 *		Not a macro -- is it a constant definition?
		 */
		if (compos < comlen) {
			/*
			 *		There's a parameter after it; check is it an equals sign
			 */
			equals = getstring();
			if (equals[0] != CHAR_EQUALS || equals[1] != CHAR_NULL) {
				/*
				 *		Wasn't an equals, so probably just a wrong command
				 *		with some superfluous parameters.
				 */
				scripterror("unknown command ");
				print2(cmd, "\n");
				Cleanup(10);
			}
			/*
			 *		Okay, it's a constant definition. If it's already been
			 *		defined, print a warning but carry on anyway.
			 */
			var = findconst(cmd);
			if (var) {
				scripterror("constant ");
				print2(cmd, " redefined.\n");
			} else
				var = addconst(cmd);
			/*
			 *		Now see was a value specified for the constant.
			 *		If it was, copy it into constant, else just 
			 *		setup a null definition.
			 */
			if (compos < comlen) {
				/*
				 *		Copy user's definition
				 */
				cmd = getstring();
				if (strlen(cmd) >= MAXCONST) {
					scripterror("constant name too long.\n");
					Cleanup(10);
				}
				var->value = mymalloc(strlen(cmd)+1);
				strcpy(var->value, cmd);
			} else {
				/*
				 *		Setup null constant definition
				 */
				var->value = mymalloc(1);
				strcpy(var->value, "");
			}
		} else {
			/*
			 *		Not a command, not a macro, not a constant definition.
			 *		Therefore, must be an error.
			 */
			scripterror("unknown command ");
			print2(cmd, "\n");
			Cleanup(10);
		}
	} else {
		/*
		 *		It's a macro. Save a copy of the macro parameters into
		 *		$0 to $9, and then execute each line of the macro definition,
		 *		expanding parameter usages as necessary.
		 */
		curmac = macros[macronum];
		length = 0;
		if (nestlevel >= MAXNEST) {
			scripterror("macros can only be nested ");
			print2(MAXNEST, " deep\n");
			Cleanup(10);
		}
		par = SafeAllocMem(PARAMSIZE + comlen + 1);
		params[nestlevel++] = par;
		par->size = comlen + 1;

		/*
		 *		Now initialise parameters $0 to $9
		 */
		ps[0] = par->params;
		for (i = 1; i < 10; i++) {
			if (compos < comlen)
				/* Save parameter for later */
				ps[i] = par->params + (getstring() - combuf);
			else
				ps[i] = NULL;
		}
		memcpy(par->params, combuf, comlen+1);

		/*
		 *		Now recursively call execline() with command buffer setup
		 *		to be the current macro buffer line.
		 */
		length = 0;
		s = curmac->text;
		for (length = 0; length < curmac->size;
						length += strlen(s) + 1, s = curmac->text + length) {
			/* Copy macro line into buffer, doing expansion */
			comlen = 0;
			for (p = s; *p; p++) {
				if (*p == '$') {
					p++;
					if (!*p) {
						scripterror("'$' must be followed by something\n");
						Cleanup(10);
					}
					if (isdigit(*p)) {
						int num = *p - '0';

						if (ps[num]) {
							/* Copy macro definition */
							if ((strlen(ps[num]) + comlen) >= MAXCOM - 10)
								goto endcommand;	/* Eek! my first C Goto! */
							strcpy(combuf+comlen, ps[num]);
							comlen += strlen(ps[num]);
						}
						continue;

					}
				}
				combuf[comlen++] = *p;
			}
endcommand:
			combuf[comlen] = CHAR_NULL;
			compos = 0;
			execline();
		}
		/*
		 *		Now free memory allocated for macro parameters
		 */
		FreeMem(par, PARAMSIZE + par->size);
		nestlevel--;
	}
}


/*
 *		execscript()
 *		------------
 *		This function goes through all the commands in the script file,
 *		executing them one by one. Each command is parsed, and then
 *		the appropriate function called. Any errors that occur cause the
 *		script to be aborted, so if this call returns, the script was
 *		successfully executed.
 */
void execscript()
{
	while (readcommand(combuf, MAXCOM))
		execline();
}

/*
 *		====> Now the actual commands follow <====
 */

/*
 *		com_append()
 *		------------
 *		This file opens the specified file for appending. If there was
 *		already a file open, it is closed.
 */

void com_append()
{
	char *filename = getstring();
	BPTR lock;

	com_close();

	if (lock = Lock(filename, ACCESS_READ)) {
		UnLock(lock);
		outfile = Open(filename, MODE_OLDFILE);
	} else
		outfile = Open(filename, MODE_NEWFILE);

	if (!outfile) {
		scripterror("error appending to file ");
		print2(filename, "\n");
		Cleanup(10);
	}
	Seek(outfile, 0, OFFSET_END);
	toscreen = IsInteractive(outfile);
}

/*
 *		com_open()
 *		----------
 *		This command opens a file for output. The output of the ECHO,
 *		FOREIGN and LIST commands goes to this file. If an output file
 *		was already open, it is closed first.
 */
void com_open()
{
	char *filename = getstring();

	com_close();
	outfile = Open(filename, MODE_NEWFILE);
	if (!outfile) {
		scripterror("error opening file ");
		print2(filename, "\n");
		Cleanup(10);
	}
	toscreen = IsInteractive(outfile);
}

/*
 *		com_close()
 *		-----------
 *		Closes the current output file, if any. All future output goes to
 *		stdout.
 */
void com_close()
{
	if (outfile != Output()) {
		flushout();
		Close(outfile);
		outfile = Output();
		toscreen = IsInteractive(outfile);
	}
}

/*
 *		com_config()
 *		------------
 *		This command sets the name of the configuration file used by
 *		CHECKFILES to get the names of the directories to scan from.
 */
void com_config()
{
	strcpy(configname, getstring());
}

/*
 *		com_database()
 *		--------------
 *		This command sets the name of the database file read in
 *		which contains the details of all the file headers.
 */
void com_database()
{
	strcpy(databasename, getstring());
}

/*
 *		com_norequest()
 *		---------------
 *		This command stops AmigaDos from putting up requesters when an
 *		error occurs, or a volume isn't mounted. 
 */
void com_norequest()
{
	struct Process *me = (struct Process *)FindTask(0L);
	me->pr_WindowPtr = (APTR)-1L;
}

/*
 *		com_echo()
 *		----------
 *		This command echoes the specified string to the output file.
 *		A number of meta characters may be present in the string to be
 *		output. See the documentation for more info. If any other parameter
 *		is present on the line after the string, no newline is added
 *		to the output, otherwise a newline is added automatically.
 *
 */
void com_echo()
{
	strcpy(line, getstring());
	if (compos >= comlen) 	/* Add NL if no second parameter */
		strcat(line, "\n");
	putstring(echoformat(out, MAXOUT, line));
}

/*
 *		com_exec()
 *		----------
 *		This command executes the indicated AmigaDOS command. The output
 *		from the command goes into the current output file, if any.
 */
void com_exec()
{
	flushout();
	Execute(getstring(), NULL, outfile);
}

/*
 *		com_format()
 *		------------
 *		This command sets up the format string used by LIST and FOREIGN.
 *		For details of what the format may contain, see the documentation.
 */
void com_format()
{
	strcpy(formatstring, getstring());
	if (compos >= comlen)				/* No second parameter */
		strcat(formatstring, "\n");
}

/*
 *		com_list()
 *		----------
 *		This command is the biggy! It scans through the entire file
 *		database, selecting records as specified with SELECT, and printing
 *		them using the order specified with FORMAT. The order of printing
 *		is as specified with SORT.
 */
void com_list()
{
	int i;

	CHECKDATABASE();
	curbytes = 0;
	curfiles = 0;
    for (i = 0; i < numrecs; i++) {
        if (ptrblock[i]->type == 0 && match(ptrblock[i], tree)) {
            format(out, MAXOUT, formatstring, ptrblock[i], checkfiles);
			putstring(out);
			curfiles++;
			curbytes += ptrblock[i]->length;
		}
		chkabort();
	}
	totalbytes += curbytes;
	totalfiles += curfiles;
}

/*
 *		com_msg()
 *		---------
 *		This command is identical to the ECHO command, except that
 *		the output goes to the screen (i.e. stderr) instead of the
 *		current output file. It is intended for printing informational
 *		messages to let the user know of the program's progress, while
 *		processing a large script.
 */
void com_msg()
{
	strcpy(line, getstring());
	if (compos >= comlen) 	/* Add NL if no second parameter */
		strcat(line, "\n");
	print(echoformat(out, MAXOUT, line));
}

/*
 *		com_reset()
 *		-----------
 *		This command resets the running totals which are maintained, 
 *		which give the total number of bytes and total number of files
 *		output so far.
 */
void com_reset()
{
	totalfiles = 0;
	totalbytes = 0;
}

/*
 *		com_scan()
 *		----------
 *		This command is identical to com_list(), except that no output is
 *		produced. It is provided to allow counters to be updated, without
 *		producing output, so that for example, the total number of files
 *		in a listing can be output before the files themselves.
 */

void com_scan()
{
	int i;

	CHECKDATABASE();
	curbytes = 0;
	curfiles = 0;
    for (i = 0; i < numrecs; i++) {
        if (ptrblock[i]->type == 0 && match(ptrblock[i], tree)) {
			curfiles++;
			curbytes += ptrblock[i]->length;
		}
		chkabort();
	}
	totalbytes += curbytes;
	totalfiles += curfiles;
}

/*
 *		com_macro()
 *		-----------
 *		This command lets you define a macro. The syntax is:
 *
 *			MACRO name
 *				  ..
 *				commands
 *				  ..
 *			ENDM
 *
 *		The macro can contain $1 to $9, which will be substituted at
 *		run time by the appropriate parameters that were passed when
 *		the macro was invoked.
 */
void com_macro()
{
	static char macroname[MACROLEN];
	int curline, curpos;	/* Saves current line # and position in script */
	int length;				/* Length of macro script					   */
	int macronum;
	MACRO *curmac;
	char *buf, *p;

	if (nummacros >= MAXMACRO) {
		scripterror("maximum of ");
		print2(itoa(MAXMACRO), " macros allowed\n");
		Cleanup(10);
	}

	curline = linenum;
	curpos = scriptpos;
	
	p = getstring();
	strncpy(macroname, p, MACROLEN-1);
	macroname[MACROLEN-1] = CHAR_NULL;

	/*
	 *		First of all, find out how much space the macro occupies
	 */
	length = 0;
	while (readcommand(combuf, MAXCOM) && strcmp(combuf, "ENDM"))
		length += comlen + 1;	/* The extra 1 is for the terminating \0 */

	/*
	 *		Now, restore linenumber and script position so we can read
	 *		in macro for real the second time.
	 */
	linenum = curline;
	if (scriptpos >= scriptsize) {
		scripterror("missing ENDM in macro definition\n");
		Cleanup(10);
	}

	scriptpos = curpos;

	if ((macronum = findmacro(macroname)) == -1) 		/* New macro */
		macronum = nummacros++;
	else
		/* Macro redefinition, so release earlier definition */
		FreeMem(macros[macronum], macros[macronum]->size + MACROSIZE);

	curmac = SafeAllocMem(MACROSIZE + length);
	macros[macronum] = curmac;
	strcpy(curmac->name, macroname);
	curmac->size = length;
	buf = curmac->text;

	/*
	 *		Initialised macro definition, now copy macro text from script
	 *		file into macro buffer.
	 */
	while (readcommand(combuf, MAXCOM) && strcmp("ENDM", combuf)) {
		strcpy(buf, combuf);
		buf += comlen + 1;
	}
}

/*
 *		com_trace()
 *		-----------
 *		Turns on or off trace mode. When trace mode is on, every line that
 *		is executed is displayed first. This is handy when executing
 *		macros, to see exactly what is happening. Note that the -t option
 *		on the command line will enable tracing for the whole file,
 *		but if TRACE ON or OFF is encountered, this overrides -t.
 */
void com_trace()
{
	char *opt = getstring();

	if (!strcmp(opt, "ON"))
		tracemode = TRUE;
	else if (!strcmp(opt, "OFF"))
		tracemode = FALSE;
	else {
		scripterror("TRACE ON or TRACE OFF expected\n");
		Cleanup(10);
	}
}

/*
 *		com_ignore()
 *		------------
 *		This command takes a list of filenames. Each filename listed here
 *		is remembered, and when CHECKFILES is done, any filename listed
 *		here will be marked as valid, regardless of whether it really IS
 *		valid or not. This is useful if you have some files online which
 *		are updated by external programs (such as, for example, the output
 *		from BBSINFO), and hence have "wandering" file sizes.
 */
void com_ignore()
{
	IGNORE *ig;
	char *p;

	while (compos < comlen) {
		ig = mymalloc(sizeof(IGNORE));
		p = getstring();
		if (strlen(p) >= CAT_LEN) {
			scripterror("filename too long.\n");
			Cleanup(10);
		}
		strcpy(ig->name, p);
		ig->next = firstignore;
		firstignore = ig;
	}
}
