import bpy
import os
import random
import mathutils
import enum
import math
import time
from . import utils
from .commun import *


class Infos_globales_placement_batiments:
	def __init__(self,
				 probaAucunBatiment=.2,
				 échelle=.01,
				 seed=None,
				 réduction_proba_selon_taille_percent=1,
				 ):
		self.probaAucunBatiment = probaAucunBatiment
		self.seed = seed
		self.échelle = échelle
		self.réduction_proba_selon_taille_percent = réduction_proba_selon_taille_percent


'''class Infos_grille:
	def __init__(self, 
			éléments_par_côté=10,
			espace_entre_éléments=10,
			variation_aléatoire=1,
		):
		self.éléments_par_côté = éléments_par_côté
		self.espace_entre_éléments = espace_entre_éléments
		self.variation_aléatoire = variation_aléatoire'''


def dessiner_grille_de_lots(grille_de_lots):
	bpy.data.images.new(name='grille todel', width=len(grille_de_lots.lots), height=len(grille_de_lots.lots))
	image = bpy.data.images['grille todel']
	image.name = 'SceneCity map'
	pixels = [0 for i in range(len(grille_de_lots.lots) ** 2 * 4)]
	minHeight = 9999
	maxHeight = -9999
	for i in range(len(grille_de_lots.lots)):
		for j in range(len(grille_de_lots.lots)):
			lot = grille_de_lots.lots[i][j]
			minHeight = min(minHeight, lot.positionDuCentre()[2])
			maxHeight = max(maxHeight, lot.positionDuCentre()[2])
	for i in range(len(grille_de_lots.lots)):
		for j in range(len(grille_de_lots.lots)):
			height = grille_de_lots.lots[i][j].positionDuCentre()[2]
			if maxHeight - minHeight == 0:
				rouge = 0
			else:
				rouge = (height - minHeight) / (maxHeight - minHeight)
			lot = grille_de_lots.lots[i][j]
			vert = 0
			if lot.routeConstruiteDessus:
				vert = 1
			bleu = 0
			if lot.batimentConstruitDessus:
				bleu = 1
			utils.définirPixel(pixels, len(grille_de_lots.lots), i, j, (rouge, vert, bleu, 1))
	image.pixels = pixels


# les batiments à placer sont retrouvés en inspectant tous les meshes et object groups directement
def placer_batiments_dans_scène(infos_globales_placement_batiments, func_de_placement, **paramètres_nommés_pour_func):
	print('SceneCity | Starting to create and to location the buildings objects in the scene. This operation will take a few minutes for large cities')
	tempsDébut = time.time()

	utils.definir_blender_mode('OBJECT')

	# nettoyage scène: batiments uniquement (pourrait aussi supprimer par le groupe 'SceneCity buildings only')
	# objetsASupprimer = [objet for objet in bpy.context.scene.objects if objet.name.startswith('SceneCity building')]
	# for objet in objetsASupprimer:
	# 	bpy.context.scene.collection.objects.unlink(objet)
	# 	objet.user_clear()
	# 	bpy.data.objects.remove(objet)
	objetsASupprimer = [objet for objet in bpy.context.scene.objects if objet.name.startswith('SceneCity building')]
	for objet in objetsASupprimer:
		# bpy.context.scene.objects.unlink(objet)
		# try: bpy.context.scene.collection.objects.unlink(objet)
		# except: pass
		objet.user_clear()
		bpy.data.objects.remove(objet)

	# préparer objet parent
	objParent = bpy.data.objects.new(name='SceneCity buildings', object_data=None)
	# bpy.context.scene.collection.objects.link(objParent)

	# créé les groupes Blender pour organiser les futures instances
	# if 'SceneCity buildings only' not in bpy.data.groups:
	# 	bpy.data.groups.new(name='SceneCity buildings only')
	# if 'SceneCity' not in bpy.data.groups:
	# 	bpy.data.groups.new(name='SceneCity')
	if 'SceneCity output buildings' not in bpy.data.collections:
		c = bpy.data.collections.new(name='SceneCity output buildings')
		bpy.context.scene.collection.children.link(c)

	# trouver tous les batiments (meshes et groups), et leur donner une probabilité d'apparaître
	dico_batiments_par_longueur_max = {}
	for datablock in bpy.data.meshes.values() + bpy.data.collections.values():
		if 'SceneCity' not in datablock or not datablock.SceneCity.utiliser_comme_batiment:
			continue
		# le poids de proba est l'inverse de leur taille multiplié par leur importance relative aux autres
		longueur_max_bat = max(datablock.SceneCity.batiment_surface_10m_x, datablock.SceneCity.batiment_surface_10m_y)

		if longueur_max_bat not in dico_batiments_par_longueur_max:
			dico_batiments_par_longueur_max[longueur_max_bat] = []

		dico_batiments_par_longueur_max[longueur_max_bat].append(
			(datablock, 1 / (
						longueur_max_bat ** infos_globales_placement_batiments.réduction_proba_selon_taille_percent) * datablock.SceneCity.batiment_probabilité_relative)
		)

	# on les place logiquement
	random.seed(infos_globales_placement_batiments.seed)
	liste_batiments_à_créer = func_de_placement(infos_globales_placement_batiments, dico_batiments_par_longueur_max, **paramètres_nommés_pour_func)
	# for batiment in liste_batiments_à_créer:
	# 	batiment['noms_groupes'] = 'SceneCity output buildings'
	print('SceneCity | Finished placing ' + str(len(liste_batiments_à_créer)) + ' logical buildings in total')

	# création des objets visuels
	print('SceneCity | Step 2/2 - creating the buildings in the 3d scene...')
	placer_objets(
		liste_dict_choses_à_placer=liste_batiments_à_créer,
		parent=objParent,
		prefixe_nom_dupli_placeur='SceneCity buildings',
		prefixe_nom_dupli_placé='SceneCity building',
		couches=[0, 3],
	)

	objParent.scale = (infos_globales_placement_batiments.échelle, infos_globales_placement_batiments.échelle, infos_globales_placement_batiments.échelle)
	objParent.empty_display_size = 200
	objParent.empty_display_type = 'SINGLE_ARROW'

	print('SceneCity | Finished to create and to location ' + str(len(liste_batiments_à_créer)) + ' buildings objects in the scene, in ' + str(
		round((time.time() - tempsDébut) / 60, 2)) + 'm\n')


def placer_batiments_sur_grille_de_lots(infos_globales_placement_batiments, dico_batiments_par_longueur_max, grille_de_lots):
	# nettoyage logique
	for i in range(len(grille_de_lots.lots)):
		for j in range(len(grille_de_lots.lots)):
			grille_de_lots.lots[i][j].batimentConstruitDessus = None

	# préparation des données nécessaires pour le placement
	dict_batis_datablock_poids = {}
	for taille_batiments, batiments in dico_batiments_par_longueur_max.items():
		for batiment in batiments:
			dict_batis_datablock_poids[batiment[0]] = batiment[1]

	# placer les meshes et groupes d'objets s'il y en a de spécifiés par l'utilisateur
	liste_batiments_à_créer = []
	if len(dict_batis_datablock_poids) > 0:
		no_batiment_actuel = 0

		print('SceneCity | Step 1/2 - starting to place the logical buildings')
		temps_dernier_affichage = time.time()
		for i in range(grille_de_lots.nbLotsCôté):
			for j in range(grille_de_lots.nbLotsCôté):
				# ignore de façon aléatoire
				if random.random() <= infos_globales_placement_batiments.probaAucunBatiment:
					continue

				lot_où_construire = grille_de_lots.lots[i][j]

				# construit bâtiment là où c'est libre
				if lot_où_construire.routeConstruiteDessus or lot_où_construire.batimentConstruitDessus:
					continue
				# ... et près des routes
				if not lot_où_construire.avoir_lot_voisin_qui_contient_route():
					continue

				position_lot_relative_x = i / (grille_de_lots.nbLotsCôté-1) * 2 - 1
				# position_lot_relative_x = i / (grille_de_lots.nbLotsCôté - 1)
				position_lot_relative_y = j / (grille_de_lots.nbLotsCôté-1) * 2 - 1
				# position_lot_relative_y = j / (grille_de_lots.nbLotsCôté - 1)

				# essayer de placer un batiment dans l'espace disponible
				dict_batis_datablock_poids_copie = dict_batis_datablock_poids.copy()
				while len(dict_batis_datablock_poids_copie) > 0:
					# on recalcule le poids de chaque datablock selon sa distribution texture et son emplacement
					liste_batis_datablock_poids_avec_distrib_tex = []
					tous_les_batis_utilisent_distrib_tex = True
					somme_poids = 0
					for bati_datablock, nouveaux_poids in dict_batis_datablock_poids_copie.items():
						bati_possède_distrib_tex = bati_datablock.SceneCity.batiment_nom_texture_controle_distribution in bpy.data.textures
						# bati a une distrib tex => utilise pour calculer poids
						if bati_possède_distrib_tex:
							texture = bpy.data.textures[bati_datablock.SceneCity.batiment_nom_texture_controle_distribution]
							pixel_red_pourcent = texture.evaluate((position_lot_relative_x, position_lot_relative_y, 0))[0]
							# print((position_lot_relative_x, position_lot_relative_y, 0), pixel_red_pourcent)
							nouveaux_poids *= pixel_red_pourcent
						# bati a pas de distrib tex => utilise son poids original
						else:
							tous_les_batis_utilisent_distrib_tex = False
						somme_poids += nouveaux_poids
						liste_batis_datablock_poids_avec_distrib_tex.append((bati_datablock, nouveaux_poids))
					# si tous les batiments utilisent une distrib tex, et que la somme des poids < 1, il y a une chance pour qu'il n'y ai aucun bati à l'emplacement actuel
					if tous_les_batis_utilisent_distrib_tex and somme_poids < 1:
						liste_batis_datablock_poids_avec_distrib_tex.append((None, 1 - somme_poids))

					# choisir le bâtiment à instancier
					batiment_datablock = choisir_aléatoire_pondéré(liste_batis_datablock_poids_avec_distrib_tex)
					positionBâtiment = None
					# si batiment est aucun, on passe au lot suivant
					if not batiment_datablock:
						break
					positionBâtiment = trouver_meilleure_position_batiment(
						grille_de_lots, batiment_datablock.SceneCity.batiment_surface_10m_x, batiment_datablock.SceneCity.batiment_surface_10m_y, i, j
					)
					# peut placer => On arrête de chercher, lot suivant
					if positionBâtiment:
						break
					# si atteint cette linge => peut pas placer bati actuel, essayer avec un autre batiment à la prochaine itération
					# mais avant on supprime de la liste le batiment déjà choisi
					dict_batis_datablock_poids_copie.pop(batiment_datablock)

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

				no_batiment_actuel += 1

				# choisir sa rotation

				rotationBâtiment = 0
				if positionBâtiment.directionDevant == DIRECTION4.EST:
					rotationBâtiment = 0
				elif positionBâtiment.directionDevant == DIRECTION4.OUEST:
					rotationBâtiment = 180
				elif positionBâtiment.directionDevant == DIRECTION4.NORD:
					rotationBâtiment = -90
				elif positionBâtiment.directionDevant == DIRECTION4.SUD:
					rotationBâtiment = 90

				# obtenir la couleur de la size texture si elle existe, à l'emplacement du batiment
				valeur_pixel_red_size_texture = 1  # par défaut c'est la taille normale, soit 100%
				try:
					valeur_pixel_red_size_texture = bpy.data.textures[batiment_datablock.SC_proc_building.bati_procedural_nom_size_texture].evaluate(
					(position_lot_relative_x, position_lot_relative_y, 0))[0]
				except:
					pass

				liste_batiments_à_créer.append({
					'datablock': batiment_datablock,
					'name': 'SceneCity building instance ' + batiment_datablock.name + ' ' + str(no_batiment_actuel),
					'location': positionBâtiment.pointCentralDansLaScene,
					'rotation_euler_radians': (0, 0, -math.radians(rotationBâtiment)),
					'noms_groupes': ['SceneCity output buildings'],
					'valeur_pixel_red_size_texture': valeur_pixel_red_size_texture,
				})

				# marquer lots comme ayant un bâtiment dessus
				for ii in range(positionBâtiment.minLotX, positionBâtiment.maxLotX + 1):
					for jj in range(positionBâtiment.minLotY, positionBâtiment.maxLotY + 1):
						lot = grille_de_lots.lots[ii][jj]
						lot.batimentConstruitDessus = True

				if time.time() - temps_dernier_affichage >= 1:
					temps_dernier_affichage = time.time()
					print('SceneCity | ' + str(no_batiment_actuel) + ' logical buildings placed so far...')
	return liste_batiments_à_créer


def trouver_meilleure_position_batiment(grille_de_lots, longueur_bati_x, longueur_bati_y, lotDésiréX, lotDésiréY):
	positionsPotentielles = []

	# 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(positionsPotentielles, grille_de_lots, lotDésiréX, lotDésiréY, longueur_bati_x, longueur_bati_y, direction)

	# 2) ordonner par note/distance
	positionsPotentielles.sort(key=lambda position: position.distanceDuCentreAuCentreDuLotDésiré);

	# 3) prendre la première = la plus proche, sinon retourner null si aucune
	if len(positionsPotentielles) > 0:
		return positionsPotentielles[0]
	return None


def trouver_positions_batiment(positionsPotentielles, grille_de_lots, lotDésiréX, lotDésiréY, longueur_bati_x, longueur_bati_y, directionDevant):
	longueur_effective_x = longueur_bati_x
	longueur_effective_y = longueur_bati_y
	if directionDevant == DIRECTION4.NORD or directionDevant == DIRECTION4.SUD:
		longueur_effective_x = longueur_bati_y
		longueur_effective_y = longueur_bati_x

	for i in range(lotDésiréX - longueur_effective_x + 1, lotDésiréX + 1):
		for j in range(lotDésiréY - longueur_effective_y + 1, lotDésiréY + 1):
			positionPotentielle = Position_potentielle_dun_batiment(
				grille_de_lots,
				directionDevant,
				i, j,
				i + longueur_effective_x - 1, j + longueur_effective_y - 1,
				lotDésiréX, lotDésiréY)
			if not positionPotentielle.peutConstruire():
				continue
			positionsPotentielles.append(positionPotentielle)


# 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,
				 grille_de_lots,
				 directionDevant,
				 minLotX, minLotY,
				 maxLotX, maxLotY,
				 lotDésiréX,
				 lotDésiréY):
		self.grille_de_lots = grille_de_lots
		self.directionDevant = directionDevant
		self.minLotX = minLotX
		self.minLotY = minLotY
		self.maxLotX = maxLotX
		self.maxLotY = maxLotY
		self.lotDésiréX = lotDésiréX
		self.lotDésiréY = lotDésiréY

		for i in range(lotDésiréX - 1, lotDésiréX + 2):
			if i < 0 or i >= len(grille_de_lots.lots):
				continue
			for j in range(lotDésiréY - 1, lotDésiréY + 2):
				if j < 0 or j >= len(grille_de_lots.lots):
					continue
				lot = grille_de_lots.lots[i][j]
				if lot.routeConstruiteDessus:
					self.hauteurDesRoutesLesPlusProches = lot.positionDuCentre()[2]

		self.nbRoutesDevant = self._nbRoutesDevant()

		# calculer point central dans la scene
		self.pointCentralDansLaScene = [0, 0, 0]
		if self.peutConstruire():
			positionCentreLotMin = grille_de_lots.positionCentraleDunLot(self.minLotX, self.minLotY)
			positionCentreLotMax = grille_de_lots.positionCentraleDunLot(self.maxLotX, self.maxLotY)
			self.pointCentralDansLaScene = [
				positionCentreLotMin[0] + (positionCentreLotMax[0] - positionCentreLotMin[0]) / 2,
				positionCentreLotMin[1] + (positionCentreLotMax[1] - positionCentreLotMin[1]) / 2,
				positionCentreLotMin[2]]  # la hauteur sera la meme pour tous les lots si ceux-ci sont tous plats

			# calculer note/distance
			positionCentreLotDésiré = grille_de_lots.positionCentraleDunLot(lotDésiréX, lotDésiréY)
			self.distanceDuCentreAuCentreDuLotDésiré = abs(positionCentreLotDésiré[0] - self.pointCentralDansLaScene[0]) + abs(
				positionCentreLotDésiré[1] - self.pointCentralDansLaScene[1])

	def peutConstruire(self):
		# hauteurCommune = None # tous les lots doivent avoir la même hauteur que la rue la plus proche
		for i in range(self.minLotX, self.maxLotX + 1):
			if i < 0 or i >= len(self.grille_de_lots.lots):
				return False
			for j in range(self.minLotY, self.maxLotY + 1):
				if j < 0 or j >= len(self.grille_de_lots.lots):
					return False
				lot = self.grille_de_lots.lots[i][j]
				hauteur = lot.positionDuCentre()[2]
				# if hauteurCommune == None:
				# hauteurCommune = hauteur
				if abs(hauteur - self.hauteurDesRoutesLesPlusProches) >= 1e-3:
					return False
				if lot.routeConstruiteDessus or lot.batimentConstruitDessus or not lot.estPlat() or not lot.estEmergé():
					return False

		# peux pas construire si batiment fait pas face à des routes
		if self.nbRoutesDevant <= 0:
			return False

		return True

	def _nbRoutesDevant(self):
		résultat = 0
		if self.directionDevant == DIRECTION4.NORD:
			if self.maxLotY + 1 >= 0 and self.maxLotY + 1 < len(self.grille_de_lots.lots):
				for i in range(self.minLotX, self.maxLotX + 1):
					if i >= 0 and i < len(self.grille_de_lots.lots) and self.grille_de_lots.lots[i][self.maxLotY + 1].routeConstruiteDessus: résultat += 1
		elif self.directionDevant == DIRECTION4.SUD:
			if self.minLotY - 1 >= 0 and self.minLotY - 1 < len(self.grille_de_lots.lots):
				for i in range(self.minLotX, self.maxLotX + 1):
					if i >= 0 and i < len(self.grille_de_lots.lots) and self.grille_de_lots.lots[i][self.minLotY - 1].routeConstruiteDessus: résultat += 1
		elif self.directionDevant == DIRECTION4.EST:
			if self.maxLotX + 1 >= 0 and self.maxLotX + 1 < len(self.grille_de_lots.lots):
				for j in range(self.minLotY, self.maxLotY + 1):
					if j >= 0 and j < len(self.grille_de_lots.lots) and self.grille_de_lots.lots[self.maxLotX + 1][j].routeConstruiteDessus: résultat += 1
		elif self.directionDevant == DIRECTION4.OUEST:
			if self.minLotX - 1 >= 0 and self.minLotX - 1 < len(self.grille_de_lots.lots):
				for j in range(self.minLotY, self.maxLotY + 1):
					if j >= 0 and j < len(self.grille_de_lots.lots) and self.grille_de_lots.lots[self.minLotX - 1][j].routeConstruiteDessus: résultat += 1
		return résultat
