import bpy, mathutils, bmesh
import math, copy, time
from typing import List
import shapely.geometry
from . import my_globals
from .nodes import DATA_Geometries, DATA_Mesh
from pathlib import Path


def get_addon_preferences():
	return bpy.context.preferences.addons['scenecity'].preferences


def get_icon_value(name: str):
	return my_globals.icônes[name].icon_id


def truncate_float(n, decimals=0):
	multiplier = 10 ** decimals
	return int(n * multiplier) / multiplier


def sc_mesh_to_2d_geoms(sc_mesh: DATA_Mesh, conside_verts=True, consider_all_verts=False, consider_edges=True, consider_faces=True, bm=None) -> DATA_Geometries:
	"""
	bm: Si un bmesh est donné, c'est à l'appelant de le créer et le détruire comme il faut, sinon cette fonction s'en charge si aucun n'est fourni.
	consider_all_verts: si on doit considérer les verts, doit-on aussi considéré ceux des edges et des faces, pas uniquement les loose?
	"""
	geoms = DATA_Geometries()
	if not bm:
		bm = bmesh.new()
		bm.from_mesh(sc_mesh.bl_mesh)

	if conside_verts:
		# TODO: get verts based on selection
		if consider_all_verts:
			for v in bm.verts:
				geoms.shapely_geoms.append(shapely.geometry.Point(tuple(v.co)))
		else:
			verts = []
			for v in bm.verts:
				v_is_connected_to_edge = v.is_wire
				v_is_connected_to_face = len(v.link_faces) > 0
				v_is_loose = not v_is_connected_to_edge and not v_is_connected_to_face
				if v_is_loose:
					verts.append(v)
			for v in verts:
				geoms.shapely_geoms.append(shapely.geometry.Point(tuple(v.co)))

	if consider_edges:
		# TODO: get edges based on selection
		# connected edges up to crossings
		list_of_connected_edges_coords = get_connected_edges(bm, False)
		for connected_edges_coords in list_of_connected_edges_coords:
			# if connected_edges_coords[0] ==connected_edges_coords[-1]:
			# 	geoms.shapely_geoms.append(shapely.geometry.LinearRing(connected_edges_coords))
			# 	# print('ring found')
			# else:
			# 	geoms.shapely_geoms.append(shapely.geometry.LineString(connected_edges_coords))
			linestring = shapely.geometry.LineString(connected_edges_coords)
			# linestring = linestring.buffer(0.00001, cap_style=shapely.geometry.CAP_STYLE.square)
			# linestring = shapely.ops.linemerge(linestring)
			geoms.shapely_geoms.append(linestring)

	if consider_faces:
		# TODO: get faces based on selection
		# faces = []
		# for f in bm.faces:
		# 	f_total_edges = len(f.loops)
		# 	should_add_f = self.faces_to_consider == 'all' or \
		# 				   self.faces_to_consider == 'triangles_only' and f_total_edges == 3 or \
		# 				   self.faces_to_consider == 'quads_only' and f_total_edges == 4 or \
		# 				   self.faces_to_consider == 'ngons_only' and f_total_edges > 4
		# 	if should_add_f:
		# 		faces.append(f)
		# for f in faces:
		# 	v_coords = [tuple(v.co) for v in f.verts]
		# 	geoms.shapely_geoms.append(shapely.geometry.Polygon(v_coords))
		for f in bm.faces:
			# v_coords = [tuple(v.co) for v in f.verts]
			v_coords = [tuple(loop.vert.co) for loop in f.loops]
			geoms.shapely_geoms.append(shapely.geometry.Polygon(v_coords))

	if not bm:
		bm.free()
	return geoms


def geoms_to_sc_mesh(geoms: List[DATA_Geometries], simplify_exteriors=False) -> List[DATA_Mesh]:
	result_sc_meshes: List[DATA_Mesh] = []
	for geom in geoms:
		bm = bmesh.new()
		definir_blender_mode('OBJECT')
		bpy.ops.object.select_all(action='DESELECT')

		def add_bezier_point(bl_spline, point_tuple, point_nb):
			bl_bezier_point = bl_spline.bezier_points[point_nb]
			bl_bezier_point.co = (point_tuple[0], point_tuple[1], 0)
			bl_bezier_point.handle_left_type = 'VECTOR'
			bl_bezier_point.handle_right_type = 'VECTOR'

		for obj in bpy.context.view_layer.objects:
			obj.select_set(False)

		for shapely_geom in geom.shapely_geoms:
			# if shapely_geom.is_empty or not isinstance(shapely_geom, (shapely.geometry.Polygon, shapely.geometry.MultiPolygon)):
			if shapely_geom.is_empty:
				continue

			if isinstance(shapely_geom, (shapely.geometry.Point, shapely.geometry.MultiPoint)):
				points: List[shapely.geometry.Point] = []
				if isinstance(shapely_geom, shapely.geometry.MultiPoint):
					for point in shapely_geom.geoms:
						points.append(point)
				else:
					points.append(shapely_geom)
				for point in points:
					bm.verts.new((point.x, point.y, point.z))
			elif isinstance(shapely_geom, (shapely.geometry.LineString, shapely.geometry.MultiLineString)):
				lines: List[shapely.geometry.LineString] = []
				if isinstance(shapely_geom, shapely.geometry.MultiLineString):
					for linestring in shapely_geom.geoms:
						lines.append(linestring)
				else:
					lines.append(shapely_geom)
				for linestring in lines:
					prev_vert = None
					for coord in linestring.coords:
						new_vert = bm.verts.new(coord)
						if prev_vert:
							bm.edges.new((prev_vert, new_vert))
						prev_vert = new_vert
			elif isinstance(shapely_geom, (shapely.geometry.Polygon, shapely.geometry.MultiPolygon)):
				try:
					bl_curve = bpy.data.curves.new('sc tmp curve', 'CURVE')
					bl_curve.dimensions = '2D'
					bl_curve.fill_mode = 'BOTH'
					bl_obj = bpy.data.objects.new(bl_curve.name, bl_curve)
					bpy.context.collection.objects.link(bl_obj)
					bpy.context.view_layer.objects.active = bl_obj
					bl_obj.select_set(True)

					polygons: List[shapely.geometry.Polygon] = []
					if isinstance(shapely_geom, shapely.geometry.MultiPolygon):
						for polygon in shapely_geom.geoms:
							polygons.append(polygon)
					else:
						polygons.append(shapely_geom)

					for shapely_polygon in polygons:
						bl_spline = bl_curve.splines.new('BEZIER')
						bl_spline.use_cyclic_u = True
						final_coords_to_add_to_curve = []
						polygon_exterior_coords = shapely_polygon.exterior.coords[:-1]
						prev_point = polygon_exterior_coords[-1]
						next_point = polygon_exterior_coords[1]

						# enlève les points de la courbe qui sont alignés. => Provoque des résultats indésirables si pas fait aussi pour les interiors
						# Ca arrive quand un coté d'un route est sur un interieur du buffer, et l'autre sur l'extérieur
						for point_i, point_tuple in enumerate(polygon_exterior_coords):
							Ax = point_tuple[0]
							Ay = point_tuple[1]
							Bx = prev_point[0]
							By = prev_point[1]
							Cx = next_point[0]
							Cy = next_point[1]
							if point_tuple == prev_point or point_tuple == next_point or prev_point == next_point:
								continue
							if simplify_exteriors:
								if abs((Ax * (By - Cy) + Bx * (Cy - Ay) + Cx * (Ay - By))) > 0.0001:
									final_coords_to_add_to_curve.append(point_tuple)
							else:
								final_coords_to_add_to_curve.append(point_tuple)
							prev_point = point_tuple
							try:
								next_point = polygon_exterior_coords[point_i + 2]
							except IndexError:
								next_point = polygon_exterior_coords[0]
						bl_spline.bezier_points.add(len(final_coords_to_add_to_curve) - 1)  # -1 parce que il y a déjà un point par défaut
						for point_nb, point_tuple in enumerate(final_coords_to_add_to_curve):
							add_bezier_point(bl_spline, point_tuple, point_nb)

						for linear_ring in shapely_polygon.interiors:
							bl_spline = bl_curve.splines.new('BEZIER')
							bl_spline.use_cyclic_u = True
							bl_spline.bezier_points.add(len(linear_ring.coords) - 2)
							for point_nb, point_tuple in enumerate(linear_ring.coords[:-1]):
								add_bezier_point(bl_spline, point_tuple, point_nb)

					bpy.ops.object.convert(target='MESH')
					definir_blender_mode('EDIT')
					bpy.ops.mesh.select_all(action='SELECT')
					bpy.ops.mesh.beautify_fill()

					# TODO: delete faces not overlapping original geom

					bpy.ops.mesh.tris_convert_to_quads(shape_threshold=math.radians(180))
					definir_blender_mode('OBJECT')
					bm.from_mesh(bl_obj.data)
				finally:
					bpy.data.meshes.remove(bl_obj.data)
					bpy.data.curves.remove(bl_curve)
				try: bpy.context.collection.objects.unlink(bl_obj)
				except: pass
				try: bpy.data.objects.remove(bl_obj)
				except: pass

		bl_mesh = bpy.data.meshes.new(geom.name)
		bm.to_mesh(bl_mesh)
		bm.free()
		sc_mesh = DATA_Mesh(bl_mesh)
		# sc_mesh.bl_mesh.name = geom.name
		result_sc_meshes.append(sc_mesh)
	return result_sc_meshes


# pixels = tableau 1d qui doit avoir la même taille que celui des pixels de l'image finale. Ne pas oublier de remplacer ensuite ceux de l'image par ce tableau
# couleur = (r,g,b,a)
def définirPixel(pixels, imageWidth, x, y, couleur):
	indicePixel = y * imageWidth * 4 + x * 4
	pixels[indicePixel + 0] = couleur[0]
	pixels[indicePixel + 1] = couleur[1]
	pixels[indicePixel + 2] = couleur[2]
	pixels[indicePixel + 3] = couleur[3]


# retourne R,G,B,A
def avoir_rgba_pixel(image, x, y):
	indicePixel = y * image.size[0] * 4 + x * 4
	r = image.pixels[indicePixel + 0]
	g = image.pixels[indicePixel + 1]
	b = image.pixels[indicePixel + 2]
	a = image.pixels[indicePixel + 3]
	return r, g, b, a


# retourne R,G,B,A
def avoir_rgba_pixel_from_pixels(pixels, image_size_x, x, y):
	indicePixel = y * image_size_x * 4 + x * 4
	r = pixels[indicePixel + 0]
	g = pixels[indicePixel + 1]
	b = pixels[indicePixel + 2]
	a = pixels[indicePixel + 3]
	return r, g, b, a


# retourne R,G,B
def avoir_rgb_pixel(image, x, y):
	rgba = avoir_rgba_pixel(image, x, y)
	return rgba[0], rgba[1], rgba[2]


def definir_blender_mode(mode):
	if bpy.context.mode != mode:
		# vérifie qu'au moins un objet est actif, sinon ça fera une erreur
		if mode == 'OBJECT' and not bpy.context.view_layer.objects.active:
			bpy.context.view_layer.objects.active = bpy.context.scene.objects[0]
		elif mode.startswith('EDIT'):
			mode = 'EDIT'
		elif 'PAINT_VERTEX' == mode:
			mode = 'VERTEX_PAINT'
		elif 'PAINT_WEIGHT' == mode:
			mode = 'WEIGHT_PAINT'
		elif 'PAINT_TEXTURE' == mode:
			mode = 'TEXTURE_PAINT'
		bpy.ops.object.mode_set(mode=mode)


def get_bl_objects_and_mode_state():
	"""Retourne toutes les données nécessaires pour rétablir l'état de Blender avant l'execution de l'operateur"""
	mode = bpy.context.mode
	active_object = bpy.context.view_layer.objects.active
	selected_objects = list(bpy.context.view_layer.objects.selected)
	mesh_select_mode = tuple(bpy.context.scene.tool_settings.mesh_select_mode)
	return mode, active_object, selected_objects, mesh_select_mode


def set_bl_state(data):
	definir_blender_mode('OBJECT')
	bpy.context.view_layer.objects.active = data[1]
	bpy.ops.object.select_all(action='DESELECT')
	for obj in data[2]:
		obj.select_set(True)
	definir_blender_mode(data[0])
	bpy.context.scene.tool_settings.mesh_select_mode = data[3]


# bpy.context.scene.tool_settings.mesh_select_mode = (True, True, True)


def selectionner_un_seul_obj(bl_obj):
	definir_blender_mode('OBJECT')
	bpy.ops.object.select_all(action='DESELECT')
	bpy.context.view_layer.objects.active = bl_obj
	bl_obj.select_set(True)


def get_connected_edges(bm, consider_faces_edges: bool) -> List[List[mathutils.Vector]]:
	"""Retourne la liste des edges connectés entre eux. Ignore les edges connectés aux faces"""
	result = []

	def get_connected_vert_on_other_side_of_next_edge(initial_bm_edge, bm_vert):
		""" Suppose que les verts sont connectés à pas plus de deux edges.
		Retourne un tuple qui contient le bmesh vert connecté au vert spécifié, pas de l'autre côté du edge donné, mais de l'autre côté du edge en face;
		et le bmesh edge en face. Sinon None si on est en bout de la séquence de edges connectés"""
		# if len(bm_vert.link_faces) > 0:
		# 	return None, None
		if len(bm_vert.link_edges) == 1:
			assert bm_vert.link_edges[0] == initial_bm_edge
			return None, None

		if len(bm_vert.link_edges) == 2:
			other_edge = bm_vert.link_edges[0] if bm_vert.link_edges[0] != initial_bm_edge else bm_vert.link_edges[1]
			other_v = other_edge.other_vert(bm_vert)
			return other_v, other_edge

		raise ValueError('The given mesh must not have a vert connected to 3 or more edges')

	untreated_edges = list(bm.edges)
	# isolate connected edges by sets
	while len(untreated_edges) > 0:
		first_edge = untreated_edges.pop()
		if consider_faces_edges or len(first_edge.link_faces) <= 0:
			first_vert = first_edge.verts[0]
			direction0_connected_verts = [first_vert.co]
			try:
				connected_vert, other_edge = get_connected_vert_on_other_side_of_next_edge(first_edge, first_vert)
				while connected_vert != None and other_edge != None:
					untreated_edges.remove(other_edge)
					last_vert = connected_vert
					last_edge = other_edge
					direction0_connected_verts.append(last_vert.co)
					connected_vert, other_edge = get_connected_vert_on_other_side_of_next_edge(last_edge, last_vert)
			# si on tombe sur un vert connecté à trois ou plus edges, on stoppe la recherche de nouveaux edges de ce côté là
			except ValueError:
				pass
			finally:
				# dans tous les cas, on inverse l'ordre des verts trouvés le long des edges connectés, pour les ajouter à la liste de ceux dans l'autre direction
				direction0_connected_verts.reverse()

			second_vert = first_edge.verts[1]
			direction1_connected_verts = [second_vert.co]
			try:
				connected_vert, other_edge = get_connected_vert_on_other_side_of_next_edge(first_edge, second_vert)
				while connected_vert != None and other_edge != None:
					untreated_edges.remove(other_edge)
					last_vert = connected_vert
					last_edge = other_edge
					direction1_connected_verts.append(last_vert.co)
					connected_vert, other_edge = get_connected_vert_on_other_side_of_next_edge(last_edge, last_vert)
			# si on tombe sur un vert connecté à trois ou plus edges, on stoppe la recherche de nouveaux edges de ce côté là
			except ValueError:
				pass

			direction0_connected_verts.extend(direction1_connected_verts)
			result.append(direction0_connected_verts)
	return result


def createCube(vertices, faces, pos, size, bottomFace=True, rotZ=0.0, trans=(0, 0, 0)):
	'''
	creates a 3d cube, with base at z=0, and x,y centered around pos. Supports basic 3d transformations
	
	vertices: array of 3d points [(x,y,z), (x,y,z), (x,y,z)...]
	faces: array of vertex indices representing 3d quads [(i1,i2,i3,i4), (i1,i2,i3,i4), (i1,i2,i3,i4)...] 
	pos: ABSOLUTE POSITION, new vertices will be placed (with "trans" and rotated relatively to this 3d point (x,y,z)
	rotZ: you can apply a rotation (in radians) around the z-axis
	trans: relative (to "pos") location AFTER the rotZ is applied
	'''

	hypo = math.sqrt(size[0] * size[0] / 4 + size[1] * size[1] / 4)

	aco = math.acos(size[0] / 2. / hypo)  # we need to know this angle. May not be correct (neg vs pos angles), but taking care below
	asi = math.asin(size[1] / 2. / hypo)

	dx = trans[0] * math.cos(rotZ) + trans[1] * math.cos(rotZ + math.pi / 2)  # displacement along x after rotZ has been applied
	dy = trans[0] * math.sin(rotZ) + trans[1] * math.sin(rotZ + math.pi / 2)

	# taking care here of neg vs pos angles coz' we know it's cube and where each point stands
	x04 = pos[0] + math.cos(aco + rotZ + math.pi) * hypo + dx  # final x pos for points 0 and 4
	y04 = pos[1] + math.sin(asi + rotZ + math.pi) * hypo + dy
	x15 = pos[0] + math.cos(2 * math.pi - aco + rotZ) * hypo + dx
	y15 = pos[1] + math.sin(2 * math.pi - asi + rotZ) * hypo + dy
	x26 = pos[0] + math.cos(math.pi - aco + rotZ) * hypo + dx
	y26 = pos[1] + math.sin(math.pi - asi + rotZ) * hypo + dy
	x37 = pos[0] + math.cos(aco + rotZ) * hypo + dx
	y37 = pos[1] + math.sin(asi + rotZ) * hypo + dy

	l = len(vertices)
	verts = [ \
		[x04, y04, pos[2] + trans[2]], \
		[x15, y15, pos[2] + trans[2]], \
		[x26, y26, pos[2] + trans[2]], \
		[x37, y37, pos[2] + trans[2]], \
 \
		[x04, y04, pos[2] + size[2] + trans[2]], \
		[x15, y15, pos[2] + size[2] + trans[2]], \
		[x26, y26, pos[2] + size[2] + trans[2]], \
		[x37, y37, pos[2] + size[2] + trans[2]], \
		]

	fac = [ \
		[0 + l, 1 + l, 5 + l, 4 + l],  # face 0 sur -y
		[1 + l, 3 + l, 7 + l, 5 + l],  # face 1 sur +x
		[2 + l, 0 + l, 4 + l, 6 + l],  # face 2 sur -x
		[3 + l, 2 + l, 6 + l, 7 + l],  # face 3 sur +y
		[4 + l, 5 + l, 7 + l, 6 + l]  # face 4 du dessus +z
	]
	if bottomFace:
		fac.append([2 + l, 3 + l, 1 + l, 0 + l])  # face 5 du dessous -z

	vertices.extend(verts)
	faces.extend(fac)


# if uvs:
# uvs.extend([(0,0), (1,1), (0,size[0]), (0,size[0]), (0,size[0]), (0,size[0]), (0,size[0]), (0,size[0]), ])


class Vector2:

	def __init__(self, x, y):
		self.x = x
		self.y = y

	def getLength(self):
		return math.sqrt(self.x ** 2 + self.y ** 2)

	def normalize(self):
		magnitude = self.getLength()
		self.x /= magnitude
		self.y /= magnitude

	def getNormalizedCopy(self):
		result = copy.copy(self)
		result.normalize()
		return result

	def setLength(self, newLength):
		self.normalize()
		self.x *= newLength
		self.y *= newLength

	def getAngleRadians(self):
		normalized = self.getNormalizedCopy()
		angle = math.acos(normalized.x)
		if self.y < 0:
			angle = -angle

	def getAngleDegrees(self):
		return math.degrees(self.getAngleRadians())

	def setRotation(self, angleRadians):
		length = self.getLength()
		self.x = math.cos(angleRadians) * length
		self.y = math.sin(angleRadians) * length
		if angleRadians < 0:
			self.y = -self.y

	def rotate(self, byAngleRadians):
		self.setRotation(self.getAngleRadians() + byAngleRadians)

	@classmethod
	def dotProduct(cls, v1, v2):
		return v1.x * v2.x + v1.y * v2.y

	@classmethod
	def crossProduct(cls, v1, v2):
		return v1.x * v2.y - v1.y * v2.x

	@classmethod
	def angleBetween(cls, v1, v2):
		angle = math.acos(cls.dotProduct(v1.getNormalizedCopy(), v2.getNormalizedCopy()))
		cross = cls.crossProduct(v1, v2)
		if cross < 0:
			angle = -angle
		return angle


# def copy_label_to_sub_geoms(geom):
# 	if not geom.labels:
# 		return
# 	sub_geoms = []
# 	print(geom.geoms, geom.geoms)
# 	for sub_geom in geom.geoms:
# 		# print('copy_label_to_sub_geoms')
# 		sub_geom.labels = geom.labels[:]
# 		# sub_geom.labels = ['truc']
# 		sub_geoms.append(sub_geom)
# 		# print(sub_geom, sub_geom.labels)
# 	return sub_geoms


def print_time(message: str, last_time: float = None, format: str = None, long_time_s=0.5):
	"""
	Affiche des durées d'exécution et retourne le time actuel si last_time est précisé, sinon affiche juste le message et retourne None.
	:param message:
	:param last_time:
	:param format:
	:return:
	"""
	if last_time is None:
		if format:
			print(format, end="")
		print(f"{message:>50}", end="")
		if format:
			print("\x1b[0m", end="")
		print()
	else:
		current_time = time.process_time()
		elapsed_time = current_time - last_time
		print(
			f"{message:>50}: \x1b[38;2;{max(min(255, round(elapsed_time / long_time_s * 255)), 0)};{max(min(255, round((1 - elapsed_time / long_time_s) * 255)), 0)};0m{elapsed_time:5.3f}s\x1b[0m")
		return current_time


def get_addon_dir() -> Path:
	return Path(__file__).parent


def get_sc_data_dir() -> Path:
	return Path.home() / ".SceneCity"


def get_terrains_data_dir() -> Path:
	return get_sc_data_dir() / "Terrains"


def get_vegetation_data_dir() -> Path:
	return get_sc_data_dir() / "Vegetation"

def get_city_data_dir() -> Path:
	return get_sc_data_dir() / "City"


class SC_OT_Fix_scene_unit_settings(bpy.types.Operator):
	bl_idname = 'scene.sc_fix_unit_settings'
	bl_description = "Set the scene unit settings to the correct values for SceneCity. In the scene properties, the unit system will be changed to metric, and the scale to 100"
	bl_label = 'Set correct unit settings'
	bl_options = {'REGISTER', 'UNDO'}

	def execute(self, context):
		context.scene.unit_settings.system = 'METRIC'
		context.scene.unit_settings.scale_length = 100
		return {'FINISHED'}


class SC_OT_Fix_render_settings(bpy.types.Operator):
	bl_idname = 'scene.sc_fix_render_settings'
	bl_description = "Set the Cycles render settings to the correct values for SceneCity. In the scene render properties, will set the engine to Cycles, activate transparent shadows, set transparent and volume bounces to 12"
	bl_label = 'Set correct render settings'
	bl_options = {'REGISTER', 'UNDO'}

	def execute(self, context):
		context.scene.render.engine = "CYCLES"
		context.scene.cycles.use_transparent_shadows = True
		context.scene.cycles.transparent_max_bounces = 12
		context.scene.cycles.volume_bounces = 12
		return {'FINISHED'}
