// Scintilla source code edit control
// @file LexPowerPro.cxx
// PowerPro utility, written by Bruce Switzer, is available from http://powerpro.webeddie.com
// PowerPro lexer is written by Christopher Bean (cbean@cb-software.net)
//
// Lexer code heavily borrowed from: 
//	LexAU3.cxx by Jos van der Zande
//	LexCPP.cxx by Neil Hodgson
//	LexVB.cxx by Neil Hodgson
//
// Changes:
// 	2008-10-25 - Initial release
//	2008-10-26 - Changed how <name> is hilighted in  'function <name>' so that 
//				 local isFunction = "" and local functions = "" don't get falsely highlighted
//	2008-12-14 - Added bounds checking for szKeyword and szDo
//			   - Replaced SetOfCharacters with CharacterSet
//			   - Made sure that CharacterSet::Contains is passed only positive values
//			   - Made sure that the return value of Accessor::SafeGetCharAt is positive before 
//				 passsing to functions that require positive values like isspacechar()
//			   - Removed unused visibleChars processing from ColourisePowerProDoc()
//			   - Fixed bug with folding logic where line continuations didn't end where 
//				 they were supposed to
//			   - Moved all helper functions to the top of the file
//
// Copyright 1998-2005 by Neil Hodgson <neilh@scintilla.org>
// The License.txt file describes the conditions under which this software may be distributed.

#include <ctype.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "Platform.h"
#include "PropSet.h"
#include "Accessor.h"
#include "StyleContext.h"
#include "KeyWords.h"
#include "Scintilla.h"
#include "SciLexer.h"
#include "CharacterSet.h"

#ifdef SCI_NAMESPACE
using namespace Scintilla;
#endif

static inline bool IsStreamCommentStyle(int style) {
	return style == SCE_POWERPRO_COMMENTBLOCK;
}

static bool IsContinuationLine(unsigned int szLine, Accessor &styler)
{
	int nsPos = styler.LineStart(szLine);
	int nePos = styler.LineStart(szLine + 1) - 2;
	while (nsPos < nePos)
	{
		int stylech = styler.StyleAt(nsPos);
		if (!(stylech == SCE_POWERPRO_COMMENTBLOCK)) {
			char ch = styler.SafeGetCharAt(nePos);
			char chPrev = styler.SafeGetCharAt(nePos-1);
			char chPrevPrev = styler.SafeGetCharAt(nePos-2);
			if (ch > 0 && chPrev > 0 && chPrevPrev > 0 && !isspacechar(ch) && !isspacechar(chPrev) && !isspacechar(chPrevPrev) ) {
				if (chPrevPrev == ';' && chPrev == ';' && ch == '+')
					return true;
				else
					return false;
			}
		}
		nePos--; // skip to next char
	}
	return false;
}

// Routine to find first none space on the current line and return its Style
// needed for comment lines not starting on pos 1 
static int GetStyleFirstWord(unsigned int szLine, Accessor &styler)
{
	int nsPos = styler.LineStart(szLine);
	int nePos = styler.LineStart(szLine+1) - 1;
	char ch = styler.SafeGetCharAt(nsPos);
	
	while (ch > 0 && isspacechar(ch) && nsPos < nePos)
	{
		nsPos++; // skip to next char
		ch = styler.SafeGetCharAt(nsPos);

	}
	return styler.StyleAt(nsPos);
}

//returns true if there is a function to highlight
//used to highlight <name> in 'function <name>'
static bool HasFunction(Accessor &styler, unsigned int currentPos) {
	
	//check for presence of 'function '
	return 	(styler.SafeGetCharAt(currentPos) == ' ' 	
	&& tolower(styler.SafeGetCharAt(currentPos-1)) == 'n' 
	&& tolower(styler.SafeGetCharAt(currentPos-2)) == 'o'
	&& tolower(styler.SafeGetCharAt(currentPos-3)) == 'i'
	&& tolower(styler.SafeGetCharAt(currentPos-4)) == 't'
	&& tolower(styler.SafeGetCharAt(currentPos-5)) == 'c'
	&& tolower(styler.SafeGetCharAt(currentPos-6)) == 'n'
	&& tolower(styler.SafeGetCharAt(currentPos-7)) == 'u'
	&& tolower(styler.SafeGetCharAt(currentPos-8)) == 'f'
	//only allow 'function ' to appear at the beginning of a line
	&& (styler.SafeGetCharAt(currentPos-9) == '\n'   		
		|| styler.SafeGetCharAt(currentPos-9) == '\r'
		|| (styler.SafeGetCharAt(currentPos -9, '\0')) == '\0') //is the first line
	);
}

static void ColourisePowerProDoc(unsigned int startPos, int length, int initStyle, WordList *keywordlists[],
                            Accessor &styler, bool caseSensitive) {

	WordList &keywords  = *keywordlists[0];
	WordList &keywords2 = *keywordlists[1];
	WordList &keywords3 = *keywordlists[2];
	WordList &keywords4 = *keywordlists[3];
	
	//define the character sets 
	CharacterSet setWordStart(CharacterSet::setAlpha, "_@", 0x80, true);
	CharacterSet setWord(CharacterSet::setAlphaNum, "._", 0x80, true);

	StyleContext sc(startPos, length, initStyle, styler);
	char s_save[100]; //for last line highlighting
	
	for (; sc.More(); sc.Forward()) {
			
		// **********************************************
		// save the total current word for eof processing
		char s[100];
		sc.GetCurrentLowered(s, sizeof(s));
		 
		if ((sc.ch > 0) && setWord.Contains(sc.ch)) 
		{
			strcpy(s_save,s);
			int tp = strlen(s_save);
			if (tp < 99) {
				s_save[tp] = static_cast<char>(tolower(sc.ch));
				s_save[tp+1] = '\0';
			}
		}
		// **********************************************
		//

		if (sc.atLineStart) {
			if (sc.state == SCE_POWERPRO_DOUBLEQUOTEDSTRING) {
				// Prevent SCE_POWERPRO_STRINGEOL from leaking back to previous line which
				// ends with a line continuation by locking in the state upto this position.
				sc.SetState(SCE_POWERPRO_DOUBLEQUOTEDSTRING);
			}
		}

		// Determine if the current state should terminate.
		switch (sc.state) {
			case SCE_POWERPRO_OPERATOR:
				sc.SetState(SCE_POWERPRO_DEFAULT);
				break;
			
			case SCE_POWERPRO_NUMBER:

				if (!IsADigit(sc.ch))
					sc.SetState(SCE_POWERPRO_DEFAULT);
				
				break;

			case SCE_POWERPRO_IDENTIFIER:
				//if ((sc.ch > 0) && !setWord.Contains(sc.ch) || (sc.ch == '.')) { // use this line if don't want to match keywords with . in them. ie: win.debug will match both win and debug so win debug will also be colorized
				if ((sc.ch > 0) && !setWord.Contains(sc.ch)){  // || (sc.ch == '.')) { // use this line if you want to match keywords with a . ie: win.debug will only match win.debug neither win nor debug will be colorized separately
					char s[1000];
					if (caseSensitive) {
						sc.GetCurrent(s, sizeof(s));
					} else {
						sc.GetCurrentLowered(s, sizeof(s));
					}
					if (keywords.InList(s)) {
						sc.ChangeState(SCE_POWERPRO_WORD);
					} else if (keywords2.InList(s)) {
						sc.ChangeState(SCE_POWERPRO_WORD2);
					} else if (keywords3.InList(s)) {
						sc.ChangeState(SCE_POWERPRO_WORD3);
					} else if (keywords4.InList(s)) {
						sc.ChangeState(SCE_POWERPRO_WORD4);
					}
					sc.SetState(SCE_POWERPRO_DEFAULT);
				}
				break;

			case SCE_POWERPRO_LINECONTINUE:
				if (sc.atLineStart) {
					sc.SetState(SCE_POWERPRO_DEFAULT);
				} else if (sc.Match('/', '*') || sc.Match('/', '/')) {
					sc.SetState(SCE_POWERPRO_DEFAULT);
				}
				break;

			case SCE_POWERPRO_COMMENTBLOCK:
				if (sc.Match('*', '/')) {
					sc.Forward();
					sc.ForwardSetState(SCE_POWERPRO_DEFAULT);
				}
				break;

			case SCE_POWERPRO_COMMENTLINE:
				if (sc.atLineStart) {
					sc.SetState(SCE_POWERPRO_DEFAULT);
				}
				break;

			case SCE_POWERPRO_DOUBLEQUOTEDSTRING:
				if (sc.atLineEnd) {
					sc.ChangeState(SCE_POWERPRO_STRINGEOL);
				} else if (sc.ch == '\\') {
					if (sc.chNext == '\"' || sc.chNext == '\'' || sc.chNext == '\\') {
						sc.Forward();
					}
				} else if (sc.ch == '\"') {
					sc.ForwardSetState(SCE_POWERPRO_DEFAULT);
				}
				break;

			case SCE_POWERPRO_SINGLEQUOTEDSTRING:
				if (sc.atLineEnd) {
					sc.ChangeState(SCE_POWERPRO_STRINGEOL);
				} else if (sc.ch == '\\') {
					if (sc.chNext == '\"' || sc.chNext == '\'' || sc.chNext == '\\') {
						sc.Forward();
					}
				} else if (sc.ch == '\'') {
					sc.ForwardSetState(SCE_POWERPRO_DEFAULT);
				}
				break;

			case SCE_POWERPRO_STRINGEOL:
				if (sc.atLineStart) {
					sc.SetState(SCE_POWERPRO_DEFAULT);
				}
				break;

			case SCE_POWERPRO_VERBATIM:
				if (sc.ch == '\"') {
					if (sc.chNext == '\"') {
						sc.Forward();
					} else {
						sc.ForwardSetState(SCE_POWERPRO_DEFAULT);
					}
				}
				break;

			case SCE_POWERPRO_ALTQUOTE:
				if (sc.ch == '#') {
					if (sc.chNext == '#') {
						sc.Forward();
					} else {
						sc.ForwardSetState(SCE_POWERPRO_DEFAULT);
					}
				}
				break;
				
			case SCE_POWERPRO_FUNCTION:
				if (sc.ch == '\r' || sc.ch == '\n' || sc.ch == ' ' || sc.ch == '(') {
					sc.SetState(SCE_POWERPRO_DEFAULT);
				}
			break;
		}

		// Determine if a new state should be entered.
		if (sc.state == SCE_POWERPRO_DEFAULT) {
			if (sc.Match('?', '\"')) {
				sc.SetState(SCE_POWERPRO_VERBATIM);
				sc.Forward();
			} else if (IsADigit(sc.ch) || (sc.ch == '.' && IsADigit(sc.chNext))) {
				sc.SetState(SCE_POWERPRO_NUMBER);
			}else if (sc.Match('?','#')) {
				if (sc.ch == '?' && sc.chNext == '#') {
					sc.SetState(SCE_POWERPRO_ALTQUOTE);
					sc.Forward();
				}
			} else if (HasFunction(styler, sc.currentPos)) {	//highlight <name> in 'function <name>'
				sc.SetState(SCE_POWERPRO_FUNCTION); 
			} else if (sc.ch == '@' && sc.atLineStart) { 		//alternate function definition [label]
				sc.SetState(SCE_POWERPRO_FUNCTION);
			} else if ((sc.ch > 0) && (setWordStart.Contains(sc.ch) || (sc.ch == '?'))) {
				sc.SetState(SCE_POWERPRO_IDENTIFIER);
			} else if (sc.Match(";;+")) { 
				sc.SetState(SCE_POWERPRO_LINECONTINUE);
			} else if (sc.Match('/', '*')) {
				sc.SetState(SCE_POWERPRO_COMMENTBLOCK);
				sc.Forward();	// Eat the * so it isn't used for the end of the comment
			} else if (sc.Match('/', '/')) {
				sc.SetState(SCE_POWERPRO_COMMENTLINE);
			} else if (sc.atLineStart && sc.ch == ';') {		//legacy comment that can only appear at the beginning of a line
				sc.SetState(SCE_POWERPRO_COMMENTLINE);
			} else if (sc.Match(";;")) {
				sc.SetState(SCE_POWERPRO_COMMENTLINE);
			} else if (sc.ch == '\"') {
				sc.SetState(SCE_POWERPRO_DOUBLEQUOTEDSTRING);
			} else if (sc.ch == '\'') {
				sc.SetState(SCE_POWERPRO_SINGLEQUOTEDSTRING);
			} else if (isoperator(static_cast<char>(sc.ch))) {
				sc.SetState(SCE_POWERPRO_OPERATOR);
			}
		}
	}

	//*************************************
	// Colourize the last word correctly 
	//*************************************
	if (sc.state == SCE_POWERPRO_IDENTIFIER)
	{
		if (keywords.InList(s_save)) {
			sc.ChangeState(SCE_POWERPRO_WORD);
			sc.SetState(SCE_POWERPRO_DEFAULT);
		}
		else if (keywords2.InList(s_save)) {
			sc.ChangeState(SCE_POWERPRO_WORD2);
			sc.SetState(SCE_POWERPRO_DEFAULT);
		}
		else if (keywords3.InList(s_save)) {
			sc.ChangeState(SCE_POWERPRO_WORD3);
			sc.SetState(SCE_POWERPRO_DEFAULT);
		}
		else if (keywords4.InList(s_save)) {
			sc.ChangeState(SCE_POWERPRO_WORD4);
			sc.SetState(SCE_POWERPRO_DEFAULT);
		}
		else {
			sc.SetState(SCE_POWERPRO_DEFAULT);
		}
	}
	sc.Complete();
}

static void FoldPowerProDoc(unsigned int startPos, int length, int, WordList *[], Accessor &styler)
{
	//define the character sets
	CharacterSet setWordStart(CharacterSet::setAlpha, "_@", 0x80, true);
	CharacterSet setWord(CharacterSet::setAlphaNum, "._", 0x80, true);

	bool isFoldingAll = true; //used to tell if we're recursively folding the whole document, or just a small piece (ie: if statement or 1 function)
	int endPos = startPos + length;
	int lastLine = styler.GetLine(styler.Length()); //used to help fold the last line correctly

	// get settings from the config files for folding comments and preprocessor lines
	bool foldComment = styler.GetPropertyInt("fold.comment") != 0;
	bool foldInComment = styler.GetPropertyInt("fold.comment") == 2;
	bool foldCompact = true;
	
	// Backtrack to previous line in case need to fix its fold status
	int lineCurrent = styler.GetLine(startPos);
	if (startPos > 0) {
		isFoldingAll = false;
		if (lineCurrent > 0) {
			lineCurrent--;
			startPos = styler.LineStart(lineCurrent);
		}
	}
	// vars for style of previous/current/next lines 
	int style = GetStyleFirstWord(lineCurrent,styler);
	int stylePrev = 0;
	
	// find the first previous line without continuation character at the end
	while ((lineCurrent > 0 && IsContinuationLine(lineCurrent,styler)) ||
	       (lineCurrent > 1 && IsContinuationLine(lineCurrent-1,styler))) {
		lineCurrent--;
		startPos = styler.LineStart(lineCurrent);
	}
	if (lineCurrent > 0) {
		stylePrev = GetStyleFirstWord(lineCurrent-1,styler);
	}
	// vars for getting first word to check for keywords
	bool FirstWordStart = false;
	bool FirstWordEnd = false;
		
	const unsigned int KEYWORD_MAX = 10;
	char szKeyword[KEYWORD_MAX]="";
	unsigned int	 szKeywordlen = 0;
	
	char szDo[3]="";
	int	 szDolen = 0;
	bool DoFoundLast = false;
	
	// var for indentlevel
	int levelCurrent = SC_FOLDLEVELBASE;
	if (lineCurrent > 0) {
		levelCurrent = styler.LevelAt(lineCurrent-1) >> 16;
	}
	int levelNext = levelCurrent;
	
	int	visibleChars = 0;
	int functionCount = 0;
	
	char chNext = styler.SafeGetCharAt(startPos);
	char chPrev = '\0';
	char chPrevPrev = '\0';
	char chPrevPrevPrev = '\0';
	
	for (int i = startPos; i < endPos; i++) {

		char ch = chNext;
		chNext = styler.SafeGetCharAt(i + 1);
		
		if ((ch > 0) && setWord.Contains(ch)) {
			visibleChars++;
		}
		
		// get the syle for the current character neede to check in comment
		int stylech = styler.StyleAt(i);
		
		// get first word for the line for indent check max 9 characters
		if (FirstWordStart && (!(FirstWordEnd))) {
			if ((ch > 0) && !setWord.Contains(ch)) {
				FirstWordEnd = true;
			}
			else if (szKeywordlen < KEYWORD_MAX - 1) {
				szKeyword[szKeywordlen++] = static_cast<char>(tolower(ch));
				szKeyword[szKeywordlen] = '\0';
			}
		}
		
		// start the capture of the first word 
		if (!(FirstWordStart)) {
			if ((ch > 0) && (setWord.Contains(ch) || setWordStart.Contains(ch) || ch == ';' || ch == '/')) {
				FirstWordStart = true;
				if (szKeywordlen < KEYWORD_MAX - 1) {
					szKeyword[szKeywordlen++] = static_cast<char>(tolower(ch));
					szKeyword[szKeywordlen] = '\0';
				}
			}
		}
		// only process this logic when not in comment section
		if (stylech != SCE_POWERPRO_COMMENTLINE) {
			if (DoFoundLast) {
				if (DoFoundLast && (ch > 0) && setWord.Contains(ch)) {
					DoFoundLast = false;
				}		
			}
			// find out if the word "do" is the last on a "if" line
			if (FirstWordEnd && strcmp(szKeyword,"if") == 0) {
				if (szDolen == 2) {
					szDo[0] = szDo[1];
					szDo[1] = static_cast<char>(tolower(ch));
					szDo[2] = '\0';
					if (strcmp(szDo,"do") == 0 ) {
						DoFoundLast = true;
					}
				}
				else if (szDolen < 2) {
					szDo[szDolen++] = static_cast<char>(tolower(ch));
					szDo[szDolen] = '\0';
				}
			}
		}

		// End of Line found so process the information 
		 if ((ch == '\r' && chNext != '\n') || (ch == '\n') || (i == endPos)) {
		 
			// **************************
			// Folding logic for Keywords
			// **************************
			
			// if a keyword is found on the current line and the line doesn't end with ;;+ (continuation)
			//    and we are not inside a commentblock.
			if (szKeywordlen > 0 && 
				(!(chPrev == '+' && chPrevPrev == ';' && chPrevPrevPrev ==';')) && 
				((!(IsStreamCommentStyle(style)) || foldInComment)) ) {
			
				// only fold "if" last keyword is "then"  (else its a one line if)
				if (strcmp(szKeyword,"if") == 0  && DoFoundLast) {
						levelNext++;
				}
				// create new fold for these words 
				if (strcmp(szKeyword,"for") == 0) {
					levelNext++;
				}
				
				//handle folding for functions/labels
				//Note: Functions and labels don't have an explicit end like [end function]
				//	1. functions/labels end at the start of another function
				//	2. functions/labels end at the end of the file
				if ((strcmp(szKeyword,"function") == 0) || (szKeywordlen > 0 && szKeyword[0] == '@')) {
					if (isFoldingAll) { //if we're folding the whole document (recursivly by lua script)
						
						if (functionCount > 0) {
							levelCurrent--;
						} else {
							levelNext++;
						}
						functionCount++;	
						
					} else { //if just folding a small piece (by clicking on the minus sign next to the word)
						levelCurrent--;
					}
				}
												
				// end the fold for these words before the current line
				if (strcmp(szKeyword,"endif") == 0 || strcmp(szKeyword,"endfor") == 0) {
						levelNext--;
						levelCurrent--;
				}
				// end the fold for these words before the current line and Start new fold 
				if (strcmp(szKeyword,"else") == 0 || strcmp(szKeyword,"elseif") == 0 ) {
						levelCurrent--;
				}
			}
			// Preprocessor and Comment folding
			int styleNext = GetStyleFirstWord(lineCurrent + 1,styler);

			// *********************************
			// Folding logic for Comment blocks
			// *********************************
			if (foldComment && IsStreamCommentStyle(style)) {
				// Start of a comment block
				if (!(stylePrev==style) && IsStreamCommentStyle(styleNext) && styleNext==style) {
				    levelNext++;
				} 
				// fold till the last line for normal comment lines
				else if (IsStreamCommentStyle(stylePrev) 
						&& !(styleNext == SCE_POWERPRO_COMMENTLINE)
						&& stylePrev == SCE_POWERPRO_COMMENTLINE 
						&& style == SCE_POWERPRO_COMMENTLINE) {
					levelNext--;
				}
				// fold till the one but last line for Blockcomment lines
				else if (IsStreamCommentStyle(stylePrev) 
						&& !(styleNext == SCE_POWERPRO_COMMENTBLOCK)
						&& style == SCE_POWERPRO_COMMENTBLOCK) {
					levelNext--;
					levelCurrent--;
				}
			}

			int levelUse = levelCurrent;
			int lev = levelUse | levelNext << 16;
			if (visibleChars == 0 && foldCompact)
				lev |= SC_FOLDLEVELWHITEFLAG;
			if (levelUse < levelNext) {
				lev |= SC_FOLDLEVELHEADERFLAG;
			}
			if (lev != styler.LevelAt(lineCurrent)) {
				styler.SetLevel(lineCurrent, lev);
			}

			// reset values for the next line
			lineCurrent++;
			stylePrev = style;
			style = styleNext;
			levelCurrent = levelNext;
			visibleChars = 0;
			
			// if the last characters are ;;+ then don't reset since the line continues on the next line.
			if (chPrev == '+' && chPrevPrev == ';' && chPrevPrevPrev == ';') {
				//do nothing
			} else {
				szKeywordlen = 0;
				szDolen = 0;
				FirstWordStart = false;
				FirstWordEnd = false;
				DoFoundLast = false;
				//blank out keyword
				for (unsigned int i = 0; i < KEYWORD_MAX; i++) {
					szKeyword[i] = '\0';
				}
			}
		}

		// save the last processed characters
		if ((ch > 0) && !isspacechar(ch)) {
			chPrevPrevPrev = chPrevPrev;
			chPrevPrev = chPrev;
			chPrev = ch;
			visibleChars++;
		}
	}

	//close folds on the last line - without this a 'phantom' 
	//fold can appear when an open fold is on the last line
	//this can occur because functions and labels don't have an explicit end
	if (lineCurrent >= lastLine) {
		int lev = 0;
		lev |= SC_FOLDLEVELWHITEFLAG;
		styler.SetLevel(lineCurrent, lev);
	}

}

static const char * const powerProWordLists[] = {
            "Keyword list 1",
            "Keyword list 2",
            "Keyword list 3",
            "Keyword list 4",
            0,
        };

static void ColourisePowerProDocWrapper(unsigned int startPos, int length, int initStyle, WordList *keywordlists[],
                                       Accessor &styler) {
	ColourisePowerProDoc(startPos, length, initStyle, keywordlists, styler, false);
}

LexerModule lmPowerPro(SCLEX_POWERPRO, ColourisePowerProDocWrapper, "powerpro", FoldPowerProDoc, powerProWordLists);
