# syntax.py

import sys

from PyQt5 import QtCore, QtGui, QtWidgets


def format(color, style=''):
	"""Return a QTextCharFormat with the given attributes.
    """
	_color = QtGui.QColor()
	_color.setNamedColor(color)

	_format = QtGui.QTextCharFormat()
	_format.setForeground(_color)
	if 'bold' in style:
		_format.setFontWeight(QtGui.QFont.Bold)
	if 'italic' in style:
		_format.setFontItalic(True)

	return _format


# Syntax styles that can be shared by all languages
STYLES = {
	'keyword': format('blue'),
	'operator': format('red'),
	'brace': format('darkGray'),
	'defclass': format('black', 'bold'),
	'string': format('magenta'),
	'string2': format('darkMagenta'),
	'comment': format('darkGreen', 'italic'),
	'self': format('black', 'italic'),
	'numbers': format('brown'),
}


class PythonHighlighter(QtGui.QSyntaxHighlighter):
	"""Syntax highlighter for the Python language.
    """
	# Python keywords
	keywords = [
		'and', 'assert', 'break', 'class', 'continue', 'def',
		'del', 'elif', 'else', 'except', 'exec', 'finally',
		'for', 'from', 'global', 'if', 'import', 'in',
		'is', 'lambda', 'not', 'or', 'pass', 'print',
		'raise', 'return', 'try', 'while', 'yield',
		'None', 'True', 'False', 'define', 'include'
	]

	# Python operators
	operators = [
		'=',
		# Comparison
		'==', '!=', '<', '<=', '>', '>=',
		# Arithmetic
		'\\+', '-', '\\*', '/', '//', '\\%', '\\*\\*',
		# In-place
		'\\+=', '-=', '\\*=', '/=', '\\%=',
		# Bitwise
		'\\^', '\\|', '\\&', '\\~', '>>', '<<',
	]

	# Python braces
	braces = [
		'\\{', '\\}', '\\(', '\\)', '\\[', '\\]',
	]

	def __init__(self, parent: QtGui.QTextDocument) -> None:
		super().__init__(parent)

		# Multi-line strings (expression, flag, style)
		self.tri_single = (QtCore.QRegExp("'''"), 1, STYLES['string2'])
		self.tri_double = (QtCore.QRegExp('"""'), 2, STYLES['string2'])

		rules = []

		# Keyword, operator, and brace rules
		rules += [(r'\b%s\b' % w, 0, STYLES['keyword'])
		          for w in PythonHighlighter.keywords]
		rules += [(r'%s' % o, 0, STYLES['operator'])
		          for o in PythonHighlighter.operators]
		rules += [(r'%s' % b, 0, STYLES['brace'])
		          for b in PythonHighlighter.braces]

		# All other rules
		rules += [
			# 'self'
			(r'\bself\b', 0, STYLES['self']),

			# 'def' followed by an identifier
			(r'\bvoid\b\s*(\w+)', 1, STYLES['defclass']),
			# 'class' followed by an identifier
			(r'\bint\b\s*(\w+)', 1, STYLES['defclass']),

			# Numeric literals
			(r'\b[+-]?[0-9]+[lL]?\b', 0, STYLES['numbers']),
			(r'\b[+-]?0[xX][0-9A-Fa-f]+[lL]?\b', 0, STYLES['numbers']),
			(r'\b[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\b', 0, STYLES['numbers']),

			# Double-quoted string, possibly containing escape sequences
			(r'"[^"\\]*(\\.[^"\\]*)*"', 0, STYLES['string']),
			# Single-quoted string, possibly containing escape sequences
			(r"'[^'\\]*(\\.[^'\\]*)*'", 0, STYLES['string']),

			# From '#' until a newline
			(r'//[^\n]*', 0, STYLES['comment']),
		]

		# Build a QRegExp for each pattern
		self.rules = [(QtCore.QRegExp(pat), index, fmt)
		              for (pat, index, fmt) in rules]

	def highlightBlock(self, text):
		"""Apply syntax highlighting to the given block of text.
        """
		self.tripleQuoutesWithinStrings = []
		# Do other syntax formatting
		for expression, nth, format in self.rules:
			index = expression.indexIn(text, 0)
			if index >= 0:
				# if there is a string we check
				# if there are some triple quotes within the string
				# they will be ignored if they are matched again
				if expression.pattern() in [r'"[^"\\]*(\\.[^"\\]*)*"', r"'[^'\\]*(\\.[^'\\]*)*'"]:
					innerIndex = self.tri_single[0].indexIn(text, index + 1)
					if innerIndex == -1:
						innerIndex = self.tri_double[0].indexIn(text, index + 1)

					if innerIndex != -1:
						tripleQuoteIndexes = range(innerIndex, innerIndex + 3)
						self.tripleQuoutesWithinStrings.extend(tripleQuoteIndexes)

			while index >= 0:
				# skipping triple quotes within strings
				if index in self.tripleQuoutesWithinStrings:
					index += 1
					expression.indexIn(text, index)
					continue

				# We actually want the index of the nth match
				index = expression.pos(nth)
				length = len(expression.cap(nth))
				self.setFormat(index, length, format)
				index = expression.indexIn(text, index + length)

		self.setCurrentBlockState(0)

		# Do multi-line strings
		in_multiline = self.match_multiline(text, *self.tri_single)
		if not in_multiline:
			in_multiline = self.match_multiline(text, *self.tri_double)

	def match_multiline(self, text, delimiter, in_state, style):
		"""Do highlighting of multi-line strings. ``delimiter`` should be a
        ``QRegExp`` for triple-single-quotes or triple-double-quotes, and
        ``in_state`` should be a unique integer to represent the corresponding
        state changes when inside those strings. Returns True if we're still
        inside a multi-line string when this function is finished.
        """
		# If inside triple-single quotes, start at 0
		if self.previousBlockState() == in_state:
			start = 0
			add = 0
		# Otherwise, look for the delimiter on this line
		else:
			start = delimiter.indexIn(text)
			# skipping triple quotes within strings
			if start in self.tripleQuoutesWithinStrings:
				return False
			# Move past this match
			add = delimiter.matchedLength()

		# As long as there's a delimiter match on this line...
		while start >= 0:
			# Look for the ending delimiter
			end = delimiter.indexIn(text, start + add)
			# Ending delimiter on this line?
			if end >= add:
				length = end - start + add + delimiter.matchedLength()
				self.setCurrentBlockState(0)
			# No; multi-line string
			else:
				self.setCurrentBlockState(in_state)
				length = len(text) - start + add
			# Apply formatting
			self.setFormat(start, length, style)
			# Look for the next match
			start = delimiter.indexIn(text, start + length)

		# Return True if still inside a multi-line string, False otherwise
		if self.currentBlockState() == in_state:
			return True
		else:
			return False


class CHighlighter(QtGui.QSyntaxHighlighter):
	"""Syntax highlighter for the C language.
    """

	# C keywords
	keywords = [
		'and', 'assert', 'break', 'class', 'continue', 'def',
		'del', 'elif', 'else', 'except', 'exec', 'finally',
		'for', 'from', 'global', 'if', 'import', 'in',
		'is', 'lambda', 'not', 'or', 'pass', 'print',
		'raise', 'return', 'try', 'while', 'yield',
		'None', 'True', 'False', 'define', 'include', 'uint16_t', 'uint8_t'
	]

	# Python operators
	operators = [
		'=',
		# Comparison
		'==', '!=', '<', '<=', '>', '>=',
		# Arithmetic
		'\\+', '-', '\\*', '/', '//', '\\%', '\\*\\*',
		# In-place
		'\\+=', '-=', '\\*=', '/=', '\\%=',
		# Bitwise
		'\\^', '\\|', '\\&', '\\~', '>>', '<<',
	]

	# Python braces
	braces = [
		'\\{', '\\}', '\\(', '\\)', '\\[', '\\]',
	]

	def __init__(self, parent: QtGui.QTextDocument) -> None:
		super().__init__(parent)

		# Multi-line strings (expression, flag, style)
		self.tri_single = (QtCore.QRegExp("'''"), 1, STYLES['string2'])
		self.tri_double = (QtCore.QRegExp('"""'), 2, STYLES['string2'])

		rules = []

		# Keyword, operator, and brace rules
		rules += [(r'\b%s\b' % w, 0, STYLES['keyword'])
		          for w in self.keywords]
		rules += [(r'%s' % o, 0, STYLES['operator'])
		          for o in self.operators]
		rules += [(r'%s' % b, 0, STYLES['brace'])
		          for b in self.braces]

		# All other rules
		rules += [
			# 'self'
			(r'\bself\b', 0, STYLES['self']),

			# 'def' followed by an identifier
			(r'\bvoid\b\s*(\w+)', 1, STYLES['defclass']),
			# 'class' followed by an identifier
			(r'\bint\b\s*(\w+)', 1, STYLES['defclass']),

			# Numeric literals
			(r'\b[+-]?[0-9]+[lL]?\b', 0, STYLES['numbers']),
			(r'\b[+-]?0[xX][0-9A-Fa-f]+[lL]?\b', 0, STYLES['numbers']),
			(r'\b[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\b', 0, STYLES['numbers']),

			# Double-quoted string, possibly containing escape sequences
			(r'"[^"\\]*(\\.[^"\\]*)*"', 0, STYLES['string']),
			# Single-quoted string, possibly containing escape sequences
			(r"'[^'\\]*(\\.[^'\\]*)*'", 0, STYLES['string']),

			# From '#' until a newline
			(r'//[^\n]*', 0, STYLES['comment']),
		]

		# Build a QRegExp for each pattern
		self.rules = [(QtCore.QRegExp(pat), index, fmt)
		              for (pat, index, fmt) in rules]

	def highlightBlock(self, text):
		"""Apply syntax highlighting to the given block of text.
        """
		self.tripleQuoutesWithinStrings = []
		# Do other syntax formatting
		for expression, nth, format in self.rules:
			index = expression.indexIn(text, 0)
			if index >= 0:
				# if there is a string we check
				# if there are some triple quotes within the string
				# they will be ignored if they are matched again
				if expression.pattern() in [r'"[^"\\]*(\\.[^"\\]*)*"', r"'[^'\\]*(\\.[^'\\]*)*'"]:
					innerIndex = self.tri_single[0].indexIn(text, index + 1)
					if innerIndex == -1:
						innerIndex = self.tri_double[0].indexIn(text, index + 1)

					if innerIndex != -1:
						tripleQuoteIndexes = range(innerIndex, innerIndex + 3)
						self.tripleQuoutesWithinStrings.extend(tripleQuoteIndexes)

			while index >= 0:
				# skipping triple quotes within strings
				if index in self.tripleQuoutesWithinStrings:
					index += 1
					expression.indexIn(text, index)
					continue

				# We actually want the index of the nth match
				index = expression.pos(nth)
				length = len(expression.cap(nth))
				self.setFormat(index, length, format)
				index = expression.indexIn(text, index + length)

		self.setCurrentBlockState(0)

		# Do multi-line strings
		in_multiline = self.match_multiline(text, *self.tri_single)
		if not in_multiline:
			in_multiline = self.match_multiline(text, *self.tri_double)

	def match_multiline(self, text, delimiter, in_state, style):
		"""Do highlighting of multi-line strings. ``delimiter`` should be a
        ``QRegExp`` for triple-single-quotes or triple-double-quotes, and
        ``in_state`` should be a unique integer to represent the corresponding
        state changes when inside those strings. Returns True if we're still
        inside a multi-line string when this function is finished.
        """
		# If inside triple-single quotes, start at 0
		if self.previousBlockState() == in_state:
			start = 0
			add = 0
		# Otherwise, look for the delimiter on this line
		else:
			start = delimiter.indexIn(text)
			# skipping triple quotes within strings
			if start in self.tripleQuoutesWithinStrings:
				return False
			# Move past this match
			add = delimiter.matchedLength()

		# As long as there's a delimiter match on this line...
		while start >= 0:
			# Look for the ending delimiter
			end = delimiter.indexIn(text, start + add)
			# Ending delimiter on this line?
			if end >= add:
				length = end - start + add + delimiter.matchedLength()
				self.setCurrentBlockState(0)
			# No; multi-line string
			else:
				self.setCurrentBlockState(in_state)
				length = len(text) - start + add
			# Apply formatting
			self.setFormat(start, length, style)
			# Look for the next match
			start = delimiter.indexIn(text, start + length)

		# Return True if still inside a multi-line string, False otherwise
		if self.currentBlockState() == in_state:
			return True
		else:
			return False
