/*
*	$Id: vim.c 762 2010-07-28 11:38:19Z dfishburn $
*
*	Copyright (c) 2000-2003, Darren Hiebert
*
*	This source code is released for free distribution under the terms of the
*	GNU General Public License.
*
*	Thanks are due to Jay Glanville for significant improvements.
*
*	This module contains functions for generating tags for user-defined
*	functions for the Vim editor.
*/

/*
*	INCLUDE FILES
*/
#include "libc/ctype.h"
#include "third_party/ctags/general.h"  /* must always come first */

#include "libc/mem/alg.h"
#include "libc/str/str.h"
#include "libc/runtime/runtime.h"
#ifdef DEBUG
#include "libc/calls/calls.h"
#include "libc/stdio/dprintf.h"
#include "libc/calls/weirdtypes.h"
#include "libc/stdio/stdio.h"
#include "libc/temp.h"
#include "third_party/musl/tempnam.h"
#endif


#include "third_party/ctags/parse.h"
#include "third_party/ctags/read.h"
#include "third_party/ctags/vstring.h"

#if 0
typedef struct sLineInfo {
	tokenType	type;
	keywordId	keyword;
	vString *	string;
	vString *	scope;
	unsigned long lineNumber;
	fpos_t filePosition;
} lineInfo;
#endif

/*
*	DATA DEFINITIONS
*/
typedef enum {
	K_AUGROUP,
	K_COMMAND,
	K_FUNCTION,
	K_MAP,
	K_VARIABLE
} vimKind;

static kindOption VimKinds [] = {
	{ TRUE,  'a', "augroup",  "autocommand groups" },
	{ TRUE,  'c', "command",  "user-defined commands" },
	{ TRUE,  'f', "function", "function definitions" },
	{ TRUE,  'm', "map",      "maps" },
	{ TRUE,  'v', "variable", "variable definitions" },
};

/*
 *	 DATA DECLARATIONS
 */

#if 0
typedef enum eException {
	ExceptionNone, ExceptionEOF 
} exception_t;
#endif

/*
 *	DATA DEFINITIONS
 */

#if 0
static jmp_buf Exception;
#endif

/*
 *	FUNCTION DEFINITIONS
 */

/* This function takes a char pointer, tries to find a scope separator in the
 * string, and if it does, returns a pointer to the character after the colon,
 * and the character defining the scope.
 * If a colon is not found, it returns the original pointer.
 */
static const unsigned char* skipPrefix (const unsigned char* name, int *scope)
{
	const unsigned char* result = name;
	int counter;
	size_t length;
	length = strlen((const char*)name);
	if (scope != NULL)
		*scope = '\0';
	if (length > 3 && name[1] == ':')
	{
		if (scope != NULL)
			*scope = *name;
		result = name + 2;
	}
	else if (length > 5 && strncasecmp ((const char*) name, "<SID>", (size_t) 5) == 0)
	{
		if (scope != NULL)
			*scope = *name;
		result = name + 5;
	}
	else
	{
		/*
		 * Vim7 check for dictionaries or autoload function names
		 */
		counter = 0;
		do
		{
			switch ( name[counter] )
			{
				case '.':
					/* Set the scope to d - Dictionary */
					*scope = 'd';
					break;
				case '#':
					/* Set the scope to a - autoload */
					*scope = 'a';
					break;
			}
			++counter;
		} while (isalnum ((int) name[counter]) ||  
				name[counter] == '_'		   ||  
				name[counter] == '.'		   ||  
				name[counter] == '#'
				);
	}
	return result;
}

static boolean isMap (const unsigned char* line)
{
	/*
	 * There are many different short cuts for specifying a map.
	 * This routine should capture all the permutations.
	 */
	if (
			strncmp ((const char*) line, "map",      (size_t) 3) == 0 ||
			strncmp ((const char*) line, "nm",       (size_t) 2) == 0 ||
			strncmp ((const char*) line, "nma",      (size_t) 3) == 0 ||
			strncmp ((const char*) line, "nmap",     (size_t) 4) == 0 ||
			strncmp ((const char*) line, "vm",       (size_t) 2) == 0 ||
			strncmp ((const char*) line, "vma",      (size_t) 3) == 0 ||
			strncmp ((const char*) line, "vmap",     (size_t) 4) == 0 ||
			strncmp ((const char*) line, "om",       (size_t) 2) == 0 ||
			strncmp ((const char*) line, "oma",      (size_t) 3) == 0 ||
			strncmp ((const char*) line, "omap",     (size_t) 4) == 0 ||
			strncmp ((const char*) line, "im",       (size_t) 2) == 0 ||
			strncmp ((const char*) line, "ima",      (size_t) 3) == 0 ||
			strncmp ((const char*) line, "imap",     (size_t) 4) == 0 ||
			strncmp ((const char*) line, "lm",       (size_t) 2) == 0 ||
			strncmp ((const char*) line, "lma",      (size_t) 3) == 0 ||
			strncmp ((const char*) line, "lmap",     (size_t) 4) == 0 ||
			strncmp ((const char*) line, "cm",       (size_t) 2) == 0 ||
			strncmp ((const char*) line, "cma",      (size_t) 3) == 0 ||
			strncmp ((const char*) line, "cmap",     (size_t) 4) == 0 ||
			strncmp ((const char*) line, "no",       (size_t) 2) == 0 ||
			strncmp ((const char*) line, "nor",      (size_t) 3) == 0 ||
			strncmp ((const char*) line, "nore",     (size_t) 4) == 0 ||
			strncmp ((const char*) line, "norem",    (size_t) 5) == 0 ||
			strncmp ((const char*) line, "norema",   (size_t) 6) == 0 ||
			strncmp ((const char*) line, "noremap",  (size_t) 7) == 0 ||
			strncmp ((const char*) line, "nno",      (size_t) 3) == 0 ||
			strncmp ((const char*) line, "nnor",     (size_t) 4) == 0 ||
			strncmp ((const char*) line, "nnore",    (size_t) 5) == 0 ||
			strncmp ((const char*) line, "nnorem",   (size_t) 6) == 0 ||
			strncmp ((const char*) line, "nnorema",  (size_t) 7) == 0 ||
			strncmp ((const char*) line, "nnoremap", (size_t) 8) == 0 ||
			strncmp ((const char*) line, "vno",      (size_t) 3) == 0 ||
			strncmp ((const char*) line, "vnor",     (size_t) 4) == 0 ||
			strncmp ((const char*) line, "vnore",    (size_t) 5) == 0 ||
			strncmp ((const char*) line, "vnorem",   (size_t) 6) == 0 ||
			strncmp ((const char*) line, "vnorema",  (size_t) 7) == 0 ||
			strncmp ((const char*) line, "vnoremap", (size_t) 8) == 0 ||
			strncmp ((const char*) line, "ono",      (size_t) 3) == 0 ||
			strncmp ((const char*) line, "onor",     (size_t) 4) == 0 ||
			strncmp ((const char*) line, "onore",    (size_t) 5) == 0 ||
			strncmp ((const char*) line, "onorem",   (size_t) 6) == 0 ||
			strncmp ((const char*) line, "onorema",  (size_t) 7) == 0 ||
			strncmp ((const char*) line, "onoremap", (size_t) 8) == 0 ||
			strncmp ((const char*) line, "ino",      (size_t) 3) == 0 ||
			strncmp ((const char*) line, "inor",     (size_t) 4) == 0 ||
			strncmp ((const char*) line, "inore",    (size_t) 5) == 0 ||
			strncmp ((const char*) line, "inorem",   (size_t) 6) == 0 ||
			strncmp ((const char*) line, "inorema",  (size_t) 7) == 0 ||
			strncmp ((const char*) line, "inoremap", (size_t) 8) == 0 ||
			strncmp ((const char*) line, "lno",      (size_t) 3) == 0 ||
			strncmp ((const char*) line, "lnor",     (size_t) 4) == 0 ||
			strncmp ((const char*) line, "lnore",    (size_t) 5) == 0 ||
			strncmp ((const char*) line, "lnorem",   (size_t) 6) == 0 ||
			strncmp ((const char*) line, "lnorema",  (size_t) 7) == 0 ||
			strncmp ((const char*) line, "lnoremap", (size_t) 8) == 0 ||
			strncmp ((const char*) line, "cno",      (size_t) 3) == 0 ||
			strncmp ((const char*) line, "cnor",     (size_t) 4) == 0 ||
			strncmp ((const char*) line, "cnore",    (size_t) 5) == 0 ||
			strncmp ((const char*) line, "cnorem",   (size_t) 6) == 0 ||
			strncmp ((const char*) line, "cnorema",  (size_t) 7) == 0 ||
			strncmp ((const char*) line, "cnoremap", (size_t) 8) == 0 
			)
			return TRUE;

	return FALSE;
}

static const unsigned char * readVimLine (void)
{
	const unsigned char *line;

	while ((line = fileReadLine ()) != NULL)
	{
		while (isspace ((int) *line))
			++line;

		if ((int) *line == '"')
			continue;  /* skip comment */

		break;
	}

	return line;
}

static void parseFunction (const unsigned char *line)
{
	vString *name = vStringNew ();
	/* boolean inFunction = FALSE; */
	int scope;

	const unsigned char *cp = line + 1;

	if ((int) *++cp == 'n'	&&	(int) *++cp == 'c'	&&
		(int) *++cp == 't'	&&	(int) *++cp == 'i'	&&
		(int) *++cp == 'o'	&&	(int) *++cp == 'n')
			++cp;
	if ((int) *cp == '!')
		++cp;
	if (isspace ((int) *cp))
	{
		while (*cp && isspace ((int) *cp))
			++cp;

		if (*cp)
		{
			cp = skipPrefix (cp, &scope);
			if (isupper ((int) *cp)  ||  
					scope == 's'  ||  /* script scope */
					scope == '<'  ||  /* script scope */
					scope == 'd'  ||  /* dictionary */
					scope == 'a')	  /* autoload */
			{
				do
				{
					vStringPut (name, (int) *cp);
					++cp;
				} while (isalnum ((int) *cp) ||  *cp == '_' ||	*cp == '.' ||  *cp == '#');
				vStringTerminate (name);
				makeSimpleTag (name, VimKinds, K_FUNCTION);
				vStringClear (name);
			}
		}
	}

	/* TODO - update struct to indicate inside function */
	while ((line = readVimLine ()) != NULL)
	{
		/* 
		 * Vim7 added the for/endfo[r] construct, so we must first
		 * check for an "endfo", before a "endf"
		 */
		if ( (!strncmp ((const char*) line, "endfo", (size_t) 5) == 0) && 
				(strncmp ((const char*) line, "endf", (size_t) 4) == 0)   )
			break;
		/* TODO - call parseVimLine */
	}
	vStringDelete (name);
}

static void parseAutogroup (const unsigned char *line)
{
	vString *name = vStringNew ();

	/* Found Autocommand Group (augroup) */
	const unsigned char *cp = line + 2;
	if ((int) *++cp == 'r' && (int) *++cp == 'o' &&
			(int) *++cp == 'u' && (int) *++cp == 'p')
		++cp;
	if (isspace ((int) *cp))
	{
		while (*cp && isspace ((int) *cp))
			++cp; 

		if (*cp)
		{
			if (strncasecmp ((const char*) cp, "end", (size_t) 3) != 0)
			{	 
				do
				{
					vStringPut (name, (int) *cp);
					++cp;
				} while (isalnum ((int) *cp)  ||  *cp == '_');
				vStringTerminate (name);
				makeSimpleTag (name, VimKinds, K_AUGROUP);
				vStringClear (name);
			}
		}
	}
	vStringDelete (name);
}

static boolean parseCommand (const unsigned char *line)
{
	vString *name = vStringNew ();
	boolean cmdProcessed = TRUE;

	/* 
	 * Found a user-defined command 
	 *
	 * They can have many options preceeded by a dash
	 * command! -nargs=+ -complete Select  :call s:DB_execSql("select " . <q-args>)
	 * The name of the command should be the first word not preceeded by a dash
	 *
	 */
	const unsigned char *cp = line;

	if ( (int) *cp == '\\' ) 
	{
		/*
		 * We are recursively calling this function is the command
		 * has been continued on to the next line
		 *
		 * Vim statements can be continued onto a newline using a \
		 * to indicate the previous line is continuing.
		 *
		 * com -nargs=1 -bang -complete=customlist,EditFileComplete
		 * 			\ EditFile edit<bang> <args>
		 *
		 * If the following lines do not have a line continuation
		 * the command must not be spanning multiple lines and should
		 * be synatically incorrect.
		 */
		if ((int) *cp == '\\')
			++cp;

		while (*cp && isspace ((int) *cp))
			++cp; 
	}
	else if ( (!strncmp ((const char*) line, "comp", (size_t) 4) == 0) && 
		     (!strncmp ((const char*) line, "comc", (size_t) 4) == 0) && 
				(strncmp ((const char*) line, "com", (size_t) 3) == 0) )
	{
		cp += 2;
		if ((int) *++cp == 'm' && (int) *++cp == 'a' &&
				(int) *++cp == 'n' && (int) *++cp == 'd')
			++cp;

		if ((int) *cp == '!')
			++cp;

		if ((int) *cp != ' ')
		{
			/*
			 * :command must be followed by a space.  If it is not, it is 
			 * not a valid command.
			 * Treat the line as processed and continue.
			 */
			cmdProcessed = TRUE;
			goto cleanUp;
		}

		while (*cp && isspace ((int) *cp))
			++cp; 
	} 
	else 
	{
		/*
		 * We are recursively calling this function.  If it does not start
		 * with "com" or a line continuation character, we have moved off
		 * the command line and should let the other routines parse this file.
		 */
		cmdProcessed = FALSE;
		goto cleanUp;
	}

	/*
	 * Strip off any spaces and options which are part of the command.
	 * These should preceed the command name.
	 */
	do
	{
		if (isspace ((int) *cp))
		{
			++cp;
		}
		else if (*cp == '-')
		{
			/* 
			 * Read until the next space which separates options or the name
			 */
			while (*cp && !isspace ((int) *cp))
				++cp; 
		}
		else
			break;
	} while ( *cp );

	if ( ! *cp )
	{
		/*
		 * We have reached the end of the line without finding the command name.
		 * Read the next line and continue processing it as a command.
		 */
		line = readVimLine();
		parseCommand(line);
		goto cleanUp;
	}

	do
	{
		vStringPut (name, (int) *cp);
		++cp;
	} while (isalnum ((int) *cp)  ||  *cp == '_');

	vStringTerminate (name);
	makeSimpleTag (name, VimKinds, K_COMMAND);
	vStringClear (name);

cleanUp:
	vStringDelete (name);

	return cmdProcessed;
}

static void parseLet (const unsigned char *line)
{
	vString *name = vStringNew ();

	/* we've found a variable declared outside of a function!! */
	const unsigned char *cp = line + 3;
	const unsigned char *np = line;
	/* get the name */
	if (isspace ((int) *cp))
	{
		while (*cp && isspace ((int) *cp))
			++cp;

		/* 
		 * Ignore lets which set:
		 *    &  - local buffer vim settings
		 *    @  - registers
		 *    [  - Lists or Dictionaries
		 */
		if (!*cp || *cp == '&' || *cp == '@' || *cp == '[' )
			goto cleanUp;

		/* 
		 * Ignore vim variables which are read only
		 *    v: - Vim variables.
		 */
		np = cp;
		++np;
		if ((int) *cp == 'v' && (int) *np == ':' )
			goto cleanUp;

		/* deal with spaces, $, @ and & */
		while (*cp && *cp != '$' && !isalnum ((int) *cp))
			++cp;

		if (!*cp)
			goto cleanUp;

		/* cp = skipPrefix (cp, &scope); */
		do
		{
			if (!*cp)
				break;

			vStringPut (name, (int) *cp);
			++cp;
		} while (isalnum ((int) *cp)  ||  *cp == '_'  ||  *cp == '#'  ||  *cp == ':'  ||  *cp == '$');
		vStringTerminate (name);
		makeSimpleTag (name, VimKinds, K_VARIABLE);
		vStringClear (name);
	}

cleanUp:
	vStringDelete (name);
}

static boolean parseMap (const unsigned char *line)
{
	vString *name = vStringNew ();

	const unsigned char *cp = line;

	/* Remove map */
	while (*cp && isalnum ((int) *cp))
		++cp;

	if ((int) *cp == '!')
		++cp;

	/*
	 * Maps follow this basic format
	 *     map 
     *    nnoremap <silent> <F8> :Tlist<CR>
     *    map <unique> <Leader>scdt <Plug>GetColumnDataType
     *    inoremap ,,, <esc>diwi<<esc>pa><cr></<esc>pa><esc>kA
     *    inoremap <buffer> ( <C-R>=PreviewFunctionSignature()<LF> 
	 *
	 * The Vim help shows the various special arguments available to a map:
	 * 1.2 SPECIAL ARGUMENTS					*:map-arguments*
     *    <buffer>
	 *    <silent>
	 *    <script>
	 *    <unique>
	 *    <special>
	 *    <expr>
	 *
	 * Strip the special arguments from the map command, this should leave
	 * the map name which we will use as the "name".
	 */
	
	do
	{
		while (*cp && isspace ((int) *cp))
			++cp; 

		if (strncmp ((const char*) cp, "<Leader>", (size_t) 8) == 0)
			break;
	
		if (
				strncmp ((const char*) cp, "<buffer>", (size_t) 8) == 0 ||
				strncmp ((const char*) cp, "<silent>", (size_t) 8) == 0 ||
				strncmp ((const char*) cp, "<script>", (size_t) 8) == 0 ||
				strncmp ((const char*) cp, "<unique>", (size_t) 8) == 0
		   )
		{
			cp += 8;
			continue;
		}
	
		if (strncmp ((const char*) cp, "<expr>", (size_t) 6) == 0)
		{
			cp += 6;
			continue;
		}
	
		if (strncmp ((const char*) cp, "<special>", (size_t) 9) == 0)
		{
			cp += 9;
			continue;
		}
	
		break;
	} while (*cp);

	do
	{
		vStringPut (name, (int) *cp);
		++cp;
	} while (*cp && *cp != ' ');

	vStringTerminate (name);
	makeSimpleTag (name, VimKinds, K_MAP);
	vStringClear (name);

	vStringDelete (name);

	return TRUE;
}

static boolean parseVimLine (const unsigned char *line)
{
	boolean readNextLine = TRUE;

	if ( (!strncmp ((const char*) line, "comp", (size_t) 4) == 0) && 
			(!strncmp ((const char*) line, "comc", (size_t) 4) == 0) && 
			(strncmp ((const char*) line, "com", (size_t) 3) == 0) )
	{
		readNextLine = parseCommand(line);
		/* TODO - Handle parseCommand returning FALSE */
	}

	if (isMap(line))
	{
		parseMap(line);
	}

	if (strncmp ((const char*) line, "fu", (size_t) 2) == 0)
	{
		parseFunction(line);
	}

	if	(strncmp ((const char*) line, "aug", (size_t) 3) == 0)
	{
		parseAutogroup(line);
	}

	if ( strncmp ((const char*) line, "let", (size_t) 3) == 0 )
	{
		parseLet(line);
	}

	return readNextLine;
}

static void parseVimFile (const unsigned char *line)
{
	boolean readNextLine = TRUE;
	line = readVimLine();

	while (line != NULL)
	{
		readNextLine = parseVimLine(line);

		if ( readNextLine )
			line = readVimLine();

	}
}

static void findVimTags (void)
{
	const unsigned char *line;
		/* TODO - change this into a structure */

	line = '\0';

	parseVimFile (line);
}

extern parserDefinition* VimParser (void)
{
	static const char *const extensions [] = { "vim", NULL };
	parserDefinition* def = parserNew ("Vim");
	def->kinds		= VimKinds;
	def->kindCount	= KIND_COUNT (VimKinds);
	def->extensions = extensions;
	def->parser		= findVimTags;
	return def;
}

/* vi:set tabstop=4 shiftwidth=4 noexpandtab: */
