/*
 * 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
 * 88-11-11 v1.2	fixed some bugs and added environment variables
 * 89-07-12 v1.3	stop appending shell commands, and flush output
 * 89-08-01 v1.4 AB	lots of new options and code
 * 89-10-30 v1.5	-f -S -q options, took some changes from v1.4
 * 90-04-18 v1.6	-b -- -W options, emulate <<, non-BSD cleanup
 */
#include <stdio.h>
#include <ctype.h>
#include <string.h>
#ifdef	MSDOS
#include <stdlib.h>
#endif

#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...] [; shell cmd ]	(target line)
 */
parse(fd)
FILE   *fd;
{
	char   *input;
	char   *ip;
	char   *colonp;
	char    schar;
	int     ntargs, npreqs, nshell;
	int     tmax, pmax, smax;
	targptr *targs;
	fileptr *preqs;
	shellptr *shells;

	if (fd == NULL)
		return (0);

	/* 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", 7))
		{
			/* skip spaces AFTER "include" */
			for (ip += 7; isspace(*ip); ++ip);

			/* process macros in the filename */
			ip = breakout(ip);

			/* parse the makefile */
			if (!parse(fopen(ip, "r")))
				terror(1, tstrcat("cannot open ", ip));

			/* free up the broken-out string */
			tfree(ip);
			continue;	/* get next input line */
		}

		/* display the makefile line ? */
		if (opts.display)
			puts(input);

		/* get rid of comments and preceeding spaces */
		for (colonp = ip; *colonp && *colonp != '#'; ++colonp)
		{
			if (*colonp == '\'' || *colonp == '"')
				colonp = tstrspan(colonp);
		}

		for (--colonp; colonp >= ip && isspace(*colonp); --colonp);

		/* we *know* that some non-space is on this line, from above */
		if (colonp >= ip)
			*++colonp = '\0';

		/* see if we have a shell command */
		if (isspace(*input))
		{
			if (ntargs == 0)
				terror(1, "rules must be after target");
	got_shell:
			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;
		}

		/* don't break out symbols until macro is invoked */
		if (add_macro(ip, 0))
			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, &schar);
		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, &schar);
		}

		/* a target line without a colon?  naughty, naughty! */
		if (!colonp)
			terror(-1, "Unexpected end of line seen");

/*
 *		 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, &schar);

		/* link the pre-req's */
		while (ip)
		{
			if ((colonp = strchr(ip, ';')) != NULL)
			{
				ip[strlen(ip)] = schar;
				*colonp = '\0';
			}

			if (*ip)
			{
				if (npreqs == pmax)
				{
					preqs = (fileptr *)
						grow_list((char **) preqs,
							  &pmax);
				}

				preqs[npreqs++] = add_file(ip);
			}

			if (colonp)
			{
				ip = colonp + 1;
				goto got_shell;
			}

			ip = token(NULL, NULL, &schar);
		}

		/* 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 (1);
}


/*
 * link_targs	- force a list of targs to point to same preq's and shell's
 */
link_targs(targs, preqs, shells)
targptr *targs;
fileptr *preqs;
shellptr *shells;
{
	while (targs && *targs)
	{
		/* process some special targets */
		if ((*targs)->tfile->fname[0] == '.')
		{
			if (equal((*targs)->tfile->fname, ".SILENT"))
				opts.silent = 1;
			else
			if (equal((*targs)->tfile->fname, ".IGNORE"))
				opts.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 (*preqs == NULL && (*targs)->tpreq != NULL)
			{
				tfree((*targs)->tpreq);
				(*targs)->tpreq = NULL;
			}

			/* special rules have their shell commands replaced */
			if ((*targs)->tshell != NULL && *shells != NULL)
			{
				shellptr *sp;

				for (sp = (*targs)->tshell; *sp; ++sp)
					tfree(*sp);
				tfree((*targs)->tshell);
				(*targs)->tshell = NULL;
			}
		}

		/* each target in the list points to the preq's and shell's */
		(*targs)->tpreq = append_preq((*targs)->tpreq, preqs);

		/* we cannot expand the list of shell commands */
		if ((*targs)->tshell != NULL && *shells != NULL)
		{
			terror(1, tstrcat("Too many rules defined for target ",
					  (*targs)->tfile->fname));
		}
		(*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, scmd)
char   *input;
int     scmd;
{
	char   *eqsign;
	char   *value;
	symptr  symp;

	/* gotta have an '=' to be a macro */
	eqsign = strchr(input, '=');
	if (eqsign == NULL)
		return (0);

	/* make sure we catch imbedded '='s (e.g. MACRO=STUFF) */
	for (value = input; *value && !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 */
		*eqsign = '\0';
		symp = get_symbol(input, scmd);
		if (symp->scmd && !scmd)
			return (1);
		if (symp->slevel < make_level)
			symp = dup_symbol(symp, symp->svalue);
		if (symp->svalue)
		{
			eqsign = tstrcat(symp->svalue, " ");
			value = tstrcat(eqsign, value);
			tfree(eqsign);
			tfree(symp->svalue);
			symp->svalue = value;
			return (1);
		}
	}

	add_symbol(input, value, scmd);
	return (1);
}


/*
 * add_symbol	- add a <name,value> pair to the symbol table
 *		- override existing symbol value
 *		- mark as either command-line macro or not
 */
add_symbol(name, value, scmd)
char   *name;
char   *value;
int     scmd;
{
	symptr  symp;

	symp = get_symbol(name, scmd);
	if (symp->scmd & !scmd)
		return;
	if (symp->slevel < make_level)
		symp = dup_symbol(symp, NULL);	/* don't dup the value */
	if (symp->svalue)
		tfree(symp->svalue);
	symp->svalue = tstrcpy(value);
	symp->scmd = scmd;
}


/*
 * 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, scmd)
char   *name;
int     scmd;
{
	symptr  symp;
	t_mask  mask;
	char   *np;

	/* use `mask' to screen out most string comparisons */
	mask = 0;
	np = name;
	while (*np)
		mask += *np++;

	/* 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->scmd = scmd;	/* command line macro? */
	symp->slevel = make_level;	/* current new_make() level */

	/* get the value from the environment, if it is there */

	if ((symp->svalue = getenv(name)) != NULL)
	{
		symp->svalue = tstrcpy(symp->svalue);

		/*
		 * if `-e', let command line macros override, but not macro
		 * assignments in the makefile. 
		 */
		if (opts.envirn)
			symp->scmd = 1;
	}

	symp->snext = symbol_list;	/* link to head of symbol list */
	symbol_list = symp;

	return (symp);
}


/*
 * dup_sym	- duplicate a symbol node, but at current new_make() level
 */
symptr  dup_symbol(symp, value)
symptr  symp;
char   *value;
{
	symptr  nsp;

	nsp = tnew(symnode);	/* allocate symbol node */
	nsp->smask = symp->smask;	/* record mask for later */
	nsp->sname = tstrcpy(symp->sname);	/* allocate string and copy
						 * name */
	nsp->svalue = (value == NULL) ? NULL : tstrcpy(value);
	nsp->scmd = symp->scmd;	/* command line macro? */
	nsp->slevel = make_level;	/* current new_make() level */

	nsp->snext = symbol_list;	/* link to head of symbol list */
	symbol_list = nsp;

	return (nsp);
}


/*
 * add_target	- return extant target node, or create new one
 */
targptr add_target(name)
char   *name;
{
	t_mask  mask;
	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;
{
	targptr targp;
	t_mask  mask;
	char   *np;

	/* use `mask' to screen out most string comparisons */
	mask = 0;
	np = name;
	while (*np)
		mask += *np++;

	/* 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);
}


/*
 * add_file	- return a found or created file node
 */
fileptr add_file(name)
char   *name;
{
	t_mask  mask;
	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;
{
	fileptr filep;
	t_mask  mask;
	char   *np;

	/* use `mask' to screen out most string comparisons */
	mask = 0;
	np = name;
	while (*np)
		mask += *np++;

	/* 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;
{
	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;
{
	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->slink = shell_list;	/* attach to global list */
	shell_list = snode;

	return (snode);
}


/*
 * breakout	- replace macro names with values
 *		- apply recursively
 * note: allocates (and returns) a string which must be freed
 */
char   *breakout(input)
char   *input;
{
	char   *dest, *dend;
	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 '$':
				*dp++ = '$';
				state = 0;
				break;
			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;

			if ((sp - symname) >= (sizeof symname / sizeof symname[0]))
			{
				sp[-1] = '\0';
				terror(1,
				tstrcat("Macro too long (limit 100 chars): ",
					symname));
			}

			++input;/* make sure we skip end char */
			break;

		case 3:
			*sp = '\0';
			symp = get_symbol(symname, 0);
			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 */
}
