/*
 *   Copyright (c) 2019, Masatake YAMATO
 *
 *   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 Moose perl extension.
 *   https://metacpan.org/pod/Moose
 *
 *   This module can gather tags for Moo perl extension, too.
 *   https://metacpan.org/pod/Moo
 *
 */

/* NOTE about kind/role design:
 *
 * sub foo { ... }
 * after 'foo' => sub { ...}
 *
 * There were two ideas to capture 'foo':
 *
 * A: capturing 'foo' as a reference tag with 'method' kind and 'wrapped' role, and
 * B: capturing 'foo' as a definition tag with 'wrapper' kind.
 *
 * This implementation takes the idea B. */

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

#include "debug.h"
#include "entry.h"
#include "kind.h"
#include "parse.h"
#include "perl.h"
#include "read.h"
#include "routines.h"
#include "trace.h"

#include <string.h>

/*
 *   DATA DECLARATIONS
 */

enum MooseKind {
	K_CLASS,
	K_METHOD,
	K_ATTR,
	K_WRAPPER,
	K_ROLE,
};

static kindDefinition MooseKinds[] = {
	{ true, 'c', "class", "classes"  },
	{ true, 'm', "method", "methods" },
	{ true, 'a', "attribute", "attributes" },
	{ true, 'w', "wrapper", "wrappers" },
	{ true, 'r', "role", "roles" },
};

typedef enum {
	F_WRAPPING,
} MooseField;

static fieldDefinition MooseFields [] = {
	{
		.name = "wrapping",
		.description = "how a wrapper wrapping the method (around, after, or before)",
		.enabled = true,
	},
};

enum Wrapping {
	W_UNKNOWN,
	W_AROUND,
	W_BEFORE,
	W_AFTER,
};

static char * WrappingStrings[] = {
	"unknown",
	"around",
	"before",
	"after",
};

struct mooseSubparser {
	perlSubparser perl;
	bool notInMoose;
	bool inPod;
	int packageCork;
	int classCork;
	bool notContinuousExtendsLines;

	int indexForFunctionParameters;

	/* functionParametersModifiersStateCounter is for tracking the conditions
	 * that both "use Moose;" and "use Functions::Parameters qw/:modifiers/"
	 * are specified.
	 * functionParametersModifiersStateCounter is initialized to 2.
	 * Decrement functionParametersModifiersStateCounter when finding one of two.
	 * When functionParametersModifiersStateCounter is 0, the state is
	 * propagated to notInFunctionParametersModifiers. */
	bool notInFunctionParametersModifiers;
	int  functionParametersModifiersStateCounter;
#define RESET_FunctionParameters_STATE(MOOSE)				\
	do {													\
		MOOSE->notInFunctionParametersModifiers = true;		\
		MOOSE->functionParametersModifiersStateCounter = 2;	\
	} while (0)
#define DEC_FunctionParameters_STATE(MOOSE)							\
	do {															\
		MOOSE->functionParametersModifiersStateCounter--;			\
		if (MOOSE->functionParametersModifiersStateCounter == 0)	\
			MOOSE->notInFunctionParametersModifiers = false;		\
		if (MOOSE->functionParametersModifiersStateCounter < 0)		\
			MOOSE->functionParametersModifiersStateCounter = 0; 	\
	} while (0)
#define INC_FunctionParameters_STATE(MOOSE)						\
	do {														\
		MOOSE->functionParametersModifiersStateCounter++;		\
		if (MOOSE->functionParametersModifiersStateCounter > 0)	\
			MOOSE->notInFunctionParametersModifiers = true;		\
		if (MOOSE->functionParametersModifiersStateCounter > 2)	\
			MOOSE->functionParametersModifiersStateCounter = 2;	\
	} while (0)

	vString *supersOrRoles;
};

/*
 *   FUNCTION PROTOTYPES
 */
static void inputStart (subparser *s);
static void inputEnd (subparser *s);
static void makeTagEntryNotify (subparser *s, const tagEntryInfo *tag, int corkIndex);
static void enterMoose (struct mooseSubparser *moose, bool role);
static void leaveMoose (struct mooseSubparser *moose);
static void enteringPodNotify (perlSubparser *perl);
static void leavingPodNotify  (perlSubparser *perl);
static void findingQuotedWordNotify (perlSubparser *perl, int moduleIndex, const char *qwd);

/*
 *   DATA DEFINITIONS
 */

static struct mooseSubparser mooseSubparser = {
	.perl = {
		.subparser = {
			.direction  = SUBPARSER_BI_DIRECTION,
			.inputStart = inputStart,
			.inputEnd   = inputEnd,
			.makeTagEntryNotify = makeTagEntryNotify,
		},
		.enteringPodNotify = enteringPodNotify,
		.leavingPodNotify  = leavingPodNotify,
		.findingQuotedWordNotify = findingQuotedWordNotify,
	},
};


/*
 *   FUNCTION DEFINITIONS
 */

static void inputStart (subparser *s)
{
	struct mooseSubparser *moose = (struct mooseSubparser *)s;

	moose->notInMoose = true;
	moose->packageCork = CORK_NIL;
	moose->classCork = CORK_NIL;
	moose->inPod = false;
	moose->supersOrRoles = vStringNew ();
	moose->notContinuousExtendsLines = true;
	moose->indexForFunctionParameters = CORK_NIL;
	RESET_FunctionParameters_STATE (moose);
}

static void inputEnd (subparser *s)
{
	struct mooseSubparser *moose = (struct mooseSubparser *)s;
	setTagEndLineToCorkEntry (moose->classCork, getInputLineNumber ());

	vStringDelete (moose->supersOrRoles);
	moose->supersOrRoles = NULL;
}

static void makeTagEntryNotify (subparser *s, const tagEntryInfo *tag, int corkIndex)
{
	perlSubparser *perl = (perlSubparser *)s;
	struct mooseSubparser *moose = (struct mooseSubparser *)perl;

	if (isTagExtraBitMarked(tag, XTAG_QUALIFIED_TAGS))
		return;

	if (tag->kindIndex == KIND_PERL_PACKAGE)
		moose->packageCork = corkIndex;
	else if ((!moose->notInMoose) && tag->kindIndex == KIND_PERL_SUBROUTINE)
	{
		tagEntryInfo moose_e;
		initTagEntry (&moose_e, tag->name, K_METHOD);
		setTagPositionFromTag (&moose_e, tag);
		moose_e.extensionFields.scopeIndex = moose->classCork;
		makeTagEntry (&moose_e);
	}
	else if (tag->kindIndex == KIND_PERL_MODULE)
	{
		if (isRoleAssigned(tag, ROLE_PERL_MODULE_USED))
		{
			if (strcmp (tag->name, "Moose") == 0
				|| strcmp (tag->name, "Moo") == 0)
				enterMoose (moose, false);
			else if (strcmp (tag->name, "Moose::Role") == 0)
				enterMoose (moose, true);
			else if (strcmp (tag->name, "Function::Parameters") == 0)
				moose->indexForFunctionParameters = corkIndex;
		}
		else if (isRoleAssigned(tag, ROLE_PERL_MODULE_UNUSED))
		{
			if (strcmp (tag->name, "Moose") == 0
				|| strcmp (tag->name, "Moo") == 0)
				leaveMoose (moose);
			else if (strcmp (tag->name, "Function::Parameters") == 0)
			{
				moose->indexForFunctionParameters = CORK_NIL;
				INC_FunctionParameters_STATE(moose);
			}
		}
	}
}

static void enteringPodNotify (perlSubparser *perl)
{
	struct mooseSubparser *moose = (struct mooseSubparser *)perl;
	moose->inPod = true;
}

static void leavingPodNotify  (perlSubparser *perl)
{
	struct mooseSubparser *moose = (struct mooseSubparser *)perl;
	moose->inPod = false;
}

static void findingQuotedWordNotify (perlSubparser *perl,
									 int moduleIndex, const char *qwd)
{
	struct mooseSubparser *moose = (struct mooseSubparser *)perl;
	if (moose->indexForFunctionParameters != moduleIndex)
		return;

	if (strcmp (qwd, ":modifiers") == 0)
		DEC_FunctionParameters_STATE(moose);
}

static void leaveMoose (struct mooseSubparser *moose)
{
	moose->notContinuousExtendsLines = true;

	setTagEndLineToCorkEntry (moose->classCork, getInputLineNumber ());

	moose->classCork = CORK_NIL;
	moose->notInMoose = true;
	moose->packageCork = CORK_NIL;
	INC_FunctionParameters_STATE(moose);
}

static void enterMoose (struct mooseSubparser *moose, bool role)
{
	moose->notContinuousExtendsLines = true;

	tagEntryInfo *perl_e = getEntryInCorkQueue (moose->packageCork);
	if (!perl_e)
		return;

	moose->notInMoose = false;

	tagEntryInfo moose_e;
	initTagEntry (&moose_e, perl_e->name, role? K_ROLE: K_CLASS);
	updateTagLine(&moose_e, perl_e->lineNumber, perl_e->filePosition);
	moose->classCork = makeTagEntry (&moose_e);
	vStringClear (moose->supersOrRoles);

	DEC_FunctionParameters_STATE(moose);

	return;
}

static void parseExtendsClass (const char *input,
							   size_t input_len,
							   vString *inherits,
							   bool *notContinuousLine)
{
	size_t i = 0;
	do
	{
		if (input [i] == ',')
			i++;

		for (; (i < input_len) && input[i] != '\n' && input[i] != '\0'; i++)
		{
			if (input[i] == '\'' || input[i] == ' '  || input[i] == '\t')
				continue;
			else if (input[i] == ',')
			{
				vStringPut (inherits, ',');
				*notContinuousLine = false;
				break;
			}
			else if (input[i] == ';')
			{
				*notContinuousLine = true;
				break;
			}
			else
				vStringPut (inherits, input[i]);
		}
	}
	while (input[i] == ',');
}

static bool findExtendsClass (const char *line,
							  const regexMatch *matches,
							  unsigned int count,
							  void *data)
{
	struct mooseSubparser *moose = data;
	moose->notContinuousExtendsLines = true;

	if (moose->inPod)
		return true;

	tagEntryInfo *e = getEntryInCorkQueue (moose->classCork);
	if (!e)
		return true;

	const char *input = line + matches[2].start;
	vString *str = moose->supersOrRoles;

	vStringPutUnlessEmpty (str, ',');
	parseExtendsClass (input, matches[2].length, str,
					   &moose->notContinuousExtendsLines);

	if (moose->notContinuousExtendsLines == true
		&& vStringLength (str) > 0)
	{
		if (e->extensionFields.inheritance)
			eFree ((void *)e->extensionFields.inheritance);
		e->extensionFields.inheritance = vStringStrdup (str);
	}

	return true;
}

static bool findExtendsClassContinuation (const char *line,
										  const regexMatch *matches,
										  unsigned int count,
										  void *data)
{
	struct mooseSubparser *moose = data;
	moose->notContinuousExtendsLines = true;

	tagEntryInfo *e = getEntryInCorkQueue (moose->classCork);
	if (!e)
		return true;

	const char *input = line + matches[1].start;
	vString *str = moose->supersOrRoles;

	parseExtendsClass (input, matches[1].length, str,
					   &moose->notContinuousExtendsLines);

	if (moose->notContinuousExtendsLines == true
		&& vStringLength (str) > 0)
	{
		if (e->extensionFields.inheritance)
			eFree ((void *)e->extensionFields.inheritance);
		e->extensionFields.inheritance = vStringStrdup (str);
	}

	return true;
}

static const char *parseAttributeOrWrapper (const char *str, int parentCorkIndex, int extraTerminator,
											int kind, enum Wrapping wrapping)
{
	int i;

	for (i = 0;
		 str[i]
			 && str[i] != extraTerminator
			 && (isalnum ((unsigned char)str[i]) || str[i] == '_');
		 i++)
		;						/* Just advancing `i' */

	if (i == 0)
		return NULL;

	tagEntryInfo e;
	char *buf = eStrndup (str, i);

	initTagEntry (&e, buf, kind);
	if (parentCorkIndex != CORK_NIL)
		e.extensionFields.scopeIndex = parentCorkIndex;

	int corkIndex = makeTagEntry (&e);
	if (kind == K_WRAPPER)
		attachParserFieldToCorkEntry (corkIndex, MooseFields[F_WRAPPING].ftype,
									  WrappingStrings [wrapping]);
	eFree (buf);

	return str[i] == '\0'? NULL: str + i;
}

static void solveKindAndWrapping (const char *str, int *kind, enum Wrapping *wrapping)
{
	*kind = K_WRAPPER;
	*wrapping = W_UNKNOWN;
	switch (str[0])
	{
	case 'h':					/* has */
		*kind = K_ATTR;
		break;
	case 'a':					/* around or after */
		if (str[1] == 'r')
			*wrapping = W_AROUND;
		else if (str[1] == 'f')
			*wrapping = W_AFTER;
		break;
	case 'b':					/* before */
		*wrapping = W_BEFORE;
		break;
	case 'o':					/* override */
		*kind = K_METHOD;
	}
}

static bool findAttributeOrWrapperOne (const char *line,
									   const regexMatch *matches,
									   unsigned int count,
									   void *data)
{
	struct mooseSubparser *moose = data;
	int kind;
	enum Wrapping wrapping;

	if (moose->inPod)
		return true;

	moose->notContinuousExtendsLines = true;

	if (count < 3)
		return true;

	solveKindAndWrapping (line + matches[1].start, &kind, &wrapping);
	parseAttributeOrWrapper (line + matches[2].start, moose->classCork, -1,
							 kind, wrapping);
	return true;
}

static bool findAttributeOrWrapperMulti (const char *line,
										 const regexMatch *matches,
										 unsigned int count,
										 void *data)
{

	struct mooseSubparser *moose = data;
	int kind;
	enum Wrapping wrapping;
	int terminator;

	if (moose->inPod)
		return true;

	moose->notContinuousExtendsLines = true;

	if (count < 4)
		return true;

	solveKindAndWrapping (line + matches[1].start, &kind, &wrapping);
	terminator = line[matches[2].start];


	const char *str = line + matches[3].start;
	while ((str = parseAttributeOrWrapper (str, moose->classCork, terminator,
										   kind, wrapping)))
	{
		int i;
		for (i = 0; (str[i] == ' ') || (str[i] == '\t'); i++)
			;
		if (str[i] == '\0' || str[i] == terminator)
			break;
		str = str + i;
	}

	return true;
}

static void findMooseTags (void)
{
	scheduleRunningBaseparser (RUN_DEFAULT_SUBPARSERS);
}

static void initializeMooseParser (langType language)
{
	addLanguageCallbackRegex (language, "^[ \t]*(extends|with) *(.+)",
							  "{exclusive}",
							  findExtendsClass, &mooseSubparser.notInMoose,
							  &mooseSubparser);
	addLanguageCallbackRegex (language, "^[ \t]*(has|after|before|around|override) +"
							  /* foo => ()
							   * 'foo' => ()
							   * "foo" => ()
							   * ( foo => ())
							   * ( "foo" => ())
							   * ( 'foo' => ()) */
							  "\\(?[ \t]*[\"']?"
							  ""
							  "([a-zA-Z_][a-zA-Z0-9_]*([ \t][a-zA-Z_][a-zA-Z0-9_]*)*).*=>",
							  "{exclusive}",
							  findAttributeOrWrapperOne, &mooseSubparser.notInMoose,
							  &mooseSubparser);
	addLanguageCallbackRegex (language, "^[ \t]*(has|after|before|around) +\\(?\\[qw([^ \t])[ \t]*"
							  /* [qw/foo bar/] => ()
							   * ([qw/foo bar/] => ()) */
							  "([a-zA-Z_][a-zA-Z0-9_]*([ \t][a-zA-Z_][a-zA-Z0-9_]*)*).*=>",
							  "{exclusive}",
							  findAttributeOrWrapperMulti, &mooseSubparser.notInMoose,
							  &mooseSubparser);
	addLanguageCallbackRegex (language, "^[ \t]*(has|after|before|around|override) +"
							  "([a-zA-Z_][a-zA-Z0-9_]*)[ \t]*\\(",
							  "{exclusive}",
							  findAttributeOrWrapperOne, &mooseSubparser.notInFunctionParametersModifiers,
							  &mooseSubparser);
	addLanguageCallbackRegex (language, "^[ \t]*(.+)",
							  "{exclusive}",
							  findExtendsClassContinuation, &mooseSubparser.notContinuousExtendsLines,
							  &mooseSubparser);
}

extern parserDefinition* MooseParser (void)
{
	parserDefinition* const def = parserNew("Moose");

	static parserDependency dependencies [] = {
		[0] = { DEPTYPE_SUBPARSER, "Perl", &mooseSubparser },
	};

	def->dependencies = dependencies;
	def->dependencyCount = ARRAY_SIZE (dependencies);

	def->kindTable = MooseKinds;
	def->kindCount = ARRAY_SIZE(MooseKinds);

	def->fieldTable = MooseFields;
	def->fieldCount = ARRAY_SIZE (MooseFields);

	def->initialize = initializeMooseParser;
	def->parser = findMooseTags;
	def->useCork = CORK_QUEUE;

	return def;
}
