# from __future__ import annotations
import shutil, math, random, time, enum, os, pathlib, typing, copy
from typing import List, Tuple, Dict, Set, Iterable, Callable, FrozenSet, Type, Union
from functools import wraps
from enum import Enum, auto
import bpy, nodeitems_utils, mathutils, bmesh
from nodeitems_utils import NodeCategory, NodeItem
from bpy.types import Node, NodeSocket
import shapely, shapely.geometry
from datetime import datetime
from bpy_extras.io_utils import ExportHelper
from .. import utils, my_globals

# node_doc_base_url = 'https://www.cgchan.com/static/doc/scenecity/1.5/nodes/'
# node_doc_base_url = 'https://sites.google.com/view/scenecity16doc/'
node_doc_base_url = 'https://scenecity18doc.cgchan.com/references/nodes/'
tmp_collection_name = 'sc tmp (you can delete safely)'


class SC_NodeTree(bpy.types.NodeTree):
	bl_idname = 'sc_node_tree_c3ey4om9toq4iwzf8ynl'
	bl_label = 'SceneCity node editor'
	bl_icon = 'GRID'
	still_working: bpy.props.BoolProperty(default=False)

	def set_all_nodes_ready(self):
		for node in self.nodes:
			# node.show_node_work_state(Node.WORK_STATE.NEUTRAL)
			if isinstance(node, Node):
				node.work_state = str(Node.WORK_STATE2.READY)
				node.update_visual_state()

	def update(self):
		# print("nodes tree update")
		if my_globals.todel_derniere_operation_par_operator_non_standard:
			return
		if self.still_working:
			return
		self.set_all_nodes_ready()
	# for node in self.nodes:
	# 	node.update_visual_state()


class DATA_Map:
	def get_value(self, xPercent: float, yPercent: float, **kwargs) -> float:
		return 0


class DATA_Terrain:
	def __init__(self, mesh_object):
		self.mesh_object = mesh_object


class DATA_Terrain_Shape:
	def __init__(self,
				 map: DATA_Map,
				 physical_size_meters: Tuple[float, float, float]):
		self.map = map
		self.physical_size_meters = physical_size_meters

	# def get_height_meters_at_percent(self, x_percent, y_percent):
	# 	return self.map.get_value(x_percent, y_percent) * self.physical_size_meters[2]

	def get_height_meters_at_meters(self, x_meters, y_meters):
		return self.map.get_value(
			x_meters / self.physical_size_meters[0],
			y_meters / self.physical_size_meters[1]) * self.physical_size_meters[2]


class DATA_Image:
	def __init__(self, bl_img):
		self.bl_img = bl_img


class DATA_TextureSet:
	"""Une texture a un nom et une image associée"""

	def __init__(self):
		self.textures = {}


class DATA_Material:
	def __init__(self, bl_material):
		self.bl_material = bl_material


class DATA_Geometries:
	def __init__(self):
		self.shapely_geoms: List[shapely.geometry.base.BaseGeometry] = []
		self.name = '2D Geometries'

	# self.shapely_geoms: shapely.geometry.GeometryCollection = shapely.geometry.GeometryCollection()

	@staticmethod
	def break_geom_collection(geom_collection):
		result = []
		for geom in geom_collection.geoms:
			if isinstance(geom, shapely.geometry.GeometryCollection):
				result.extend(DATA_Geometries.break_geom_collection(geom))
			else:
				result.append(geom)
		return result

	@staticmethod
	def make_sure_no_geom_collections(geoms: List):
		geoms_no_collections = []
		for geom in geoms:
			if isinstance(geom, shapely.geometry.GeometryCollection):
				geoms_no_collections.extend(DATA_Geometries.break_geom_collection(geom))
			else:
				geoms_no_collections.append(geom)
		return geoms_no_collections

	def get_shapely_geom_collection(self) -> shapely.geometry.GeometryCollection:
		return shapely.geometry.GeometryCollection(self.shapely_geoms)

	def __add__(self, other):
		"""Retourne une nlle instance"""
		result = DATA_Geometries()
		result.shapely_geoms = copy.deepcopy(self.shapely_geoms)
		other_geoms = copy.deepcopy(other.shapely_geoms)
		# self.shapely_geoms.extend(other.shapely_geoms)
		result.shapely_geoms.extend(other_geoms)
		# return self
		return result

	@classmethod
	def from_geom_collection(cls, geom_collection: shapely.geometry.GeometryCollection):
		result = DATA_Geometries()
		for geom in geom_collection.geoms:
			result.shapely_geoms.append(geom)
		return result

	def copy_from_other_except_geom_data(self, other):
		self.name = other.name


class DATA_bldata:
	"""Classe parent pour tous les datablocks qui peuvent être assignés à des objets dans Blender"""

	def __init__(self, bl_data, bl_data_collection, should_create_tmp_obj):
		self.bl_data = bl_data
		self.bl_data_collection = bl_data_collection  # peut être bpy.data.meshes, bpy.data.objects, bpy.data.curves...
		self.tmp_obj = None
		if should_create_tmp_obj:
			self.tmp_obj = bpy.data.objects.new('tmp ' + self.bl_data.name, self.bl_data)
			# target_collection = bpy.context.scene.collection
			try:
				target_collection = bpy.data.collections[tmp_collection_name]
			except KeyError:
				target_collection = bpy.data.collections.new(tmp_collection_name)
				bpy.context.scene.collection.children.link(target_collection)
			target_collection.objects.link(self.tmp_obj)


class DATA_Mesh(DATA_bldata):
	def __init__(self, bl_mesh):
		# vieux attributs, à supprimer quand ils ne seront plus utilisés
		self.verts: List[Tuple[float, float, float]] = []
		self.edges: List[Tuple[int, int]] = []
		self.faces: List[Tuple[int]] = []
		# self.faces_edge_keys: List[Tuple[int, int]] = [] # défini une face par ses edges et non ses verts
		self.colorLayers: Dict[str, List[Tuple[float, float, float, float]]] = {}
		self.verts_groups: Dict[str, List[int]] = {}
		self.uvs_per_face_layers = {'generated main': []}
		self.faces_material_indices = []
		# self.bmesh = bmesh.new()
		self.bmesh = None

		self.bl_mesh = bl_mesh
		if self.bl_mesh:  # ce test est nécessaire uniquement parce que les vieux nodes passent None pour bl_mesh, car ils utilisent les attributs en haut
			super().__init__(self.bl_mesh, bpy.data.meshes, True)

	def __str__(self):
		return 'DATA_Mesh(' + str(len(self.verts)) + ' verts, ' + str(len(self.faces)) + ' faces)'

	def __add__(self, other):
		# add verts
		total_verts_before_add = len(self.verts)
		self.verts.extend(other.verts)
		self.faces_material_indices.extend(other.faces_material_indices)
		self.uvs_per_face_layers['generated main'].extend(other.uvs_per_face_layers['generated main'])

		# add faces and material_indices: shift all indices
		for other_face_nb, other_face in enumerate(other.faces):
			self.faces.append(tuple(vertex_index + total_verts_before_add for vertex_index in other_face))
		return self

	def translate(self, values):
		for v_nb, v_coord in enumerate(self.verts):
			self.verts[v_nb] = tuple([v_coord[i] + values[i] for i in range(3)])

	def scale(self, values):
		for v_nb, v_coord in enumerate(self.verts):
			self.verts[v_nb] = tuple([v_coord[i] * values[i] for i in range(3)])

	def rotate_z(self, angle):
		for v_nb, v_coord in enumerate(self.verts):
			dist_to_center = math.sqrt(v_coord[0] ** 2 + v_coord[1] ** 2)
			# don't rotate if vert is exactly on center z
			if dist_to_center != 0:
				vect_angle = math.acos(v_coord[0] / dist_to_center)
				if v_coord[1] < 0:
					vect_angle = -vect_angle
				vect_angle += angle
				self.verts[v_nb] = (dist_to_center * math.cos(vect_angle), dist_to_center * math.sin(vect_angle), v_coord[2])


class DATA_Object(DATA_bldata):
	def __init__(self, bl_object):
		self.bl_object = bl_object
		super().__init__(self.bl_object, bpy.data.objects, False)


class SOCKET:
	"""
	Needs the following class attributes:
		compatible_output_socket_classes: Tuple[Socket] # assumes all sockets are compatible with their own type. Specify here the other compatible types
		color: Tuple[float]
		default_label: str
		display_shape (facultative): see Blender API, includes CIRCLE, DIAMOND, SQUARE, and the DOT versions (CIRCLE_DOT etc...)
	"""
	internal_icon = None
	external_icon_name = None
	display_shape = 'SQUARE_DOT'

	is_required: bpy.props.BoolProperty(
		name='Required',
		description='',
		default=False)
	"""True si l'output est un tout nouvel object, faux si c'est un existant en input modifié sur place"""
	is_new_data_output: bpy.props.BoolProperty(
		name='Is new data',
		description='',
		default=True)
	is_input_selection_considered: bpy.props.BoolProperty(
		name='Selection is considered',
		description='',
		default=False)

	def draw(self, context, layout, node, text):
		# icon = None
		# icon_value = None
		# if self.internal_icon:
		# 	icon = self.internal_icon
		# elif self.external_icon_name:
		# 	color_scheme = 'LIGHT' if utils.get_addon_preferences().adapt_colors_to == 'DARK' else 'DARK'
		# 	icon_name = self.external_icon_name
		# 	if self.is_output and self.is_new_data_output:
		# 		icon_name += ' new'
		# 	try:
		# 		icon_value = utils.get_icon_value(icon_name + ' ' + color_scheme)
		# 	except KeyError:
		# 		try:
		# 			icon_value = utils.get_icon_value(icon_name)
		# 		except KeyError:
		# 			icon_value = utils.get_icon_value(self.external_icon_name)
		final_text = self.name

		if self.is_output:
			if self.is_new_data_output:
				final_text = text + ' (new)'
			else:
				final_text = text + ' (existing)'
		# layout.label(text=text + ' (Modified)', icon=icon)
		else:
			is_linked = self.is_linked
			if self.is_required and not is_linked:
				# layout.label(text=text + ' (REQUIRED!!)', icon='ERROR')
				final_text += ' REQUIRED!!'
			# icon = 'ERROR'
			# elif is_linked and (not isinstance(self.links[0].from_node, self.compatible_types)):
			# 	# elif is_linked and not isinstance(self.links[0].from_node, self.compatible_types):
			# 	# layout.label(text=text+' (WRONG INPUT TYPE!!)', icon='ERROR')
			# 	self.node.id_data.links.remove(self.links[0])
			elif is_linked and (not isinstance(self.links[0].from_socket, (type(self),) + self.compatible_output_socket_classes)):
				self.node.id_data.links.remove(self.links[0])
			else:
				if self.is_input_selection_considered:
					# layout.label(text=text + ' (selection)', icon=icon)
					final_text = text + ' (selection)'
				else:
					# layout.label(text=text + ' (whole)')
					final_text = text

		# if icon:
		# 	layout.label(text=final_text, icon=icon)
		# elif icon_value:
		# 	layout.label(text=final_text, icon_value=icon_value)
		# else:
		# 	layout.label(text=final_text)
		layout.label(text=final_text)

	def draw_color(self, context, node):
		alpha = 1
		# if self.is_output:
		# 	if not self.is_new_data_output:
		# 		alpha = .5
		if not self.is_output and not self.is_required:
			alpha = .5
		return (self.color[0], self.color[1], self.color[2], alpha)

	def get_data(self, *args, **kwargs):
		"""Uniquement à appeler sur des input sockets"""
		connected_onput_node: DATA_GETTER_NODE = self.links[0].from_node
		return connected_onput_node.get_data()


class SOCKET_Grid(bpy.types.NodeSocket, SOCKET):
	# external_icon_name = 'socket geoms2d'
	bl_idname = 'Socket_fi2g2uqn96joml0ipr7g'
	color = (1, 1, 1, 1)
	default_label = 'Grid'
	compatible_output_socket_classes = ()
	display_shape = 'DIAMOND'


class SOCKET_Any(NodeSocket, SOCKET):
	"""Ne doit pas être utilisé comme output, seulement comme input socket"""
	bl_idname = 'Socket_9vrufzoxj2igkv92y3u1'
	compatible_output_socket_classes = (SOCKET,)
	color = (0.5, 0.5, 0.5)
	default_label = 'Any data'
	internal_icon = None
	external_icon_name = None
	display_shape = 'SQUARE_DOT'


class Sockets_bldata(SOCKET):
	# external_icon_name = 'blender_icon'

	# def get_data(self, *args, **kwargs):
	# 	return self.get_input_sc_bldata()

	def get_input_sc_bldata(self) -> List[DATA_bldata]:
		"""
		Should be called on input sockets only.
		Default implementation. Pulls and returns bl_data from connected output_socket.
		Override if sublcass socket needs to do more specific stuff, like converting different input data types, or work on asked data somehow.

		Ne pas s'occuper des exceptions ici, elles seront traitées par les nodes eux-mêmes
		"""
		connected_onput_node: DATA_GETTER_NODE_SC_bldata = self.links[0].from_node
		return connected_onput_node.get_sc_bldata()


class SOCKET_bldata(NodeSocket, Sockets_bldata):
	bl_idname = 'Socket_3ju5j31eihq2ie05mbkn'
	# compatible_types = (SC_bldata_Getter_Node,)
	compatible_output_socket_classes = (Sockets_bldata,)
	# compatible_output_socket_classes = ()
	color = (0.5, 0.5, 0.5, 1)
	default_label = 'Blender data (one or many) for objects'
	display_shape = 'CIRCLE_DOT'


class SOCKET_Light_Scene(bpy.types.NodeSocket, SOCKET):
	# external_icon_name = 'socket geoms2d'
	bl_idname = 'Socket_kjkdzk3ns6gjwyea3zjp'
	color = (0, 0, 1, 1)
	default_label = 'Light objects'
	compatible_output_socket_classes = ()
	display_shape = 'DIAMOND'


class SOCKET_Light_Scene_Optional(NodeSocket, SOCKET):
	# external_icon_name = 'socket geoms2d'
	bl_idname = 'Socket_kp5c20oa13wnqtkz6wnc'
	color = (0, 0, 1, 1)
	default_label = 'Light objects'
	compatible_output_socket_classes = ()
	display_shape = 'DIAMOND'

	def draw(self, context, layout, node, text):
		layout.label(text='Light objects')
		op = layout.operator(DeleteInputSocketOperator.bl_idname)
		op.source_node_path = 'bpy.data.node_groups["' + self.node.id_data.name + '"].' + self.node.path_from_id()
		op.input_socket_to_delete = self.name


class SOCKET_Geoms2D(bpy.types.NodeSocket, SOCKET):
	bl_idname = 'Socket_e8r5mVD3p39mHQVCsW8e'
	color = (0, 1, 1, 1)
	default_label = '2D Geometries sets'
	compatible_output_socket_classes = ()
	display_shape = 'DIAMOND'
	external_icon_name = 'socket geoms2d'

	def get_input_2d_geometries(self, *args, **kwargs) -> List[DATA_Geometries]:
		"""
		Traiter ici les converstions des types de données qui peuvent être connectées à ce type de socket
		Ne pas s'occuper des exceptions ici, elles seront traitées par les nodes eux-mêmes
		"""
		source_node: DATA_GETTER_NODE_Geometries = self.links[0].from_node
		# source_socket = self.links[0].from_socket
		# source_node: Geometries_Getter_Node = source_node
		return source_node.get_geometries(*args, **kwargs)


class SOCKET_Road_Portion(bpy.types.NodeSocket, SOCKET):
	# external_icon_name = 'socket geoms2d'
	bl_idname = 'Socket_urkyz6s3aesnixslif5q'
	color = (1, 0, 1, 1)
	default_label = 'Road portions'
	compatible_output_socket_classes = ()
	display_shape = 'DIAMOND'


class SOCKET_Road_Portion_Weighted(NodeSocket, SOCKET):
	bl_idname = 'Socket_zna9u9rkb1eg1j9y3cn4'
	color = (1, 0, 1, 1)
	default_label = 'Light objects'
	compatible_output_socket_classes = ()
	display_shape = 'DIAMOND'

	weight: bpy.props.FloatProperty(
		name='Weight',
		description='Relative proba of this (or these) road portions to be chosen if there are others of the same type and size',
		default=1)

	def draw(self, context, layout, node, text):
		layout.prop(self, 'weight')
		op = layout.operator(DeleteInputSocketOperator.bl_idname)
		op.source_node_path = 'bpy.data.node_groups["' + self.node.id_data.name + '"].' + self.node.path_from_id()
		op.input_socket_to_delete = self.name

	def draw_color(self, context, node):
		return (1, 0, 1, 1)


class SOCKET_Image(NodeSocket, SOCKET):
	# external_icon_name = 'socket geoms2d'
	bl_idname = 'Socket_alnbbyhifnci4vr3wld4'
	color = (1, 1, 0, 1)
	default_label = 'Images'
	compatible_output_socket_classes = ()
	display_shape = 'SQUARE'

	def get_input_images(self, *args, **kwargs) -> List[DATA_Image]:
		"""
		Traiter ici les converstions des types de données qui peuvent être connectées à ce type de socket
		Ne pas s'occuper des exceptions ici, elles seront traitées par les nodes eux-mêmes
		"""
		source_node: DATA_GETTER_NODE_Image = self.links[0].from_node
		return source_node.get_images(*args, **kwargs)


class SOCKET_TextureSet(NodeSocket, SOCKET):
	# external_icon_name = 'socket geoms2d'
	bl_idname = 'Socket_jgaz7qqvfrxx4rh664or'
	color = (1, 1, 0.25, 1)
	default_label = 'Texture set'
	compatible_output_socket_classes = ()
	display_shape = 'DIAMOND'


class SOCKET_Terrain(NodeSocket, SOCKET):
	# external_icon_name = 'socket geoms2d'
	bl_idname = 'Socket_ehe0ubi85txkju7zeuxk'
	color = (0, 1, 0, 1)
	default_label = 'Terrain'
	compatible_output_socket_classes = ()
	display_shape = 'DIAMOND'


class SOCKET_Material(NodeSocket, SOCKET):
	# external_icon_name = 'socket geoms2d'
	bl_idname = 'Socket_6z7sh71tamnvpyucwroc'
	color = (1, 0, 1, 1)
	default_label = 'Materials'
	compatible_output_socket_classes = ()
	display_shape = 'SQUARE'

	def get_input_materials(self, *args, **kwargs) -> List[DATA_Material]:
		"""
		Traiter ici les converstions des types de données qui peuvent être connectées à ce type de socket
		Ne pas s'occuper des exceptions ici, elles seront traitées par les nodes eux-mêmes
		"""
		source_node: DATA_GETTER_NODE_Material = self.links[0].from_node
		return source_node.get_materials(*args, **kwargs)


class SOCKET_Map(NodeSocket, SOCKET):
	# external_icon_name = 'socket geoms2d'
	bl_idname = 'Socket_61sznxqgqk7y54gnai0i'
	color = (1, .5, 0, 1)
	default_label = 'Map'
	compatible_output_socket_classes = ()
	display_shape = 'DIAMOND'

	def get_input_map(self, *args, **kwargs) -> DATA_Map:
		"""
		Traiter ici les converstions des types de données qui peuvent être connectées à ce type de socket
		Ne pas s'occuper des exceptions ici, elles seront traitées par les nodes eux-mêmes
		"""
		try:
			source_node: DATA_GETTER_NODE_Map = self.links[0].from_node
			return source_node.get_map(*args, **kwargs)
		except IndexError:
			# Le input socket n'est pas connecté
			return None


class SOCKET_Map_WithDefaultValuePercent(NodeSocket, SOCKET):
	# external_icon_name = 'socket geoms2d'
	bl_idname = 'Socket_dobhq1vzpsekyvhe4y45'
	color = (1, .5, 0, 1)
	default_label = 'Map'
	compatible_output_socket_classes = ()
	display_shape = 'DIAMOND'

	default_constant_value: bpy.props.FloatProperty(
		name='Default',
		description='Constant value to use if no map specified',
		min=0, max=100, default=50, subtype='PERCENTAGE')

	def draw(self, context, layout, node, text):
		layout.label(text=text)
		if len(self.links) <= 0:
			layout.prop(self, 'default_constant_value')


class SOCKET_Map_WithDefaultValue(NodeSocket, SOCKET):
	# external_icon_name = 'socket geoms2d'
	bl_idname = 'Socket_sfy8lsym4m4t1dnqb8qe'
	color = (1, .5, 0, 1)
	default_label = 'Map'
	compatible_output_socket_classes = ()
	display_shape = 'DIAMOND'

	default_constant_value: bpy.props.FloatProperty(
		name='Default',
		description='Constant value to use if no map specified',
		default=0)

	def draw(self, context, layout, node, text):
		layout.label(text=text)
		if len(self.links) <= 0:
			layout.prop(self, 'default_constant_value')


class SOCKET_Terrain_Shape(NodeSocket, SOCKET):
	# external_icon_name = 'socket geoms2d'
	bl_idname = 'Socket_2xxndex2ab28ijwe0gen'
	color = (.5, .4, 0.3, 1)
	default_label = 'Terrain shapes'
	compatible_output_socket_classes = ()
	display_shape = 'DIAMOND'

	def get_terrain_shapes(self, *args, **kwargs) -> List[DATA_Terrain_Shape]:
		"""
		Traiter ici les converstions des types de données qui peuvent être connectées à ce type de socket
		Ne pas s'occuper des exceptions ici, elles seront traitées par les nodes eux-mêmes
		"""
		source_node: DATA_GETTER_NODE_TerrainShapes = self.links[0].from_node
		return source_node.get_terrain_shapes(*args, **kwargs)


class SOCKET_MapWithOptionToAdaptToTerrain(NodeSocket, SOCKET):
	# external_icon_name = 'socket geoms2d'
	bl_idname = 'Socket_6auwwn5xkiigp09arxeu'
	color = (1, .5, 0, 1)
	default_label = 'Terrain map'
	compatible_output_socket_classes = ()
	display_shape = 'DIAMOND'

	should_fit_to_terrain: bpy.props.BoolProperty(
		name='Fit to terrain',
		description='If enabled, the map will be evaluated relative to the terrain surface, and not to the specified layout surface',
		default=False)

	should_display_option: bpy.props.BoolProperty(
		name='',
		description='',
		default=False)

	def draw(self, context, layout, node, text):
		layout.label(text=text)
		row = layout.row()
		row.prop(self, 'should_fit_to_terrain')
		row.enabled = self.should_display_option


class SOCKET_Path(bpy.types.NodeSocket, SOCKET):
	# external_icon_name = 'socket geoms2d'
	bl_idname = 'Socket_eenxfx3pfjwjwg0mtz18'
	color = (0, .5, 0, 1)
	default_label = 'Paths'
	compatible_output_socket_classes = ()
	display_shape = 'DIAMOND'


class SOCKET_old_SceneObjects(NodeSocket, SOCKET):
	# external_icon_name = 'socket geoms2d'
	bl_idname = 'Socket_o9w2pvdweoc1x9364428'
	color = (0, 0, 0, 1)
	default_label = 'Objects'
	compatible_output_socket_classes = ()
	display_shape = 'DIAMOND'


# def __del__(self):
# 	if self.tmp_obj:
# 		bpy.context.scene.collection.objects.unlink(self.tmp_obj)
# 		bpy.data.objects.remove(self.tmp_obj)
# 	if self.bl_data.users <= 0:
# 		self.bl_data_collection.remove(self.bl_data)


# print('SC_NodeTree updated')

class SimpleNode:
	def create_input(self, socketClass: type, is_required=False, label=None):
		socket = self.inputs.new(socketClass.bl_idname, socketClass.default_label if not label else label)
		socket.is_required = is_required
		socket.display_shape = socketClass.display_shape
		return socket

	def create_output(self, socketClass: type, label=None, is_new_data_output=True):
		socket = self.outputs.new(socketClass.bl_idname, socketClass.default_label if not label else label)
		socket.display_shape = socketClass.display_shape
		socket.is_new_data_output = is_new_data_output
		return socket

	def create_operator(self, layout, operator_class: Type, icon_value=None, text=None):
		if icon_value:
			op = layout.operator(operator_class.bl_idname, icon_value=icon_value, text=text)
		else:
			op = layout.operator(operator_class.bl_idname, text=text)
		op.source_node_path = 'bpy.data.node_groups["' + self.id_data.name + '"].' + self.path_from_id()
		# op.node_method_name_to_call = node_method_to_call
		return op


# Voir SC_Operator pour la raison pour laquelle impossible de faire dériver cette classe de bpy.types.Node
class Node(SimpleNode):
	"""
		Attributs de classe à ajouter dans sous-classes si besoin, avec leur valeur par défaut donnée ici, si non ajoutés
			at_least_one_input_socket_required = True
	"""
	last_operation_time: bpy.props.FloatProperty(default=0)
	last_operation_time_total: bpy.props.FloatProperty(default=0)
	should_output_console_message: bpy.props.BoolProperty(default=True, name='Write progress to console')
	inputs_state: bpy.props.StringProperty(default='INPUTS_STATE.CORRECT')
	work_state: bpy.props.StringProperty()
	still_working: bpy.props.BoolProperty(default=False)
	should_display_options_on_node_too: bpy.props.BoolProperty(name='Display options on node too', default=True)
	icon_value = None
	icon_scale = 1

	def print(self, message):
		if self.should_output_console_message:
			print('\t' + message)

	# def print_time(self, 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 and self.should_output_console_message:
	# 		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
	# 		if self.should_output_console_message:
	# 			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

	@classmethod
	def poll(cls, ntree):
		return ntree.bl_idname == SC_NodeTree.bl_idname

	def sc_init(self, context):
		"""Override in subclasses. Take care of size and sockets, and maybe node subclass specific stuff"""

	def sc_free(self):
		"""Override in subclasses"""

	def free(self):
		self.sc_free()

	def get_result_hash(self, output_socket_nb):
		"""
		Override in subclasses
		Returns a hash value of this node's result, per socket nb. Don't take previous nodes into account.
		Useful to make a sort of result cache for each node, inside a tree execution, AND between tree executions
		"""

	def _get_result_hash(self, output_socket_nb):
		"""

		"""

	def init(self, context):
		"""DO NOT override"""
		self.width = 200
		self.sc_init(context)
		self.prop_updated(context)

	def prop_updated(self, context=None):
		self.update()
		self.id_data.update()

	def getNameURL(self):
		name = self.__class__.__name__.replace('Node', '')
		newName = ''
		for i, c in enumerate(name):
			if i > 0 and c.isupper() and name[i - 1].islower():
				newName += '-'
			newName += c.lower()
		return newName

	# def getNameNode(self):
	# 	name = self.__class__.__name__.replace('Node', '')
	# 	return name

	def _draw_ui(self, context, layout):
		self.sc_draw_buttons(context, layout)

	def draw_buttons(self, context, layout):
		# return
		"""DO NOT override"""
		# self.ui_display_doc_and_last_job_done2(layout)
		col = layout.column(align=True)
		col.label(text=self.get_last_job_done_time())
		if self.icon_value:
			# col.template_icon(icon_value=my_globals.icônes[self.icon_value].icon_id, scale=self.icon_scale)
			adapt_colors_to = utils.get_addon_preferences().adapt_colors_to
			try:
				col.template_icon(icon_value=utils.get_icon_value(self.icon_value + '_' + adapt_colors_to), scale=self.icon_scale)
			except KeyError:
				col.template_icon(icon_value=utils.get_icon_value(self.icon_value), scale=self.icon_scale)
		if self.should_display_options_on_node_too:
			self._draw_ui(context, layout)

	def draw_buttons_ext(self, context, layout):
		"""DO NOT override"""
		layout.prop(self, 'should_display_options_on_node_too')
		layout.prop(self, 'should_output_console_message')
		self.ui_display_doc_and_last_job_done2(layout)
		self._draw_ui(context, layout)

	def _are_all_inputs_correct(self):
		required_sockets_all_linked = True
		for input in self.inputs:
			if input.is_required and not input.is_linked:
				required_sockets_all_linked = False
				break
		try:
			enoug_input_sockets = not self.at_least_one_input_socket_required or len(self.inputs) >= 1
		except Exception as e:
			enoug_input_sockets = len(self.inputs) >= 1
		return enoug_input_sockets and required_sockets_all_linked and self.are_all_inputs_correct()

	def are_all_inputs_correct(self):
		"""Override this in sub classes. Ignore sockets marked as required, they're handled already"""
		return True

	def sc_update(self):
		pass

	def update(self):
		self.sc_update()
		if self._are_all_inputs_correct():
			self.inputs_state = str(self.INPUTS_STATE.CORRECT)
		else:
			self.inputs_state = str(self.INPUTS_STATE.INCORRECT)
		self.update_visual_state()

	# APPELER update de tous les sockets here
	# Vérifier tous les inputs (input sockets et props qui peuvent être incorrects) en appelant une méthode de la sous classe qui renvoie vraou faux
	# print('Node updated: ', self.name)

	def sc_draw_buttons(self, context, layout):
		"""Override this method in sub-classes"""
		pass

	def are_all_inputs_ready(self) -> bool:
		"""Override this method in sub-classes"""
		return False

	class WORK_STATE2(Enum):
		READY = 'READY'
		WAITING_FOR_INPUTS = 'WAITING_FOR_INPUTS'
		FAILED = 'FAILED'
		SUCCESS = 'SUCCESS'

	class INPUTS_STATE(Enum):
		CORRECT = 'CORRECT'
		INCORRECT = 'INCORRECT'

	def update_visual_state(self):
		adapt_colors_to = utils.get_addon_preferences().adapt_colors_to
		# if not self._are_all_inputs_correct():
		# 	if adapt_colors_to == 'DARK':
		# 		self.color = (.7, .0, .0)
		# 	else:
		# 		self.color = (1, .3, .3)
		# 	self.use_custom_color = True

		if str(self.WORK_STATE2.READY) == self.work_state:
			if str(self.INPUTS_STATE.CORRECT) == self.inputs_state:
				self.use_custom_color = False
			else:
				if adapt_colors_to == 'DARK':
					self.color = (.7, .3, .3)
				else:
					self.color = (1, .5, .5)
				self.use_custom_color = True
		elif str(self.WORK_STATE2.SUCCESS) == self.work_state:
			if adapt_colors_to == 'DARK':
				self.color = (.3, .6, .3)
			else:
				self.color = (.5, 1, .5)
			self.use_custom_color = True
		elif str(self.WORK_STATE2.FAILED) == self.work_state:
			if adapt_colors_to == 'DARK':
				self.color = (.7, .0, .0)
			else:
				self.color = (1, .3, .3)
			self.use_custom_color = True
		else:
			if adapt_colors_to == 'DARK':
				self.color = (.6, .6, .0)
			else:
				self.color = (1, 1, .3)
			self.use_custom_color = True

	def set_work_state(self, state: WORK_STATE2):
		self.work_state = str(state)

	def set_input_state(self, state: INPUTS_STATE):
		self.inputs_state = str(state)

	# def show_node_work_state2(self, state: WORK_STATE2):
	# 	if state is self.WORK_STATE2.SUCCESS:
	# 		self.color = (.5, 1, .5)
	# 		self.use_custom_color = True
	# 	elif state is self.WORK_STATE2.FAILED:
	# 		self.color = (1, .5, .5)
	# 		self.use_custom_color = True
	# 	elif state is self.WORK_STATE2.WAITING_FOR_INPUTS:
	# 		self.color = (1, 1, .5)
	# 		self.use_custom_color = True
	# 	elif state is self.WORK_STATE2.READY:
	# 		self.use_custom_color = False

	# class WORK_STATE(Enum):
	# 	INPUTS_READY = auto()
	# 	INPUTS_ERROR = auto()
	# 	LAUNCHED_BUT_WAITING = auto()
	# 	SUCCESS = auto()
	# 	ERROR = auto()
	# 	NEUTRAL = auto()

	# def show_node_work_state(self, state):
	# 	if state is self.WORK_STATE.INPUTS_READY:
	# 		self.color = (.5, 1, .5)
	# 		self.use_custom_color = False
	# 	elif state is self.WORK_STATE.INPUTS_ERROR:
	# 		self.color = (1, .5, .5)
	# 		self.use_custom_color = True
	# 	elif state is self.WORK_STATE.SUCCESS:
	# 		self.color = (.5, 1, .5)
	# 		self.use_custom_color = True
	# 	elif state is self.WORK_STATE.ERROR:
	# 		self.color = (1, .5, .5)
	# 		self.use_custom_color = True
	# 	elif state is self.WORK_STATE.LAUNCHED_BUT_WAITING:
	# 		self.color = (1, 1, .5)
	# 		self.use_custom_color = True
	# 	elif state is self.WORK_STATE.NEUTRAL:
	# 		self.use_custom_color = False

	# def create_operator(self, layout, operator_class: Type, node_method_to_call: str = ''):

	# def ui_display_doc(self, layout, doc_page):
	# 	layout.operator("wm.url_open", text="Node doc", icon='INFO').url = node_doc_base_url + doc_page + ('.html' if not doc_page.endswith('.html') else '')

	# def ui_display_doc2(self, layout):
	# 	layout.operator("wm.url_open", text="Node doc", icon='INFO').url = node_doc_base_url + self.getNameURL()

	def ui_display_doc_and_last_job_done2(self, layout):
		split = layout.split(factor=.33)
		split.operator("wm.url_open", text="Node doc", icon='INFO').url = node_doc_base_url + self.getNameURL()
		split.label(text=self.get_last_job_done_time())

	# def ui_display_last_job_done(self, layout):
	# 	split = layout.split(factor=.33)
	# 	split.operator("wm.url_open", text="Node doc", icon='INFO').url = node_doc_base_url + self.getNameURL()
	# 	split.label(text=self.get_last_job_done_time())

	# def ui_display_doc_and_last_job_done(self, layout, doc_page):
	# 	split = layout.split(factor=.33)
	# 	split.operator("wm.url_open", text="Node doc", icon='INFO').url = node_doc_base_url + doc_page + ('.html' if not doc_page.endswith('.html') else '')
	# 	split.label(text=self.get_last_job_done_time())

	def afficher_message(self, message: str):
		pass

	def afficher_barre_progression(self, pourcent, job_elapsed_time_seconds, prefix='', no_message=False):
		total_barres = 30
		# premier essai pour connaître la longueur de la ligne sans la barre de progression
		if no_message:
			message = prefix
		else:
			message = prefix + (self.label if self.label else self.name) + ': working'
		résultat = message + ' ['
		résultat += '] %3d%% - %6.2fs  \r' % (round(pourcent * 100), round(job_elapsed_time_seconds, 2))

		longueur_ligne_sans_barre = len(résultat)
		total_barres = min(
			shutil.get_terminal_size().columns - longueur_ligne_sans_barre,
			total_barres)

		résultat = message + ' ['
		total_barres_affichées = round(pourcent * total_barres)
		for nbCar in range(total_barres_affichées):
			résultat += '|'
		for nbCar in range(total_barres - total_barres_affichées):
			résultat += '_'
		résultat += '] %3d%% - %6.2fs  \r' % (round(pourcent * 100), round(job_elapsed_time_seconds, 2))

		if pourcent < 1:
			print(résultat, end='', flush=True)
		else:
			print(résultat)

	def get_last_job_done_time(self):
		seconds_this_node = self.last_operation_time
		minutes_this_node = int(seconds_this_node // 60)
		seconds_this_node -= minutes_this_node * 60
		seconds_this_node = round(seconds_this_node, 3)
		if minutes_this_node == 0:
			minutes_this_node = ''
		else:
			minutes_this_node = '%01dm ' % (minutes_this_node)
		if seconds_this_node == 0:
			seconds_this_node = '0s'
		else:
			seconds_this_node = '%05.3fs ' % (seconds_this_node)

		seconds_total = self.last_operation_time_total
		minutes_total = int(seconds_total // 60)
		seconds_total -= minutes_total * 60
		seconds_total = round(seconds_total, 3)
		if minutes_total == 0:
			minutes_total = ''
		else:
			minutes_total = '%01dm ' % (minutes_total)
		if seconds_total == 0:
			seconds_total = '0s'
		else:
			seconds_total = '%05.3fs ' % (seconds_total)

		return f'Work done in: {minutes_this_node}{seconds_this_node} | Time up to this node: {minutes_total}{seconds_total}'

	def get_data_first(the_func: Callable):
		"""
		Décorator pour décorer les méthode d'instance des sous-classes qui extraient de la donnée du node directement, et pas à partir d'un operator attaché
		au node, comme getGrid par exemple
		"""

		@wraps(the_func)
		def wrapper(*args, **kwargs):
			startTime = time.process_time()
			# args et kwargs sont les arguments de la méthode décorée, Python le fait tout seul
			# On peut accéder et agir sur l'objet de la méthode décorée, comme changer sa couleur par exemple
			self: Node = args[0]
			self.still_working = True
			self.set_work_state(Node.WORK_STATE2.WAITING_FOR_INPUTS)
			# print(f'Le décorateur a accès à self, donc au node, la preuve: self.couleur={self.couleur} | self.prop={self.prop}')

			# Appel à _necessary_data(), avec ses paramètres si besoin, plus son accès à self
			try:
				data = getattr(self, f'_{the_func.__name__}_necessary_data')()
			# if not data:
			# 	data = []
			# elif not isinstance(data, Callable):
			# 	data = [data]
			except:
				self.still_working = False
				self.last_operation_time = 0
				self.last_operation_time_total = time.process_time() - startTime
				raise
			finally:
				# if failed not because of previous inputs operation, but because of wrong inputs (like unlinked socket), then this node is the error source
				if not self._are_all_inputs_correct():
					self.set_work_state(Node.WORK_STATE2.FAILED)
				self.update_visual_state()

			# Appel à l'action en elle-même,
			this_node_work_start_time = time.process_time()
			try:
				# Appel à getGrid(), où on lui passe les données nécessaires, ainsi que tout ses paramètres d'origine, plus self
				if self.should_output_console_message:
					print(f'{datetime.now().time()} --- NODE --- BEGINS --- {self.name} ----------------------------------------')
				# donnée_finale = the_func(*args, *data, **kwargs)
				# if not data:
				# 	donnée_finale = the_func(*args, **kwargs)
				# else:
				donnée_finale = the_func(*args, data, **kwargs)
				# donnée_finale = the_func(data, *args, **kwargs)
				# if data == None:
				# 	donnée_finale = the_func(*data, *args, **kwargs)
				# elif data
				self.set_work_state(Node.WORK_STATE2.SUCCESS)
				return donnée_finale
			except:
				self.set_work_state(Node.WORK_STATE2.FAILED)
				raise
			finally:
				self.still_working = False
				self.update_visual_state()
				self.last_operation_time = time.process_time() - this_node_work_start_time
				self.last_operation_time_total = time.process_time() - startTime
				if self.should_output_console_message:
					print(f'{datetime.now().time()} --- NODE --- ENDS ----- {self.name} ------------------------------------------')

		return wrapper


#
# def get_data(self, *args, **kwargs):
# 	pass


class DATA_GETTER_NODE:
	def get_data(self, *args, **kwargs):
		pass


class DATA_GETTER_NODE_Terrain(DATA_GETTER_NODE):
	def get_terrains(self, *args, **kwargs) -> DATA_Terrain:
		pass

	def get_data(self, *args, **kwargs):
		self.get_terrains()


class DATA_GETTER_NODE_SC_bldata(DATA_GETTER_NODE):
	def get_data(self, *args, **kwargs):
		return self.get_sc_bldata(*args, **kwargs)

	def _get_sc_bldata_necessary_data(self, *args, **kwargs):
		"""Default implementation: return sc_bldata from first input socket"""
		return self.inputs[0].get_input_sc_bldata()

	def get_sc_bldata(self, *args, **kwargs) -> List[DATA_bldata]:
		"""Must override in subclass"""
		pass


# Les sous classes doivent hériter aussi de bpy.types.Operator, impossible de faire dériver cette classe de bpy.types.Operator,
# sinon Blender ne registera pas les properties déclarés ici.
# Impossible aussi de déclarer des attributs de classe custom, ici comme dans sous-classes, car c'est Blender qui instancie les operators, et quand il le fait,
# il ignore tout ce qu'ils ne reconnait pas. Et les objets qu'il créé ne peuvent pas non plus recevoir de nouveaux attributs après leur création
class SC_Operator:
	source_node_path: bpy.props.StringProperty(options={'HIDDEN'})
	should_clean_datablocks_without_users: bpy.props.BoolProperty(default=False)

	def execute(self, context):
		bl_state = utils.get_bl_objects_and_mode_state()
		source_node: Node = eval(self.source_node_path)
		print(f'{datetime.now().time()} --- ############################# SceneCity starts: "{source_node.id_data.name}"')
		source_node.id_data.set_all_nodes_ready()
		source_node.id_data.still_working = True
		try:
			# make sure at least one object is active in the current view layer first
			if not bpy.context.view_layer.objects.active:
				bpy.context.view_layer.objects.active = bpy.context.view_layer.objects[0]
			self.SC_execute(source_node)
			# if self.node_method_name_to_call:
			if hasattr(self, 'node_method_name_to_call') and self.node_method_name_to_call:
				source_node.__getattribute__(self.node_method_name_to_call)()
		except:
			print(f'{datetime.now().time()} --- ############################# SceneCity executed "{source_node.id_data.name}", but errors occured')
			raise
		finally:
			# clean garbage temporary data

			# Blender BUG: enter edit mode on a dummy object, cleans Blender of some internal data it seems
			tmp_mesh = bpy.data.meshes.new('todel')
			tmp_obj = bpy.data.objects.new('todel', tmp_mesh)
			bpy.context.scene.collection.objects.link(tmp_obj)
			utils.selectionner_un_seul_obj(tmp_obj)
			utils.definir_blender_mode('EDIT')
			utils.definir_blender_mode('OBJECT')
			bpy.context.scene.collection.objects.unlink(tmp_obj)

			try:
				bpy.data.collections.remove(bpy.data.collections[tmp_collection_name])
			except KeyError:
				pass

			if self.should_clean_datablocks_without_users:
				objects = list(bpy.data.objects)
				for obj in objects:
					if obj.users <= 0:
						bpy.data.objects.remove(obj)
				meshes = list(bpy.data.meshes)
				for mesh in meshes:
					if mesh.users <= 0:
						bpy.data.meshes.remove(mesh)
				curves = list(bpy.data.curves)
				for curve in curves:
					if curve.users <= 0:
						bpy.data.curves.remove(curve)
				materials = list(bpy.data.materials)
				for mat in materials:
					if mat.users <= 0:
						bpy.data.materials.remove(mat)

			source_node.id_data.still_working = False
			utils.set_bl_state(bl_state)

		print(f'{datetime.now().time()} --- ############################# SceneCity finished "{source_node.id_data.name}" successfully')
		return {'FINISHED'}

	def SC_execute(self, source_node: Node, *args):
		"""
		Will excute before calling the actual logic code, in the node's function.
		Override just if needed. Must include very operator-specific code (eg opening and managing a file browser).
		Not logic code that belongs to the node itself.
		"""


class SC_Node_WithFileBrowser:
	"""Sub-classes supposed to extend Node too"""
	filepath: bpy.props.StringProperty(name='File path', update=Node.prop_updated)

	def draw_create_file_browser_op(self, layout, file_save_extension='.txt', file_browser_visible_extensions='*', label=None):
		row = layout.row(align=True)
		# row.label(text=label)
		row.prop(self, 'filepath')
		op = row.operator(self.SC_OT_FileBrowserOp.bl_idname, icon='FILEBROWSER', text='')
		op.source_node_path = 'bpy.data.node_groups["' + self.id_data.name + '"].' + self.path_from_id()
		# print(op.filename_ext)
		# op.filename_ext = '*.png'
		# op.filter_glob = '*.png'
		op.filter_glob = file_browser_visible_extensions
		# op.filename_ext = '.png'
		# op.filename_ext = random.choice(['.html', '.png', '.blend'])
		op.filename_ext = file_save_extension
		op.filepath = self.filepath

	# print(op.filter_glob)

	class SC_OT_FileBrowserOp(bpy.types.Operator, ExportHelper, SC_Operator):
		bl_idname = 'sc_op.kppaqqsxrgrkvpuqhvvg'
		bl_description = 'Open file browser'
		bl_label = 'Choose file'
		# filename_ext = '.html'
		filename_ext: bpy.props.StringProperty(
			default=".html",
		)
		filter_glob: bpy.props.StringProperty(
			default="*.html",
			options={'HIDDEN'},
			maxlen=255,  # Max internal buffer length, longer would be clamped.
		)

		def SC_execute(self, source_node):
			source_node.filepath = self.filepath


class SC_Node_variable_inputs(Node):
	class SC_OT_AddInput(bpy.types.Operator, SC_Operator):
		bl_idname = 'sc_op.lpoyxh4vzl9omwx3vqll'
		bl_description = ''
		bl_label = 'Add input'
		socket_idname: bpy.props.StringProperty()
		node_method_name_to_call = 'add_input'

	class SC_OT_RemoveInput(bpy.types.Operator, SC_Operator):
		bl_idname = 'sc_op.dp344djdlq4mxkylw5qr'
		bl_description = ''
		bl_label = 'Remove input'
		node_method_name_to_call = 'remove_input'

	new_inputs_socket_type = None  # changer dans la sous-classe
	new_inputs_are_required = False  # changer dans la sous-classe

	def add_input(self):
		self.create_input(self.new_inputs_socket_type, self.new_inputs_are_required)
		self.prop_updated()

	def remove_input(self):
		try:
			self.inputs.remove(self.inputs[-1])
		except:
			pass


# class SceneObject:
# 	pass
#
#
# class SceneObjectsGetter:
# 	def get_scene_objects(self, **kwargs) -> SceneObject:
# 		return None


# our own base class with an appropriate poll function, so the categories only show BlenderMeshToMeshDataNode_props_updated in our own tree type
class SC_NodeCategory(NodeCategory):
	# def __init__(self, identifier, name, description="", items=None):
	# 	super().__init__(identifier, name, description, items)

	@classmethod
	def poll(cls, context):
		return context.space_data.tree_type == SC_NodeTree.bl_idname


class DATA_GETTER_NODE_SC_Objects(DATA_GETTER_NODE_SC_bldata):
	def get_sc_objects(self, *args, **kwargs) -> List[DATA_Object]:
		"""Must override in subclass"""
		pass

	def _get_sc_objects_necessary_data(self, *args, **kwargs):
		"""Override in subclass if needed"""
		return super()._get_sc_bldata_necessary_data(*args, **kwargs)

	def _get_sc_bldata_necessary_data(self, *args, **kwargs):
		return self._get_sc_objects_necessary_data(*args, **kwargs)

	def get_sc_bldata(self, *args, **kwargs):
		return self.get_sc_objects(*args, **kwargs)


# def _get_sc_bldata_necessary_data(self, *args, **kwargs):
# 	return self._get_sc_objects_necessary_data(*args, **kwargs)
#
# def get_sc_bldata(self, *args, **kwargs):
# 	return self.get_sc_objects(*args, **kwargs)


class SC_Objects_Socket(NodeSocket, Sockets_bldata):
	bl_idname = 'Socket_zskm7a7gsty0bjke9vw6'
	compatible_output_socket_classes = ()
	color = (0, 0, 0, 1)
	default_label = 'Object(s)'
	display_shape = 'CIRCLE'
	internal_icon = 'OBJECT_DATA'

	def get_input_sc_objects(self, *args, **kwargs) -> List[DATA_Object]:
		"""
		Traiter ici les converstions des types de données qui peuvent être connectées à ce type de socket
		Ne pas s'occuper des exceptions ici, elles seront traitées par les nodes eux-mêmes
		"""
		source_node: DATA_GETTER_NODE_SC_Objects = self.links[0].from_node
		return source_node.get_sc_objects(*args, **kwargs)

	def get_input_sc_bldata(self, *args, **kwargs) -> List[DATA_bldata]:
		return self.get_input_sc_objects(*args, **kwargs)


class DATA_GETTER_NODE_SC_Meshes(DATA_GETTER_NODE_SC_bldata):
	"""Only nodes should inherit from this class"""

	def get_sc_meshes(self, *args, **kwargs) -> List[DATA_Mesh]:
		pass

	def _get_sc_meshes_necessary_data(self, *args, **kwargs):
		return super()._get_sc_bldata_necessary_data(*args, **kwargs)

	def _get_sc_bldata_necessary_data(self, *args, **kwargs):
		return self._get_sc_meshes_necessary_data(*args, **kwargs)

	def get_sc_bldata(self, *args, **kwargs):
		return self.get_sc_meshes(*args, **kwargs)

	def is_dynamic_mesh(self) -> bool:
		return False

	def is_mesh_param_supported(self, paramName: str) -> bool:
		return False


class SOCKET_Meshes(NodeSocket, Sockets_bldata):
	bl_idname = 'Socket_6ab9hwtopkx8cgnpeutg'
	# compatible_types = (SC_Meshes_Getter_Node,)
	# compatible_output_socket_classes = (SC_Objects_Socket,)
	compatible_output_socket_classes = (SC_Objects_Socket,)
	color = (.5, .5, 0.5, 1)
	default_label = 'Meshes'
	display_shape = 'CIRCLE'
	external_icon_name = 'socket meshes'

	# internal_icon = 'MESH_DATA'

	def get_input_sc_meshes(self, *args, **kwargs) -> List[DATA_Mesh]:
		"""
		Traiter ici les converstions des types de données qui peuvent être connectées à ce type de socket
		Ne pas s'occuper des exceptions ici, elles seront traitées par les nodes eux-mêmes
		"""
		source_node = self.links[0].from_node
		if isinstance(source_node, DATA_GETTER_NODE_SC_Meshes):
			return source_node.get_sc_meshes(*args, **kwargs)
		elif isinstance(source_node, DATA_GETTER_NODE_SC_Objects):
			sc_meshes: List[DATA_Mesh] = []
			sc_objects = source_node.get_sc_objects(*args, **kwargs)
			for sc_object in sc_objects:
				assert isinstance(sc_object.bl_data.data, bpy.types.Mesh)
				sc_meshes.append(DATA_Mesh(sc_object.bl_data.data))
			return sc_meshes

	def get_input_sc_bldata(self, *args, **kwargs) -> List[DATA_bldata]:
		return self.get_input_sc_meshes(*args, **kwargs)


class BlDataGetter:
	def _get_bl_data_necessary_data(self, *args, **kwargs):
		return

	def get_bl_data(self, **kwargs):
		return


# class BlMeshGetter(BlDataGetter):
# 	def _get_bl_mesh_necessary_data(self, *args, **kwargs):
# 		return
#
# 	def get_bl_mesh(self, **kwargs):
# 		return
#
#
# BlMeshGetter._get_sc_bldata_necessary_data = BlMeshGetter._get_bl_mesh_necessary_data
# BlMeshGetter.get_sc_bldata = BlMeshGetter.get_bl_mesh


# class BlMeshSocket(NodeSocket, Socket):
# 	compatible_types = (BlMeshGetter,)
# 	color = (.5, .5, 0.5, 1)


# display_shape = 'DIAMOND'

# def init(self):
# 	print('BlMeshSocket init')
# 	self.display_shape = 'DIAMOND'


class SceneObjectsGetter:
	def get_scene_objects(self, **kwargs):
		return []


# class AssetInstancesSocket(NodeSocket):
# 	def draw(self, context, layout, node, text):
# 		layout.label(text=text)
#
# 	def draw_color(self, context, node):
# 		return (0.5, 0.5, 1, 1)


# class CityDefSocket(NodeSocket):
# 	def draw(self, context, layout, node, text):
# 		layout.label(text=text)
#
# 	def draw_color(self, context, node):
# 		return (0.25, 1, 0.25, 1)


class MapGetter:
	def get_map(self, **kwargs) -> DATA_Map:
		return DATA_Map()


class Taggable:
	def __init__(self):
		self.tags = None  # dictionary
		self.tags_string = None

	def evaluate_tags_string(self):
		# for modules, and read-only variables unless the user explicitely uses the "global" keyword, won't finish in the object attr
		my_globals = {'random': globals()['random'], 'math': globals()['math'], }
		# specified locals param is for user-defined variables that I will get after exec. I can pre-define variables here that can be modified and will stay in the dict
		self.tags = {}
		exec(self.tags_string, my_globals, self.tags)


# class IntSocketWithDefaultValue(NodeSocket):
# 	default_value = bpy.props.IntProperty(
# 		name='Value',
# 		description='Default value if not plugged',
# 		default=0, )
#
# 	def draw(self, context, layout, node, text):
# 		if self.is_output or self.is_linked:
# 			layout.label(text=text)
# 		else:
# 			layout.prop(self, 'default_value', text=text)
#
# 	def draw_color(self, context, node):
# 		return (0.5, 0.5, 0.5, 1)


# class GridSurfaceSocket(NodeSocket):
# 	def draw(self, context, layout, node, text):
# 		layout.label(text=text)
#
# 	def draw_color(self, context, node):
# 		return (1.0, 0.4, 1, 1)


# class DatablockSocket(NodeSocket):
# 	def draw(self, context, layout, node, text):
# 		layout.label(text=text)
#
# 	# layout.label(text='Datablock')
#
# 	def draw_color(self, context, node):
# 		return (0.2, 0.2, 0.2, 1)


# class DeleteOptionalDatablockSocketOperator(bpy.types.Operator):
# 	bl_idname = 'node.delete_optional_datablock_socket'
# 	bl_description = 'Delete this datablock link'
# 	bl_label = 'X'
# 	source_node_path: bpy.props.StringProperty()
# 	input_socket_to_delete: bpy.props.StringProperty()
#
# 	def execute(self, context):
# 		source_node = eval(self.source_node_path)
# 		source_node.inputs.remove(source_node.inputs[self.input_socket_to_delete])
# 		return {'FINISHED'}


# class OptionalDatablockSocket(NodeSocket):
# 	def draw(self, context, layout, node, text):
# 		# layout.label(text=text)
# 		# layout.label(text='Datablock')
# 		split = layout.split(factor=.75)
# 		split.label(text='Datablock')
# 		# split.label(text='Datablock')
#
# 		op = split.operator('node.delete_optional_datablock_socket')
# 		op.source_node_path = 'bpy.data.node_groups["' + self.node.id_data.name + '"].' + self.node.path_from_id()
# 		op.input_socket_to_delete = self.name
#
# 	def draw_color(self, context, node):
# 		return (0.2, 0.2, 0.2, 1)


# class CityAssetSocket(NodeSocket):
# 	def draw(self, context, layout, node, text):
# 		layout.label(text=text)
#
# 	def draw_color(self, context, node):
# 		return (.5, 0.5, .5, 1)


# class DeleteOptionalCityAssetSocketOperator(bpy.types.Operator):
# 	bl_idname = 'node.delete_optional_city_asset_socket'
# 	bl_description = 'Delete this city asset link'
# 	bl_label = 'X'
# 	source_node_path: bpy.props.StringProperty()
# 	input_socket_to_delete: bpy.props.StringProperty()
#
# 	def execute(self, context):
# 		source_node = eval(self.source_node_path)
# 		source_node.inputs.remove(source_node.inputs[self.input_socket_to_delete])
# 		return {'FINISHED'}


# class DeleteOptionalCityAssetGroupSocketOperator(bpy.types.Operator):
# 	bl_idname = 'node.delete_optional_city_asset_group_socket'
# 	bl_description = 'Delete this city asset group link'
# 	bl_label = 'X'
# 	source_node_path: bpy.props.StringProperty()
# 	input_socket_to_delete: bpy.props.StringProperty()
#
# 	def execute(self, context):
# 		source_node = eval(self.source_node_path)
# 		source_node.inputs.remove(source_node.inputs[self.input_socket_to_delete])
# 		return {'FINISHED'}


# class OptionalCityAssetSocket(NodeSocket):
# 	def draw(self, context, layout, node, text):
# 		split = layout.split(factor=.75)
# 		split.label(text='City asset')
# 		op = split.operator('node.delete_optional_city_asset_socket')
# 		op.source_node_path = 'bpy.data.node_groups["' + self.node.id_data.name + '"].' + self.node.path_from_id()
# 		op.input_socket_to_delete = self.name
#
# 	def draw_color(self, context, node):
# 		return (0.5, 0.5, 0.5, 1)


# class OptionalWeightedCityAssetSocket(NodeSocket):
# 	weight: bpy.props.FloatProperty(
# 		name='Weight',
# 		description='Relative weight, compared to other assets of the same type, ie among buildings or among road portions of the same type etc...',
# 		default=1)
#
# 	def draw(self, context, layout, node, text):
# 		# split = layout.split(factor=.75)
# 		layout.label(text='City asset')
# 		layout.prop(self, 'weight')
# 		op = layout.operator('node.delete_optional_city_asset_socket')
# 		op.source_node_path = 'bpy.data.node_groups["' + self.node.id_data.name + '"].' + self.node.path_from_id()
# 		op.input_socket_to_delete = self.name
#
# 	def draw_color(self, context, node):
# 		return (0.5, 0.5, 0.5, 1)


# class CityAssetGroupSocket(NodeSocket):
# 	def draw(self, context, layout, node, text):
# 		layout.label(text=text)
#
# 	def draw_color(self, context, node):
# 		return (1, 1, 1, 1)


# class OptionalCityAssetGroupSocket(NodeSocket):
# 	def draw(self, context, layout, node, text):
# 		split = layout.split(factor=.75)
# 		split.label(text='City asset group')
# 		op = split.operator('node.delete_optional_city_asset_group_socket')
# 		op.source_node_path = 'bpy.data.node_groups["' + self.node.id_data.name + '"].' + self.node.path_from_id()
# 		op.input_socket_to_delete = self.name
#
# 	def draw_color(self, context, node):
# 		return (1, 1, 1, 1)


# class ImageSocket(NodeSocket):
# 	def draw(self, context, layout, node, text):
# 		layout.label(text=text)
#
# 	def draw_color(self, context, node):
# 		return (1, 1, 0, 1)


# class DeleteOptionalMapSocketOperator(bpy.types.Operator):
# 	bl_idname = 'node.delete_optional_map_socket'
# 	bl_description = 'Delete this map link'
# 	bl_label = 'X'
# 	source_node_path: bpy.props.StringProperty()
# 	input_socket_to_delete: bpy.props.StringProperty()
#
# 	def execute(self, context):
# 		source_node = eval(self.source_node_path)
# 		source_node.inputs.remove(source_node.inputs[self.input_socket_to_delete])
# 		return {'FINISHED'}


# class OptionalMapSocket(NodeSocket):
# 	map_name: bpy.props.StringProperty(
# 		name='Name',
# 		description='')
#
# 	def draw(self, context, layout, node, text):
# 		split = layout.split(factor=.15)
# 		split.label(text='Map')
# 		split2 = split.split(factor=.75)
# 		split2.prop(self, 'map_name')
# 		op = split2.operator('node.delete_optional_map_socket')
# 		op.source_node_path = 'bpy.data.node_groups["' + self.node.id_data.name + '"].' + self.node.path_from_id()
# 		op.input_socket_to_delete = self.name
#
# 	def draw_color(self, context, node):
# 		return (1, 1, 0.25, 1)


class SC_OT_RandomizeSeedNode(bpy.types.Operator):
	bl_idname = 'sc.randomize_node_seed_operator'
	bl_description = 'Choose a random seed'
	bl_label = 'Randomize seed'
	source_node_path: bpy.props.StringProperty()

	def execute(self, context):
		source_node = eval(self.source_node_path)
		random.seed()
		source_node.random_seed = random.randint(-9e5, 9e5)
		return {'FINISHED'}


class SC_OT_RandomizePositionNode(bpy.types.Operator):
	bl_idname = 'node.randomize_position_operator'
	bl_description = 'Set a different location randomly'
	bl_label = 'Randomize location'
	source_node_path: bpy.props.StringProperty()

	def execute(self, context):
		source_node = eval(self.source_node_path)
		random.seed()
		source_node.pos[0] = random.uniform(-9e2, 9e2)
		source_node.pos[1] = random.uniform(-9e2, 9e2)
		source_node.pos[2] = random.uniform(-9e2, 9e2)
		return {'FINISHED'}


# class CityAsset():
#
# 	def __init__(self):
# 		self.type = None # ROAD, BUILDING, SIDEWALK, ...
#
# 	def get_building(self):
# 		pass

# class CityAssetSocket(NodeSocket):
# 	def draw(self, context, layout, node, text):
# 		layout.label(text=text)
#
# 	def draw_color(self, context, node):
# 		return (.5, 0.5, .5, 1)


# class DirectionalPoints2dCollectionSocket(NodeSocket):
# 	def draw(self, context, layout, node, text):
# 		layout.label(text=text)
#
# 	def draw_color(self, context, node):
# 		return (.1, 0.1, .1, 1)


class DirectionalPoint2d():
	def __init__(self, pos2d, angleFromRightRadians):
		self.pos2d = pos2d
		self.angleFromRightRadians = angleFromRightRadians


class Rect2d():
	def __init__(self, minPoint, maxPoint):
		self.minPoint = minPoint
		self.maxPoint = maxPoint

	def setFromCenterAndSize(self, centerPos, size):
		self.minPoint = (centerPos[0] - size[0] / 2, centerPos[1] - size[1] / 2)
		self.maxPoint = (centerPos[0] + size[0] / 2, centerPos[1] + size[1] / 2)

	def getWidth(self):
		return abs(self.maxPoint[0] - self.minPoint[0])

	def getHeight(self):
		return abs(self.maxPoint[1] - self.minPoint[1])

	def __str__(self):
		return 'Rect2d({}, {})'.format(self.minPoint, self.maxPoint)


class DirectionalPoints2dCollection():
	def __init__(self, points, rect2d):
		self.points = points
		self.rect2d = rect2d


# class SC_OT_RandomizeNodeSeed(bpy.types.Operator):
# 	bl_idname = 'node.randomize_node_seed_operator'
# 	bl_description = 'Chooses a seed randomly'
# 	bl_label = 'Randomize'
# 	source_node_path: bpy.props.StringProperty()
#
# 	def execute(self, context):
# 		node_source = eval(self.source_node_path)
# 		try:
# 			random.seed()
# 			node_source.random_seed = random.randint(-99999, 99999)
# 		except:
# 			pass
# 		node_source.update()
# 		return {'FINISHED'}


# class AddAssetGroupSocketOperator(bpy.types.Operator):
# 	bl_idname = 'node.add_asset_group_socket'
# 	bl_label = 'Add asset'
# 	bl_description = ''
# 	source_node_path: bpy.props.StringProperty()
#
# 	def execute(self, context):
# 		source_node = eval(self.source_node_path)
# 		random_string = time.time()
# 		source_node.inputs.new('OptionalCityAssetSocket', str(random_string))
# 		return {'FINISHED'}


class Pixel:
	def __init__(self, r: float, g: float, b: float, a: float):
		self.r = r
		self.g = g
		self.b = b
		self.a = a


class Image:
	def __init__(self, resolution):
		"""Resolution est un tuple-2, et pixels un tableau 2d avec des Pixels dedans"""
		self.resolution: Tuple[int] = resolution
		self.pixels: List[List[Pixel]] = [[Pixel(0, 0, 0, 1) for j in range(self.resolution[1])] for i in range(self.resolution[0])]


# class ImageDataGetter:
# 	def get_image_data(self, **kwargs):
# 		return None


class TextureSetGetter:
	def get_texture_set(self, **kwargs) -> DATA_TextureSet:
		return None


def colorize_square(image, topLeft, bottomRight, color, noiseColDiff=0.0):
	"""returns true if at least a single pixel has been drawn, false otherwise. never touches border pixels. The top right coordinates are exclusive"""
	un_pixel_a_ete_modifie = False
	for x in range(topLeft[0], bottomRight[0]):
		if x < 1 or image.resolution[0] - 1 <= x:
			continue
		for y in range(bottomRight[1], topLeft[1]):
			# within img boundaries?
			if y < 1 or image.resolution[1] - 1 <= y:
				continue

			pixel = image.pixels[x][y]  # type: Pixel
			if noiseColDiff > 0.01:
				pixel.r = max(0, min(1, color[0] * random.uniform(0.9, 1.1)))
				pixel.g = max(0, min(1, color[1] * random.uniform(0.9, 1.1)))
				pixel.b = max(0, min(1, color[2] * random.uniform(0.9, 1.1)))
				pixel.a = color[3]
			else:
				pixel.r = color[0]
				pixel.g = color[1]
				pixel.b = color[2]
				pixel.a = color[3]
			if not un_pixel_a_ete_modifie:
				un_pixel_a_ete_modifie = True
	return un_pixel_a_ete_modifie


class Path:
	def __init__(self):
		self.points = []
		self.loop = False

	def __repr__(self):
		return self.__str__()

	def __str__(self):
		return 'Path(loop=' + str(self.loop) + ', total points=' + str(len(self.points)) + ')'


class PathsGetter:
	def get_paths(self, **kwargs) -> Iterable[Path]:
		return None


class CurveBezierPoint:
	def __init__(self):
		self.pos = (0, 0, 0)
		self.left_handle_pos = (0, 0, 0)
		self.left_handle_type = 'VECTOR'
		self.right_handle_pos = (0, 0, 0)
		self.right_handle_type = 'VECTOR'


class SC_Curve(DATA_bldata):
	def __init__(self, bl_curve):
		self.bezier_points: List[CurveBezierPoint] = []  # à supprimer?
		self.loop = False  # à supprimer?
		self.bl_curve = bl_curve
		if self.bl_curve:  # ce test est nécessaire uniquement parce que les vieux nodes passent None pour bl_mesh, car ils utilisent les attributs en haut
			super().__init__(self.bl_curve, bpy.data.curves, True)

	def __repr__(self):
		return self.__str__()

	def __str__(self):
		return 'SC_Curve(loop=' + str(self.loop) + ', total points=' + str(len(self.bezier_points)) + ')'


def get_blender_curve_length(blender_curve):
	spline = blender_curve.splines[0]
	total_length = 0
	final_resolution = blender_curve.render_resolution_u
	if final_resolution == 0:
		final_resolution = blender_curve.resolution_u
	total_segments = len(spline.bezier_points) - 1
	if spline.use_cyclic_u:
		total_segments += 1
	resolution_per_segment = final_resolution / total_segments
	# for point_nb, bezier_point in enumerate(spline.bezier_points):
	# 	total_length +=
	return total_length


class DATA_GETTER_NODE_SC_Curves(DATA_GETTER_NODE_SC_bldata):
	def get_sc_curves(self, *args, **kwargs) -> List[SC_Curve]:
		pass

	def _get_sc_curves_necessary_data(self, *args, **kwargs):
		return super()._get_sc_bldata_necessary_data(*args, **kwargs)

	def _get_sc_bldata_necessary_data(self, *args, **kwargs):
		return self._get_sc_curves_necessary_data(*args, **kwargs)

	def get_sc_bldata(self, *args, **kwargs):
		return self.get_sc_curves(*args, **kwargs)


class SC_Curves_Socket(NodeSocket, Sockets_bldata):
	bl_idname = 'Socket_hpqs91mwupngzbpsoou5'
	# compatible_types = (SC_Curves_Getter,)
	compatible_output_socket_classes = ()
	color = (0, 1, 0, 1)
	default_label = 'Curves'
	display_shape = 'CIRCLE'
	# internal_icon = 'CURVE_DATA'
	external_icon_name = 'socket curves'

	def get_input_sc_curves(self, *args, **kwargs) -> List[SC_Curve]:
		"""
		Traiter ici les converstions des types de données qui peuvent être connectées à ce type de socket
		Ne pas s'occuper des exceptions ici, elles seront traitées par les nodes eux-mêmes
		"""
		source_node: DATA_GETTER_NODE_SC_Curves = self.links[0].from_node
		return source_node.get_sc_curves(*args, **kwargs)

	def get_input_sc_bldata(self, *args, **kwargs) -> List[DATA_bldata]:
		return self.get_input_sc_curves(*args, **kwargs)


class Grid:
	def __init__(self, grid_size, cell_size):
		self.cell_size = cell_size
		self.grid_size = grid_size
		self.data = [[{} for _ in range(grid_size[1])] for _ in range(grid_size[0])]


class GridGetter:
	def get_grid(self, **kwargs) -> Grid:
		return

	def _get_grid_necessary_data(self, *args, **kwargs):
		return


def generic_copy(source, target, string=""):
	"""Copy attributes from source to target that have string in them"""
	for attr in dir(source):
		if attr.find(string) > -1:
			try:
				setattr(target, attr, getattr(source, attr))
			except:
				pass
	return


class SceneCity_Object:
	def __init__(self):
		self.name = None

		self.matrix_local = mathutils.Matrix.Identity(4)
		self.matrix_world = mathutils.Matrix.Identity(4)
		# self.location = (0, 0, 0)
		# self.rotation_euler_radians = (0, 0, 0)
		# self.scale = (1, 1, 1)

		self.data = None
		self.source_bl_object = None

		self.parent = None
		self.children = set()

	def get_matrix_world(self):
		parent = self.parent
		last_matrix = self.matrix_local
		while parent:
			last_matrix = parent.matrix_local @ last_matrix
			parent = parent.parent
		return last_matrix

	def get_all_children_flat(self):
		children = set()
		for child in self.children:
			children.add(child)
			children |= child.get_all_children_flat()
		return children

	def copy_modifiers_to_other_bl_object(self, bl_object):
		for modifier in self.source_bl_object.modifiers.values():
			new_modifier = bl_object.modifiers.new(name=modifier.name, type=modifier.type)
			generic_copy(modifier, new_modifier)


class SceneCityScene:
	def __init__(self):
		self._objects_hierarchy = set()
		self._objects_flat = set()

	def add_sc_object(self, sc_object: SceneCity_Object):
		self._objects_flat.add(sc_object)
		self._objects_flat |= sc_object.get_all_children_flat()
		self._objects_hierarchy.add(sc_object)

	def get_objects_by_data(self):
		objects_by_data = {}
		for sc_object in self._objects_flat:
			try:
				objects_by_data[sc_object.data].add(sc_object)
			except KeyError:
				objects_by_data[sc_object.data] = set()
				objects_by_data[sc_object.data].add(sc_object)
		return objects_by_data

	# def add(self, other: SceneCityScene):
	def add(self, other):
		self._objects_hierarchy |= other._objects_hierarchy
		self._objects_flat |= other._objects_flat


# def set_location(self, new_loc):
# 	self.location = new_loc

# def set_rotation(self, new_rot_euler_radians):
# 	self.rotation_euler_radians = new_rot_euler_radians


# class SceneCityObjectsGetter:
# 	def get_scenecity_objects(self, **kwargs):
# 		return None


class SceneCitySceneGetter:
	def get_scenecity_scene(self, **kwargs) -> SceneCityScene:
		return None


# class Socket_2d_Geoms(bpy.types.NodeSocket, Socket):
# 	bl_idname = 'Socket_e8r5mVD3p39mHQVCsW8e'
# 	color = (0, 1, 1, 1)
# 	default_label = '2D Geometries sets'
# 	compatible_output_socket_classes = ()
# 	display_shape = 'DIAMOND'
# 	external_icon_name = 'socket geoms2d'
#
# 	def get_input_2d_geometries(self, *args, **kwargs) -> List[DATA_Geometries]:
# 		"""
# 		Traiter ici les converstions des types de données qui peuvent être connectées à ce type de socket
# 		Ne pas s'occuper des exceptions ici, elles seront traitées par les nodes eux-mêmes
# 		"""
# 		source_node = self.links[0].from_node
# 		source_socket = self.links[0].from_socket
# 		source_node: GeometriesGetter_Node = source_node
# 		return source_node.get_geometries(*args, **kwargs)


def delete_datablocks_with_name_prefix(datablock_type, prefix):
	assert prefix
	source_array = bpy.data.objects
	if datablock_type == 'meshes': source_array = bpy.data.meshes
	elif datablock_type == 'curves': source_array = bpy.data.curves

	things_to_delete = []
	for thing in source_array:
		if thing.name.startswith(prefix):
			things_to_delete.append(thing)
	for thing_to_delete in things_to_delete:
		# Dans Blender 2.8 on dirait qu'on peut supprimer les objets à la source et ils disparaitront des scenes et collections malgré tout
		# if datablock_type == 'objects':
		# 	try: bpy.context.scene.objects.unlink(thing_to_delete)
		# 	except: pass
		try: source_array.remove(thing_to_delete)
		except: pass


class RoadPortion:
	@enum.unique
	class Type(enum.Enum):
		STRAIGHT = 1
		T_CROSSING = 2
		X_CROSSING = 3
		DEAD_END = 4
		TURN = 5

	class Sizes:
		def __init__(self):
			self.ranges = set()  # {(range(1,10),range(1,10)), (..., ...), ...}
			self.exact_sizes = set()  # {(1,1), (2,1), ...}

	def __init__(self):
		self.size = (1, 1)
		self.type = RoadPortion.Type.STRAIGHT
		# self.objects = None
		self.scene = None


class DeleteInputSocketOperator(bpy.types.Operator):
	bl_idname = 'node.delete_input_socket'
	bl_description = 'Delete this input socket'
	bl_label = 'Delete'
	source_node_path: bpy.props.StringProperty()
	input_socket_to_delete: bpy.props.StringProperty()

	def execute(self, context):
		source_node = eval(self.source_node_path)
		source_node.inputs.remove(source_node.inputs[self.input_socket_to_delete])
		return {'FINISHED'}


class RoadPortionGetter:
	def get_road_portion(self, **kwargs):
		return None

	def get_roads_sizes_by_type(self):
		"""Retourne un dict: type, toutes les tailles possibles"""
		return None


def weighted_choice(list_of_things, list_of_weights):
	"""returns (index, element)"""
	added_weights = []
	last_total_weight = 0
	for weight in list_of_weights:
		last_total_weight += weight
		added_weights.append(last_total_weight)
	random_value = random.uniform(0, last_total_weight)
	for i in range(len(list_of_weights)):
		if random_value <= added_weights[i]:
			return (i, list_of_things[i])
	assert False


class BuildingSocket(bpy.types.NodeSocket, SOCKET):
	bl_idname = 'Socket_jwgnxqofwxiywzlari2h'
	color = (.5, 0, .5, 1)
	default_label = 'Buildings'
	compatible_output_socket_classes = ()
	display_shape = 'DIAMOND'


# external_icon_name = 'socket geoms2d'


class WeightedBuildingSocket(NodeSocket, SOCKET):
	bl_idname = 'Socket_w29kzqkyzt330udcupc9'
	color = (0.5, 0, 0.5, 1)
	default_label = 'Light objects'
	compatible_output_socket_classes = ()
	display_shape = 'DIAMOND'

	weight: bpy.props.FloatProperty(
		name='Weight',
		description='Relative proba of this (or these) buildings to be chosen if there are others of the same and size',
		default=1)

	def draw(self, context, layout, node, text):
		layout.prop(self, 'weight')
		op = layout.operator(DeleteInputSocketOperator.bl_idname)
		op.source_node_path = 'bpy.data.node_groups["' + self.node.id_data.name + '"].' + self.node.path_from_id()
		op.input_socket_to_delete = self.name


class Building:
	def __init__(self):
		self.size: Tuple[int, int] = (1, 1)
		# self.objects = None
		self.scene: SceneCityScene = None


class BuildingGetter:
	def get_building(self, **kwargs) -> Building:
		return None

	def get_building_sizes(self) -> Set[Tuple[int, int]]:
		"""Retourne un set avec toutes les tailles possibles"""
		return None


@enum.unique
class DIRECTION4(enum.Enum):
	NORD = 1,  # +y
	SUD = 2,  # -y
	EST = 3,  # +x
	OUEST = 4  # -x


# class SC_HT_NodeTreePanelHeader(bpy.types.Header):
# 	bl_idname = "SC_HT_NodeTreePanelHeader"
# 	bl_space_type = "NODE_EDITOR"
#
# 	def draw(self, context):
# 		if context.space_data.tree_type != "sc_node_tree_c3ey4om9toq4iwzf8ynl":
# 			return
#
# 		layout = self.layout
# 		layout.menu(SC_MT_NodeTreeCityMenu.bl_idname, text="SceneCity")


class DATA_GETTER_NODE_TerrainShapes(DATA_GETTER_NODE):
	def get_data(self, *args, **kwargs):
		return self.get_terrain_shapes(*args, **kwargs)

	def _get_terrain_shapes_necessary_data(self, *args, **kwargs):
		pass

	def get_terrain_shapes(self, *args, **kwargs) -> List[DATA_Terrain_Shape]:
		return None


class DATA_GETTER_NODE_Geometries(DATA_GETTER_NODE):
	def get_data(self, *args, **kwargs):
		return self.get_geometries(*args, **kwargs)

	def _get_geometries_necessary_data(self, *args, **kwargs):
		pass

	def get_geometries(self, *args, **kwargs) -> List[DATA_Geometries]:
		pass


class DATA_GETTER_NODE_Map(DATA_GETTER_NODE):
	def get_data(self, *args, **kwargs):
		return self.get_map(*args, **kwargs)

	def _get_map_necessary_data(self, *args, **kwargs):
		pass

	def get_map(self, *args, **kwargs) -> DATA_Map:
		pass


class DATA_GETTER_NODE_Image(DATA_GETTER_NODE):
	def get_data(self, *args, **kwargs):
		return self.get_images(*args, **kwargs)

	def _get_images_necessary_data(self, *args, **kwargs):
		pass

	def get_images(self, *args, **kwargs) -> List[DATA_Image]:
		pass


class DATA_GETTER_NODE_Material(DATA_GETTER_NODE):
	def get_data(self, *args, **kwargs):
		return self.get_materials(*args, **kwargs)

	def _get_materials_necessary_data(self, *args, **kwargs):
		pass

	def get_materials(self, *args, **kwargs) -> List[DATA_Material]:
		pass


# def draw(self, context, layout, node, text):
# 	if self.is_required and (len(self.links) <= 0 or not isinstance(self.links[0].from_node, GeometriesGetter)):
# 		layout.label(text=text, icon='ERROR')
# 	else:
# 		layout.label(text=text)

class ExecuteNode(bpy.types.Node, SC_Node_variable_inputs):
	bl_idname = 'sc_node_ntf3w6ud3pjju9j2c3pa'
	bl_label = 'Execute'
	icon_value = 'LH2-Play-icon'
	icon_scale = 3
	new_inputs_socket_type = SOCKET_Any
	new_inputs_are_required = True
	execute_with_key_shortcut: bpy.props.BoolProperty(
		name='Execute when using key shortcut',
		default=True,
		description='When the key shortcut to execute this graph is used, should this node be executed?', )
	should_clean_datablocks_without_users: bpy.props.BoolProperty(
		name='Clean all unused data when done',
		default=False)

	def sc_init(self, context):
		self.width = 270
		self.create_input(SOCKET_Any, is_required=True)

	def sc_draw_buttons(self, context, layout):
		layout.prop(self, 'execute_with_key_shortcut')
		execute_op: SC_Operator = self.create_operator(layout, self.SC_OT_Execute, icon_value=my_globals.icônes['LH2-Play-icon'].icon_id)
		execute_op.should_clean_datablocks_without_users = self.should_clean_datablocks_without_users
		layout.prop(self, 'should_clean_datablocks_without_users')
		row = layout.row()
		self.create_operator(row, self.SC_OT_AddInput)
		self.create_operator(row, self.SC_OT_RemoveInput)

	def _get_data_necessary_data(self, *args, **kwargs):
		data_list = []
		for input_socket in self.inputs:
			input_socket: SOCKET_Any = input_socket
			data = input_socket.get_data(*args, **kwargs)
			try:
				data_list.extend(data)
			except TypeError:
				data_list.append(data)
		return data_list

	@Node.get_data_first
	def get_data(self, data_list, *args, **kwargs):
		my_globals.todel_derniere_operation_par_operator_non_standard = False
		return data_list

	class SC_OT_Execute(bpy.types.Operator, SC_Operator):
		bl_idname = 'sc_op.ptpezd1wnda543t1348y'
		bl_description = "Execute all connected nodes, in the order of first socket (top) to last socket (bottom)"
		bl_label = 'Execute'
		node_method_name_to_call = 'get_data'


class SC_Document:
	def __init__(self, title: str, body):
		self.body = body
		self.title = title


class SC_Document_Socket(bpy.types.NodeSocket, SOCKET):
	bl_idname = 'Socket_kx0r4ahhkqqy4mewsot5'
	color = (.5, .5, .5, 1)
	default_label = 'Text(s)'
	compatible_output_socket_classes = ()
	display_shape = 'DIAMOND'
	external_icon_name = 'notebooks'

	def get_input_sc_documents(self, *args, **kwargs) -> List[SC_Document]:
		pass


class SC_Document_Getter_Node(DATA_GETTER_NODE):
	def get_data(self, *args, **kwargs):
		return self.get_sc_documents(*args, **kwargs)

	def _get_sc_documents_necessary_data(self, *args, **kwargs):
		pass

	def get_sc_documents(self, *args, **kwargs) -> List[SC_Document]:
		"""Must override in subclass"""
		pass
