// ASEnhancer.cpp
// Copyright (c) 2018 by Jim Pattee <jimp03@email.com>.
// This code is licensed under the MIT License.
// License.md describes the conditions under which this software may be distributed.

//-----------------------------------------------------------------------------
// headers
//-----------------------------------------------------------------------------

#include "astyle/astyle.h"

//-----------------------------------------------------------------------------
// astyle namespace
//-----------------------------------------------------------------------------

namespace astyle {
//
//-----------------------------------------------------------------------------
// ASEnhancer class
//-----------------------------------------------------------------------------

/**
 * initialize the ASEnhancer.
 *
 * init() is called each time an ASFormatter object is initialized.
 */
void ASEnhancer::init(int  _fileType,
                      int  _indentLength,
                      int  _tabLength,
                      bool _useTabs,
                      bool _forceTab,
                      bool _namespaceIndent,
                      bool _caseIndent,
                      bool _preprocBlockIndent,
                      bool _preprocDefineIndent,
                      bool _emptyLineFill,
                      vector<const pair<const string, const string>* >* _indentableMacros)
{
	// formatting variables from ASFormatter and ASBeautifier
	ASBase::init(_fileType);
	indentLength = _indentLength;
	tabLength = _tabLength;
	useTabs = _useTabs;
	forceTab = _forceTab;
	namespaceIndent = _namespaceIndent;
	caseIndent = _caseIndent;
	preprocBlockIndent = _preprocBlockIndent;
	preprocDefineIndent = _preprocDefineIndent;
	emptyLineFill = _emptyLineFill;
	indentableMacros = _indentableMacros;
	quoteChar = '\'';

	// unindent variables
	lineNumber = 0;
	braceCount = 0;
	isInComment = false;
	isInQuote = false;
	switchDepth = 0;
	eventPreprocDepth = 0;
	lookingForCaseBrace = false;
	unindentNextLine = false;
	shouldUnindentLine = false;
	shouldUnindentComment = false;

	// switch struct and vector
	sw.switchBraceCount = 0;
	sw.unindentDepth = 0;
	sw.unindentCase = false;
	switchStack.clear();

	// other variables
	nextLineIsEventIndent = false;
	isInEventTable = false;
	nextLineIsDeclareIndent = false;
	isInDeclareSection = false;
}

/**
 * additional formatting for line of source code.
 * every line of source code in a source code file should be sent
 *     one after the other to this function.
 * indents event tables
 * unindents the case blocks
 *
 * @param line       the original formatted line will be updated if necessary.
 */
void ASEnhancer::enhance(string& line, bool isInNamespace, bool isInPreprocessor, bool isInSQL)
{
	shouldUnindentLine = true;
	shouldUnindentComment = false;
	lineNumber++;

	// check for beginning of event table
	if (nextLineIsEventIndent)
	{
		isInEventTable = true;
		nextLineIsEventIndent = false;
	}

	// check for beginning of SQL declare section
	if (nextLineIsDeclareIndent)
	{
		isInDeclareSection = true;
		nextLineIsDeclareIndent = false;
	}

	if (line.length() == 0
	        && !isInEventTable
	        && !isInDeclareSection
	        && !emptyLineFill)
		return;

	// test for unindent on attached braces
	if (unindentNextLine)
	{
		sw.unindentDepth++;
		sw.unindentCase = true;
		unindentNextLine = false;
	}

	// parse characters in the current line
	parseCurrentLine(line, isInPreprocessor, isInSQL);

	// check for SQL indentable lines
	if (isInDeclareSection)
	{
		size_t firstText = line.find_first_not_of(" \t");
		if (firstText == string::npos || line[firstText] != '#')
			indentLine(line, 1);
	}

	// check for event table indentable lines
	if (isInEventTable
	        && (eventPreprocDepth == 0
	            || (namespaceIndent && isInNamespace)))
	{
		size_t firstText = line.find_first_not_of(" \t");
		if (firstText == string::npos || line[firstText] != '#')
			indentLine(line, 1);
	}

	if (shouldUnindentComment && sw.unindentDepth > 0)
		unindentLine(line, sw.unindentDepth - 1);
	else if (shouldUnindentLine && sw.unindentDepth > 0)
		unindentLine(line, sw.unindentDepth);
}

/**
 * convert a force-tab indent to spaces
 *
 * @param line          a reference to the line that will be converted.
 */
void ASEnhancer::convertForceTabIndentToSpaces(string& line) const
{
	// replace tab indents with spaces
	for (size_t i = 0; i < line.length(); i++)
	{
		if (!isWhiteSpace(line[i]))
			break;
		if (line[i] == '\t')
		{
			line.erase(i, 1);
			line.insert(i, tabLength, ' ');
			i += tabLength - 1;
		}
	}
}

/**
 * convert a space indent to force-tab
 *
 * @param line          a reference to the line that will be converted.
 */
void ASEnhancer::convertSpaceIndentToForceTab(string& line) const
{
	assert(tabLength > 0);

	// replace leading spaces with tab indents
	size_t newSpaceIndentLength = line.find_first_not_of(" \t");
	size_t tabCount = newSpaceIndentLength / tabLength;		// truncate extra spaces
	line.replace(0U, tabCount * tabLength, tabCount, '\t');
}

/**
 * find the colon following a 'case' statement
 *
 * @param line          a reference to the line.
 * @param caseIndex     the line index of the case statement.
 * @return              the line index of the colon.
 */
size_t ASEnhancer::findCaseColon(const string& line, size_t caseIndex) const
{
	size_t i = caseIndex;
	bool isInQuote_ = false;
	char quoteChar_ = ' ';
	for (; i < line.length(); i++)
	{
		if (isInQuote_)
		{
			if (line[i] == '\\')
			{
				i++;
				continue;
			}
			if (line[i] == quoteChar_)          // check ending quote
			{
				isInQuote_ = false;
				quoteChar_ = ' ';
				continue;
			}
				continue;                           // must close quote before continuing
			}
		if (line[i] == '"' 		// check opening quote
		        || (line[i] == '\'' && !isDigitSeparator(line, i)))
		{
			isInQuote_ = true;
			quoteChar_ = line[i];
			continue;
		}
		if (line[i] == ':')
		{
			if ((i + 1 < line.length()) && (line[i + 1] == ':'))
				i++;                                // bypass scope resolution operator
			else
				break;                              // found it
		}
	}
	return i;
}

/**
* indent a line by a given number of tabsets
 *    by inserting leading whitespace to the line argument.
 *
 * @param line          a reference to the line to indent.
 * @param indent        the number of tabsets to insert.
 * @return              the number of characters inserted.
 */
int ASEnhancer::indentLine(string& line, int indent) const
{
	if (line.length() == 0
	        && !emptyLineFill)
		return 0;

	size_t charsToInsert = 0;

	if (forceTab && indentLength != tabLength)
	{
		// replace tab indents with spaces
		convertForceTabIndentToSpaces(line);
		// insert the space indents
		charsToInsert = indent * indentLength;
		line.insert(line.begin(), charsToInsert, ' ');
		// replace leading spaces with tab indents
		convertSpaceIndentToForceTab(line);
	}
	else if (useTabs)
	{
		charsToInsert = indent;
		line.insert(line.begin(), charsToInsert, '\t');
	}
	else // spaces
	{
		charsToInsert = indent * indentLength;
		line.insert(line.begin(), charsToInsert, ' ');
	}

	return charsToInsert;
}

/**
 * check for SQL "BEGIN DECLARE SECTION".
 * must compare case insensitive and allow any spacing between words.
 *
 * @param line          a reference to the line to indent.
 * @param index         the current line index.
 * @return              true if a hit.
 */
bool ASEnhancer::isBeginDeclareSectionSQL(const string& line, size_t index) const
{
	string word;
	size_t hits = 0;
	size_t i;
	for (i = index; i < line.length(); i++)
	{
		i = line.find_first_not_of(" \t", i);
		if (i == string::npos)
			return false;
		if (line[i] == ';')
			break;
		if (!isCharPotentialHeader(line, i))
			continue;
		word = getCurrentWord(line, i);
		for (char& character : word)
			character = (char) toupper(character);
		if (word == "EXEC" || word == "SQL")
		{
			i += word.length() - 1;
			continue;
		}
		if (word == "DECLARE" || word == "SECTION")
		{
			hits++;
			i += word.length() - 1;
			continue;
		}
		if (word == "BEGIN")
		{
			hits++;
			i += word.length() - 1;
			continue;
		}
		return false;
	}
	if (hits == 3)
		return true;
	return false;
}

/**
 * check for SQL "END DECLARE SECTION".
 * must compare case insensitive and allow any spacing between words.
 *
 * @param line          a reference to the line to indent.
 * @param index         the current line index.
 * @return              true if a hit.
 */
bool ASEnhancer::isEndDeclareSectionSQL(const string& line, size_t index) const
{
	string word;
	size_t hits = 0;
	size_t i;
	for (i = index; i < line.length(); i++)
	{
		i = line.find_first_not_of(" \t", i);
		if (i == string::npos)
			return false;
		if (line[i] == ';')
			break;
		if (!isCharPotentialHeader(line, i))
			continue;
		word = getCurrentWord(line, i);
		for (char& character : word)
			character = (char) toupper(character);
		if (word == "EXEC" || word == "SQL")
		{
			i += word.length() - 1;
			continue;
		}
		if (word == "DECLARE" || word == "SECTION")
		{
			hits++;
			i += word.length() - 1;
			continue;
		}
		if (word == "END")
		{
			hits++;
			i += word.length() - 1;
			continue;
		}
		return false;
	}
	if (hits == 3)
		return true;
	return false;
}

/**
 * check if a one-line brace has been reached,
 * i.e. if the currently reached '{' character is closed
 * with a complimentary '}' elsewhere on the current line,
 *.
 * @return     false = one-line brace has not been reached.
 *             true  = one-line brace has been reached.
 */
bool ASEnhancer::isOneLineBlockReached(const string& line, int startChar) const
{
	assert(line[startChar] == '{');

	bool isInComment_ = false;
	bool isInQuote_ = false;
	int _braceCount = 1;
	int lineLength = line.length();
	char quoteChar_ = ' ';
	char ch = ' ';

	for (int i = startChar + 1; i < lineLength; ++i)
	{
		ch = line[i];

		if (isInComment_)
		{
			if (line.compare(i, 2, "*/") == 0)
			{
				isInComment_ = false;
				++i;
			}
			continue;
		}

		if (ch == '\\')
		{
			++i;
			continue;
		}

		if (isInQuote_)
		{
			if (ch == quoteChar_)
				isInQuote_ = false;
			continue;
		}

		if (ch == '"'
		        || (ch == '\'' && !isDigitSeparator(line, i)))
		{
			isInQuote_ = true;
			quoteChar_ = ch;
			continue;
		}

		if (line.compare(i, 2, "//") == 0)
			break;

		if (line.compare(i, 2, "/*") == 0)
		{
			isInComment_ = true;
			++i;
			continue;
		}

		if (ch == '{')
			++_braceCount;
		else if (ch == '}')
			--_braceCount;

		if (_braceCount == 0)
			return true;
	}

	return false;
}

/**
 * parse characters in the current line to determine if an indent
 * or unindent is needed.
 */
void ASEnhancer::parseCurrentLine(string& line, bool isInPreprocessor, bool isInSQL)
{
	bool isSpecialChar = false;			// is a backslash escape character

	for (size_t i = 0; i < line.length(); i++)
	{
		char ch = line[i];

		// bypass whitespace
		if (isWhiteSpace(ch))
			continue;

		// handle special characters (i.e. backslash+character such as \n, \t, ...)
		if (isSpecialChar)
		{
			isSpecialChar = false;
			continue;
		}
		if (!(isInComment) && line.compare(i, 2, "\\\\") == 0)
		{
			i++;
			continue;
		}
		if (!(isInComment) && ch == '\\')
		{
			isSpecialChar = true;
			continue;
		}

		// handle quotes (such as 'x' and "Hello Dolly")
		if (!isInComment
		        && (ch == '"'
		            || (ch == '\'' && !isDigitSeparator(line, i))))
		{
			if (!isInQuote)
			{
				quoteChar = ch;
				isInQuote = true;
			}
			else if (quoteChar == ch)
			{
				isInQuote = false;
				continue;
			}
		}

		if (isInQuote)
			continue;

		// handle comments

		if (!(isInComment) && line.compare(i, 2, "//") == 0)
		{
			// check for windows line markers
			if (line.compare(i + 2, 1, "\xf0") > 0)
				lineNumber--;
			// unindent if not in case braces
			if (line.find_first_not_of(" \t") == i
			        && sw.switchBraceCount == 1
			        && sw.unindentCase)
				shouldUnindentComment = true;
			break;                 // finished with the line
		}
		if (!(isInComment) && line.compare(i, 2, "/*") == 0)
		{
			// unindent if not in case braces
			if (sw.switchBraceCount == 1 && sw.unindentCase)
				shouldUnindentComment = true;
			isInComment = true;
			size_t commentEnd = line.find("*/", i);
			if (commentEnd == string::npos)
				i = line.length() - 1;
			else
				i = commentEnd - 1;
			continue;
		}
		if ((isInComment) && line.compare(i, 2, "*/") == 0)
		{
			// unindent if not in case braces
			if (sw.switchBraceCount == 1 && sw.unindentCase)
				shouldUnindentComment = true;
			isInComment = false;
			i++;
			continue;
		}
		if (isInComment)
		{
			// unindent if not in case braces
			if (sw.switchBraceCount == 1 && sw.unindentCase)
				shouldUnindentComment = true;
			size_t commentEnd = line.find("*/", i);
			if (commentEnd == string::npos)
				i = line.length() - 1;
			else
				i = commentEnd - 1;
			continue;
		}

		// if we have reached this far then we are NOT in a comment or string of special characters

		if (line[i] == '{')
			braceCount++;

		if (line[i] == '}')
			braceCount--;

		// check for preprocessor within an event table
		if (isInEventTable && line[i] == '#' && preprocBlockIndent)
		{
			string preproc;
			preproc = line.substr(i + 1);
			if (preproc.substr(0, 2) == "if") // #if, #ifdef, #ifndef)
				eventPreprocDepth += 1;
			if (preproc.substr(0, 5) == "endif" && eventPreprocDepth > 0)
				eventPreprocDepth -= 1;
		}

		bool isPotentialKeyword = isCharPotentialHeader(line, i);

		// ----------------  wxWidgets and MFC macros  ----------------------------------

		if (isPotentialKeyword)
		{
			for (const auto* indentableMacro : *indentableMacros)
			{
				// 'first' is the beginning macro
				if (findKeyword(line, i, indentableMacro->first))
				{
					nextLineIsEventIndent = true;
					break;
				}
				// 'second' is the ending macro
				if (findKeyword(line, i, indentableMacro->second))
				{
					isInEventTable = false;
					eventPreprocDepth = 0;
					break;
				}
			}
		}

		// ----------------  process SQL  -----------------------------------------------

		if (isInSQL)
		{
			if (isBeginDeclareSectionSQL(line, i))
				nextLineIsDeclareIndent = true;
			if (isEndDeclareSectionSQL(line, i))
				isInDeclareSection = false;
			break;
		}

		// ----------------  process switch statements  ---------------------------------

		if (isPotentialKeyword && findKeyword(line, i, ASResource::AS_SWITCH))
		{
			switchDepth++;
			switchStack.emplace_back(sw);                      // save current variables
			sw.switchBraceCount = 0;
			sw.unindentCase = false;                        // don't clear case until end of switch
			i += 5;                                         // bypass switch statement
			continue;
		}

		// just want unindented case statements from this point

		if (caseIndent
		        || switchDepth == 0
		        || (isInPreprocessor && !preprocDefineIndent))
		{
			// bypass the entire word
			if (isPotentialKeyword)
			{
				string name = getCurrentWord(line, i);
				i += name.length() - 1;
			}
			continue;
		}

		i = processSwitchBlock(line, i);

	}   // end of for loop * end of for loop * end of for loop * end of for loop
}

/**
 * process the character at the current index in a switch block.
 *
 * @param line          a reference to the line to indent.
 * @param index         the current line index.
 * @return              the new line index.
 */
size_t ASEnhancer::processSwitchBlock(string& line, size_t index)
{
	size_t i = index;
	bool isPotentialKeyword = isCharPotentialHeader(line, i);

	if (line[i] == '{')
	{
		sw.switchBraceCount++;
		if (lookingForCaseBrace)                      // if 1st after case statement
		{
			sw.unindentCase = true;                     // unindenting this case
			sw.unindentDepth++;
			lookingForCaseBrace = false;              // not looking now
		}
		return i;
	}
	lookingForCaseBrace = false;                      // no opening brace, don't indent

	if (line[i] == '}')
	{
		sw.switchBraceCount--;
		if (sw.switchBraceCount == 0)                 // if end of switch statement
		{
			int lineUnindent = sw.unindentDepth;
			if (line.find_first_not_of(" \t") == i
			        && !switchStack.empty())
				lineUnindent = switchStack[switchStack.size() - 1].unindentDepth;
			if (shouldUnindentLine)
			{
				if (lineUnindent > 0)
					i -= unindentLine(line, lineUnindent);
				shouldUnindentLine = false;
			}
			switchDepth--;
			sw = switchStack.back();
			switchStack.pop_back();
		}
		return i;
	}

	if (isPotentialKeyword
	        && (findKeyword(line, i, ASResource::AS_CASE)
	            || findKeyword(line, i, ASResource::AS_DEFAULT)))
	{
		if (sw.unindentCase)					// if unindented last case
		{
			sw.unindentCase = false;			// stop unindenting previous case
			sw.unindentDepth--;
		}

		i = findCaseColon(line, i);

		i++;
		for (; i < line.length(); i++)			// bypass whitespace
		{
			if (!isWhiteSpace(line[i]))
				break;
		}
		if (i < line.length())
		{
			if (line[i] == '{')
			{
				braceCount++;
				sw.switchBraceCount++;
				if (!isOneLineBlockReached(line, i))
					unindentNextLine = true;
				return i;
			}
		}
		lookingForCaseBrace = true;
		i--;									// need to process this char
		return i;
	}
	if (isPotentialKeyword)
	{
		string name = getCurrentWord(line, i);          // bypass the entire name
		i += name.length() - 1;
	}
	return i;
}

/**
 * unindent a line by a given number of tabsets
 *    by erasing the leading whitespace from the line argument.
 *
 * @param line          a reference to the line to unindent.
 * @param unindent      the number of tabsets to erase.
 * @return              the number of characters erased.
 */
int ASEnhancer::unindentLine(string& line, int unindent) const
{
	size_t whitespace = line.find_first_not_of(" \t");

	if (whitespace == string::npos)         // if line is blank
		whitespace = line.length();         // must remove padding, if any

	if (whitespace == 0)
		return 0;

	size_t charsToErase = 0;

	if (forceTab && indentLength != tabLength)
	{
		// replace tab indents with spaces
		convertForceTabIndentToSpaces(line);
		// remove the space indents
		size_t spaceIndentLength = line.find_first_not_of(" \t");
		charsToErase = unindent * indentLength;
		if (charsToErase <= spaceIndentLength)
			line.erase(0, charsToErase);
		else
			charsToErase = 0;
		// replace leading spaces with tab indents
		convertSpaceIndentToForceTab(line);
	}
	else if (useTabs)
	{
		charsToErase = unindent;
		if (charsToErase <= whitespace)
			line.erase(0, charsToErase);
		else
			charsToErase = 0;
	}
	else // spaces
	{
		charsToErase = unindent * indentLength;
		if (charsToErase <= whitespace)
			line.erase(0, charsToErase);
		else
			charsToErase = 0;
	}

	return charsToErase;
}

}   // end namespace astyle
