/*
 * parse.c
 *
 * 88-10-01 v1.0	created by greg yachuk, placed in the public domain
 * 88-10-06 v1.1	changed prerequisite list handling
 *
 */
#include <stdio.h>
#include <ctype.h>
#include <string.h>

#include "make.h"
#include "tstring.h"
#include "decl.h"

/*
 * parse	- read (text) makefile, and parse
 *		- close file before returing
 *
 * lines have the following format:
 *	# with or without preceeding spaces/tabs	(comment line)
 *	<TAB> commands					(shell line)
 *	name = stuff					(macro)
 *	name += stuff					(macro)
 *	targ [targ...] : [pre-req...]			(target line)
 */
parse(fd)
FILE   *fd;
{
	char   *input;
	char   *ip;
	char   *colonp;
	int	ntargs, npreqs, nshell;
	int	tmax, pmax, smax;
	targptr *targs;
	fileptr *preqs;
	shellptr *shells;

	if (fd == NULL)
		return (FALSE);

	/* start off with a short list of targets */
	targs = (targptr *) grow_list(NULL, &tmax);
	preqs = (fileptr *) grow_list(NULL, &pmax);
	shells = (shellptr *) grow_list(NULL, &smax);

	ntargs = npreqs = nshell = 0;

	/* maximize buffering */
	setvbuf(fd, NULL, _IOFBF, 2048);

	while ((input = tgets(fd)) != NULL)
	{
		/* punt on comments and blank lines */
		for (ip = input; isspace(*ip); ++ip);
		if (*ip == '#' || *ip == '\0')
			continue;

		/* process include files */
		if (!strncmp(ip, "include ", 8))
		{
			/* skip spaces AFTER "include " */
			for (ip+=8; isspace(*ip); ++ip);
			if (!parse(fopen(ip, "r")))
				terror(1, tstrcat("cannot open ", ip));
			continue;		/* get next input line */
		}

		/* display the makefile line ? */
		if (display)
			puts(input);

		/* see if we have a shell command */
		if (isspace(*input))
		{
			if (ntargs == 0)
				terror(1, "rules must be after target");

			if (nshell == smax)
				shells = (shellptr *) grow_list((char **)shells,
								&smax);
			shells[nshell++] = add_shell(ip);
			continue;
		}

		/* not a shell line, so must be a target or a macro */
		if (ntargs != 0)
		{
			/* link previous preq's and shell's */
			targs[ntargs] = NULL;
			preqs[npreqs] = NULL;
			shells[nshell] = NULL;
			link_targs(targs, preqs, shells);
			ntargs = npreqs = nshell = 0;
		}

		if ((colonp = strchr(ip, '#')) != NULL)
			*colonp = '\0';		/* get rid of comment */

		/* don't break out symbols until macro is invoked */
		if (add_macro(ip))
			continue;

		/* okay, we have a target line; break out macro symbols */
		input = breakout(ip);

		/* just look for tokens with standard isspace() separators */
		ip = token(input, NULL);
		while (ip)
		{
			colonp = strchr(ip, ':');
#ifdef	MSDOS
			/* need to allow c:/bin/make.exe as a target */
			if (colonp && colonp - ip == 1)
				colonp = strchr(colonp+1, ':');
#endif
			if (colonp)
			{
				/* got a separator */
				*colonp = '\0';

				/* if at front of token, target is done */
				if (colonp == ip)
					break;
			}

			if (ntargs == tmax)
				targs = (targptr *) grow_list((char **) targs,
							      &tmax);
			targs[ntargs] = add_target(ip);

			/* make sure we don't save .INIT as our 1st target */
			if (first_targ == NULL && *ip != '.')
				first_targ = targs[ntargs];
			++ntargs;

			if (colonp)
				break;
			ip = token(NULL, NULL);
		}

		/*
		 * taking care of four possible cases:
		 * 	1)	object : source
		 *	2)	object: source
		 *	3)	object :source
		 *	4)	object:source
		 */

		if (colonp && *++colonp)
			ip = colonp;
		else
			ip = token(NULL, NULL);

		/* link the pre-req's */
		while (ip)
		{
			if (npreqs == pmax)
				preqs = (fileptr *) grow_list((char **) preqs,
							      &pmax);
			preqs[npreqs++] = add_file(ip);
			ip = token(NULL, NULL);
		}

		/* gotta free the line allocated by breakout() */
		tfree(input);
	}

	/* link up any dangling dependants */
	if (ntargs != 0)
	{
		targs[ntargs] = NULL;
		preqs[npreqs] = NULL;
		shells[nshell] = NULL;
		link_targs(targs, preqs, shells);
	}

	/* clean up our mallocs */
	tfree(targs);
	tfree(preqs);
	tfree(shells);

	fclose(fd);
	return (TRUE);
}


/*
 * link_targs	- force a list of targs to point to same preq's and shell's
 */
link_targs(targs, preqs, shells)
REGISTER targptr *targs;
fileptr *preqs;
shellptr *shells;
{
	REGISTER int plen;

	for (plen = 0; preqs[plen] != NULL; ++plen);

	while (*targs)
	{
		/* process some special targets */
		if ((*targs)->tfile->fname[0] == '.')
		{
			if (equal((*targs)->tfile->fname, ".SILENT"))
				silent = 1;
			else if (equal((*targs)->tfile->fname, ".IGNORE"))
				ignore = 1;
			else if (equal((*targs)->tfile->fname, ".SUFFIXES"))
			     /* set `suffix_targ' to speed up `default_rule' */
				suffix_targ = *targs;

			/* special rule has preq's reset */
			/* normally, preq's are merely appended */
			if (plen == 0)
			{
				tfree((*targs)->tpreq);
				(*targs)->tpreq = NULL;
			}
		}

		/* each target in the list points to the preq's and shell's */
		(*targs)->tpreq = append_preq((*targs)->tpreq, preqs);
		(*targs)->tshell = append_shell((*targs)->tshell, shells);
		++targs;
	}
}


/* macros must have the format: WORD = more stuff
 *			     	WORD= more stuff
 *				WORD =more stuff
 *				WORD=more stuff
 *			or:	WORD += more stuff
 *				WORD +=more stuff
 *
 * it is assumed that there is no leading whitespace in `input'
 */
add_macro(input)
char   *input;
{
	REGISTER char *eqsign;
	REGISTER char *value;
	symptr	symp;

	/* gotta have an '=' to be a macro */
	eqsign = strchr(input, '=');
	if (eqsign == NULL)
		return (FALSE);

	/* make sure we catch imbedded '='s (e.g. MACRO=STUFF) */
	for (value=input; !isspace(*value); ++value);
	if (value > eqsign)
		value = eqsign;

	/* terminate the macro name */
	*value++ = '\0';

	/* find start of value */
	for (value = eqsign+1; isspace(*value); ++value);

	/* look for concat character */
	--eqsign;

	if (eqsign < input || (eqsign == input && *eqsign == '+'))
		terror(1, "Badly formed macro");

	if (*eqsign == '+')
	{
		/* append to the current macro definition */
		symp = get_symbol(input);
		if (symp->svalue)
		{
			eqsign = tstrcat(symp->svalue, " ");
			value = tstrcat(eqsign, value);
			tfree(eqsign);
			tfree(symp->svalue);
			symp->svalue = value;
			return (TRUE);
		}
	}

	add_symbol(input, value);
	return (TRUE);
}


/*
 * add_symbol	- add a <name,value> pair to the symbol table
 *		- override existing symbol value
 */
add_symbol(name, value)
char   *name;
char   *value;
{
	REGISTER symptr symp;

	symp = get_symbol(name);
	if (symp->svalue)
		tfree(symp->svalue);
	symp->svalue = tstrcpy(value);
}


/*
 * get_symbol	- find a symbol in the symbol table
 *		- if non-extant, create <name,NULL>
 *		- return created or found symbol node
 */
symptr
get_symbol(name)
char   *name;
{
	REGISTER symptr symp;
	REGISTER t_mask mask;

	/* use `mask' to screen out most string comparisons */
	mask = hash_name(name);

	/* linear search through symbol list */
	for (symp = symbol_list; symp != NULL; symp = symp->snext)
	{
		if (mask != symp->smask)
			continue;

		if (equal(name, symp->sname))
			return (symp);
	}

	symp = tnew(symnode);		/* allocate symbol node */
	symp->smask = mask;		/* record mask for later */
	symp->sname = tstrcpy(name);	/* allocate string and copy name */
	symp->svalue = NULL;		/* NULL value */

	symp->snext = symbol_list;	/* link to head of symbol list */
	symbol_list = symp;

	return (symp);
}


/*
 * add_target	- return extant target node, or create new one
 */
targptr
add_target(name)
char   *name;
{
	t_mask	mask;
	REGISTER targptr targp;
	fileptr	filep;

	/* each target must have a file node */
	filep = add_file(name);

	/* see if target already exists */
	targp = hash_target(name, &mask);
	if (targp)
		return (targp);

	/* oh well, gotta create one */
	targp = tnew(targnode);		/* allocate a target node */
	targp->tmask = mask;		/* save mask for later */
	targp->tfile = filep;		/* save pointer to file node */
	targp->tpreq = NULL;		/* no pre-req's yet */
	targp->tshell = NULL;		/* no shell lines yet */

	targp->tnext = target_list;	/* link to front of target list */
	target_list = targp;

	return (targp);
}


/*
 * hash_target	- look up target (by name) in target list
 *		- return target node or NULL
 *		- if requested, also return the mask
 */
targptr
hash_target(name, maskp)
char   *name;
t_mask *maskp;
{
	REGISTER targptr targp;
	REGISTER t_mask mask;

	/* use `mask' to screen out most string comparisons */
	mask = hash_name(name);

	/* see if we gotta return it */
	if (maskp != NULL)
		*maskp = mask;

	/* linear search through target list */
	for (targp = target_list; targp != NULL; targp = targp->tnext)
	{
		if (mask != targp->tmask)
			continue;

		/* target name is ONLY stored in the file node */
		if (equal(name, targp->tfile->fname))
			return(targp);
	}

	/* nope, no target here */
	return (NULL);
}


/*
 * hash_name	- transform a character string into an integer
 */
t_mask
hash_name(name)
char   *name;
{
	REGISTER char c;
	REGISTER t_mask mask;
	int	bucket;

	/*
	 * take the sum of each pair of characters, and set a bit
	 * for this sum modulo the mask size (word size) in bits.
	 *
	 * e.g.	string:	c0, c1, c2, c3
	 *	set bit for (c0+c1) % mask_size
	 *	set bit for (c1+c2) % mask_size
	 *	set bit for (c2+c3) % mask_size
	 *	set bit for (c3)    % mask_size	(end condition)
	 *
	 * this gives a fair distribution, with not TOO much work
	 */
	mask = 0;
	while (*name) {
		c = *name++;
		mask |= 1L << ((c + *name) % (sizeof(mask) * 8));
	}

	return (mask);
}

/*
 * add_file	- return a found or created file node
 */
fileptr
add_file(name)
char   *name;
{
	t_mask	mask;
	REGISTER fileptr filep;

	/* see if file node already exists */
	filep = hash_file(name, &mask);
	if (filep)
		return (filep);

	filep = tnew(filenode);		/* allocate new file node */
	filep->fmask = mask;		/* save mask for later */
	filep->fname = tstrcpy(name);	/* allocate string and copy name */
	filep->ftime = MAXNEGTIME;	/* init MODIFY time to long time ago */

	filep->fnext = file_list;	/* link to head of file list */
	file_list = filep;

	return (filep);
}


/*
 * hash_file	- look up file (by name) in file list
 *		- return file node or NULL
 *		- if requested, also return the mask
 */
fileptr
hash_file(name, maskp)
char   *name;
t_mask *maskp;
{
	REGISTER fileptr filep;
	REGISTER t_mask mask;

	/* use `mask' to screen out most string comparisons */
	mask = hash_name(name);

	/* see if we gotta return it */
	if (maskp != NULL)
		*maskp = mask;

	/* linear search through file list */
	for (filep = file_list; filep != NULL; filep = filep->fnext)
	{
		if (filep->fmask != mask)
			continue;

		if (equal(filep->fname, name))
			return (filep);
	}

	/* nope, no file here */
	return (NULL);
}


/*
 * append_node	- add a node to the end of an array of nodes
 */
char  **
append_node(node, adds, size)
char  **node;
char  **adds;
int	size;
{
	REGISTER int addlen, len;

	for (addlen = 0; adds[addlen] != NULL; ++addlen);
	if (addlen++ == 0)
		return (node);

	len = 0;

	if (node != NULL)
	{
		for (; node[len] != NULL; ++len);
		node = (char **) trealloc((char *) node, (len+addlen) * size);
	}
	else
		node = (char **) talloc(addlen * size);

	memcpy(node+len, adds, addlen * size);
	return (node);
}

/*
 * add_shell	- create a new shell node, and add to end of given list
 */
shellptr
add_shell(input)
char   *input;
{
	REGISTER shellptr snode;

	snode = tnew(shellnode);	/* allocate a new shell node */
	snode->s_shell = snode->s_ignore = snode->s_silent = 0;

	for (;isspace(*input); ++input);	/* skip over leading spaces */
	for (;; ++input)
	{
		if (*input == '+')
			snode->s_shell = 1;	/* must use command.com */
		else if (*input == '-')
			snode->s_ignore = 1;	/* ignore return value */
		else if (*input == '@')
			snode->s_silent = 1;	/* don't echo command */
		else
			break;
	}

	snode->scmd = tstrcpy(input);	/* allocate string and copy command */
	snode->snext = NULL;		/* no list of shells for a targ, yet */

	shell_list = snode->slink;

	return (snode);
}


/*
 * breakout	- replace macro names with values
 *		- apply recursively
 * note: allocates (and returns) a string which must be freed
 */
char   *
breakout(input)
REGISTER char *input;
{
	char   *dest, *dend;
	REGISTER char *dp;
	int	dlen;
	int	tlen;
	int	state;
	char    symname[100];
	char   *sp;
	symptr	symp;
	int	slen;
	char	endch;

	/* allocate a string twice as long as input string */
	dlen = strlen(input) * 2;
	dest = dp = talloc(dlen);
	dend = dest + dlen;

	/*
	 * state machine with 4 states
	 *	0)	normal text	-- just copy
	 *	1)	starting macro	-- define end char (e.g. ')', '}')
	 *	2)	macro name	-- copy to a buffer
	 *	3)	end of macro	-- look up value, and copy
	 */
	state = 0;

	while (*input || state == 3)
	{
		/* if we don't have enough room, double size of string */
		if (dp == dend)
		{
			dlen *= 2;
			tlen = dp - dest;
			dest = trealloc(dest, dlen);
			dp = dest + tlen;
			dend = dest + dlen;
		}

		switch (state)
		{
		case 0:	if (*input == '$')
				state = 1;	/* found a macro */
			else
				*dp++ = *input++;
			break;

		case 1:	state = 2;	/* only in this state for 1 char */
			sp = symname;
			switch (*++input)
			{
			case '(':
				endch = ')';
				break;
			case '{':
				endch = '}';
				break;
			default:
				/* single char; go to state 3 immediately */
				*sp++ = *input;
				state = 3;
				break;
			}
			++input;	/* skip bracket (or character) */
			break;

		case 2:	if (*input == endch)
				state = 3;
			else
				*sp++ = *input;
			++input;	/* make sure we skip end char */
			break;

		case 3:	*sp = '\0';
			symp = get_symbol(symname);
			sp = symp->svalue;
			slen = -1;
			while (sp && *sp)
			{
				/* if value has a macro in it,
				 * we must process recursively
				 */
				if (*sp == '$')
				{
					sp = breakout(symp->svalue);
					/* now guaranteed not to have a '$' */
					slen = strlen(sp);
					break;
				}
				++sp;
			}

			if (slen == -1)
			{
				/* value did NOT have a macro */
				slen = (sp - symp->svalue);
				sp = symp->svalue;
			}

			/* if we have not enough room, expand */
			if (slen >= (dend - dp))
			{
				/* use slen to make sure that we can fit */
				dlen = dlen * 2 + slen;
				tlen = dp - dest;
				dest = trealloc(dest, dlen);
				dp = dest + tlen;
				dend = dest + dlen;
			}

			/* if length is zero, don't bother to copy */
			if (slen)
			{
				strcpy(dp, sp);
				dp += slen;
			}

			if (sp != symp->svalue)
				tfree(sp);	/* must've called `breakout' */

			state = 0;		/* and we are back to text */
			break;
		}
	}

	if (state != 0)
		terror(1, tstrcat("Improper macro.\n", dest));

	*dp = '\0';			/* terminate the string */
	return (dest);			/* and return it */
}
