from .LibraryParser import LibraryParser
from framework import Guid
from .MochaSyntaxError import MochaSyntaxError

class YAMLLibraryParser (LibraryParser):

	def __init__(self, manager):
		LibraryParser.__init__(self, manager)
		
		# FIXME: these two are kind of the same thing; can we merge them?
		self.yamlIds = dict()
		self.templates = dict()
		self.templates_from_reference = dict[bool]()

		# these two are special reserved tagNames
		# self.yamlIds["entityDefinitions"] = None
		# self.yamlIds["instance"] = { }

		self.manager = manager
		self._instances_awaiting_sugar = []

	def apply_sugar(self):
		print("applying syntactic sugar to remaining instances...")
		# _instances_also_awaiting_sugar = []

		for inst in self._instances_awaiting_sugar:
			print("applying sugar from '" + inst["from_file_name"] + "'")
			self.load_instance(inst)
			print("\n")

		# do recursively until we're all out of instances awaiting sugar
		# if len(self._instances_awaiting_sugar) > 0:
		# 	self.apply_sugar()

	def load_library(self, elem):
		print("loading library from elem")

	def load_tenant(self, elem):
		print("loading tenant from elem")

	def load_template_from_str(self, template_str : str):
		import yaml
		from io import StringIO
		file = StringIO(template_str)
		content = yaml.safe_load_all(file)
		return content

	def find_custom_tag_name(self, elem) -> tuple[str, Guid]:
		# return tuple (tagname, value)
		if "instance" in elem:
			return ("instance", Guid.parse(self.manager.expand_entity_references(elem["instance"])))

		for key in self.yamlIds:
			if key in elem:
				k = str(key)
				v = Guid.parse(self.manager.expand_entity_references(elem[key]))
				return (k, v)

		return (None, None)


	def load_entity_definitions_from_file(self, filename):
		print("loading entity defs from " + filename)

		import yaml
		with open(filename, 'r') as file:
			content = yaml.safe_load_all(file)
			for doc in content:
				for elem in doc:
					if "entityDefinitions" in elem:
						entities = elem["entityDefinitions"]
						if not entities is None:
							for entity in entities:
								for key in entity:
									if self.manager.exists_entity_reference(key):
										print("duplicate entity definition: '" + key + "'")
										exit(1)
										
									self.manager.register_entity_reference(key, entity[key], filename)

	def load_instance_with_template(self, elem, template):
		(parentCreatorKey, parentInstanceId) = self.find_custom_tag_name(template)
		(creatorKey, instanceId) = self.find_custom_tag_name(elem)

		self.apply_template(elem, template, instanceId)

	def assign_relationship_value(self, instanceId, rel, rel_iid, relationshipValue):
		if (isinstance(relationshipValue, str)):
			# single instance, this should be a GUID or entityref
			# we should only need to refer to existing instance in this case
			relationshipValueInst = Guid.parse(self.manager.expand_entity_references(relationshipValue))
			if "instance" in rel:
				self.manager.assign_relationship(instanceId, Guid.parse(self.manager.expand_entity_references(rel["instance"])), relationshipValueInst)
			else:
				print("no relationship instance specified for relationship sugar")

		else:
			# dynamically created instance
			#FIXME: this 'instance' isn't always the tag name, this can be subject to customTagName as well!and should be processed accordingly	
			(relinst_key, relinst_iid) = self.find_custom_tag_name(relationshipValue)
			if relinst_key is not None:
				# for example, 'relinst_key' is 'elementContent' and 'relinst_iid' is '{8ECA14A4...}'

				if rel_iid is not None:
					print("found customTagName '" + str(relinst_key) + "' with value '" + str(relinst_iid) + "'")

					# relval = Guid.parse(self.manager.expand_entity_references(elem[customTagName]))
					# we need to create the new instance and assign relationship
					self.manager.assign_relationship(instanceId, rel_iid, relinst_iid)
			else:
				if "globalIdentifier" in relationshipValue:
					globalIdentifier = Guid.parse(self.manager.expand_entity_references(relationshipValue["globalIdentifier"]))
					print("** using Guid from globalIdentifier '" + relationshipValue["globalIdentifier"] + "' **")
				else:
					globalIdentifier = Guid.create()
					print("** created new Guid **")

				print("creating new instance for relationship '%s' with globalid '%s'" % (rel_iid, globalIdentifier))
				
				createsInstanceOf = None
				if "type" in relationshipValue:
					print("found relationship override type '%s'" % (relationshipValue['type']))
					createsInstanceOf = Guid.parse(self.manager.expand_entity_references(relationshipValue["type"]))

				elif "customTagNameCreatesInstanceOf" in rel:
					createsInstanceOf = Guid.parse(self.manager.expand_entity_references(rel["customTagNameCreatesInstanceOf"]))

				if not createsInstanceOf is None:
					# create the new instance
					self.manager.add_instance(createsInstanceOf, globalIdentifier, None, None)

					# assign relationships
					self.manager.assign_relationship(instanceId, rel_iid, globalIdentifier)
					# self.manager.assign_relationship(globalIdentifier, relationshipInstanceId, instanceId)
					
					# FIXME: apply relationships from the parent class template

					#?	NOTE: You MUST specify 'registerForTemplate: yes' on the template definition for
					#?	      sibling relationships to be properly applied!
					
					if createsInstanceOf in self.templates:
						print("applying template for createsInstanceOf '%s'" % (createsInstanceOf))
						createsInstanceOfTemplate = self.templates[createsInstanceOf]
						self.apply_template(relationshipValue, createsInstanceOfTemplate, globalIdentifier)
					else:
						print("no template registered for createsInstanceOf '%s'" % (createsInstanceOf))
				else:
					print ("neither createsInstanceOf nor override 'type' present for relationship value")

	def apply_template(self, elem, template, instanceId):

		if "value" in elem:
			print("VALUE found in ELEM:")
			print(elem["value"])
		
		if "attributes" in template:
			attrs = template["attributes"]
			if isinstance(attrs, list):
				for attr in attrs:
					if "customTagName" in attr:
						customTagName = attr["customTagName"]
						if customTagName in elem:
							attrinst = Guid.parse(self.manager.expand_entity_references(attr["instance"]))
							attrval = elem[customTagName]
							self.manager.set_attribute_value(instanceId, attrinst, attrval)

		if "relationships" in template:
			rels = template["relationships"]
			for rel in rels:
				if "instance" in rel:
					rel_iid = rel["instance"] # the globalId of the relationship instance
				else:
					print("no relationship instance specified for relationship sugar")
					continue

				if "customTagName" in rel:
					customTagName = rel["customTagName"]
					if customTagName in elem:
						relationshipValue = elem[customTagName]
						if relationshipValue is None:
							continue

						if isinstance(relationshipValue, list):
							# multiple instances
							for v in relationshipValue:
								self.assign_relationship_value(instanceId, rel, Guid.parse(self.manager.expand_entity_references(rel_iid)), v)

						else:
							self.assign_relationship_value(instanceId, rel, Guid.parse(self.manager.expand_entity_references(rel_iid)), relationshipValue)

	def get_instance_sugar_for_elem(self, elem):
		if "instance" in elem:
			return (elem["instance"], "instance", None)
		else:
			for key in self.yamlIds:
				# no 'instance', see if we 
				if key in elem:
					template = self.yamlIds[key]
					templateKey = key
					return (elem[templateKey], templateKey, template)
				
			for key in self.templates:
				if key in elem:
					template = self.templates[key]
					templateKey = key
					return (elem[templateKey], templateKey, template)

		return (None, None, None) # should never get here
	
	def register_template(self, key, value, from_reference : bool = False):
		self.templates[key] = value
		self.templates_from_reference[key] = from_reference


	def load_instance(self, elem, elemParent = None):
		# 'class' and 'relationship' are both at the 'same level' 
		# so we can't define 'relationship' without first defining 'class'

		# we could try to keep looping as long as we're still getting "unknown tag name" 
		# (instanceID is None), but we could get stuck that way...

		# for now, let's just forward define important classes (e.g. IDC_Relationship)


		# all of these problems could be avoided  by simply
		# LOADING EVERYTHING INTO MEMORY (like the .NET version does)
		# and only committing to the DB at the very end
		# this will resolve problems where we can't find instances, or use syntactic sugar

		(globalIdentifier, templateKey, template) = self.get_instance_sugar_for_elem(elem)
		print ("globalIdentifier = " + str(globalIdentifier) + ", templateKey = " + str(templateKey))

		if template is None:
			# no sugar
			pass

		customTagName = None
		if "customTagName" in elem:
			customTagName = elem["customTagName"]
			self.yamlIds[customTagName] = elem
			self.templates[customTagName] = elem
			print("registering customTagName '" + customTagName + "'")
		
		if "registerForTemplate" in elem or True:
			# if elem["registerForTemplate"] == True or True:
			guid = Guid.parse(self.manager.expand_entity_references(globalIdentifier))
			self.templates[guid] = elem
			print("registering '" + str(globalIdentifier) + "' for template as '" + str(guid) + "'")

		classId = None
		classIndex = None

		if template is None and elemParent is not None:
			template = elemParent
			
		if template is not None:
			if "instance" in template:
				classId = Guid.parse(self.manager.expand_entity_references(template["instance"]))
			else:
				for key in self.yamlIds:
					if key in template:
						classId = Guid.parse(self.manager.expand_entity_references(template[key]))
						break
				for key in self.templates:
					if key in template:
						classId = Guid.parse(self.manager.expand_entity_references(template[key]))
						break

			if "index" in template:
				classIndex = template["index"]
			
		instanceId = None
		creatorKey = None
		if "instance" in elem:
			if "templateOnly" in elem and elem["templateOnly"] == True:
				# instanceId = None
				instanceId = Guid.parse(self.manager.expand_entity_references(elem["instance"]))
			else:
				instanceId = Guid.parse(self.manager.expand_entity_references(elem["instance"]))

			creatorKey = 'instance'
		else:
			for key in self.yamlIds:
				if key in elem:
					instanceId = Guid.parse(self.manager.expand_entity_references(elem[key]))
					creatorKey = key
					break
			for key in self.templates:
				if key in elem:
					instanceId = Guid.parse(self.manager.expand_entity_references(elem[key]))
					creatorKey = key
					break

		index = None
		if "index" in elem:
			index = elem["index"]
		# else:
		# 	print("index not found for element")
		# 	print(elem)

		if classId is None and instanceId is not None:
			#!!! HACK HACK HACK !!!
			classId = instanceId
			if classIndex is None and index is None:
				classIndex = 1
				index = 1
			print("WARNING: class hack used for instanceId " + str(instanceId) + " ( index " + str(classIndex) + "$" + str(index) + ")")
		
		if instanceId is not None:
			self.manager.add_instance(classId, instanceId, classIndex, index)
			# !FIXME: this ads the instance, but it still doesn't appear in the instance list??

			if creatorKey == 'class': #FIXME: remove this special-case behavior
				print("creating class " + str(instanceId))
				
				# # assign relationship `Instance.for Class`
				# self.manager.assign_relationship(instanceId, Guid.parse('494D5A6D04BE477B8763E3F57D0DD8C8'), Guid.parse('B9C9B9B7AD8A4CBDAA6BE05784630B6B'))

				# # assign relationship `Class.has Instance`
				# self.manager.assign_relationship(Guid.parse('B9C9B9B7AD8A4CBDAA6BE05784630B6B'), Guid.parse('7EB41D3C2AE9488483A4E59441BCAEFB'), instanceId)
				
				if "instances" in elem:
					classInsts = elem["instances"]
					for inst in classInsts:
						self.load_instance(inst, elem)

			else:
				print("creating instance " + str(instanceId) + " (of class " + str(classId) + ")")

				# # assign relationship `Instance.for Class`
				# self.manager.assign_relationship(instanceId, Guid.parse('494D5A6D04BE477B8763E3F57D0DD8C8'), classId)

				# # assign relationship `Class.has Instance`
				# self.manager.assign_relationship(classId, Guid.parse('7EB41D3C2AE9488483A4E59441BCAEFB'), instanceId)
				
				# self.manager.create_instance_of(classId, instanceId, classIndex, index)

		if templateKey is not None:
			if template is not None:
				print("using template (" + templateKey + ")")
				#print(template)

				self.load_instance_with_template(elem, template)
			else:
				#print("ignoring '" + templateKey + "'")
				pass

	def load_instances_from_file(self, filename):
		print("loading instances from " + filename)

		import yaml
		with open(filename, 'r') as file:
			content = yaml.safe_load_all(file)
			for doc in content:
				for elem in doc:
					if "entityDefinitions" in elem:
						continue
					elif "library" in elem:
						self.load_library(filename, elem)
					elif "tenant" in elem:
						self.load_tenant(filename, elem)
					else:
						raise MochaSyntaxError("neither 'library' nor 'tenant' top level element found")
	
	def load_library(self, filename, elem):
		if "instances" in elem:
			instances = elem["instances"]
			for elem1 in instances:
				if "instance" in elem1:
					# this is an instance definition (no sugar), so just load it
					self.load_instance(elem1)
				else:
					# this is a syntactic sugar definition, so store it for later use
					elem1["from_file_name"] = filename
					self._instances_awaiting_sugar.append(elem1)
		
	def load_tenant(self, filename, elem):
		pass