import bpy, mathutils
import random, math, time
from . import Node, BuildingGetter, Building, weighted_choice, GridGetter, DIRECTION4, SceneCityScene, SceneCitySceneGetter, \
	SOCKET_Light_Scene, BuildingSocket, WeightedBuildingSocket, SOCKET_Grid, SC_OT_RandomizeSeedNode


class StaticBuildingNode(bpy.types.Node, Node, BuildingGetter):
	bl_idname = 'sc_node_r5qa266fpxijq70wv0c2'
	bl_label = 'Static building'

	size: bpy.props.IntVectorProperty(
		name='Size',
		description='In grid cells, on X and Y',
		size=2, default=(1, 1), min=1)

	def sc_init(self, context):
		self.width = 200
		# self.inputs.new(SceneCityObjectsSocket.__name__, 'Objects')
		self.create_input(SOCKET_Light_Scene, is_required=True)
		# self.outputs.new(BuildingSocket.__name__, 'Building')
		self.create_output(BuildingSocket)

	def sc_draw_buttons(self, context, layout):
		# self.ui_display_doc2(layout)
		layout.prop(self, 'size')

	def get_building(self, **kwargs):
		asked_size = kwargs['size']
		if (self.size[0], self.size[1]) != asked_size:
			return None
		building = Building()
		building.size = (self.size[0], self.size[1])
		objects_source: SceneCitySceneGetter = self.inputs[0].links[0].from_node
		# objects_source: SceneCitySceneGetter = self.input_sc_objects.links[0].from_node
		# building.objects = objects_source.get_scenecity_objects()
		building.scene = objects_source.get_scenecity_scene()
		return building

	def get_building_sizes(self):
		return {(self.size[0], self.size[1])}


class BuildingsCollectionNode(bpy.types.Node, Node, BuildingGetter):
	bl_idname = 'sc_node_aw5hal03wfjm6cjj8ldk'
	bl_label = 'Buildings collection'

	def sc_init(self, context):
		self.width = 300
		self.create_output(BuildingSocket)

	def sc_draw_buttons(self, context, layout):
		op = layout.operator(SC_OT_BuildingsCollectionNodeAddInput.bl_idname)
		op.source_node_path = 'bpy.data.node_groups["' + self.id_data.name + '"].' + self.path_from_id()

	def get_building(self, **kwargs):
		asked_size = kwargs['size']
		eligible_inputs = []
		weights = []
		# find all inputs that can return the building asked
		for input in self.inputs:
			try:
				source_node = input.links[0].from_node  # type: BuildingGetter
			except:
				continue
			if asked_size in source_node.get_building_sizes():
				eligible_inputs.append(input)
				weights.append(input.weight)
		if len(eligible_inputs) > 0:
			final_input = weighted_choice(eligible_inputs, weights)[1]
			return final_input.links[0].from_node.get_building(size=asked_size)
		else:
			return None

	def get_building_sizes(self):
		sizes = set()
		for input in self.inputs:
			try:
				source_node = input.links[0].from_node  # type: BuildingGetter
			except:
				continue
			sizes |= source_node.get_building_sizes()
		return sizes


class SC_OT_BuildingsCollectionNodeAddInput(bpy.types.Operator):
	bl_idname = 'node.buildings_collection_node_add_input'
	bl_description = ''
	bl_label = 'Add buildings'
	source_node_path: bpy.props.StringProperty()

	def execute(self, context):
		source_node: BuildingsCollectionNode = eval(self.source_node_path)
		random_string = time.time()
		# source_node.inputs.new(WeightedBuildingSocket.__name__, str(random_string))
		source_node.create_input(WeightedBuildingSocket, is_required=True, label=str(random_string))
		return {'FINISHED'}


class BuildingsInstancerNode(bpy.types.Node, Node, SceneCitySceneGetter):
	bl_idname = 'sc_node_x6skxcvg4a6e72ay3dwg'
	bl_label = 'Buildings instancer'

	grid_keys_values_restrict_any: bpy.props.StringProperty(
		name='Place on grid values',
		description='The buildings will be placed on the cells of the grid containing any of these key/value pairs. The format is key1=value1;key2=value2;..',
		default="district = res")

	random_seed: bpy.props.IntProperty(
		name='Seed',
		description='Random seed', )

	# last_operation_time: bpy.props.FloatProperty(
	# 	name='',
	# 	description='',
	# 	default=0)

	# should_fill_far_from_road = bpy.props.BoolProperty(
	# 	name='Buildings must touch roads',
	# 	description='', )

	must_touch_keys_values_any: bpy.props.StringProperty(
		name='Must touch grid values',
		description='The buildings must touch any of these key/value pairs. The format is key1 = value1; key2 = value2; ..',
		default="")

	fill_amount: bpy.props.FloatProperty(
		name='Fill amount',
		description='', default=100, min=0, max=100, subtype='PERCENTAGE')

	orient_towards_keys_values_any: bpy.props.StringProperty(
		name='Orient to grid values',
		description='The buildings touching any of these key/value pairs will be oriented towards them. The format is key = value',
		default="road = all")

	def sc_init(self, context):
		self.random_seed = random.randint(0, 9e5)
		self.width = 300
		# self.inputs.new(GridSocket.__name__, 'Grid')
		self.create_input(SOCKET_Grid, is_required=True)
		# self.inputs.new(BuildingSocket.__name__, 'Buildings')
		self.create_input(BuildingSocket, is_required=True)
		# self.outputs.new(Socket_Scene.__name__, 'Objects')
		self.create_output(SOCKET_Light_Scene)

	def sc_draw_buttons(self, context, layout):
		# if len(self.inputs['Grid'].links) <= 0:
		# 	layout.label(text="Input grid needed", icon="ERROR")
		# self.ui_display_doc_and_last_job_done2(layout)
		row = layout.row()
		row.prop(self, 'random_seed')
		op = row.operator(SC_OT_RandomizeSeedNode.bl_idname)
		# op = row.operator('node.randomize_seed_operator')
		op.source_node_path = 'bpy.data.node_groups["' + self.id_data.name + '"].' + self.path_from_id()
		layout.prop(self, 'grid_keys_values_restrict_any')
		# layout.prop(self, 'fill_amount')
		# layout.prop(self, 'must_touch_keys_values_any')
		layout.prop(self, 'orient_towards_keys_values_any')

	def get_scenecity_scene(self, **kwargs):
		grid_source: GridGetter = self.inputs['Grid'].links[0].from_node
		grid = grid_source.get_grid()
		buildings_source: BuildingGetter = self.inputs['Buildings'].links[0].from_node
		all_possible_buildings_sizes = buildings_source.get_building_sizes()

		startTime = time.time()
		# all_objects = []
		scene = SceneCityScene()
		random.seed(self.random_seed)
		# sort sizes by size on y, because this is the front size, but it doesn't really matter
		all_sizes_and_weights_list = list(all_possible_buildings_sizes)
		for i, size in enumerate(all_sizes_and_weights_list):
			all_sizes_and_weights_list[i] = (size, 1 / (size[0] * size[1]))
		all_sizes_and_weights_list.sort(key=lambda size_and_weight: size_and_weight[0][1])
		all_sizes_list = [size_and_weight[0] for size_and_weight in all_sizes_and_weights_list]
		all_weights_list = [size_and_weight[1] for size_and_weight in all_sizes_and_weights_list]

		keys_values_restrict_any = {}
		if self.grid_keys_values_restrict_any:
			keys_values_restrict_any_strings = self.grid_keys_values_restrict_any.split(';')
			for element in keys_values_restrict_any_strings:
				key_value_strings = element.split('=')
				try:
					keys_values_restrict_any[key_value_strings[0].strip()].add(key_value_strings[1].strip())
				except KeyError:
					keys_values_restrict_any[key_value_strings[0].strip()] = {key_value_strings[1].strip()}
		keys_values_orient_towards_any = set()
		if self.orient_towards_keys_values_any:
			orient_towards_keys_values_any_strings = self.orient_towards_keys_values_any.split(';')
			for element in orient_towards_keys_values_any_strings:
				key_value_strings = element.split('=')
				keys_values_orient_towards_any.add((key_value_strings[0].strip(), key_value_strings[1].strip()))

		print(self.name, ': starting to place buildings')
		temps_dernier_affichage = time.time()
		total_steps = 2
		no_batiment_actuel = 0
		total_cells_to_test = grid.grid_size[0] * grid.grid_size[1] * total_steps
		current_tested_cell = -1
		for step in range(total_steps):
			for i in range(grid.grid_size[0]):
				if time.time() - temps_dernier_affichage >= 1:
					temps_dernier_affichage = time.time()
					# print('SceneCity | ' + str(no_batiment_actuel) + ' logical buildings placed so far...')
					print(self.name, ': placing buildings ', math.floor(current_tested_cell / total_cells_to_test * 100), '% - ',
						no_batiment_actuel, ' buildings created and placed so far')

				for j in range(grid.grid_size[1]):
					current_tested_cell += 1

					# ignore de façon aléatoire
					# if random.random() <= infos_globales_placement_batiments.probaAucunBatiment:
					if random.random() > (self.fill_amount / 100):
						continue

					cell = grid.data[i][j]

					# ignore cases occupées déjà
					# try:
					# 	if cell['a'] == 3 or cell['b']:
					# 		continue
					# except KeyError:
					# 	pass
					# ... et loin des routes
					if step == 0 and not get_total_voisins_qui_contiennent(grid, (i, j), keys_values_orient_towards_any):
						continue
					# ... et considère uniquement celles que l'utilisateur à spécifiées
					if keys_values_restrict_any:
						cell_contains_a_user_key_value = False
						for key, value in cell.items():
							try:
								if value in keys_values_restrict_any[key]:
									cell_contains_a_user_key_value = True
									break
							except KeyError:
								pass
						if not cell_contains_a_user_key_value:
							continue

					# essayer de placer un batiment dans l'espace disponible:
					# prendre une taille au hasard, essayer de la placer
					# Si peut alors demander un batiment de cette taille, et le placer. Puis passer à la case suivante
					# Sinon essayer une autre taille, jusqu'à ce qu'il n'en reste plus aucune de dispo, à ce moment passer à la case suivante
					sizes_not_tested_yet = all_sizes_list.copy()
					weights_not_tested_yet = all_weights_list.copy()
					buildingPos = None
					while sizes_not_tested_yet:
						index, size = weighted_choice(sizes_not_tested_yet, weights_not_tested_yet)

						buildingPos = trouver_meilleure_position_batiment(
							grid,
							size,
							(i, j),
							keys_values_orient_towards_any if step == 0 else set(),
							keys_values_restrict_any)
						# peut placer => On arrête de chercher, lot suivant
						if buildingPos:
							break
						else:
							# si atteint cette linge => peut pas placer size actuelle, essayer avec une autre à la prochaine itération
							# mais avant on supprime de la liste la taille déjà testée
							del sizes_not_tested_yet[index]
							del weights_not_tested_yet[index]

					# si n'a pu trouver aucun batiment à placer, passer au lot suivant
					if not buildingPos:
						continue

					no_batiment_actuel += 1

					# choisir sa rotation
					buildingRot = 0
					if buildingPos.forwardDir == DIRECTION4.EST:
						buildingRot = 0
					elif buildingPos.forwardDir == DIRECTION4.OUEST:
						buildingRot = math.pi
					elif buildingPos.forwardDir == DIRECTION4.NORD:
						buildingRot = math.pi / 2
					elif buildingPos.forwardDir == DIRECTION4.SUD:
						buildingRot = -math.pi / 2

					building = buildings_source.get_building(size=size)
					# for object in building.objects:
					# 	object.location = (buildingPos.central_pos[0] * grid.cell_size - grid.grid_size[0] * grid.cell_size / 2,
					# 					   buildingPos.central_pos[1] * grid.cell_size - grid.grid_size[1] * grid.cell_size / 2,
					# 					   0)
					# 	object.rotation_euler_radians = (0, 0, buildingRot)
					for sc_object in building.scene._objects_hierarchy:
						sc_object.matrix_local = mathutils.Matrix.Identity(4)
						sc_object.matrix_local @= mathutils.Matrix.Translation(
							(buildingPos.central_pos[0] * grid.cell_size - grid.grid_size[0] * grid.cell_size / 2,
							 buildingPos.central_pos[1] * grid.cell_size - grid.grid_size[1] * grid.cell_size / 2,
							 0))
						sc_object.matrix_local @= mathutils.Matrix.Rotation(buildingRot, 4, 'Z')
						sc_object.matrix_world = mathutils.Matrix.Identity(4)
						sc_object.matrix_world @= mathutils.Matrix.Translation(
							(buildingPos.central_pos[0] * grid.cell_size - grid.grid_size[0] * grid.cell_size / 2,
							 buildingPos.central_pos[1] * grid.cell_size - grid.grid_size[1] * grid.cell_size / 2,
							 0))
						sc_object.matrix_world @= mathutils.Matrix.Rotation(buildingRot, 4, 'Z')
					# all_objects.extend(building.objects)
					scene.add(building.scene)

					# marquer lots comme ayant un bâtiment dessus
					for ii in range(buildingPos.minX, buildingPos.maxX + 1):
						for jj in range(buildingPos.minY, buildingPos.maxY + 1):
							cell = grid.data[ii][jj]
							cell['b'] = 1
		self.last_operation_time = time.time() - startTime
		print(self.name, ': total buildings created and placed: ', no_batiment_actuel)
		# return all_objects
		return scene


def getGridCellCentralPos(grid, cellX, cellY):
	moitieTailleGrilleXMetres = grid.grid_size[0] * grid.cell_size / 2
	moitieTailleGrilleYMetres = grid.grid_size[1] * grid.cell_size / 2
	return (
		cellX * grid.cell_size - moitieTailleGrilleXMetres + grid.cell_size / 2,
		cellY * grid.cell_size - moitieTailleGrilleYMetres + grid.cell_size / 2,
		0,
	)


def get_total_voisins_qui_contiennent(grid, pos, keys_values_set):
	total = 0
	for i in range(pos[0] - 1, pos[0] + 2):
		if i < 0: continue
		for j in range(pos[1] - 1, pos[1] + 2):
			if j < 0: continue
			try:
				cell = grid.data[i][j]
				for key, value in keys_values_set:
					try:
						if cell[key] == value:
							total += 1
					except KeyError:
						pass
			except IndexError:
				pass

	return total


# permet de savoir où placer le batiment, sachant qu'on veut un de ses lots sur le lot désiré
class Position_potentielle_dun_batiment:
	def __init__(self,
				 grid,
				 forwardDir,
				 minCellPos,
				 maxCellPos,
				 askedCellPos,
				 keys_values_to_touch_front_any,
				 keys_values_restrict_any):
		self.grid = grid
		self.forwardDir = forwardDir
		self.minX = minCellPos[0]
		self.minY = minCellPos[1]
		self.maxX = maxCellPos[0]
		self.maxY = maxCellPos[1]
		self.askedX = askedCellPos[0]
		self.askedY = askedCellPos[1]
		self.keys_values_to_touch_front_any = keys_values_to_touch_front_any
		self.keys_values_restrict_any = keys_values_restrict_any
		self.total_front_cells_with_any_keys_values = self._get_total_front_cells_with_keys_values()
		self.peutConstruire = self._peutConstruire()
		self.central_pos = (
			(self.minX + (self.maxX - self.minX) / 2),
			(self.minY + (self.maxY - self.minY) / 2),
		)

		# calculer point central dans la scene
		if self.peutConstruire:
			minCellCentralPos = getGridCellCentralPos(grid, self.minX, self.minY)
			maxCellCentralPos = getGridCellCentralPos(grid, self.maxX, self.maxY)
			self.centralPos = (
				minCellCentralPos[0] + (maxCellCentralPos[0] - minCellCentralPos[0]) / 2,
				minCellCentralPos[1] + (maxCellCentralPos[1] - minCellCentralPos[1]) / 2,
				minCellCentralPos[2])  # la hauteur sera la meme pour tous les lots si ceux-ci sont tous plats

			# calculer note/distance
			askedCellCentralPos = getGridCellCentralPos(grid, self.askedX, self.askedY)
			self.distCentre2AskedCell = abs(askedCellCentralPos[0] - self.centralPos[0]) + \
										abs(askedCellCentralPos[1] - self.centralPos[1])

	def _peutConstruire(self):
		for i in range(self.minX, self.maxX + 1):
			if i < 0: return False  # obligé parce que les index négatifs sont supportés en Python, donc le except ne les attrape pas
			for j in range(self.minY, self.maxY + 1):
				if j < 0: return False  # obligé parce que les index négatifs sont supportés en Python, donc le except ne les attrape pas
				try:
					cell = self.grid.data[i][j]
				except IndexError:
					return False
				# if cell is a road or other building => can't put building there
				try:
					# if cell['a'] == 3 or cell['b']:
					if cell['b']:
						return False
				except KeyError:
					pass
				# peut construire uniquement sur cells que l'utilisateur a spécififées
				if self.keys_values_restrict_any:
					cell_contains_a_user_key_value = False
					for key, value in cell.items():
						try:
							if value in self.keys_values_restrict_any[key]:
								cell_contains_a_user_key_value = True
								break
						except KeyError:
							pass
					if not cell_contains_a_user_key_value:
						return False

		# teste les front key values
		if self.keys_values_to_touch_front_any and self.total_front_cells_with_any_keys_values <= 0:
			return False

		return True

	def _get_total_front_cells_with_keys_values(self):
		result = 0
		if self.forwardDir == DIRECTION4.NORD:
			for i in range(self.minX, self.maxX + 1):
				for key, value in self.keys_values_to_touch_front_any:
					try:
						if self.grid.data[i][self.maxY + 1][key] == value:
							result += 1
							break
					except KeyError: continue
					except IndexError: continue
		elif self.forwardDir == DIRECTION4.SUD:
			for i in range(self.minX, self.maxX + 1):
				for key, value in self.keys_values_to_touch_front_any:
					try:
						if self.grid.data[i][self.minY - 1][key] == value:
							result += 1
							break
					except KeyError: continue
					except IndexError: continue
		elif self.forwardDir == DIRECTION4.EST:
			for j in range(self.minY, self.maxY + 1):
				for key, value in self.keys_values_to_touch_front_any:
					try:
						if self.grid.data[self.maxX + 1][j][key] == value:
							result += 1
							break
					except KeyError: continue
					except IndexError: continue
		elif self.forwardDir == DIRECTION4.OUEST:
			for j in range(self.minY, self.maxY + 1):
				for key, value in self.keys_values_to_touch_front_any:
					try:
						if self.grid.data[self.minX - 1][j][key] == value:
							result += 1
							break
					except KeyError: continue
					except IndexError: continue
		return result


def trouver_meilleure_position_batiment(
		grid,
		taille_bati,
		asked_cell_pos,
		keys_values_to_touch_front_any,
		keys_values_restrict_any):
	positionsPotentiellesBatis = []

	# avoir positions possibles, puis les ordonner par distance à la location désirée, puis par nombre de routes auxquelles la location fait face
	directions = [DIRECTION4.EST, DIRECTION4.OUEST, DIRECTION4.NORD, DIRECTION4.SUD]
	random.shuffle(directions)
	for direction in directions:
		trouver_positions_batiment(
			positionsPotentiellesBatis,
			grid,
			asked_cell_pos,
			taille_bati,
			direction,
			keys_values_to_touch_front_any,
			keys_values_restrict_any)

	# 2) ordonner par note/distance
	positionsPotentiellesBatis.sort(key=lambda position: position.distCentre2AskedCell)

	# ordonner par nombre de routes auxquelles la location fait face???

	# 3) prendre la première = la plus proche, sinon retourner null si aucune
	try:
		return positionsPotentiellesBatis[0]
	except IndexError:
		return None


def trouver_positions_batiment(
		positionsPotentiellesBatis,
		grid,
		asked_cell_pos,
		taille_bati,
		forward_dir,
		keys_values_to_touch_front_any,
		keys_values_restrict_any):
	longueur_effective_x = taille_bati[0]
	longueur_effective_y = taille_bati[1]
	if forward_dir == DIRECTION4.NORD or forward_dir == DIRECTION4.SUD:
		longueur_effective_x = taille_bati[1]
		longueur_effective_y = taille_bati[0]

	for i in range(asked_cell_pos[0] - longueur_effective_x + 1, asked_cell_pos[0] + 1):
		for j in range(asked_cell_pos[1] - longueur_effective_y + 1, asked_cell_pos[1] + 1):
			positionPotentielleBati = Position_potentielle_dun_batiment(
				grid,
				forward_dir,
				(i, j),
				(i + longueur_effective_x - 1, j + longueur_effective_y - 1),
				asked_cell_pos,
				keys_values_to_touch_front_any,
				keys_values_restrict_any)
			if not positionPotentielleBati.peutConstruire:
				continue
			positionsPotentiellesBatis.append(positionPotentielleBati)
