import io
import re

from .argument import IntegerArgument
from .argument import DeltaArgument
from .data import Data
from .error import HumbleError
from .instruction import Instruction
from .program import Program
from .tokenizer import Tokenizer


def parseFiles(sourceFiles, definedNames):
	if isinstance(sourceFiles, str):
		sourceFiles = [sourceFiles]

	joined = ""
	for sourceFile in sourceFiles:
		with open(sourceFile) as sourceStream:
			joined += sourceStream.read()

	if definedNames:
		for name in definedNames:
			joined = joined.replace("#ifdef {} ".format(name), "")

	joinedStream = io.StringIO(joined)
	return Parser().parse(joinedStream)


class Parser:

	def __init__(self):
		self.tokenizer = None
		self.data = None

	def parse(self, stream):
		self.tokenizer = Tokenizer(stream)
		self.data = Data()
		try:
			self.__root()
		except HumbleError as e:
			e.position = self.tokenizer.position
			raise
		return self.data

	def __root(self):
		self.__ws()
		name = self.tokenizer.peek()
		while name:
			if   name == "flags"  : self.__flags()
			elif name == "head"   : self.__head()
			elif name == "maxp"   : self.__maxp()
			elif name == "gasp"   : self.__gasp()
			elif name == "cvt"    : self.__cvt()
			elif name == "storage": self.__storage()
			elif name == "fpgm"   : self.__fpgm()
			elif name == "prep"   : self.__prep()
			else: self.__glyph()
			name = self.tokenizer.peek()

	def __flags(self):
		self.__skip("flags")
		self.__open()
		while self.tokenizer.peek() != "}":
			self.__ws()
			value = self.__flagbits()
			name = self.__flagname()
			self.data._addFlagName(name, value)
			self.__nl()
		self.__close()

	def __head(self):
		self.data._initHead()
		self.__skip("head")
		self.__open()
		while self.tokenizer.peek() != "}":
			self.__ws()
			value = self.__uint()
			name = self.tokenizer.get()
			self.data._addHeadItem(name, value)
			self.__nl()
		self.__close()

	def __maxp(self):
		self.data._initMaxp()
		self.__skip("maxp")
		self.__open()
		while self.tokenizer.peek() != "}":
			self.__ws()
			value = self.__uint()
			name = self.tokenizer.get()
			self.data._addMaxpItem(name, value)
			self.__nl()
		self.__close()

	def __gasp(self):
		self.data._initGasp()
		self.__skip("gasp")
		self.__open()
		while self.tokenizer.peek() != "}":
			self.__ws()
			size = self.__uint()
			doGridFit = False
			doGray = False
			symSmoothing = False
			symGridfit = False
			while self.tokenizer.peek() != "\n":
				flag = self.tokenizer.get()
				if   flag == "doGridfit"   : doGridFit = True
				elif flag == "doGray"      : doGray = True
				elif flag == "symSmoothing": symSmoothing = True
				elif flag == "symGridfit"  : symGridfit = True
				else:
					raise HumbleError("Unknown gasp flag: {}".format(flag))
			self.data._addGaspItem(size, doGridFit, doGray, symSmoothing, symGridfit)
			self.__nl()
		self.__close()

	def __cvt(self):
		self.data._initCvt()
		self.__skip("cvt")
		self.__open()
		while self.tokenizer.peek() != "}":
			self.__ws()
			value = self.__num().value
			name = self.__id()
			self.data._addCvtItem(name, value)
			self.__nl()
		self.__close()

	def __storage(self):
		self.__skip("storage")
		self.__open()
		while self.tokenizer.peek() != "}":
			self.__ws()
			index = self.__num().value
			name = self.__id()
			self.data._addStorageName(name, index)
			self.__nl()
		self.__close()

	def __fpgm(self):
		self.__skip("fpgm")
		self.data._addFpgm(self.__program())

	def __prep(self):
		self.__skip("prep")
		self.data._addPrep(self.__program())

	def __glyph(self):
		name = self.__id()
		program = self.__program()
		self.data._addGlyph(name, program)

	def __program(self):
		program = Program()
		self.__open()
		while self.tokenizer.peek() != "}":
			self.__ws()
			program.add(self.__instr())
			self.__nl()
		self.__close()
		return program

	def __open(self):
		self.__ws()
		self.__skip("{")
		self.__ws()

	def __close(self):
		self.__ws()
		self.__skip("}")
		self.__ws()

	def __instr(self):
		instr = self.tokenizer.get()
		instruction = Instruction.newInstruction(instr)
		instruction.setFlag(self.__flag())
		self.__recipe(instruction)
		return instruction

	def __flag(self):
		flag = 0
		if self.tokenizer.peek() == "[":
			self.__skip("[")
			try:
				self.tokenizer.mark()
				flag = self.__flagbits()
			except HumbleError:
				self.tokenizer.rewind()
				name = self.__flagname()
				flag = self.data._getFlagValue(name)
			finally:
				self.tokenizer.unmark()
			self.__skip("]")
		return flag

	def __flagname(self):
		token = self.tokenizer.get()
		if re.match("[0-9A-Za-z_\.<>,]+$", token):
			return token
		else:
			raise HumbleError("Invalid flag name: {}".format(token))

	def __val(self):
		if self.tokenizer.peek() == "(":
			self.__skip("(")
			try:
				self.tokenizer.mark()
				val = self.__instr()
			except HumbleError:
				self.tokenizer.rewind()
				val = self.__operation()
			finally:
				self.tokenizer.unmark()
			self.__skip(")")
			return val
		else:
			return self.__num()

	def __operation(self):
		argument = self.__val()
		instruction = Instruction.newOperationInstruction(self.tokenizer.get())
		instruction.add(argument)
		argument = self.__val()
		instruction.add(argument)
		return instruction

	def __nl(self):
		self.__skip("\n")
		self.__ws()

	def __ws(self):
		while self.tokenizer.peek() == "\n":
			self.tokenizer.get()

	def __id(self):
		token = self.tokenizer.get()

		# The # character is included because FontTools
		# uses it in auto-generated glyph names.
		if re.match("[0-9A-Za-z_\.#]+$", token):
			return token
		else:
			raise HumbleError("Invalid identifier: {}".format(token))

	def __num(self):
		token = self.tokenizer.get()
		if token == "+" or token == "-":
			token += self.tokenizer.get()
		try:
			if token.find("0x") == 0 or token.find("0b") == 0:
				value = int(token, 0)
				if value >= 2**15 and value < 2**16:
					value -= 2**16
			elif token.find(":") >= 0:
				value = int(round(float(token.replace(":", ".")) * 2**14))
			elif token.find(".") >= 0:
				value = int(round(float(token) * 2**6))
			else:
				value = int(token)

			if value >= -2**15 and value < 2**15:
				return IntegerArgument(value)
		except ValueError:
			pass
		raise HumbleError("Invalid numeric argument: {}".format(token))

	def __uint(self):
		token = self.tokenizer.get()
		try:
			value = int(token)
			if value >= 0 and value < 2**16:
				return value
		except ValueError:
			pass
		raise HumbleError("Invalid unsigned integer: {}".format(token))

	def __flagbits(self):
		token = self.tokenizer.get()
		try:
			return int(token, 2)
		except ValueError:
			raise HumbleError("Invalid bit value: {}".format(token))

	def __delta(self):
		ppemToken = self.tokenizer.get()
		signToken = self.tokenizer.get()
		stepsToken = self.tokenizer.get()
		try:
			ppem = int(ppemToken)
			steps = int(signToken + stepsToken)
			if ppem >= 0 and ppem < 2**15 and steps != 0 and abs(steps) <= 8:
				return DeltaArgument(ppem, steps)
		except ValueError:
			pass
		raise HumbleError("Invalid delta modifier: {}{}{}".format(ppemToken, signToken, stepsToken))

	# Helper methods

	def __recipe(self, instruction):
		if self.tokenizer.peek() == "\n" or \
		   self.tokenizer.peek() == ")":
			return
		for item in instruction.recipe:
			if item == 'getCVT':
				name = self.__id()
				index = self.data._getCvtIndex(name)
				instruction.add(IntegerArgument(index))
			elif item == 'getSTOR':
				name = self.__id()
				index = self.data._getStorageIndex(name)
				instruction.add(IntegerArgument(index))
			elif item == 'setFUNC' or \
			     item == 'setVOID':
				index = self.__uint()
				name = self.__id()
				recipe = self.__parameters()
				self.data._addFunction(index, name, recipe, item == 'setVOID')
				instruction.add(IntegerArgument(index))
			elif item == 'getFUNC':
				name = self.__id()
				index = self.data._getFunctionIndex(name)
				instruction.add(IntegerArgument(index))
			elif item == 'CALL':
				name = self.__id()
				index = self.data._getFunctionIndex(name)
				recipe = self.data._getFunctionRecipe(index)
				instruction.add(IntegerArgument(index))
				instruction.recipe = recipe
				if instruction.name == "LOOPCALL":
					instruction.recipe *= instruction.arguments[-2].value
				if self.data._isVoidFunction(index):
					instruction.spoilsStack = False
				# Parse new recipe
				self.__recipe(instruction)
				return
			elif item == 'getVAL':
				value = self.__val()
				instruction.add(value)
			elif item == 'getVALS':
				while self.tokenizer.peek() != "\n" and \
				      self.tokenizer.peek() != ")":
					argument = self.__val()
					instruction.add(argument)
			elif item == 'getDELTA':
				delta = self.__delta()
				instruction.add(delta)
			elif item == 'getDELTAS':
				while self.tokenizer.peek() != "\n" and \
				      self.tokenizer.peek() != ")":
					delta = self.__delta()
					instruction.add(delta)
			else:
				raise ValueError("Invalid recipe item: {}".format(item))

	def __parameters(self):
		parameters = []
		while self.tokenizer.peek() != "\n" and \
		      self.tokenizer.peek() != ")":
			parameters.append(self.__parameter())
		return tuple(parameters)

	def __parameter(self):
		token = self.tokenizer.get()
		if   token.startswith("val"):  return 'getVAL'
		elif token.startswith("pt"):   return 'getVAL'
		elif token.startswith("cvt"):  return 'getCVT'
		elif token.startswith("func"): return 'getFUNC'
		elif token.startswith("stor"): return 'getSTOR'
		else:
			raise HumbleError("Invalid parameter: {}".format(token))

	def __skip(self, string):
		token = self.tokenizer.get()
		if token != string:
			raise HumbleError("Expected: {} Got: {}".format(string, token))
