/**
 * @file LexFSharp.cxx
 * Lexer for F# 5.0
 * Copyright (c) 2021 Robert Di Pardo <dipardo.r@gmail.com>
 * Parts of LexerFSharp::Lex were adapted from LexCaml.cxx by Robert Roessler ("RR").
 * Parts of LexerFSharp::Fold were adapted from LexCPP.cxx by Neil Hodgson and Udo Lechner.
 * The License.txt file describes the conditions under which this software may be distributed.
 */
// clang-format off
#include <cstdlib>
#include <cassert>

#include <string>
#include <string_view>
#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"
// clang-format on

using namespace Scintilla;
using namespace Lexilla;

static const char *const lexerName = "fsharp";
static constexpr int WORDLIST_SIZE = 5;
static const char *const fsharpWordLists[] = {
	"standard language keywords",
	"core functions, including those in the FSharp.Collections namespace",
	"built-in types, core namespaces, modules",
	"optional",
	"optional",
	nullptr,
};
static constexpr int keywordClasses[] = {
	SCE_FSHARP_KEYWORD, SCE_FSHARP_KEYWORD2, SCE_FSHARP_KEYWORD3, SCE_FSHARP_KEYWORD4, SCE_FSHARP_KEYWORD5,
};

namespace {

struct OptionsFSharp {
	bool fold;
	bool foldCompact;
	bool foldComment;
	bool foldCommentStream;
	bool foldCommentMultiLine;
	bool foldPreprocessor;
	bool foldImports;
	OptionsFSharp() {
		fold = true;
		foldCompact = true;
		foldComment = true;
		foldCommentStream = true;
		foldCommentMultiLine = true;
		foldPreprocessor = false;
		foldImports = true;
	}
};

struct OptionSetFSharp : public OptionSet<OptionsFSharp> {
	OptionSetFSharp() {
		DefineProperty("fold", &OptionsFSharp::fold);
		DefineProperty("fold.compact", &OptionsFSharp::foldCompact);
		DefineProperty("fold.comment", &OptionsFSharp::foldComment,
				   "Setting this option to 0 disables comment folding in F# files.");

		DefineProperty("fold.fsharp.comment.stream", &OptionsFSharp::foldCommentStream,
				   "Setting this option to 0 disables folding of ML-style comments in F# files when "
				   "fold.comment=1.");

		DefineProperty("fold.fsharp.comment.multiline", &OptionsFSharp::foldCommentMultiLine,
				   "Setting this option to 0 disables folding of grouped line comments in F# files when "
				   "fold.comment=1.");

		DefineProperty("fold.fsharp.preprocessor", &OptionsFSharp::foldPreprocessor,
				   "Setting this option to 1 enables folding of F# compiler directives.");

		DefineProperty("fold.fsharp.imports", &OptionsFSharp::foldImports,
				   "Setting this option to 0 disables folding of F# import declarations.");

		DefineWordListSets(fsharpWordLists);
	}
};

const CharacterSet setOperators = CharacterSet(CharacterSet::setNone, "~^'-+*/%=@|&<>()[]{};,:!?");
const CharacterSet setClosingTokens = CharacterSet(CharacterSet::setNone, ")}]");
const CharacterSet setFormatSpecs = CharacterSet(CharacterSet::setNone, ".%aAbcdeEfFgGiMoOstuxX0123456789");
const CharacterSet setFormatFlags = CharacterSet(CharacterSet::setNone, ".-+0 ");
const CharacterSet numericMetaChars1 = CharacterSet(CharacterSet::setNone, "_IbeEflmnosuxy");
const CharacterSet numericMetaChars2 = CharacterSet(CharacterSet::setNone, "lnsy");
std::map<int, int> numericPrefixes = { { 'b', 2 }, { 'o', 8 }, { 'x', 16 } };
constexpr Sci_Position ZERO_LENGTH = -1;

struct FSharpString {
	Sci_Position startPos;
	int startChar;
	FSharpString() {
		startPos = ZERO_LENGTH;
		startChar = '"';
	}
	constexpr bool HasLength() const {
		return startPos > ZERO_LENGTH;
	}
};

class UnicodeChar {
	enum class Notation { none, asciiDec, asciiHex, utf16, utf32 };
	Notation type = Notation::none;
	// single-byte Unicode char (000 - 255)
	int asciiDigits[3] = { 0 };
	int maxDigit = '9';
	int toEnd = 0;
	bool invalid = false;

public:
	UnicodeChar() noexcept = default;
	explicit UnicodeChar(const int prefix) {
		if (IsADigit(prefix)) {
			*asciiDigits = prefix;
			if (*asciiDigits >= '0' && *asciiDigits <= '2') {
				type = Notation::asciiDec;
				// count first digit as "prefix"
				toEnd = 2;
			}
		} else if (prefix == 'x' || prefix == 'u' || prefix == 'U') {
			switch (prefix) {
				case 'x':
					type = Notation::asciiHex;
					toEnd = 2;
					break;
				case 'u':
					type = Notation::utf16;
					toEnd = 4;
					break;
				case 'U':
					type = Notation::utf32;
					toEnd = 8;
					break;
			}
		}
	}
	void Parse(const int ch) {
		invalid = false;
		switch (type) {
			case Notation::asciiDec: {
				maxDigit = (*asciiDigits < '2') ? '9' : (asciiDigits[1] <= '4') ? '9' : '5';
				if (IsADigit(ch) && asciiDigits[1] <= maxDigit && ch <= maxDigit) {
					asciiDigits[1] = ch;
					toEnd--;
				} else {
					invalid = true;
				}
				break;
			}
			case Notation::asciiHex:
			case Notation::utf16:
				if (IsADigit(ch, 16)) {
					toEnd--;
				} else {
					invalid = true;
				}
				break;
			case Notation::utf32:
				if ((toEnd > 6 && ch == '0') || (toEnd <= 6 && IsADigit(ch, 16))) {
					toEnd--;
				} else {
					invalid = true;
				}
				break;
			case Notation::none:
				break;
		}
	}
	constexpr bool AtEnd() noexcept {
		return invalid || type == Notation::none || (type != Notation::none && toEnd < 0);
	}
};

inline bool MatchStreamCommentStart(StyleContext &cxt) {
	// match (* ... *), but allow point-free usage of the `*` operator,
	// e.g.  List.fold (*) 1 [ 1; 2; 3 ]
	return (cxt.Match('(', '*') && cxt.GetRelative(2) != ')');
}

inline bool MatchStreamCommentEnd(const StyleContext &cxt) {
	return (cxt.ch == ')' && cxt.chPrev == '*');
}

inline bool MatchLineComment(const StyleContext &cxt) {
	// style shebang lines as comments in F# scripts:
	// https://fsharp.org/specs/language-spec/4.1/FSharpSpec-4.1-latest.pdf#page=30&zoom=auto,-98,537
	return cxt.Match('/', '/') || cxt.Match('#', '!');
}

inline bool MatchLineNumberStart(StyleContext &cxt) {
	return cxt.atLineStart && (cxt.MatchIgnoreCase("#line") ||
		(cxt.ch == '#' && (IsADigit(cxt.chNext) || IsADigit(cxt.GetRelative(2)))));
}

inline bool MatchPPDirectiveStart(const StyleContext &cxt) {
	return (cxt.atLineStart && cxt.ch == '#' && iswordstart(cxt.chNext));
}

inline bool MatchTypeAttributeStart(const StyleContext &cxt) {
	return cxt.Match('[', '<');
}

inline bool MatchTypeAttributeEnd(const StyleContext &cxt) {
	return (cxt.ch == ']' && cxt.chPrev == '>');
}

inline bool MatchQuotedExpressionStart(const StyleContext &cxt) {
	return cxt.Match('<', '@');
}

inline bool MatchQuotedExpressionEnd(const StyleContext &cxt) {
	return (cxt.ch == '>' && cxt.chPrev == '@');
}

inline bool MatchStringStart(const StyleContext &cxt) {
	return (cxt.ch == '"' || cxt.Match('@', '"') || cxt.Match('$', '"') || cxt.Match('`', '`'));
}

inline bool MatchStringEnd(StyleContext &cxt, const FSharpString &fsStr) {
	return (fsStr.HasLength() &&
		// end of quoted identifier?
		((cxt.ch == '`' && cxt.chPrev == '`') ||
		// end of triple-quoted-string?
		(fsStr.startChar == '"' && cxt.MatchIgnoreCase("\"\"\"")) ||
		// end of verbatim string?
		(fsStr.startChar == '@' &&
			// embedded quotes must be in pairs
			cxt.ch == '"' && cxt.chNext != '"' &&
			(cxt.chPrev != '"' || (cxt.chPrev == '"' &&
				// empty verbatim string?
				(cxt.GetRelative(-2) == '@' ||
				// pair of quotes at end of string?
				(cxt.GetRelative(-2) == '"' && cxt.GetRelative(-3) != '@'))))))) ||
		(!fsStr.HasLength() && cxt.ch == '"' &&
			(cxt.chPrev != '\\' ||
			// treat backslashes as char literals in verbatim strings
			(fsStr.startChar == '@' && cxt.chPrev == '\\')));
}

inline bool MatchCharacterStart(StyleContext &cxt) {
	// don't style generic type parameters: 'a, 'b, 'T, etc.
	return (cxt.ch == '\'' && !(cxt.chPrev == ':' || cxt.GetRelative(-2) == ':'));
}

inline bool CanEmbedQuotes(StyleContext &cxt) {
	// allow unescaped double quotes inside verbatim and triple-quoted strings, and quoted identifiers:
	// - https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/strings
	// - https://fsharp.org/specs/language-spec/4.1/FSharpSpec-4.1-latest.pdf#page=25&zoom=auto,-98,600
	return cxt.MatchIgnoreCase("\"\"\"") || cxt.Match('@', '"') || cxt.Match('`', '`');
}

inline bool IsNumber(StyleContext &cxt, const int base = 10) {
	return IsADigit(cxt.ch, base) || (IsADigit(cxt.chPrev, base) && numericMetaChars1.Contains(cxt.ch)) ||
		(IsADigit(cxt.GetRelative(-2), base) && numericMetaChars2.Contains(cxt.ch));
}

inline bool IsFloat(const StyleContext &cxt) {
	return (cxt.ch == '.' && IsADigit(cxt.chPrev)) ||
		((cxt.ch == '+' || cxt.ch == '-' ) && IsADigit(cxt.chNext));
}

class LexerFSharp : public DefaultLexer {
	WordList keywords[WORDLIST_SIZE];
	OptionsFSharp options;
	OptionSetFSharp optionSet;

public:
	explicit LexerFSharp() : DefaultLexer(lexerName, SCLEX_FSHARP) {
	}
	static ILexer5 *LexerFactoryFSharp() {
		return new LexerFSharp();
	}
	virtual ~LexerFSharp() {
	}
	void SCI_METHOD Release() noexcept override {
		delete this;
	}
	int SCI_METHOD Version() const noexcept override {
		return lvRelease5;
	}
	const char *SCI_METHOD GetName() noexcept override {
		return lexerName;
	}
	int SCI_METHOD GetIdentifier() noexcept override {
		return SCLEX_FSHARP;
	}
	int SCI_METHOD LineEndTypesSupported() noexcept override {
		return SC_LINE_END_TYPE_DEFAULT;
	}
	void *SCI_METHOD PrivateCall(int, void *) noexcept override {
		return nullptr;
	}
	const char *SCI_METHOD DescribeWordListSets() override {
		return optionSet.DescribeWordListSets();
	}
	const char *SCI_METHOD PropertyNames() override {
		return optionSet.PropertyNames();
	}
	int SCI_METHOD PropertyType(const char *name) override {
		return optionSet.PropertyType(name);
	}
	const char *SCI_METHOD DescribeProperty(const char *name) override {
		return optionSet.DescribeProperty(name);
	}
	const char *SCI_METHOD PropertyGet(const char *key) override {
		return optionSet.PropertyGet(key);
	}
	Sci_Position SCI_METHOD PropertySet(const char *key, const char *val) override {
		if (optionSet.PropertySet(&options, key, val)) {
			return 0;
		}
		return ZERO_LENGTH;
	}
	Sci_Position SCI_METHOD WordListSet(int n, const char *wl) override;
	void SCI_METHOD Lex(Sci_PositionU start, Sci_Position length, int initStyle, IDocument *pAccess) override;
	void SCI_METHOD Fold(Sci_PositionU start, Sci_Position length, int initStyle,IDocument *pAccess) override;
};

Sci_Position SCI_METHOD LexerFSharp::WordListSet(int n, const char *wl) {
	WordList *wordListN = nullptr;
	Sci_Position firstModification = ZERO_LENGTH;

	if (n < WORDLIST_SIZE) {
		wordListN = &keywords[n];
	}
	if (wordListN) {
		WordList wlNew;
		wlNew.Set(wl);

		if (*wordListN != wlNew) {
			wordListN->Set(wl);
			firstModification = 0;
		}
	}
	return firstModification;
}

void SCI_METHOD LexerFSharp::Lex(Sci_PositionU start, Sci_Position length, int initStyle, IDocument *pAccess) {
	LexAccessor styler(pAccess);
	StyleContext sc(start, static_cast<Sci_PositionU>(length), initStyle, styler);
	Sci_PositionU cursor = 0;
	UnicodeChar uniCh = UnicodeChar();
	FSharpString fsStr = FSharpString();
	constexpr Sci_Position MAX_WORD_LEN = 64;
	constexpr int SPACE = ' ';
	int currentBase = 10;

	while (sc.More()) {
		Sci_PositionU colorSpan = sc.currentPos - 1;
		int state = -1;
		bool advance = true;

		switch (sc.state & 0xff) {
			case SCE_FSHARP_DEFAULT:
				cursor = sc.currentPos;

				if (MatchLineNumberStart(sc)) {
					state = SCE_FSHARP_LINENUM;
				} else if (MatchPPDirectiveStart(sc)) {
					state = SCE_FSHARP_PREPROCESSOR;
				} else if (MatchLineComment(sc)) {
					state = SCE_FSHARP_COMMENTLINE;
					sc.Forward();
					sc.ch = SPACE;
				} else if (MatchStreamCommentStart(sc)) {
					state = SCE_FSHARP_COMMENT;
					sc.Forward();
					sc.ch = SPACE;
				} else if (MatchTypeAttributeStart(sc)) {
					state = SCE_FSHARP_ATTRIBUTE;
					sc.Forward();
				} else if (MatchQuotedExpressionStart(sc)) {
					state = SCE_FSHARP_QUOTATION;
					sc.Forward();
				} else if (MatchCharacterStart(sc)) {
					state = SCE_FSHARP_CHARACTER;
				} else if (MatchStringStart(sc)) {
					fsStr.startChar = sc.ch;
					fsStr.startPos = ZERO_LENGTH;
					if (CanEmbedQuotes(sc)) {
						// double quotes after this position should be non-terminating
						fsStr.startPos = static_cast<Sci_Position>(sc.currentPos - cursor);
					}
					if (sc.ch == '`') {
						state = SCE_FSHARP_QUOT_IDENTIFIER;
					} else if (sc.ch == '@') {
						state = SCE_FSHARP_VERBATIM;
					} else {
						state = SCE_FSHARP_STRING;
					}
				} else if (IsADigit(sc.ch, currentBase) ||
					   ((sc.ch == '+' || sc.ch == '-') && IsADigit(sc.chNext))) {
					state = SCE_FSHARP_NUMBER;
					if (sc.ch == '0') {
						const int prefix = sc.chNext;
						if (numericPrefixes.find(prefix) != numericPrefixes.end()) {
							currentBase = numericPrefixes[prefix];
						}
					}
				} else if (setOperators.Contains(sc.ch) &&
					   // don't use operator style in async keywords (e.g. `return!`)
					   !(sc.ch == '!' && iswordstart(sc.chPrev)) &&
					   // don't use operator style in member access, array/string indexing
					   !(sc.ch == '.' && (sc.chPrev == '\"' || iswordstart(sc.chPrev)) &&
					     (iswordstart(sc.chNext) || sc.chNext == '['))) {
					state = SCE_FSHARP_OPERATOR;
				} else if (iswordstart(sc.ch)) {
					state = SCE_FSHARP_IDENTIFIER;
				} else {
					state = SCE_FSHARP_DEFAULT;
				}
				break;
			case SCE_FSHARP_LINENUM:
			case SCE_FSHARP_PREPROCESSOR:
			case SCE_FSHARP_COMMENTLINE:
				if (sc.MatchLineEnd()) {
					state = SCE_FSHARP_DEFAULT;
					advance = false;
				}
				break;
			case SCE_FSHARP_COMMENT:
			case SCE_FSHARP_ATTRIBUTE:
			case SCE_FSHARP_QUOTATION:
				if (MatchStreamCommentEnd(sc) || MatchTypeAttributeEnd(sc) || MatchQuotedExpressionEnd(sc)) {
					state = SCE_FSHARP_DEFAULT;
					colorSpan++;
				}
				break;
			case SCE_FSHARP_CHARACTER:
				if (sc.chPrev == '\\' && sc.GetRelative(-2) != '\\') {
					uniCh = UnicodeChar(sc.ch);
				} else if (sc.ch == '\'' &&
					   ((sc.chPrev == ' ' && sc.GetRelative(-2) == '\'') || sc.chPrev != '\\' ||
						(sc.chPrev == '\\' && sc.GetRelative(-2) == '\\'))) {
					// byte literal?
					if (sc.Match('\'', 'B')) {
						sc.Forward();
						colorSpan++;
					}
					if (!sc.atLineEnd) {
						colorSpan++;
					} else {
						sc.ChangeState(SCE_FSHARP_IDENTIFIER);
					}
					state = SCE_FSHARP_DEFAULT;
				} else {
					uniCh.Parse(sc.ch);
					if (uniCh.AtEnd() && (sc.currentPos - cursor) >= 2) {
						// terminate now, since we left the char behind
						sc.ChangeState(SCE_FSHARP_IDENTIFIER);
						advance = false;
					}
				}
				break;
			case SCE_FSHARP_STRING:
			case SCE_FSHARP_VERBATIM:
			case SCE_FSHARP_QUOT_IDENTIFIER:
				if (MatchStringEnd(sc, fsStr)) {
					const Sci_Position strLen = static_cast<Sci_Position>(sc.currentPos - cursor);
					// backtrack to start of string
					for (Sci_Position i = -strLen; i < 0; i++) {
						const int startQuote = sc.GetRelative(i);
						if (startQuote == '\"' || (startQuote == '`' && sc.GetRelative(i - 1) == '`')) {
							// byte array?
							if (sc.Match('\"', 'B')) {
								sc.Forward();
								colorSpan++;
							}
							if (!sc.atLineEnd) {
								colorSpan++;
							} else {
								sc.ChangeState(SCE_FSHARP_IDENTIFIER);
							}
							state = SCE_FSHARP_DEFAULT;
							break;
						}
					}
				} else if (sc.ch == '%' && !(fsStr.startChar == '`' || fsStr.startChar == '$') &&
					   (setFormatSpecs.Contains(sc.chNext) || setFormatFlags.Contains(sc.chNext))) {
					state = SCE_FSHARP_FORMAT_SPEC;
				}
				break;
			case SCE_FSHARP_IDENTIFIER:
				if (!(iswordstart(sc.ch) || sc.ch == '\'')) {
					const Sci_Position wordLen = static_cast<Sci_Position>(sc.currentPos - cursor);
					if (wordLen < MAX_WORD_LEN) {
						// wordLength is believable as keyword, [re-]construct token - RR
						char token[MAX_WORD_LEN] = { 0 };
						for (Sci_Position i = -wordLen; i < 0; i++) {
							token[wordLen + i] = static_cast<char>(sc.GetRelative(i));
						}
						token[wordLen] = '\0';
						// a snake_case_identifier can never be a keyword
						if (!(sc.ch == '_' || sc.GetRelative(-wordLen - 1) == '_')) {
							for (int i = 0; i < WORDLIST_SIZE; i++) {
								if (keywords[i].InList(token)) {
									sc.ChangeState(keywordClasses[i]);
									break;
								}
							}
						}
					}
					state = SCE_FSHARP_DEFAULT;
					advance = false;
				}
				break;
			case SCE_FSHARP_OPERATOR:
				// special-case "()" and "[]" tokens as KEYWORDS - RR
				if (setClosingTokens.Contains(sc.ch) &&
					((sc.ch == ')' && sc.chPrev == '(') || (sc.ch == ']' && sc.chPrev == '['))) {
					sc.ChangeState(SCE_FSHARP_KEYWORD);
					colorSpan++;
				} else {
					advance = false;
				}
				state = SCE_FSHARP_DEFAULT;
				break;
			case SCE_FSHARP_NUMBER:
				state = (IsNumber(sc, currentBase) || IsFloat(sc))
					? SCE_FSHARP_NUMBER
					// change style even when operators aren't spaced
					: setOperators.Contains(sc.ch) ? SCE_FSHARP_OPERATOR : SCE_FSHARP_DEFAULT;
				currentBase = (state == SCE_FSHARP_NUMBER) ? currentBase : 10;
				break;
			case SCE_FSHARP_FORMAT_SPEC:
				if (!setFormatSpecs.Contains(sc.chNext) ||
				    !(setFormatFlags.Contains(sc.ch) || IsADigit(sc.ch)) ||
				    (setFormatFlags.Contains(sc.ch) && sc.ch == sc.chNext)) {
					colorSpan++;
					state = (fsStr.startChar == '@') ? SCE_FSHARP_VERBATIM : SCE_FSHARP_STRING;
				}
				break;
		}

		if (state >= SCE_FSHARP_DEFAULT) {
			styler.ColourTo(colorSpan, sc.state);
			sc.ChangeState(state);
		}

		if (advance) {
			sc.Forward();
		}
	}

	sc.Complete();
}

bool LineContains(LexAccessor &styler, const char *word, const Sci_Position start, const Sci_Position end = 1,
		  const int chAttr = SCE_FSHARP_DEFAULT);

void FoldLexicalGroup(LexAccessor &styler, int &levelNext, const Sci_Position lineCurrent, const char *word,
		      const int chAttr);

void SCI_METHOD LexerFSharp::Fold(Sci_PositionU start, Sci_Position length, int initStyle, IDocument *pAccess) {
	if (!options.fold) {
		return;
	}

	LexAccessor styler(pAccess);
	const Sci_Position startPos = static_cast<Sci_Position>(start);
	const Sci_PositionU endPos = start + length;
	Sci_Position lineCurrent = styler.GetLine(startPos);
	Sci_Position lineNext = lineCurrent + 1;
	Sci_Position lineStartNext = styler.LineStart(lineNext);
	int style = initStyle;
	int styleNext = styler.StyleAt(startPos);
	char chNext = styler[startPos];
	int levelNext;
	int levelCurrent = SC_FOLDLEVELBASE;
	int visibleChars = 0;

	if (lineCurrent > 0) {
		levelCurrent = styler.LevelAt(lineCurrent - 1) >> 0x10;
	}

	levelNext = levelCurrent;

	for (Sci_PositionU i = start; i < endPos; i++) {
		const Sci_Position currentPos = static_cast<Sci_Position>(i);
		const bool atEOL = currentPos == (lineStartNext - 1);
		const bool atLineOrDocEnd = (atEOL || (i == (endPos - 1)));
		const bool atDosOrMacEOL = (atEOL || styler.SafeGetCharAt(currentPos) == '\r');
		const int stylePrev = style;
		const char ch = chNext;
		const bool inLineComment = (stylePrev == SCE_FSHARP_COMMENTLINE);
		style = styleNext;
		styleNext = styler.StyleAt(currentPos + 1);
		chNext = styler.SafeGetCharAt(currentPos + 1);

		if (options.foldComment) {
			if (options.foldCommentMultiLine && inLineComment && atDosOrMacEOL &&
			    (lineCurrent > 0 || styler.StyleAt(lineStartNext) == SCE_FSHARP_COMMENTLINE)) {
				FoldLexicalGroup(styler, levelNext, lineCurrent, "//", SCE_FSHARP_COMMENTLINE);
			}

			if (options.foldCommentStream && style == SCE_FSHARP_COMMENT && !inLineComment) {
				if (stylePrev != SCE_FSHARP_COMMENT) {
					levelNext++;
				} else if (styleNext != SCE_FSHARP_COMMENT && !atEOL) {
					levelNext--;
				}
			}
		}

		if (options.foldPreprocessor && style == SCE_FSHARP_PREPROCESSOR) {
			if (styler.Match(currentPos, "#if")) {
				levelNext++;
			} else if (styler.Match(currentPos, "#endif")) {
				levelNext--;
			}
		}

		if (options.foldImports && atLineOrDocEnd &&
		    LineContains(styler, "open ", lineCurrent, 1, SCE_FSHARP_KEYWORD)) {
			FoldLexicalGroup(styler, levelNext, lineCurrent, "open ", SCE_FSHARP_KEYWORD);
		}

		if (!IsASpace(ch)) {
			visibleChars++;
		}

		if (atLineOrDocEnd) {
			int levelUse = levelCurrent;
			int lev = levelUse | levelNext << 16;

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

			visibleChars = 0;
			lineCurrent++;
			lineNext = lineCurrent + 1;
			lineStartNext = styler.LineStart(lineNext);
			levelCurrent = levelNext;

			if (atEOL && (currentPos == (styler.Length() - 1))) {
				styler.SetLevel(lineCurrent, (levelCurrent | levelCurrent << 16) | SC_FOLDLEVELWHITEFLAG);
			}
		}
	}
}

bool LineContains(LexAccessor &styler, const char *word, const Sci_Position start, const Sci_Position end,
		  const int chAttr) {
	bool found = false;
	bool requireStyle = (chAttr > SCE_FSHARP_DEFAULT);
	const Sci_Position limit = (end > 1) ? end : start;
	for (Sci_Position i = start; i < styler.LineEnd(limit); i++) {
		if (styler.Match(i, word)) {
			found = requireStyle ? styler.StyleAt(i) == chAttr : true;
			break;
		}
	}
	return found;
}

void FoldLexicalGroup(LexAccessor &styler, int &levelNext, const Sci_Position lineCurrent, const char *word,
		      const int chAttr) {
	const Sci_Position linePrev = lineCurrent - 1;
	const Sci_Position lineNext = lineCurrent + 1;
	const Sci_Position lineStartPrev = styler.LineStart(linePrev);
	const Sci_Position lineStartNext = styler.LineStart(lineNext);
	const bool atFoldGroupStart =
	    (lineCurrent == 0 || !LineContains(styler, word, lineStartPrev, linePrev, chAttr)) &&
	    LineContains(styler, word, lineStartNext, lineNext, chAttr);
	const bool atFoldGroupEnd =
		LineContains(styler, word, lineStartPrev, linePrev, chAttr) &&
		!LineContains(styler, word, lineStartNext, lineNext, chAttr);

	if (atFoldGroupStart) {
		levelNext++;
	} else if (atFoldGroupEnd && levelNext > SC_FOLDLEVELBASE) {
		levelNext--;
	}
}
} // namespace

LexerModule lmFSharp(SCLEX_FSHARP, LexerFSharp::LexerFactoryFSharp, "fsharp", fsharpWordLists);
