/*
*   Copyright (c) 2000-2002, Darren Hiebert
*
*   This source code is released for free distribution under the terms of the
*   GNU General Public License version 2 or (at your option) any later version.
*
*   This module contains functions for generating tags for scripts for the
*   Bourne shell (and its derivatives, the Korn and Z shells).
*/

/*
*   INCLUDE FILES
*/
#include "general.h"  /* must always come first */

#include <string.h>

#include "entry.h"
#include "kind.h"
#include "parse.h"
#include "read.h"
#include "promise.h"
#include "routines.h"
#include "vstring.h"
#include "xtag.h"

/*
*   DATA DEFINITIONS
*/
typedef enum {
	K_NOTHING = -1,		/* place holder. Never appears on tags file. */
	K_ALIAS,
	K_FUNCTION,
	K_SOURCE,
	K_HEREDOCLABEL,
} shKind;

typedef enum {
	R_SCRIPT_LOADED,
} shScriptRole;

static roleDefinition ShScriptRoles [] = {
	{ true, "loaded", "loaded" },
};

typedef enum {
	R_HEREDOC_ENDMARKER,
} shHeredocRole;

static roleDefinition ShHeredocRoles [] = {
	{ true, "endmarker", "end marker" },
};

static kindDefinition ShKinds [] = {
	{ true, 'a', "alias", "aliases"},
	{ true, 'f', "function", "functions"},
	{ true, 's', "script", "script files",
	  .referenceOnly = true, ATTACH_ROLES (ShScriptRoles) },
	{ true, 'h', "heredoc", "label for here document",
	  .referenceOnly = false, ATTACH_ROLES (ShHeredocRoles) },
};

/*
*   FUNCTION DEFINITIONS
*/

static bool isFileChar  (int c)
{
	return (isalnum (c)
		|| c == '_' || c == '-'
		|| c == '/' || c == '.'
		|| c == '+' || c == '^'
		|| c == '%' || c == '@'
		|| c == '~');
}

static bool isIdentChar (int c)
{
	return (isalnum (c) || c == '_' || c == '-');
}

/* bash allows all kinds of crazy stuff as the identifier after 'function' */
static bool isBashFunctionChar (int c)
{
	return (c > 1 /* NUL and SOH are disallowed */ && c != 0x7f &&
	        /* blanks are disallowed, but VT and FF (and CR to some extent, but
	         * let's not fall into the pit of craziness) */
	        c != ' ' && c != '\t' && c != '\n' && c != '\r' &&
	        c != '"' && c != '\'' && c != '$' && c != '`' && c != '\\' &&
	        c != '&' && c != ';' &&
	        c != '(' && c != ')' &&
	        c != '<' && c != '>');
}

static const unsigned char *skipDoubleString (const unsigned char *cp)
{
	const unsigned char* prev = cp;
	cp++;
	while ((*cp != '"' || *prev == '\\') && *cp != '\0')
	{
		prev = cp;
		cp++;
	}
	return cp;
}

static const unsigned char *skipSingleString (const unsigned char *cp)
{
	cp++;
	while (*cp != '\'' && *cp != '\0')
		cp++;
	return cp;
}

static bool isEnvCommand (const vString *cmd)
{
	const char *lc = vStringValue(cmd);
	const char * tmp = baseFilename (lc);

	return (strcmp(tmp, "env") == 0);
}

static int readDestfileName (const unsigned char *cp, vString *destfile)
{
	const unsigned char *origin = cp;

	while (isspace ((int) *cp))
		++cp;

	/* >... */
	if (*cp != '>')
		return 0;

	/* >>... */
	if (*cp == '>')
		++cp;

	while (isspace ((int) *cp))
		++cp;

	if (!isFileChar ((int) *cp))
		return 0;

	vStringClear(destfile);
	do {
		vStringPut (destfile, (int) *cp);
		++cp;
	} while (isFileChar ((int) *cp));

	if (vStringLength(destfile) > 0)
		return cp - origin;

	return 0;
}

struct hereDocParsingState {
	vString *args[2];
	vString *destfile;
	langType sublang;
	unsigned long startLine;

	int corkIndex;
};

static void hdocStateInit (struct hereDocParsingState *hstate)
{
	hstate->args[0] = vStringNew ();
	hstate->args[1] = vStringNew ();
	hstate->destfile = vStringNew ();

	hstate->corkIndex = CORK_NIL;
	hstate->sublang = LANG_IGNORE;
}

static void hdocStateClear (struct hereDocParsingState *hstate)
{
	vStringClear (hstate->args[0]);
	vStringClear (hstate->args[1]);
	vStringClear (hstate->destfile);
}

static void hdocStateFini (struct hereDocParsingState *hstate)
{
	vStringDelete (hstate->args[0]);
	vStringDelete (hstate->args[1]);
	vStringDelete (hstate->destfile);
}

static void hdocStateUpdateArgs (struct hereDocParsingState *hstate,
										   vString *name)
{
	if (vStringIsEmpty(hstate->args[0]))
		vStringCopy(hstate->args[0], name);
	else if (vStringIsEmpty(hstate->args[1]))
		vStringCopy(hstate->args[1], name);
}

static void hdocStateMakePromiseMaybe (struct hereDocParsingState *hstate)
{
	if (hstate->sublang != LANG_IGNORE)
		makePromise (getLanguageName(hstate->sublang),
					 hstate->startLine, 0,
					 getInputLineNumber(), 0,
					 0);
	hstate->sublang = LANG_IGNORE;
}

static void hdocStateRecordStartlineFromDestfileMaybe (struct hereDocParsingState *hstate)
{
	const char *f = vStringValue(hstate->destfile);

	if (hstate->sublang != LANG_IGNORE)
		return;

	hstate->sublang = getLanguageForFilename (f, 0);
	if (hstate->sublang != LANG_IGNORE)
		hstate->startLine = getInputLineNumber () + 1;
	vStringClear (hstate->destfile);
}

static void hdocStateRecordStatelineMaybe (struct hereDocParsingState *hstate)
{
	if (!vStringIsEmpty(hstate->args[0]))
	{
		const char *cmd;

		cmd = vStringValue(hstate->args[0]);
		if (isEnvCommand (hstate->args[0]))
		{
			cmd = NULL;
			if (!vStringIsEmpty(hstate->args[1]))
				cmd = vStringValue(hstate->args[1]);
		}

		if (cmd)
		{
			hstate->sublang = getLanguageForCommand (cmd, 0);
			if (hstate->sublang != LANG_IGNORE)
				hstate->startLine = getInputLineNumber () + 1;
		}
	}

	if (vStringLength(hstate->destfile) > 0)
		hdocStateRecordStartlineFromDestfileMaybe (hstate);
}

static int hdocStateReadDestfileName (struct hereDocParsingState *hstate,
									  const unsigned char* cp,
									  const vString *const hereDocDelimiter)
{
	int d = readDestfileName (cp, hstate->destfile);

	if (d > 0 && hereDocDelimiter)
		hdocStateRecordStartlineFromDestfileMaybe (hstate);

	return d;
}

static void hdocStateUpdateTag (struct hereDocParsingState *hstate, unsigned long endLine)
{
	if (hstate->corkIndex != CORK_NIL)
	{
		tagEntryInfo *tag = getEntryInCorkQueue (hstate->corkIndex);
		tag->extensionFields.endLine = endLine;
		hstate->corkIndex = CORK_NIL;
	}
}

static void findShTags (void)
{
	vString *name = vStringNew ();
	const unsigned char *line;
	vString *hereDocDelimiter = NULL;
	bool hereDocIndented = false;
	bool (* check_char)(int);

	struct hereDocParsingState hstate;
	hdocStateInit (&hstate);

	while ((line = readLineFromInputFile ()) != NULL)
	{
		const unsigned char* cp = line;
		shKind found_kind = K_NOTHING;

		if (hereDocDelimiter)
		{
			if (hereDocIndented)
			{
				while (*cp == '\t')
					cp++;
			}
			if (strcmp ((const char *) cp, vStringValue (hereDocDelimiter)) == 0)
			{
				hdocStateUpdateTag (&hstate, getInputLineNumber ());
				hdocStateMakePromiseMaybe (&hstate);

				if (!vStringIsEmpty(hereDocDelimiter))
					makeSimpleRefTag(hereDocDelimiter, K_HEREDOCLABEL, R_HEREDOC_ENDMARKER);
				vStringDelete (hereDocDelimiter);
				hereDocDelimiter = NULL;
			}
			continue;
		}

		hdocStateClear (&hstate);
		while (*cp != '\0')
		{
			/* jump over whitespace */
			while (isspace ((int)*cp))
				cp++;

			/* jump over strings */
			if (*cp == '"')
				cp = skipDoubleString (cp);
			else if (*cp == '\'')
				cp = skipSingleString (cp);
			/* jump over comments */
			else if (*cp == '#')
				break;
			/* jump over here-documents */
			else if (cp[0] == '<' && cp[1] == '<')
			{
				const unsigned char *start, *end;
				bool trimEscapeSequences = false;
				bool quoted = false;
				cp += 2;
				/* an optional "-" strips leading tabulations from the heredoc lines */
				if (*cp != '-')
					hereDocIndented = false;
				else
				{
					hereDocIndented = true;
					cp++;
				}
				while (isspace (*cp))
					cp++;
				start = end = cp;
				/* the delimiter can be surrounded by quotes */
				if (*cp == '"')
				{
					start++;
					end = cp = skipDoubleString (cp);
					/* we need not to worry about variable substitution, they
					 * don't happen in heredoc delimiter definition */
					trimEscapeSequences = true;
					quoted = true;
				}
				else if (*cp == '\'')
				{
					start++;
					end = cp = skipSingleString (cp);
					quoted = true;
				}
				else
				{
					while (isIdentChar ((int) *cp))
						cp++;
					end = cp;
				}
				if (end > start || quoted)
				{
					/* The input may be broken as a shell script but we need to avoid
					   memory leaking. */
					if (hereDocDelimiter)
						vStringClear(hereDocDelimiter);
					else
						hereDocDelimiter = vStringNew ();
					for (; end > start; start++)
					{
						if (trimEscapeSequences && *start == '\\')
							start++;
						vStringPut (hereDocDelimiter, *start);
					}
					if (vStringLength(hereDocDelimiter) > 0)
						hstate.corkIndex = makeSimpleTag(hereDocDelimiter, K_HEREDOCLABEL);

					hdocStateRecordStatelineMaybe(&hstate);
				}
			}

			check_char = isBashFunctionChar;

			if (strncmp ((const char*) cp, "function", (size_t) 8) == 0  &&
				isspace ((int) cp [8]))
			{
				found_kind = K_FUNCTION;
				cp += 8;
			}
			else if (strncmp ((const char*) cp, "alias", (size_t) 5) == 0  &&
				isspace ((int) cp [5]))
			{
				check_char = isIdentChar;
				found_kind = K_ALIAS;
				cp += 5;
			}
			else if (cp [0] == '.'
				    && isspace((int) cp [1]))
			{
				found_kind = K_SOURCE;
				++cp;
				check_char = isFileChar;
			}
			else if (strncmp ((const char*) cp, "source", (size_t) 6) == 0
					 && isspace((int) cp [6]))
			{
				found_kind = K_SOURCE;
				cp += 6;
				check_char = isFileChar;
			}

			if (found_kind != K_NOTHING)
				while (isspace ((int) *cp))
					++cp;

			// Get the name of the function, alias or file to be read by source
			if (! check_char ((int) *cp))
			{
				found_kind = K_NOTHING;

				int d = hdocStateReadDestfileName (&hstate, cp,
												   hereDocDelimiter);
				if (d > 0)
					cp += d;
				else if (*cp != '\0')
					++cp;
				continue;
			}
			while (check_char ((int) *cp))
			{
				vStringPut (name, (int) *cp);
				++cp;
			}

			while (isspace ((int) *cp))
				++cp;

			if ((found_kind != K_SOURCE)
			    && *cp == '(')
			{
				++cp;
				while (isspace ((int) *cp))
					++cp;
				if (*cp == ')')
				{
					found_kind = K_FUNCTION;
					++cp;
				}
			}

			if (found_kind != K_NOTHING)
			{
				if (found_kind == K_SOURCE)
						makeSimpleRefTag (name, K_SOURCE, R_SCRIPT_LOADED);
				else
					makeSimpleTag (name, found_kind);
				found_kind = K_NOTHING;
			}
			else if (!hereDocDelimiter)
				hdocStateUpdateArgs (&hstate, name);
			vStringClear (name);
		}
	}
	hdocStateFini (&hstate);
	vStringDelete (name);
	if (hereDocDelimiter)
		vStringDelete (hereDocDelimiter);
}

extern parserDefinition* ShParser (void)
{
	static const char *const extensions [] = {
		"sh", "SH", "bsh", "bash", "ksh", "zsh", "ash", NULL
	};
	static const char *const aliases [] = {
		"sh", "bash", "ksh", "zsh", "ash",
		/* major mode name in emacs */
		"shell-script",
		NULL
	};
	parserDefinition* def = parserNew ("Sh");
	def->kindTable      = ShKinds;
	def->kindCount  = ARRAY_SIZE (ShKinds);
	def->extensions = extensions;
	def->aliases = aliases;
	def->parser     = findShTags;
	def->useCork    = true;
	return def;
}
