// Scintilla source code edit control
/**
 * @file LexRegistry.cxx
 * @date July 26 2014
 * @brief Lexer for Windows registration files(.reg)
 * @author nkmathew
 *
 * The License.txt file describes the conditions under which this software may be
 * distributed.
 *
 */

#include <cstdlib>
#include <cassert>
#include <cctype>
#include <cstdio>

#include <string>
#include <string_view>
#include <vector>
#include <map>
#include <functional>

#include "ILexer.h"
#include "Scintilla.h"
#include "SciLexer.h"
#include "WordList.h"
#include "LexAccessor.h"
#include "StyleContext.h"
#include "CharacterSet.h"
#include "LexerModule.h"
#include "OptionSet.h"
#include "DefaultLexer.h"

using namespace Scintilla;
using namespace Lexilla;

static const char *const RegistryWordListDesc[] = {
	0
};

struct OptionsRegistry {
	bool foldCompact = false;
	bool fold = false;
};

struct OptionSetRegistry : public OptionSet<OptionsRegistry> {
	OptionSetRegistry() {
		DefineProperty("fold.compact", &OptionsRegistry::foldCompact);
		DefineProperty("fold", &OptionsRegistry::fold);
		DefineWordListSets(RegistryWordListDesc);
	}
};

class LexerRegistry : public DefaultLexer {
	OptionsRegistry options;
	OptionSetRegistry optSetRegistry;

	static bool IsStringState(int state) {
		return (state == SCE_REG_VALUENAME || state == SCE_REG_STRING);
	}

	static bool IsKeyPathState(int state) {
		return (state == SCE_REG_ADDEDKEY || state == SCE_REG_DELETEDKEY);
	}

	static bool AtValueType(LexAccessor &styler, Sci_Position start) {
		Sci_Position i = 0;
		while (i < 10) {
			i++;
			char curr = styler.SafeGetCharAt(start+i, '\0');
			if (curr == ':') {
				return true;
			} else if (!curr) {
				return false;
			}
		}
		return false;
	}

	static bool IsNextNonWhitespace(LexAccessor &styler, Sci_Position start, char ch) {
		Sci_Position i = 0;
		while (i < 100) {
			i++;
			char curr = styler.SafeGetCharAt(start+i, '\0');
			char next = styler.SafeGetCharAt(start+i+1, '\0');
			bool atEOL = (curr == '\r' && next != '\n') || (curr == '\n');
			if (curr == ch) {
				return true;
			} else if (!isspacechar(curr) || atEOL) {
				return false;
			}
		}
		return false;
	}

	// Looks for the equal sign at the end of the string
	static bool AtValueName(LexAccessor &styler, Sci_Position start) {
		bool atEOL = false;
		Sci_Position i = 0;
		bool escaped = false;
		while (!atEOL) {
			i++;
			char curr = styler.SafeGetCharAt(start+i, '\0');
			char next = styler.SafeGetCharAt(start+i+1, '\0');
			atEOL = (curr == '\r' && next != '\n') || (curr == '\n');
			if (escaped) {
				escaped = false;
				continue;
			}
			escaped = curr == '\\';
			if (curr == '"') {
				return IsNextNonWhitespace(styler, start+i, '=');
			} else if (!curr) {
				return false;
			}
		}
		return false;
	}

	static bool AtKeyPathEnd(LexAccessor &styler, Sci_Position start) {
		bool atEOL = false;
		Sci_Position i = 0;
		while (!atEOL) {
			i++;
			char curr = styler.SafeGetCharAt(start+i, '\0');
			char next = styler.SafeGetCharAt(start+i+1, '\0');
			atEOL = (curr == '\r' && next != '\n') || (curr == '\n');
			if (curr == ']' || !curr) {
				// There's still at least one or more square brackets ahead
				return false;
			}
		}
		return true;
	}

	static bool AtGUID(LexAccessor &styler, Sci_Position start) {
		int count = 8;
		int portion = 0;
		int offset = 1;
		char digit = '\0';
		while (portion < 5) {
			int i = 0;
			while (i < count) {
				digit = styler.SafeGetCharAt(start+offset);
				if (!(isxdigit(digit) || digit == '-')) {
					return false;
				}
				offset++;
				i++;
			}
			portion++;
			count = (portion == 4) ? 13 : 5;
		}
		digit = styler.SafeGetCharAt(start+offset);
		if (digit == '}') {
			return true;
		} else {
			return false;
		}
	}

public:
	LexerRegistry() : DefaultLexer("registry", SCLEX_REGISTRY) {}
	virtual ~LexerRegistry() {}
	int SCI_METHOD Version() const override {
		return lvRelease5;
	}
	void SCI_METHOD Release() override {
		delete this;
	}
	const char *SCI_METHOD PropertyNames() override {
		return optSetRegistry.PropertyNames();
	}
	int SCI_METHOD PropertyType(const char *name) override {
		return optSetRegistry.PropertyType(name);
	}
	const char *SCI_METHOD DescribeProperty(const char *name) override {
		return optSetRegistry.DescribeProperty(name);
	}
	Sci_Position SCI_METHOD PropertySet(const char *key, const char *val) override {
		if (optSetRegistry.PropertySet(&options, key, val)) {
			return 0;
		}
		return -1;
	}
	const char * SCI_METHOD PropertyGet(const char *key) override {
		return optSetRegistry.PropertyGet(key);
	}

	Sci_Position SCI_METHOD WordListSet(int, const char *) override {
		return -1;
	}
	void *SCI_METHOD PrivateCall(int, void *) override {
		return 0;
	}
	static ILexer5 *LexerFactoryRegistry() {
		return new LexerRegistry;
	}
	const char *SCI_METHOD DescribeWordListSets() override {
		return optSetRegistry.DescribeWordListSets();
	}
	void SCI_METHOD Lex(Sci_PositionU startPos,
								Sci_Position length,
								int initStyle,
								IDocument *pAccess) override;
	void SCI_METHOD Fold(Sci_PositionU startPos,
								 Sci_Position length,
								 int initStyle,
								 IDocument *pAccess) override;
};

void SCI_METHOD LexerRegistry::Lex(Sci_PositionU startPos,
								   Sci_Position length,
								   int initStyle,
								   IDocument *pAccess) {
	int beforeGUID = SCE_REG_DEFAULT;
	int beforeEscape = SCE_REG_DEFAULT;
	CharacterSet setOperators = CharacterSet(CharacterSet::setNone, "-,.=:\\@()");
	LexAccessor styler(pAccess);
	StyleContext context(startPos, length, initStyle, styler);
	bool highlight = true;
	bool afterEqualSign = false;
	while (context.More()) {
		if (context.atLineStart) {
			Sci_Position currPos = static_cast<Sci_Position>(context.currentPos);
			bool continued = styler[currPos-3] == '\\';
			highlight = continued ? true : false;
		}
		switch (context.state) {
			case SCE_REG_COMMENT:
				if (context.atLineEnd) {
					context.SetState(SCE_REG_DEFAULT);
				}
				break;
			case SCE_REG_VALUENAME:
			case SCE_REG_STRING: {
					Sci_Position currPos = static_cast<Sci_Position>(context.currentPos);
					if (context.ch == '"') {
						context.ForwardSetState(SCE_REG_DEFAULT);
					} else if (context.ch == '\\') {
						beforeEscape = context.state;
						context.SetState(SCE_REG_ESCAPED);
						context.Forward();
					} else if (context.ch == '{') {
						if (AtGUID(styler, currPos)) {
							beforeGUID = context.state;
							context.SetState(SCE_REG_STRING_GUID);
						}
					}
					if (context.state == SCE_REG_STRING &&
						context.ch == '%' &&
						(isdigit(context.chNext) || context.chNext == '*')) {
						context.SetState(SCE_REG_PARAMETER);
					}
				}
				break;
			case SCE_REG_PARAMETER:
				context.ForwardSetState(SCE_REG_STRING);
				if (context.ch == '"') {
					context.ForwardSetState(SCE_REG_DEFAULT);
				}
				break;
			case SCE_REG_VALUETYPE:
				if (context.ch == ':') {
					context.SetState(SCE_REG_DEFAULT);
					afterEqualSign = false;
				}
				break;
			case SCE_REG_HEXDIGIT:
			case SCE_REG_OPERATOR:
				context.SetState(SCE_REG_DEFAULT);
				break;
			case SCE_REG_DELETEDKEY:
			case SCE_REG_ADDEDKEY: {
					Sci_Position currPos = static_cast<Sci_Position>(context.currentPos);
					if (context.ch == ']' && AtKeyPathEnd(styler, currPos)) {
						context.ForwardSetState(SCE_REG_DEFAULT);
					} else if (context.ch == '{') {
						if (AtGUID(styler, currPos)) {
							beforeGUID = context.state;
							context.SetState(SCE_REG_KEYPATH_GUID);
						}
					}
				}
				break;
			case SCE_REG_ESCAPED:
				if (context.ch == '"') {
					context.SetState(beforeEscape);
					context.ForwardSetState(SCE_REG_DEFAULT);
				} else if (context.ch == '\\') {
					context.Forward();
				} else {
					context.SetState(beforeEscape);
					beforeEscape = SCE_REG_DEFAULT;
				}
				break;
			case SCE_REG_STRING_GUID:
			case SCE_REG_KEYPATH_GUID: {
					if (context.ch == '}') {
						context.ForwardSetState(beforeGUID);
						beforeGUID = SCE_REG_DEFAULT;
					}
					Sci_Position currPos = static_cast<Sci_Position>(context.currentPos);
					if (context.ch == '"' && IsStringState(context.state)) {
						context.ForwardSetState(SCE_REG_DEFAULT);
					} else if (context.ch == ']' &&
							   AtKeyPathEnd(styler, currPos) &&
							   IsKeyPathState(context.state)) {
						context.ForwardSetState(SCE_REG_DEFAULT);
					} else if (context.ch == '\\' && IsStringState(context.state)) {
						beforeEscape = context.state;
						context.SetState(SCE_REG_ESCAPED);
						context.Forward();
					}
				}
				break;
		}
		// Determine if a new state should be entered.
		if (context.state == SCE_REG_DEFAULT) {
			Sci_Position currPos = static_cast<Sci_Position>(context.currentPos);
			if (context.ch == ';') {
				context.SetState(SCE_REG_COMMENT);
			} else if (context.ch == '"') {
				if (AtValueName(styler, currPos)) {
					context.SetState(SCE_REG_VALUENAME);
				} else {
					context.SetState(SCE_REG_STRING);
				}
			} else if (context.ch == '[') {
				if (IsNextNonWhitespace(styler, currPos, '-')) {
					context.SetState(SCE_REG_DELETEDKEY);
				} else {
					context.SetState(SCE_REG_ADDEDKEY);
				}
			} else if (context.ch == '=') {
				afterEqualSign = true;
				highlight = true;
			} else if (afterEqualSign) {
				bool wordStart = isalpha(context.ch) && !isalpha(context.chPrev);
				if (wordStart && AtValueType(styler, currPos)) {
					context.SetState(SCE_REG_VALUETYPE);
				}
			} else if (isxdigit(context.ch) && highlight) {
				context.SetState(SCE_REG_HEXDIGIT);
			}
			highlight = (context.ch == '@') ? true : highlight;
			if (setOperators.Contains(context.ch) && highlight) {
				context.SetState(SCE_REG_OPERATOR);
			}
		}
		context.Forward();
	}
	context.Complete();
}

// Folding similar to that of FoldPropsDoc in LexOthers
void SCI_METHOD LexerRegistry::Fold(Sci_PositionU startPos,
									Sci_Position length,
									int,
									IDocument *pAccess) {
	if (!options.fold) {
		return;
	}
	LexAccessor styler(pAccess);
	Sci_Position currLine = styler.GetLine(startPos);
	int visibleChars = 0;
	Sci_PositionU endPos = startPos + length;
	bool atKeyPath = false;
	for (Sci_PositionU i = startPos; i < endPos; i++) {
		atKeyPath = IsKeyPathState(styler.StyleAt(i)) ? true : atKeyPath;
		char curr = styler.SafeGetCharAt(i);
		char next = styler.SafeGetCharAt(i+1);
		bool atEOL = (curr == '\r' && next != '\n') || (curr == '\n');
		if (atEOL || i == (endPos-1)) {
			int level = SC_FOLDLEVELBASE;
			if (currLine > 0) {
				int prevLevel = styler.LevelAt(currLine-1);
				if (prevLevel & SC_FOLDLEVELHEADERFLAG) {
					level += 1;
				} else {
					level = prevLevel;
				}
			}
			if (!visibleChars && options.foldCompact) {
				level |= SC_FOLDLEVELWHITEFLAG;
			} else if (atKeyPath) {
				level = SC_FOLDLEVELBASE | SC_FOLDLEVELHEADERFLAG;
			}
			if (level != styler.LevelAt(currLine)) {
				styler.SetLevel(currLine, level);
			}
			currLine++;
			visibleChars = 0;
			atKeyPath = false;
		}
		if (!isspacechar(curr)) {
			visibleChars++;
		}
	}

	// Make the folding reach the last line in the file
	int level = SC_FOLDLEVELBASE;
	if (currLine > 0) {
		int prevLevel = styler.LevelAt(currLine-1);
		if (prevLevel & SC_FOLDLEVELHEADERFLAG) {
			level += 1;
		} else {
			level = prevLevel;
		}
	}
	styler.SetLevel(currLine, level);
}

extern const LexerModule lmRegistry(SCLEX_REGISTRY,
					   LexerRegistry::LexerFactoryRegistry,
					   "registry",
					   RegistryWordListDesc);

