// Scintilla source code edit control
/** @file LexErrorList.cxx
 ** Lexer for error lists. Used for the output pane in SciTE.
 **/
// Copyright 1998-2001 by Neil Hodgson <neilh@scintilla.org>
// The License.txt file describes the conditions under which this software may be distributed.

#include <cstdlib>
#include <cassert>
#include <cstring>
#include <cstdio>
#include <cstdarg>

#include <string>
#include <string_view>
#include <initializer_list>

#include "ILexer.h"
#include "Scintilla.h"
#include "SciLexer.h"

#include "InList.h"
#include "WordList.h"
#include "LexAccessor.h"
#include "Accessor.h"
#include "StyleContext.h"
#include "CharacterSet.h"
#include "LexerModule.h"

using namespace Lexilla;

namespace {

bool strstart(const char *haystack, const char *needle) noexcept {
	return strncmp(haystack, needle, strlen(needle)) == 0;
}

constexpr bool Is0To9(char ch) noexcept {
	return (ch >= '0') && (ch <= '9');
}

constexpr bool Is1To9(char ch) noexcept {
	return (ch >= '1') && (ch <= '9');
}

bool AtEOL(Accessor &styler, Sci_Position i) {
	return (styler[i] == '\n') ||
	       ((styler[i] == '\r') && (styler.SafeGetCharAt(i + 1) != '\n'));
}

bool IsGccExcerpt(const char *s) noexcept {
	while (*s) {
		if (s[0] == ' ' && s[1] == '|' && (s[2] == ' ' || s[2] == '+')) {
			return true;
		}
		if (!(s[0] == ' ' || s[0] == '+' || Is0To9(s[0]))) {
			return false;
		}
		s++;
	}
	return true;
}

const std::string_view bashDiagnosticMark = ": line ";
bool IsBashDiagnostic(std::string_view sv) {
	const size_t mark = sv.find(bashDiagnosticMark);
	if (mark == std::string_view::npos) {
		return false;
	}
	std::string_view rest = sv.substr(mark + bashDiagnosticMark.length());
	if (rest.empty() || !Is0To9(rest.front())) {
		return false;
	}
	while (!rest.empty() && Is0To9(rest.front())) {
		rest.remove_prefix(1);
	}
	return !rest.empty() && (rest.front() == ':');
}


int RecogniseErrorListLine(const char *lineBuffer, Sci_PositionU lengthLine, Sci_Position &startValue) {
	if (lineBuffer[0] == '>') {
		// Command or return status
		return SCE_ERR_CMD;
	} else if (lineBuffer[0] == '<') {
		// Diff removal.
		return SCE_ERR_DIFF_DELETION;
	} else if (lineBuffer[0] == '!') {
		return SCE_ERR_DIFF_CHANGED;
	} else if (lineBuffer[0] == '+') {
		if (strstart(lineBuffer, "+++ ")) {
			return SCE_ERR_DIFF_MESSAGE;
		} else {
			return SCE_ERR_DIFF_ADDITION;
		}
	} else if (lineBuffer[0] == '-') {
		if (strstart(lineBuffer, "--- ")) {
			return SCE_ERR_DIFF_MESSAGE;
		} else {
			return SCE_ERR_DIFF_DELETION;
		}
	} else if (strstart(lineBuffer, "cf90-")) {
		// Absoft Pro Fortran 90/95 v8.2 error and/or warning message
		return SCE_ERR_ABSF;
	} else if (strstart(lineBuffer, "fortcom:")) {
		// Intel Fortran Compiler v8.0 error/warning message
		return SCE_ERR_IFORT;
	} else if (strstr(lineBuffer, "File \"") && strstr(lineBuffer, ", line ")) {
		return SCE_ERR_PYTHON;
	} else if (strstr(lineBuffer, " in ") && strstr(lineBuffer, " on line ")) {
		return SCE_ERR_PHP;
	} else if ((strstart(lineBuffer, "Error ") ||
	            strstart(lineBuffer, "Warning ")) &&
	           strstr(lineBuffer, " at (") &&
	           strstr(lineBuffer, ") : ") &&
	           (strstr(lineBuffer, " at (") < strstr(lineBuffer, ") : "))) {
		// Intel Fortran Compiler error/warning message
		return SCE_ERR_IFC;
	} else if (strstart(lineBuffer, "Error ")) {
		// Borland error message
		return SCE_ERR_BORLAND;
	} else if (strstart(lineBuffer, "Warning ")) {
		// Borland warning message
		return SCE_ERR_BORLAND;
	} else if (strstr(lineBuffer, "at line ") &&
	        (strstr(lineBuffer, "at line ") < (lineBuffer + lengthLine)) &&
	           strstr(lineBuffer, "file ") &&
	           (strstr(lineBuffer, "file ") < (lineBuffer + lengthLine))) {
		// Lua 4 error message
		return SCE_ERR_LUA;
	} else if (strstr(lineBuffer, " at ") &&
	        (strstr(lineBuffer, " at ") < (lineBuffer + lengthLine)) &&
	           strstr(lineBuffer, " line ") &&
	           (strstr(lineBuffer, " line ") < (lineBuffer + lengthLine)) &&
	        (strstr(lineBuffer, " at ") + 4 < (strstr(lineBuffer, " line ")))) {
		// perl error message:
		// <message> at <file> line <line>
		return SCE_ERR_PERL;
	} else if ((lengthLine >= 6) &&
	           (memcmp(lineBuffer, "   at ", 6) == 0) &&
	           strstr(lineBuffer, ":line ")) {
		// A .NET traceback
		return SCE_ERR_NET;
	} else if (strstart(lineBuffer, "Line ") &&
	           strstr(lineBuffer, ", file ")) {
		// Essential Lahey Fortran error message
		return SCE_ERR_ELF;
	} else if (strstart(lineBuffer, "line ") &&
	           strstr(lineBuffer, " column ")) {
		// HTML tidy style: line 42 column 1
		return SCE_ERR_TIDY;
	} else if (strstart(lineBuffer, "\tat ") &&
	           strchr(lineBuffer, '(') &&
	           strstr(lineBuffer, ".java:")) {
		// Java stack back trace
		return SCE_ERR_JAVA_STACK;
	} else if (strstart(lineBuffer, "In file included from ") ||
	           strstart(lineBuffer, "                 from ")) {
		// GCC showing include path to following error
		return SCE_ERR_GCC_INCLUDED_FROM;
	} else if (strstart(lineBuffer, "NMAKE : fatal error")) {
		// Microsoft nmake fatal error:
		// NMAKE : fatal error <code>: <program> : return code <return>
		return SCE_ERR_MS;
	} else if (strstr(lineBuffer, "warning LNK") ||
		strstr(lineBuffer, "error LNK")) {
		// Microsoft linker warning:
		// {<object> : } (warning|error) LNK9999
		return SCE_ERR_MS;
	} else if (IsBashDiagnostic(lineBuffer)) {
		// Bash diagnostic
		// <filename>: line <line>:<message>
		return SCE_ERR_BASH;
	} else if (IsGccExcerpt(lineBuffer)) {
		// GCC code excerpt and pointer to issue
		//    73 |   GTimeVal last_popdown;
		//       |            ^~~~~~~~~~~~
		return SCE_ERR_GCC_EXCERPT;
	} else {
		// Look for one of the following formats:
		// GCC: <filename>:<line>:<message>
		// Microsoft: <filename>(<line>) :<message>
		// Common: <filename>(<line>): warning|error|note|remark|catastrophic|fatal
		// Common: <filename>(<line>) warning|error|note|remark|catastrophic|fatal
		// Microsoft: <filename>(<line>,<column>)<message>
		// CTags: <identifier>\t<filename>\t<message>
		// Lua 5 traceback: \t<filename>:<line>:<message>
		// Lua 5.1: <exe>: <filename>:<line>:<message>
		const bool initialTab = (lineBuffer[0] == '\t');
		bool initialColonPart = false;
		bool canBeCtags = !initialTab;	// For ctags must have an identifier with no spaces then a tab
		enum { stInitial,
			stGccStart, stGccDigit, stGccColumn, stGcc,
			stMsStart, stMsDigit, stMsBracket, stMsVc, stMsDigitComma, stMsDotNet,
			stCtagsStart, stCtagsFile, stCtagsStartString, stCtagsStringDollar, stCtags,
			stUnrecognized
		} state = stInitial;
		for (Sci_PositionU i = 0; i < lengthLine; i++) {
			const char ch = lineBuffer[i];
			char chNext = ' ';
			if ((i + 1) < lengthLine)
				chNext = lineBuffer[i + 1];
			if (state == stInitial) {
				if (ch == ':') {
					// May be GCC, or might be Lua 5 (Lua traceback same but with tab prefix)
					if ((chNext != '\\') && (chNext != '/') && (chNext != ' ')) {
						// This check is not completely accurate as may be on
						// GTK+ with a file name that includes ':'.
						state = stGccStart;
					} else if (chNext == ' ') { // indicates a Lua 5.1 error message
						initialColonPart = true;
					}
				} else if ((ch == '(') && Is1To9(chNext) && (!initialTab)) {
					// May be Microsoft
					// Check against '0' often removes phone numbers
					state = stMsStart;
				} else if ((ch == '\t') && canBeCtags) {
					// May be CTags
					state = stCtagsStart;
				} else if (ch == ' ') {
					canBeCtags = false;
				}
			} else if (state == stGccStart) {	// <filename>:
				state = ((ch == '-') || Is0To9(ch)) ? stGccDigit : stUnrecognized;
			} else if (state == stGccDigit) {	// <filename>:<line>
				if (ch == ':') {
					state = stGccColumn;	// :9.*: is GCC
					startValue = i + 1;
				} else if (!Is0To9(ch)) {
					state = stUnrecognized;
				}
			} else if (state == stGccColumn) {	// <filename>:<line>:<column>
				if (!Is0To9(ch)) {
					state = stGcc;
					if (ch == ':')
						startValue = i + 1;
					break;
				}
			} else if (state == stMsStart) {	// <filename>(
				state = Is0To9(ch) ? stMsDigit : stUnrecognized;
			} else if (state == stMsDigit) {	// <filename>(<line>
				if (ch == ',') {
					state = stMsDigitComma;
				} else if (ch == ')') {
					state = stMsBracket;
				} else if ((ch != ' ') && !Is0To9(ch)) {
					state = stUnrecognized;
				}
			} else if (state == stMsBracket) {	// <filename>(<line>)
				if ((ch == ' ') && (chNext == ':')) {
					state = stMsVc;
				} else if ((ch == ':' && chNext == ' ') || (ch == ' ')) {
					// Possibly Delphi.. don't test against chNext as it's one of the strings below.
					char word[512];
					unsigned numstep = 0;
					if (ch == ' ')
						numstep = 1; // ch was ' ', handle as if it's a delphi errorline, only add 1 to i.
					else
						numstep = 2; // otherwise add 2.
					Sci_PositionU chPos = 0;
					for (Sci_PositionU j = i + numstep; j < lengthLine && IsUpperOrLowerCase(lineBuffer[j]) && chPos < sizeof(word) - 1; j++)
						word[chPos++] = lineBuffer[j];
					word[chPos] = 0;
					if (InListCaseInsensitive(word, {"error", "warning", "fatal", "catastrophic", "note", "remark"})) {
						state = stMsVc;
					} else {
						state = stUnrecognized;
					}
				} else {
					state = stUnrecognized;
				}
			} else if (state == stMsDigitComma) {	// <filename>(<line>,
				if (ch == ')') {
					state = stMsDotNet;
					break;
				} else if ((ch != ' ') && !Is0To9(ch)) {
					state = stUnrecognized;
				}
			} else if (state == stCtagsStart) {
				if (ch == '\t') {
					state = stCtagsFile;
				}
			} else if (state == stCtagsFile) {
				if ((lineBuffer[i - 1] == '\t') &&
				        ((ch == '/' && chNext == '^') || Is0To9(ch))) {
					state = stCtags;
					break;
				} else if ((ch == '/') && (chNext == '^')) {
					state = stCtagsStartString;
				}
			} else if ((state == stCtagsStartString) && ((lineBuffer[i] == '$') && (lineBuffer[i + 1] == '/'))) {
				state = stCtagsStringDollar;
				break;
			}
		}
		if (state == stGcc) {
			return initialColonPart ? SCE_ERR_LUA : SCE_ERR_GCC;
		} else if ((state == stMsVc) || (state == stMsDotNet)) {
			return SCE_ERR_MS;
		} else if ((state == stCtagsStringDollar) || (state == stCtags)) {
			return SCE_ERR_CTAG;
		} else if (initialColonPart && strstr(lineBuffer, ": warning C")) {
			// Microsoft warning without line number
			// <filename>: warning C9999
			return SCE_ERR_MS;
		} else {
			return SCE_ERR_DEFAULT;
		}
	}
}

#define CSI "\033["

constexpr bool SequenceEnd(int ch) noexcept {
	return (ch == 0) || ((ch >= '@') && (ch <= '~'));
}

int StyleFromSequence(const char *seq) noexcept {
	int bold = 0;
	int colour = 0;
	while (!SequenceEnd(*seq)) {
		if (Is0To9(*seq)) {
			int base = *seq - '0';
			if (Is0To9(seq[1])) {
				base = base * 10;
				base += seq[1] - '0';
				seq++;
			}
			if (base == 0) {
				colour = 0;
				bold = 0;
			}
			else if (base == 1) {
				bold = 1;
			}
			else if (base >= 30 && base <= 37) {
				colour = base - 30;
			}
		}
		seq++;
	}
	return SCE_ERR_ES_BLACK + bold * 8 + colour;
}

void ColouriseErrorListLine(
    const std::string &lineBuffer,
    Sci_PositionU endPos,
    Accessor &styler,
	bool valueSeparate,
	bool escapeSequences) {
	Sci_Position startValue = -1;
	const Sci_PositionU lengthLine = lineBuffer.length();
	const int style = RecogniseErrorListLine(lineBuffer.c_str(), lengthLine, startValue);
	if (escapeSequences && strstr(lineBuffer.c_str(), CSI)) {
		const Sci_Position startPos = endPos - lengthLine;
		const char *linePortion = lineBuffer.c_str();
		Sci_Position startPortion = startPos;
		int portionStyle = style;
		while (const char *startSeq = strstr(linePortion, CSI)) {
			if (startSeq > linePortion) {
				styler.ColourTo(startPortion + (startSeq - linePortion), portionStyle);
			}
			const char *endSeq = startSeq + 2;
			while (!SequenceEnd(*endSeq))
				endSeq++;
			const Sci_Position endSeqPosition = startPortion + (endSeq - linePortion) + 1;
			switch (*endSeq) {
			case 0:
				styler.ColourTo(endPos, SCE_ERR_ESCSEQ_UNKNOWN);
				return;
			case 'm':	// Colour command
				styler.ColourTo(endSeqPosition, SCE_ERR_ESCSEQ);
				portionStyle = StyleFromSequence(startSeq+2);
				break;
			case 'K':	// Erase to end of line -> ignore
				styler.ColourTo(endSeqPosition, SCE_ERR_ESCSEQ);
				break;
			default:
				styler.ColourTo(endSeqPosition, SCE_ERR_ESCSEQ_UNKNOWN);
				portionStyle = style;
			}
			startPortion = endSeqPosition;
			linePortion = endSeq + 1;
		}
		styler.ColourTo(endPos, portionStyle);
	} else {
		if (valueSeparate && (startValue >= 0)) {
			styler.ColourTo(endPos - (lengthLine - startValue), style);
			styler.ColourTo(endPos, SCE_ERR_VALUE);
		} else {
			styler.ColourTo(endPos, style);
		}
	}
}

void ColouriseErrorListDoc(Sci_PositionU startPos, Sci_Position length, int, WordList *[], Accessor &styler) {
	std::string lineBuffer;
	styler.StartAt(startPos);
	styler.StartSegment(startPos);

	// property lexer.errorlist.value.separate
	//	For lines in the output pane that are matches from Find in Files or GCC-style
	//	diagnostics, style the path and line number separately from the rest of the
	//	line with style 21 used for the rest of the line.
	//	This allows matched text to be more easily distinguished from its location.
	const bool valueSeparate = styler.GetPropertyInt("lexer.errorlist.value.separate", 0) != 0;

	// property lexer.errorlist.escape.sequences
	//	Set to 1 to interpret escape sequences.
	const bool escapeSequences = styler.GetPropertyInt("lexer.errorlist.escape.sequences") != 0;

	for (Sci_PositionU i = startPos; i < startPos + length; i++) {
		lineBuffer.push_back(styler[i]);
		if (AtEOL(styler, i)) {
			// End of line met, colourise it
			ColouriseErrorListLine(lineBuffer, i, styler, valueSeparate, escapeSequences);
			lineBuffer.clear();
		}
	}
	if (!lineBuffer.empty()) {	// Last line does not have ending characters
		ColouriseErrorListLine(lineBuffer, startPos + length - 1, styler, valueSeparate, escapeSequences);
	}
}

const char *const emptyWordListDesc[] = {
	nullptr
};

}

LexerModule lmErrorList(SCLEX_ERRORLIST, ColouriseErrorListDoc, "errorlist", nullptr, emptyWordListDesc);
