import bpy, mathutils, colorsys, bmesh
from timeit import timeit
import numpy as np
import time, math, random, pickle, sys, os, re
from . import (Node, DATA_Map, SOCKET_Map, DATA_Terrain_Shape, DATA_GETTER_NODE_TerrainShapes, SOCKET_Terrain_Shape, SOCKET_Meshes, DATA_GETTER_NODE_SC_Meshes,
			   DATA_Mesh, MapGetter, SimpleNode, SC_OT_RandomizeSeedNode, Node, DATA_GETTER_NODE_Terrain, SOCKET_Terrain, DATA_Terrain)
from typing import List
from bpy_extras.io_utils import ImportHelper
from bpy.props import (EnumProperty, FloatProperty, IntProperty, BoolProperty, StringProperty)
from . import terrains_optimized
from ..utils import print_time
from .. import utils
from pathlib import Path
from pprint import pprint

cache = {}


def water_material_updated(node, context):
	node.update_water_material()


def clouds_updated(node, context):
	node.update_clouds()


def terrain_material_updated(node, context):
	node.update_terrain_material()


def populations_updated(node, context):
	node.update_populations()


def stack_2_materials_image_mix_and_get_new_mat(bottom_mat, top_mat, new_mat_name, mix_image):
	assert bottom_mat.use_nodes
	assert top_mat.use_nodes

	to_material = bpy.data.materials.new(new_mat_name)
	to_material.use_nodes = True
	to_material.node_tree.nodes.clear()

	# input mat 1
	# create new nodes, compute bounding box, get output nodes
	bbox_bottom_mat = {"minX": math.inf, "maxX": -math.inf, "minY": math.inf, "maxY": -math.inf}
	output_node_bottom_mat = None
	for source_node in bottom_mat.node_tree.nodes:
		new_node = to_material.node_tree.nodes.new(source_node.bl_idname)
		if source_node.bl_idname == "ShaderNodeOutputMaterial":
			# assert not output_node_bottom_mat
			output_node_bottom_mat = new_node
		# copy node attributes
		for attr_name in dir(source_node):
			attr_value = getattr(source_node, attr_name)
			try:
				setattr(new_node, attr_name, attr_value)
			except:
				pass
		# copy default node input values
		for src_node_input in source_node.inputs:
			try:
				new_node.inputs[src_node_input.name].default_value = src_node_input.default_value
			except:
				pass

		if bbox_bottom_mat["minX"] > source_node.location[0]:
			bbox_bottom_mat["minX"] = source_node.location[0]
		if bbox_bottom_mat["maxX"] < source_node.location[0] + source_node.dimensions[0]:
			bbox_bottom_mat["maxX"] = source_node.location[0] + source_node.dimensions[0]
		if bbox_bottom_mat["minY"] > source_node.location[1] - source_node.dimensions[1]:
			bbox_bottom_mat["minY"] = source_node.location[1] - source_node.dimensions[1]
		if bbox_bottom_mat["maxY"] < source_node.location[1]:
			bbox_bottom_mat["maxY"] = source_node.location[1]

	assert output_node_bottom_mat

	# create new links
	for link in bottom_mat.node_tree.links:
		from_node = to_material.node_tree.nodes[link.from_node.name]
		# from_socket = from_node.outputs[link.from_socket.name] <= Bug quand plusieurs sockets ont le même nom
		# from_socket = from_node.outputs[int(link.from_socket.path_from_id()[-2])] <= bug pour les indices de socket >= 10
		from_socket = from_node.outputs[int(re.findall(r"\d+", link.from_socket.path_from_id())[-1])]
		to_node = to_material.node_tree.nodes[link.to_node.name]
		# to_socket = to_node.inputs[link.to_socket.name]
		# to_socket = to_node.inputs[int(link.to_socket.path_from_id()[-2])]
		to_socket = to_node.inputs[int(re.findall(r"\d+", link.to_socket.path_from_id())[-1])]
		to_material.node_tree.links.new(from_socket, to_socket)

	# input mat 2
	bbox_top_mat = {"minX": math.inf, "maxX": -math.inf, "minY": math.inf, "maxY": -math.inf}
	old_nodes_names_to_new_names = {}
	output_node_top_mat = None
	for source_node in top_mat.node_tree.nodes:
		new_node = to_material.node_tree.nodes.new(source_node.bl_idname)
		if source_node.bl_idname == "ShaderNodeOutputMaterial":
			# assert not output_node_top_mat
			output_node_top_mat = new_node
		# copy node attributes
		for attr_name in dir(source_node):
			attr_value = getattr(source_node, attr_name)
			try:
				setattr(new_node, attr_name, attr_value)
			except:
				pass
		# copy default node input values
		for src_node_input in source_node.inputs:
			try:
				new_node.inputs[src_node_input.name].default_value = src_node_input.default_value
			except:
				pass
		# save new unique names for linking next
		old_nodes_names_to_new_names[source_node.name] = new_node.name

		if bbox_top_mat["minX"] > source_node.location[0]:
			bbox_top_mat["minX"] = source_node.location[0]
		if bbox_top_mat["maxX"] < source_node.location[0] + source_node.dimensions[0]:
			bbox_top_mat["maxX"] = source_node.location[0] + source_node.dimensions[0]
		if bbox_top_mat["minY"] > source_node.location[1] - source_node.dimensions[1]:
			bbox_top_mat["minY"] = source_node.location[1] - source_node.dimensions[1]
		if bbox_top_mat["maxY"] < source_node.location[1]:
			bbox_top_mat["maxY"] = source_node.location[1]

	assert output_node_top_mat

	# create new links
	for link in top_mat.node_tree.links:
		from_node = to_material.node_tree.nodes[old_nodes_names_to_new_names[link.from_node.name]]
		# from_socket = from_node.outputs[link.from_socket.name] <= Bug quand plusieurs sockets ont le même nom
		# from_socket = from_node.outputs[int(link.from_socket.path_from_id()[-2])] <= bug pour les indices de socket >= 10
		from_socket = from_node.outputs[int(re.findall(r"\d+", link.from_socket.path_from_id())[-1])]
		to_node = to_material.node_tree.nodes[old_nodes_names_to_new_names[link.to_node.name]]
		# to_socket = to_node.inputs[link.to_socket.name]
		# to_socket = to_node.inputs[int(link.to_socket.path_from_id()[-2])]
		to_socket = to_node.inputs[int(re.findall(r"\d+", link.to_socket.path_from_id())[-1])]
		to_material.node_tree.links.new(from_socket, to_socket)

	# On décale les deux groupes de nodes, et on les centre
	hauteur_nodes_totale = (100 + bbox_bottom_mat["maxY"] - bbox_bottom_mat["minY"]) + (bbox_top_mat["maxY"] - bbox_top_mat["minY"])
	for source_node in top_mat.node_tree.nodes:
		node = to_material.node_tree.nodes[old_nodes_names_to_new_names[source_node.name]]
		node.location[0] = node.location[0] - ((bbox_top_mat["maxX"] - bbox_top_mat["minX"]) / 2) - bbox_top_mat["minX"]
		node.location[1] = node.location[1] - bbox_top_mat["minY"] + (100 + bbox_bottom_mat["maxY"] - bbox_bottom_mat["minY"]) - hauteur_nodes_totale / 2
	for source_node in bottom_mat.node_tree.nodes:
		node = to_material.node_tree.nodes[source_node.name]
		node.location[0] = node.location[0] - ((bbox_bottom_mat["maxX"] - bbox_bottom_mat["minX"]) / 2) - bbox_bottom_mat["minX"]
		node.location[1] = node.location[1] - bbox_bottom_mat["minY"] - hauteur_nodes_totale / 2

	# make sure only one output node, and mix both initial outputs into one
	surface_output_socket_top_mat = output_node_top_mat.inputs["Surface"].links[0].from_socket
	print("Top mat final node:", surface_output_socket_top_mat.node.name)
	surface_output_socket_bottom_mat = output_node_bottom_mat.inputs["Surface"].links[0].from_socket
	print("Bottom mat final node:", surface_output_socket_bottom_mat.node.name)
	to_material.node_tree.nodes.remove(output_node_bottom_mat)
	mix_shader_node = to_material.node_tree.nodes.new("ShaderNodeMixShader")
	mix_shader_node.inputs[0].default_value = random.random()
	# to_material.node_tree.links.remove(surface_output_socket_top_mat.links[0])
	to_material.node_tree.links.new(surface_output_socket_top_mat, mix_shader_node.inputs[1])
	to_material.node_tree.links.new(surface_output_socket_bottom_mat, mix_shader_node.inputs[2])
	to_material.node_tree.links.new(mix_shader_node.outputs[0], output_node_top_mat.inputs[0])
	mix_shader_node.location[0] = 300 + max((bbox_bottom_mat["maxX"] - bbox_bottom_mat["minX"]) / 2, (bbox_top_mat["maxX"] - bbox_top_mat["minX"]) / 2)
	mix_shader_node.location[1] = -100
	output_node_top_mat.location[0] = mix_shader_node.location[0] + 300
	output_node_top_mat.location[1] = 0

	# add a mix image node
	mix_texture_node = to_material.node_tree.nodes.new("ShaderNodeTexImage")
	if mix_image:
		mix_texture_node.image = mix_image
	mix_texture_node.location[0] = mix_shader_node.location[0] - 300
	mix_texture_node.location[1] = mix_shader_node.location[1] + 300
	to_material.node_tree.links.new(mix_texture_node.outputs[0], mix_shader_node.inputs[0])

	'''total_links = 0
	for node in to_material.node_tree.nodes:
		for input in node.inputs:
			total_links += len(input.links)
			assert len(input.links) <= 1
		for output in node.outputs:
			assert len(input.links) <= 1

	assert len(to_material.node_tree.links) == total_links
	print(total_links)'''

	return to_material


# class Biome(bpy.types.Node, Node, DATA_GETTER_NODE_Terrain):
# 	bl_idname = 'sc_node_g2qrbzu6qca42f026lq0'
# 	bl_label = 'Biome'
#
# 	def sc_init(self, context):
# 		self.width = 420
# 		self.create_input(SOCKET_Terrain, is_required=True)
# 		self.create_output(SOCKET_Terrain, is_new_data_output=False)
#
# 	def _get_terrains_necessary_data(self, *args, **kwargs):
# 		input: DATA_GETTER_NODE_Terrain = self.inputs[0].links[0].from_node
# 		return input.get_terrains()
#
# 	@Node.get_data_first
# 	def get_terrains(self, data: List[DATA_Terrain], *args, **kwargs):
# 		input_terrain: DATA_Terrain = data[0]
#
# 		# matériaux avec limites de slopes, altitude, surface / underwater / coastline
# 		# texture-based mask, vertex-color-based mask (pour s'adapter aux features du terrain) ou noise-based mask
#
# 		terrain_mesh = input_terrain.mesh_object.data
# 		bottom_mat = terrain_mesh.materials[0]
# 		top_mat = bpy.data.materials["Material.001"]
# 		try:
# 			bpy.data.materials.remove(bpy.data.materials["STACK TODEL"])
# 		except: pass
# 		new_mat = stack_2_materials_image_mix_and_get_new_mat(bottom_mat, top_mat, "STACK TODEL", None)
# 		terrain_mesh.materials[0] = new_mat
#
# 		return input_terrain

class Population_details:
	def __init__(self, count_percentage, dot_display_size, distribution_percentage):
		self.count_percentage = count_percentage
		self.dot_display_size = dot_display_size
		self.distribution_percentage = distribution_percentage


populations_params = {
	"SC Horse Chestnut - 40 years olds": Population_details(
		count_percentage=1 / 8,
		dot_display_size=.08,
		distribution_percentage=.4,
	),
	"SC Horse Chestnut - 30 years olds": Population_details(
		count_percentage=1 / 4,
		dot_display_size=.04,
		distribution_percentage=.6,
	),
	"SC Horse Chestnut - 20 years olds": Population_details(
		count_percentage=1 / 2,
		dot_display_size=.02,
		distribution_percentage=.8,
	),
	"SC Horse Chestnut - 10 years olds": Population_details(
		count_percentage=1,
		dot_display_size=.01,
		distribution_percentage=1,
	),
}


class Population_cache:
	_cache_per_terrain_and_population = {}

	def __init__(self):
		self.total_verts_covered_by_trees = 0

	@classmethod
	def get(cls, terrain_name, population_name):
		try:
			# tente de récupérer les caches du terrain
			terrain_caches = cls._cache_per_terrain_and_population[terrain_name]
			try:
				# tente de récupérer le cache de la population
				pop_cache = terrain_caches[population_name]
			except KeyError:
				# pas encore de cache pour la population: on créé
				pop_cache = terrain_caches[population_name] = Population_cache()
		except KeyError:
			# pas encore de caches pour le terrain: on créé
			terrain_caches = cls._cache_per_terrain_and_population[terrain_name] = {}
			# On créé le cache pour la population
			pop_cache = terrain_caches[population_name] = Population_cache()
		return pop_cache


class Terrain1(bpy.types.Node, Node, DATA_GETTER_NODE_Terrain):
	bl_idname = 'sc_node_5whzicjql80lmq61p9ol'
	bl_label = 'Easy terrain + scatter city (no roads)'
	at_least_one_input_socket_required = False

	trees_population_name = "SC Trees"
	trees_population_distribution_name = "Trees distribution"

	generator: EnumProperty(
		name='Generator',
		description='',
		items=(
			('sc', 'SceneCity', ''),
			('ant', 'Ant landscape', ''),
		))

	noise_offset: bpy.props.FloatVectorProperty(
		name='Noise offset',
		description='The shape of the terrain is the same at the same location',
		size=3,
		default=(0, 0, 0))

	terrain_size: FloatProperty(
		name='Size (meters)',
		description='Size in meters of the terrain on each side',
		min=1,
		default=200)

	noise_size: FloatProperty(
		name='Noise size (meters)',
		description='Size in meters of the procedural noise',
		min=.001,
		default=50)

	mesh_resolution_final: bpy.props.IntVectorProperty(
		name='Final resolution',
		description='Density of the mesh for the final version',
		size=2,
		default=(512, 512))

	mesh_resolution_preview: bpy.props.IntVectorProperty(
		name='Mesh resolution',
		description='Density of the mesh',
		size=2,
		default=(128, 128))

	should_shade_smooth: bpy.props.BoolProperty(
		name="Shade smooth",
		default=True,
		description="Shade smooth"
	)

	# Mega node starts from here

	mesh_resolution: EnumProperty(
		name="Terrain size",
		description="",
		items=[
			("128", "Very small - 1.3km (1.6km²) - 0.8mi (0.6mi²)", ""),
			("256", "Small - 2.5km (6.6km²) - 1.6mi (2.5mi²)", ""),
			("512", "Medium - 5km (26km²) - 3mi (10mi²)", ""),
			("1024", "Large - 10km (105km²) - 6mi (40mi²)", ""),
			("2048", "Very large - 20km (420km²) - 13mi (162mi²)", ""),
		])

	random_seed: IntProperty(
		name='Seed',
		description="A generic integer number without meaning. For the same seed, you'll have exactly the same terrain")

	should_randomize_seed_each_generation: BoolProperty(
		name='Random new terrains',
		description="Each time you generate a terrain it will be different",
		default=True)

	# total_exec_time_s: FloatProperty()
	min_height_m: FloatProperty()
	max_height_m: FloatProperty()
	# total_verts_covered_by_trees: FloatProperty()

	water_percent: FloatProperty(
		name='Water coverage',
		subtype="PERCENTAGE", min=0, max=100, default=30,
		description="Approximate percentage of the terrain area that will be covered by water")

	flat_area_percent: FloatProperty(
		name='Flat land coverage',
		subtype="PERCENTAGE", min=0, max=100, default=40,
		description="Approximate percentage of the EMERGED terrain area that will be covered by flat land. The terrain is usually flattened at low altitudes, just above sea level. The rest will be mountains")

	flat_area_altitude_m: FloatProperty(
		name='Flat land altitude',
		subtype="DISTANCE",
		unit="LENGTH",
		min=0.001, default=.02,  # x100 en unités réelles, car on règle Blender pour multiplier les distance par 100
		description="At what altitude the flat land is")

	water_material_transparency_distance_m: FloatProperty(
		name='Transparency distance',
		subtype="DISTANCE", unit="LENGTH",
		min=.001, max=.8, default=.1,  # x100 en unités réelles, car on règle Blender pour multiplier les distance par 100
		description="How far we can see under the water, approximately. >45m record clear water (Artic, tropics), 20m-45m very clear (tropics), 10m-20m clear, 5-10m normal, <5m turbid water",
		update=water_material_updated)

	# water_material_deep_blue_depth_m: FloatProperty(
	# 	name='Light decay distance',
	# 	subtype="DISTANCE", unit="LENGTH",
	# 	min=.001, max=1, default=.1,  # x100 en unités réelles, car on règle Blender pour multiplier les distance par 100
	# 	description="How far the light goes in water before it'll change to a deep blue tint",
	# 	update=water_material_updated)

	water_material_waves1_scale_m: FloatProperty(
		name='Smaller waves size',
		subtype="DISTANCE",
		unit="LENGTH",
		min=0.001, max=.2, default=.02,  # x100 en unités réelles, car on règle Blender pour multiplier les distance par 100
		description="Distance between the largest of the waves",
		update=water_material_updated)

	water_material_waves1_strength: FloatProperty(
		name='Smaller waves strengths',
		subtype="PERCENTAGE", min=0, max=100, default=40,
		description="How much the waves affect the surface of water",
		update=water_material_updated)

	water_material_waves2_scale_m: FloatProperty(
		name='Larger waves size',
		subtype="DISTANCE",
		unit="LENGTH",
		min=.01, max=.5, default=.1,  # x100 en unités réelles, car on règle Blender pour multiplier les distance par 100
		description="Distance between the largest of the waves",
		update=water_material_updated)

	water_material_waves2_strength: FloatProperty(
		name='Larger waves strengths',
		subtype="PERCENTAGE", min=0, max=100, default=60,
		description="How much the waves affect the surface of water",
		update=water_material_updated)

	water_material_wind_effect_size_m: FloatProperty(
		name='Wind effect size',
		subtype="DISTANCE",
		unit="LENGTH",
		min=.5, max=10, default=2,  # x100 en unités réelles, car on règle Blender pour multiplier les distance par 100
		description="Distance between the largest of the wind gusts",
		update=water_material_updated)

	water_material_wind_transition_scale: FloatProperty(
		name='Wind effect transition scale',
		subtype="PERCENTAGE",
		min=0.1, max=50, default=25,
		description="Distance between windy zones and calmer zones",
		update=water_material_updated)

	water_material_wind_calm_zone_waves_strength: FloatProperty(
		name='Wind calmer zones waves strength',
		subtype="PERCENTAGE",
		min=0, max=100, default=20,
		description="Strength of the waves in areas where the wind is calmer",
		update=water_material_updated)

	water_material_tint_algae_tropical_nordic: FloatProperty(
		name='Water natural color',
		subtype="FACTOR",
		min=0, max=100, default=50,
		description="Algae, tropical/artic, temperate zone",
		update=water_material_updated)

	water_material_tint_strength: FloatProperty(
		name='Water color intensity',
		subtype="PERCENTAGE",
		min=0, max=100, default=33,
		description="How intense the color of the water is",
		update=water_material_updated)

	water_material_tint_type: EnumProperty(
		name='Water color type',
		description='Different water coloring modes',
		items=[
			('NORMAL', 'Clear water', ""),
			# None,
			('MUDDY', 'Muddy water', ""),
		],
		update=water_material_updated)

	water_material_tint_muddy_strength: FloatProperty(
		name='Water mud density',
		subtype="PERCENTAGE",
		min=0, max=100, default=33,
		description="Very muddy = water is more brown and dark due to higher concentration of suspended sediments",
		update=water_material_updated)

	trees_density: FloatProperty(
		name='Trees approximate density',
		subtype="PERCENTAGE",
		min=0, max=100, default=30,
		description="Higher density = more trees per area. It depends on many factors, especially the spatial distribution of trees, so it's only an approximation",
		update=populations_updated)

	trees_coverage: FloatProperty(
		name='Trees coverage',
		subtype="PERCENTAGE",
		min=0, max=100, default=50,
		description="")

	trees_distribution_noise_size: FloatProperty(
		name='Forests size',
		subtype="DISTANCE",
		min=1, max=200, default=10,  # rajouter 2 zéros pour avoir la vraie taille, à cause de l'échelle de la scene
		description="")

	trees_distribution_noise_roughness: FloatProperty(
		name='Forests delimitation roughness',
		subtype="PERCENTAGE",
		min=0, max=100, default=50,
		description="")

	ground_material_flatarea_texture_filepath: StringProperty(
		name="Ground texture",
		subtype="FILE_PATH",  # anki
		description="Path to the texture image file for flat areas",
		update=terrain_material_updated)

	ground_material_slopes_texture_filepath: StringProperty(
		name="Slopes texture",
		subtype="FILE_PATH",
		description="Path to the texture image file for slopes areas",
		update=terrain_material_updated)

	should_display_ground_mesh_options: BoolProperty(default=True, name="Show options")
	should_display_water_options: BoolProperty(default=False, name="Show options")
	should_display_trees_options: BoolProperty(default=False, name="Show options")
	should_display_ground_materials_options: BoolProperty(default=False, name="Show options")
	should_display_city_options: BoolProperty(default=False, name="Show options")
	should_display_clouds_options: BoolProperty(default=False, name="Show options")

	scatter_city_buildings_amount_percent: FloatProperty(
		name='Land value',
		subtype="PERCENTAGE",
		min=0, max=100, default=50,
		description="50 = default land value. Above 50 = increase overall land value. Below 50 = decrease ovrall land value")

	city_buildings_height_modifier: FloatProperty(
		name='Buildings height modifier',
		min=0, max=10, default=3,
		description="Use to increase or decrease overall buildings heights. Default = 3")

	# city_buildings_skyscraper_height_modifier: FloatProperty(
	# 	name='Skyscrapers height modifier',
	# 	min=0, max=10, default=3,
	# 	description="Use to increase or decrease the difference between regular buildings and skyscrapers heights. Default = 3")

	should_add_clouds: BoolProperty(default=True, name="Add clouds over terrain", update=clouds_updated)
	should_display_clouds_in_viewport: BoolProperty(default=True, name="Display in viewport", description="Clouds can be heavy to display in viewport",
		update=clouds_updated)
	clouds_altitude_meters: FloatProperty(default=5, min=0, name="Altitude", subtype="DISTANCE", update=clouds_updated)  # x100 pour avoir en scene units anki
	# clouds_type: EnumProperty(
	# 	name='Clouds type',
	# 	description='Category of clouds to put over the terrain',
	# 	items=[
	# 		("cumulus", "Cumulus", "Clouds with flat bases and often described as puffy, cotton-like or fluffy in appearance. Low-level clouds, generally less than 2,000 m (6,600 ft) in altitude"),
	# 		("stratus", "Stratus", "Flat, hazy, featureless clouds varying in color from dark gray to nearly white. Low-level clouds, generally less than 2,000 m (6,600 ft) in altitude. No rain or light rain"),
	# 		("stratocumulus", "Stratocumulus", "Like cumulus, but much thinner. The most common type over the Earth"),
	# 	])
	clouds_bottom_density: FloatProperty(default=33, min=0, max=100, name="Bottom density", subtype="PERCENTAGE", update=clouds_updated,
		description="For more realism, make the bottom density lower than the top density")
	clouds_top_density: FloatProperty(default=80, min=0, max=100, name="Top density", subtype="PERCENTAGE", update=clouds_updated,
		description="For more realism, make the bottom density lower than the top density")
	clouds_bottom_whiteness: FloatProperty(default=20, min=0, max=100, name="Bottom whiteness", subtype="PERCENTAGE", update=clouds_updated,
		description="For more realism, make the bottom whiter than the top")
	clouds_top_whiteness: FloatProperty(default=80, min=0, max=100, name="Top whiteness", subtype="PERCENTAGE", update=clouds_updated,
		description="For more realism, make the bottom whiter than the top")

	def sc_init(self, context):
		self.width = 420
		self.ground_material_flatarea_texture_filepath = str(utils.get_terrains_data_dir() / "Textures" / "city1 seamless.jpg")
		self.ground_material_slopes_texture_filepath = str(utils.get_terrains_data_dir() / "Textures" / "countryside1 seamless.jpg")

	# self.create_output(SOCKET_Terrain)

	# def update(self):
	# 	if self.label != self.name:
	# 		self.label = self.name

	def update_populations(self):
		# MAJ nombres de particules
		for arbres_nom_collection, arbres_données in populations_params.items():
			arbres_données: Population_details = arbres_données
			try:
				terrain_datablocks_name = self.get_terrain_datablocks_name()
				terrain_ob = bpy.data.objects[terrain_datablocks_name]
				terrain_ob_particles_modifier = terrain_ob.particle_systems[arbres_nom_collection]
				terrain_ob_particles_system_trees = terrain_ob.particle_systems[arbres_nom_collection]
				particles_settings = bpy.data.particles[arbres_nom_collection]

				total_verts = int(self.mesh_resolution) ** 2
				pop_cache: Population_cache = Population_cache.get(terrain_datablocks_name, arbres_nom_collection)
				# 5 arbres au max, par aire de 10m*10m
				# particles_settings.count = 20 * self.trees_density / 100 * self.total_verts_covered_by_trees * arbres_données["count mult"]
				particles_settings.count = 20 * self.trees_density / 100 * pop_cache.total_verts_covered_by_trees * arbres_données.count_percentage
			except KeyError:
				pass

		# MAJ matériaux
		feuilles_mat = bpy.data.materials["SC Leaf 1"]
		feuilles_mat.node_tree.nodes["Tree-scale leaves amount noise"].inputs["Scale"].default_value = 33.3
		feuilles_mat.node_tree.nodes["Tree-scale leaves age variations noise"].inputs["Scale"].default_value = 33.3
		feuilles_mat.node_tree.nodes["Forest-scale leaves age variations noise"].inputs["Scale"].default_value = .666  # 150m
		feuilles_mat.node_tree.nodes["Tree-scale leaves decay variations noise"].inputs["Scale"].default_value = 20

	def sc_draw_buttons(self, context, layout):
		# Wrong units settings?
		if context.scene.unit_settings.system == "NONE" or context.scene.unit_settings.scale_length != 100:
			row = layout.row()
			row.label(text="Scene unit settings incorrect", icon="ERROR")
			op = row.operator(utils.SC_OT_Fix_scene_unit_settings.bl_idname)

		# Wrong render settings?
		# if context.scene.render.engine != "CYCLES" or not context.scene.cycles.use_transparent_shadows or not context.scene.cycles.transparent_max_bounces or not context.scene.cycles.volume_bounces:
		if context.scene.render.engine != "CYCLES" or not context.scene.cycles.transparent_max_bounces or not context.scene.cycles.volume_bounces:
			row = layout.row()
			row.label(text="Scene render settings incorrect", icon="ERROR")
			op = row.operator(utils.SC_OT_Fix_render_settings.bl_idname)

		factor_show_options_dans_split = 0.4
		################################ Ground mesh
		box = layout.box()
		split = box.split(factor=factor_show_options_dans_split)
		split.label(text="Ground mesh")
		split.prop(self, "should_display_ground_mesh_options")
		if self.should_display_ground_mesh_options:
			row = box.split(factor=0.225)
			row.template_icon(icon_value=utils.get_icon_value("node terrain3"), scale=4)
			col = row.column()
			col.prop(self, "mesh_resolution")
			if context.scene.unit_settings.system == "METRIC":
				col.label(text=f"● 10 meters between verts")
			else:
				col.label(text=f"● 32.8 feet between verts")
			col.label(text=f"● {int(self.mesh_resolution) ** 2:,} verts")
			col.label(text=f"● {(int(self.mesh_resolution) - 1) ** 2:,} quads")
			# col.label(text=f"Mesh resolution: 10m between verts | {int(self.mesh_resolution) ** 2:,} verts | {(int(self.mesh_resolution) - 1) ** 2:,} quads")

			row = box.row()
			row2 = row.row()
			row2.prop(self, "random_seed")
			self.create_operator(row2, SC_OT_RandomizeSeedNode)
			row2.enabled = not self.should_randomize_seed_each_generation
			row.prop(self, "should_randomize_seed_each_generation")

			row = box.row()
			row.prop(self, "water_percent")
			row.prop(self, "flat_area_percent")
		# row.prop(self, "flat_area_altitude_m")

		################################ Water
		# Water waves
		box = layout.box()
		split = box.split(factor=factor_show_options_dans_split)
		split.label(text="Water")
		split.prop(self, "should_display_water_options")
		if self.should_display_water_options:
			row = box.split(factor=0.225)
			row.template_icon(icon_value=utils.get_icon_value("node ocean"), scale=4)
			col = row.column()
			# row2 = col.row()
			col.prop(self, "water_material_waves1_scale_m")
			col.prop(self, "water_material_waves1_strength")
			# row2 = col.row()
			col.prop(self, "water_material_waves2_scale_m")
			col.prop(self, "water_material_waves2_strength")

			# Water transparency
			split = box.split(factor=.45)
			split.prop(self, "water_material_transparency_distance_m")
			text = "Muddy water (floating sediments)"
			if .01 < self.water_material_transparency_distance_m <= .02:
				text = "Turbid water"
			elif .02 < self.water_material_transparency_distance_m <= .05:
				text = "Slightly turbid water"
			elif .05 < self.water_material_transparency_distance_m <= .1:
				text = "Normal water"
			elif .1 < self.water_material_transparency_distance_m <= .2:
				text = "Clear water"
			elif .2 < self.water_material_transparency_distance_m <= .3:
				text = "Very clear water (tropical only)"
			elif .3 < self.water_material_transparency_distance_m <= .4:
				text = "Exceptionally clear water (tropical, artic only)"
			elif .4 < self.water_material_transparency_distance_m <= .5:
				text = "Record clear water (artic only, not in surface)"
			elif .5 < self.water_material_transparency_distance_m:
				text = "Theoretical, never measured in real"
			split.label(text=text)

			# Water wind
			split = box.split(factor=0.4)
			split.prop(self, "water_material_wind_effect_size_m")
			split.prop(self, "water_material_wind_calm_zone_waves_strength")
			box.prop(self, "water_material_wind_transition_scale")
			text = f"Distance between windy zones and calmer zones: {round(self.water_material_wind_effect_size_m * 100 * self.water_material_wind_transition_scale / 100)}m"
			if context.scene.unit_settings.system == "IMPERIAL":
				text = f"Distance between windy zones and calmer zones: {round(self.water_material_wind_effect_size_m * 3.281 * 100 * self.water_material_wind_transition_scale / 100)} feet"
			box.label(text=text)

			# Water tint type
			box.prop(self, "water_material_tint_type")

			if self.water_material_tint_type == "NORMAL":
				# Water tint hue
				row = box.row()
				row.prop(self, "water_material_tint_algae_tropical_nordic")
				text = "Algae"
				if 33 < self.water_material_tint_algae_tropical_nordic <= 66:
					text = "Tropical / artic"
				elif 66 < self.water_material_tint_algae_tropical_nordic:
					text = "Temperate zone"
				row.label(text=text)

				# Water tint saturation
				row = box.row()
				row.prop(self, "water_material_tint_strength")
				text = "Natural"
				if 40 < self.water_material_tint_strength <= 55:
					text = "Sligthly unnatural"
				elif 55 < self.water_material_tint_strength:
					text = "Unnatural"
				row.label(text=text)
			else:
				box.prop(self, "water_material_tint_muddy_strength")

		################################ Trees
		box = layout.box()
		split = box.split(factor=factor_show_options_dans_split)
		split.label(text="Trees")
		split.prop(self, "should_display_trees_options")
		if self.should_display_trees_options:
			row = box.split(factor=0.225)
			row.template_icon(icon_value=utils.get_icon_value("forest"), scale=4)
			col = row.column()
			col.prop(self, "trees_coverage")
			col.prop(self, "trees_density")
			# row = box.row()
			col.prop(self, "trees_distribution_noise_size")
			col.prop(self, "trees_distribution_noise_roughness")

		################################ Ground Materials
		box = layout.box()
		split = box.split(factor=factor_show_options_dans_split)
		split.label(text="Ground materials")
		split.prop(self, "should_display_ground_materials_options")
		if self.should_display_ground_materials_options:
			box.prop(self, "ground_material_flatarea_texture_filepath")
			box.prop(self, "ground_material_slopes_texture_filepath")
		# op = self.create_operator(box, Terrain1.SC_OT_choose_texture, )
		# op.params.directory = str(utils.get_terrains_data_dir() / "Textures")

		################################ City
		box = layout.box()
		split = box.split(factor=factor_show_options_dans_split)
		split.label(text="City")
		split.prop(self, "should_display_city_options")
		if self.should_display_city_options:
			row = box.split(factor=0.225)
			row.template_icon(icon_value=utils.get_icon_value("buildings"), scale=4)
			col = row.column()
			col.prop(self, "scatter_city_buildings_amount_percent")
			col.prop(self, "city_buildings_height_modifier")
		# box.prop(self, "city_buildings_skyscraper_height_modifier")

		################################ Clouds
		box = layout.box()
		split = box.split(factor=factor_show_options_dans_split)
		split.label(text="Clouds")
		split.prop(self, "should_display_clouds_options")
		if self.should_display_clouds_options:
			row = box.split(factor=0.225)
			row.template_icon(icon_value=utils.get_icon_value("clouds"), scale=4)
			col = row.column()
			col.prop(self, "should_add_clouds")

			# if self.should_add_clouds:
			col = box.column()
			col.enabled = self.should_add_clouds  # anki
			col.prop(self, "should_display_clouds_in_viewport")
			col.prop(self, "clouds_altitude_meters")
			col.label(text="Change altitude here, do not manually change clouds z position")
			row = col.row()
			row.prop(self, "clouds_bottom_density")
			row.prop(self, "clouds_top_density")
			col.prop(self, "clouds_bottom_whiteness")
			col.prop(self, "clouds_top_whiteness")
		# col.prop(self, "clouds_type")
		# col.prop(self, "clouds_altitude_meters")

		################################ Create button
		# layout.label(text="")
		op_text = Terrain1.SC_OT_Create_mesh_and_object.bl_label
		if not Terrain1.SC_OT_Create_mesh_and_object.poll(context):
			op_text = Terrain1.SC_OT_Create_mesh_and_object.bl_label + " (in 'object mode' only)"
		self.create_operator(
			layout,
			Terrain1.SC_OT_Create_mesh_and_object,
			text=op_text)
		# text = f"Terrain created in {round(self.total_exec_time_s, 3):,}s | Min height: {round(self.min_height_m * 10):,}m | Max height: {round(self.max_height_m * 10):,}m"
		text = f"Min height: {round(self.min_height_m * 10):,}m | Max height: {round(self.max_height_m * 10):,}m"
		if context.scene.unit_settings.system == "IMPERIAL":
			# text = f"Terrain created in {round(self.total_exec_time_s, 3):,}s | Min height: {round(self.min_height_m * 10 * 3.281):,} feet | Max height: {round(self.max_height_m * 10 * 3.281):,} feet"
			text = f"Min height: {round(self.min_height_m * 10 * 3.281):,} feet | Max height: {round(self.max_height_m * 10 * 3.281):,} feet"
		layout.label(text=text)

	# def are_all_inputs_correct(self):
	# 	ctx = bpy.context
	# 	return not (
	# 				ctx.scene.unit_settings.system == "NONE" or ctx.scene.unit_settings.scale_length != 100 or ctx.scene.render.engine != "CYCLES" or not ctx.scene.cycles.use_transparent_shadows or not ctx.scene.cycles.transparent_max_bounces or not ctx.scene.cycles.volume_bounces)

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

	@Node.get_data_first
	def get_terrains(self, *args, **kwargs):
		# cache = {}
		overall_start_time = start_time = time.process_time()
		source_node: Terrain1 = self
		context = bpy.context

		# Seed
		if source_node.should_randomize_seed_each_generation:
			source_node.random_seed = random.randint(-9e5, 9e5)
		random.seed(source_node.random_seed)
		terrain_datablocks_name = source_node.get_terrain_datablocks_name()
		# print(terrain_datablocks_name)

		# Charge le terrain choisi entier précalculé
		# heightmap_name = "heightmap 8192 81920"
		src_heightmap_name = "Standard 16384 163840"
		# heightmap_name = "strata2 8192 81920"
		try:
			src_heightmap_np = cache[src_heightmap_name]
		except KeyError:
			with open(utils.get_terrains_data_dir() / (src_heightmap_name + ".pickle"), "rb") as f:
				src_heightmap_np = pickle.load(f)
			if self.should_output_console_message: start_time = print_time("Heightmap loaded", start_time)  # ========================================
			src_heightmap_np = src_heightmap_np.astype("float32")
			if self.should_output_console_message: start_time = print_time("Heightmap converted", start_time)  # ======================================
			cache[src_heightmap_name] = src_heightmap_np

		# Mesh + object
		nbVertsCôté = int(source_node.mesh_resolution)
		try:
			terrain_mesh = bpy.data.meshes[terrain_datablocks_name]
			if len(terrain_mesh.vertices) != nbVertsCôté ** 2:
				bpy.data.meshes.remove(terrain_mesh)
				raise KeyError
		except KeyError:
			with bpy.data.libraries.load(str(utils.get_terrains_data_dir() / "Terrains meshes.blend"), link=False) as (data_from, data_to):
				data_to.meshes = ["Terrain " + str(nbVertsCôté)]
			terrain_mesh = bpy.data.meshes["Terrain " + str(nbVertsCôté)]
			terrain_mesh.name = terrain_datablocks_name
			if self.should_output_console_message: start_time = print_time("Mesh loaded", start_time)  # ===================================
		try:
			terrain_ob = bpy.data.objects[terrain_datablocks_name]
			terrain_ob.data = terrain_mesh
		except KeyError:
			terrain_ob = bpy.data.objects.new(terrain_datablocks_name, terrain_mesh)
			context.scene.collection.objects.link(terrain_ob)
			terrain_ob.scale = .1, .1, .1
		# terrain_mesh.use_auto_smooth = True

		# Charge en mémoire les structures de données pour toutes les résolutions
		try:
			terrains_data_structures = cache["terrains_data_structures"]
		except KeyError:
			with open(utils.get_terrains_data_dir() / "Terrains data structures.pickle", "rb") as f:
				terrains_data_structures = pickle.load(f)
			# conversion en float32 pour chaque résolution
			for resolution in 128, 256, 512, 1024, 2048:
				_ = terrains_data_structures[resolution]["verts_co_np_3d"].astype("float32")
				terrains_data_structures[resolution]["verts_co_np_3d"] = _
			# print_time(f"a: resolution={resolution:,} {a.nbytes:,}bytes, dims={a.ndim:}, shape={a.shape:}")
			cache["terrains_data_structures"] = terrains_data_structures
			if self.should_output_console_message: start_time = print_time("Terrains data structures loaded and converted", start_time)  # ====================

		# res_heightmap = 16384
		res_heightmap = src_heightmap_np.shape[0]
		# print(res_heightmap)
		# final_heightmap_corner_pos = 0, 0
		# final_heightmap_corner_pos = (
		# 	random.randrange(0, res_heightmap, nbVertsCôté),
		# 	random.randrange(0, res_heightmap, nbVertsCôté),
		# )
		final_heightmap_corner_pos = (
			random.randint(0, res_heightmap - nbVertsCôté),
			random.randint(0, res_heightmap - nbVertsCôté),
		)
		# final_heightmap_corner_pos = (
		# 	res_heightmap - nbVertsCôté,
		# 	res_heightmap - nbVertsCôté,
		# )
		# print(final_heightmap_corner_pos)

		if self.should_output_console_message: start_time = time.process_time()  # ==================================================================

		random_terrain_rotation_x90_deg = random.randint(0, 3)
		verts_np = np.rot90(terrains_data_structures[nbVertsCôté]["verts_co_np_3d"], random_terrain_rotation_x90_deg)
		# verts_np = terrains_data_structures[nbVertsCôté]["verts_co_np_3d"]
		final_heightmap_corner_pos0 = final_heightmap_corner_pos[0]
		final_heightmap_corner_pos1 = final_heightmap_corner_pos[1]
		source_node.min_height_m, source_node.max_height_m = terrains_optimized.compute_heights(
			verts_np, nbVertsCôté, src_heightmap_np,
			final_heightmap_corner_pos0, final_heightmap_corner_pos1,
			source_node.water_percent / 100,
			source_node.flat_area_percent / 100,
			# source_node.flat_area_altitude_m * 100 / 10)  # *100 car toutes les unités doivent être en 100ème dans la scene, puis /10 car pour des raisons de précision numérique dans la scene 3d (banding sur le terrain quand on s'éloigne de l'origine, nombres flottants trop grands), les distances sont divisées par 10 dans le pickle
			.03 * 100 / 10,
			# *100 car toutes les unités doivent être en 100ème dans la scene, puis /10 car pour des raisons de précision numérique (float16), les distances sont divisées par 10 dans le pickle
			.2,  # limite exclue: 0.39, ensuite ça fait une terrasse... pourquoi??
			self.should_output_console_message)
		# for i in range(nbVertsCôté):
		# 	for j in range(nbVertsCôté):
		# 		# mathutils.noise.ridged_multi_fractal((0, 0, 0), 0.5, 2, 5, 0, 10)
		# 		verts_ravel[i * 3 * nbVertsCôté + j * 3 + 2] = heightmap_np[final_heightmap_corner_pos0 + i, final_heightmap_corner_pos1 + j] * .1
		# 		# verts_ravel[0] = 0
		if self.should_output_console_message: start_time = print_time("Updated np verts", start_time)  # ===================================

		terrain_mesh.vertices.foreach_set("co", np.ravel(verts_np))
		if self.should_output_console_message: start_time = print_time("Updated mesh verts", start_time)  # ===================================

		terrain_mesh.update()
		if self.should_output_console_message: start_time = print_time("Updated mesh", start_time)  # ===================================

		# Terrain material
		try: bpy.data.materials.remove(bpy.data.materials[terrain_datablocks_name])
		except: pass
		# try:
		# 	terrain_material = bpy.data.materials[terrain_datablocks_name]
		# except KeyError:
		with bpy.data.libraries.load(str(utils.get_addon_dir() / "lib.blend"), link=False) as (data_from, data_to):
			data_to.materials = ["SC Terrain"]
		terrain_material = bpy.data.materials["SC Terrain"]
		terrain_material.name = terrain_datablocks_name
		self.update_terrain_material()
		terrain_material.node_tree.nodes["Underwater texture"].image = bpy.data.images.load(
			str(Path(utils.get_terrains_data_dir() / "Textures" / "land dry 1.jpg")))
		terrain_material.node_tree.nodes["Bump"].image = bpy.data.images.load(
			str(Path(utils.get_terrains_data_dir() / "Textures" / "slopes bump map.exr")))
		# Répétition de la texture = taille du mesh en mètres divisé par taille de la texture en mètres
		terrain_material.node_tree.nodes["Textures scale"].inputs["Scale"].default_value = int(self.mesh_resolution) * 10 / 6000
		if self.should_output_console_message: start_time = print_time("Loaded terrain material", start_time)  # ===================================
		try:
			terrain_mesh.materials[0] = terrain_material
		except IndexError:
			terrain_mesh.materials.append(terrain_material)
		if self.should_output_console_message: start_time = print_time("Terrain material created", start_time)  # ===================================

		# Water
		# On nettoie toutes les données avant
		nom_datablocks_eau = source_node.get_water_datablocks_name()
		try: bpy.data.materials.remove(bpy.data.materials[nom_datablocks_eau])
		except: pass
		try: bpy.data.meshes.remove(bpy.data.meshes[nom_datablocks_eau])
		except: pass
		try: bpy.data.objects.remove(bpy.data.objects[nom_datablocks_eau])
		except: pass
		with bpy.data.libraries.load(str(utils.get_addon_dir() / "lib.blend"), link=False) as (data_from, data_to):
			data_to.objects = ["SC Water"]
		water_ob = bpy.data.objects["SC Water"]
		context.scene.collection.objects.link(water_ob)
		water_ob.name = nom_datablocks_eau
		water_ob.data.name = nom_datablocks_eau
		water_ob.data.materials[0].name = nom_datablocks_eau
		water_ob.location = 0, 0, 0
		water_ob.scale = (float(source_node.mesh_resolution),
						  float(source_node.mesh_resolution),
						  abs(source_node.min_height_m))
		water_ob.parent = terrain_ob
		source_node.update_water_material()
		if self.should_output_console_message: start_time = print_time("Water created", start_time)  # ===================================

		# Populations
		# Nettoyage
		terrain_ob.vertex_groups.clear()
		terrain_ob.modifiers.clear()
		terrain_ob.modifiers.clear()  # 2x à cause d'un bug dans Blender 2.91.0

		# données communes à toutes les pop
		distribution_noise_offset = mathutils.Vector((random.uniform(-100, 100), random.uniform(-100, 100), random.uniform(-100, 100)))

		collection_nb = -1
		for arbres_nom_collection, arbres_collection_données in populations_params.items():
			arbres_collection_données: Population_details = arbres_collection_données
			collection_nb += 1  # pour avoir l'indice pour accéder aux weight layers dans les verts
			try:
				trees_collection = bpy.data.collections[arbres_nom_collection]
			except:
				with bpy.data.libraries.load(str(utils.get_vegetation_data_dir() / "trees baked.blend"), link=True) as (data_from, data_to):
					data_to.collections = [arbres_nom_collection]
				trees_collection = bpy.data.collections[arbres_nom_collection]
			try:
				bpy.context.scene.collection.children.link(trees_collection)
			except: pass
			trees_collection = trees_collection.make_local()  # il faut le rendre local APRES l'avoir ajouté à la scene, sinon l'objet ne veut pas être visible dans les rendus
			trees_collection.hide_render = False
			for tree_ob in trees_collection.objects:
				tree_ob = tree_ob.make_local()
				tree_ob.scale = .1, .1, .1  # .1 car l'objet terrain est au 1/10eme et pas au 1/100eme
				tree_ob.parent = terrain_ob  # met l'objet enfant à la même scale que le terrain
				tree_ob.rotation_mode = "XYZ"
				tree_ob.rotation_euler = 0, math.radians(90), 0
				tree_ob.location = 0, 0, 0
				tree_ob.hide_render = False

			# Place l'arbre sur le terrain avec des particules
			# Nettoyage
			try: bpy.data.particles.remove(bpy.data.particles[arbres_nom_collection])
			except: pass

			terrain_ob.modifiers.new(arbres_nom_collection, "PARTICLE_SYSTEM")  # anki création système particule sur objet
			terrain_ob_particles_system_trees = terrain_ob.particle_systems[arbres_nom_collection]
			#particles_settings = bpy.data.particles[arbres_nom_collection]
			particles_settings = terrain_ob_particles_system_trees.settings
			# particles_settings.count = 10000
			terrain_ob_particles_system_trees.seed = random.randint(0, 999999)
			particles_settings.frame_start = 0
			particles_settings.frame_end = 0
			particles_settings.lifetime = 999999
			particles_settings.distribution = "RAND"
			particles_settings.use_even_distribution = False
			particles_settings.normal_factor = 0  # vitesse initiale
			particles_settings.use_rotations = True
			particles_settings.rotation_mode = "GLOB_Z"
			particles_settings.phase_factor_random = 2
			particles_settings.physics_type = "NO"
			particles_settings.render_type = "COLLECTION"
			particles_settings.particle_size = 1
			particles_settings.size_random = 0.333
			particles_settings.instance_collection = trees_collection
			particles_settings.use_collection_pick_random = True
			particles_settings.show_unborn = True
			particles_settings.use_dead = True
			particles_settings.display_method = "DOT"
			particles_settings.use_rotation_instance = True
			particles_settings.display_percentage = 1
			particles_settings.display_size = arbres_collection_données.dot_display_size

			# limite la distribution des arbres à la surface émergée uniquement

			# creation de vertex weight layer pour les arbres
			vertex_group = terrain_ob.vertex_groups.new(name=arbres_nom_collection)
			# pour chaque vert, on doit obtenir son indice, et son altitude
			# for i in range(res_heightmap):
			# 	for j in range(res_heightmap):
			# 		pass
			vertex_group.add([i * nbVertsCôté + j for i in range(nbVertsCôté) for j in range(nbVertsCôté)], 0, "REPLACE")
			# print(arbres_nom_collection)

			pop_cache: Population_cache = Population_cache.get(terrain_datablocks_name, arbres_nom_collection)
			pop_cache.total_verts_covered_by_trees = 0
			for v in terrain_mesh.vertices:
				if v.co[2] >= .3:  # arbres au-delà de 3m d'altitude
					noise_value = mathutils.noise.fractal(
						distribution_noise_offset + v.co * .1 / self.trees_distribution_noise_size,
						(1 - self.trees_distribution_noise_roughness / 100) * 1,
						2,
						6,
						noise_basis='BLENDER') + ((self.trees_coverage - 50) / 50)
					# noise_value *= (20 - v.co[2]) / 19.7 # diminution de la densité avec l'altitude
					noise_value -= 1 - arbres_collection_données.distribution_percentage  # diminution de la densité selon le type d'arbre dans la collection en cours
					v.groups[collection_nb].weight = noise_value
					if noise_value > 0:
						pop_cache.total_verts_covered_by_trees += noise_value
				# self.total_verts_covered_by_trees += noise_value
				else:
					v.groups[collection_nb].weight = 0

			terrain_ob_particles_system_trees.vertex_group_density = arbres_nom_collection
		self.update_populations()
		if self.should_output_console_message: start_time = print_time("Added trees population", start_time)  # ===================================

		################################ Scatter city
		city_algo_to_use = "BETA 2"
		if "BETA 3" == city_algo_to_use:
			# BETA 3: essai de mette la ville à l'échelle 1 au lieu de .01, mais j'ai des soucis avec les heights
			city_datablock_name = terrain_datablocks_name + " city"
			try:
				city_mesh = bpy.data.meshes[city_datablock_name]
				city_mesh.clear_geometry()
			except KeyError:
				city_mesh = bpy.data.meshes.new(city_datablock_name)
			try:
				city_ob = bpy.data.objects[city_datablock_name]
				city_ob.data = city_mesh
			except KeyError:
				city_ob = bpy.data.objects.new(city_datablock_name, city_mesh)
				context.scene.collection.objects.link(city_ob)
			city_ob.parent = terrain_ob
			city_ob.scale = .1, .1, .1
			city_ob.rotation_mode = "XYZ"

			# print(random_terrain_rotation_x90_deg)
			city_ob.rotation_euler = 0, 0, math.radians(-random_terrain_rotation_x90_deg * 90)
			# materials
			try:
				facade_mat = bpy.data.materials["Scatter city façade 6"]
			except:
				with bpy.data.libraries.load(str(utils.get_city_data_dir() / "models" / "library.blend"), link=True) as (data_from, data_to):
					data_to.materials = ["Scatter city façade 6"]
				facade_mat = bpy.data.materials["Scatter city façade 6"]
			try:
				roof_mat = bpy.data.meshes["Scatter city roof"]
			except:
				with bpy.data.libraries.load(str(utils.get_city_data_dir() / "models" / "library.blend"), link=True) as (data_from, data_to):
					data_to.materials = ["Scatter city roof"]
				roof_mat = bpy.data.materials["Scatter city roof"]
			verts = []
			faces = []
			uvs_per_face = []
			material_indices_per_face = []
			buildings_colors_per_loop = []
			# limite_xy_world_space = int(self.mesh_resolution) * 10 * .01 / 2
			height_below_center = .1
			mesh_res_int = int(self.mesh_resolution)

			for buildingNb in range(int(mesh_res_int / 2) ** 2):
				pos_x_in_heightmap = random.randint(0, mesh_res_int - 1)
				pos_y_in_heightmap = random.randint(0, mesh_res_int - 1)
				pos_x_world = pos_x_in_heightmap * 10 - (mesh_res_int * 10 / 2) + random.uniform(-50, 50)
				pos_y_world = pos_y_in_heightmap * 10 - (mesh_res_int * 10 / 2) + random.uniform(-50, 50)
				# terrain_height_at_building_center = verts_np[pos_x_in_heightmap, pos_y_in_heightmap, 2]

				# noise_pos = (pos_x_world / 4, pos_y_world / 4, 0)
				noise_pos = (pos_x_world / 400, pos_y_world / 400, 0)
				noise_value = mathutils.noise.fractal(
					noise_pos,  # position
					2,  # H
					2,  # lacunarity,
					8,  # octaves
					noise_basis="CELLNOISE"
				)
				noise_value += mathutils.noise.fractal(
					noise_pos,  # position
					2,  # H
					2,  # lacunarity,
					8,  # octaves
					noise_basis="BLENDER"
				) * 3
				noise_value -= ((50 - self.scatter_city_buildings_amount_percent) / 100 * 10)
				# x10 ↑ trouvé par tatonnement, et augmenter la quantité possible de batis en plus ou en moins
				# noise_value = min(1, noise_value)
				if noise_value <= -1:
					continue

				size_build_x_world = max(10, random.normalvariate(noise_value * 10, 20))
				size_build_y_world = max(10, random.normalvariate(noise_value * 10, 20))
				# height = max(.02, (random.normalvariate((noise_value / 3) ** 3, noise_value / 3)).real)
				height = max(2, (random.normalvariate((noise_value * 10 * self.city_buildings_height_modifier) ** 2,
													  noise_value * 10 * self.city_buildings_height_modifier)).real)

				# min / max ratios
				buildings_ratio_xy = 2
				random_direction = random.choice(['x', 'y'])
				buildings_ratio_z = .25, 20
				if random_direction == 'x':
					if size_build_y_world < size_build_x_world / buildings_ratio_xy:
						size_build_y_world = size_build_x_world / buildings_ratio_xy
					elif size_build_y_world > size_build_x_world * buildings_ratio_xy:
						size_build_y_world = size_build_x_world * buildings_ratio_xy
				else:
					if size_build_x_world < size_build_y_world / buildings_ratio_xy:
						size_build_x_world = size_build_y_world / buildings_ratio_xy
					elif size_build_x_world > size_build_y_world * buildings_ratio_xy:
						size_build_x_world = size_build_y_world * buildings_ratio_xy
				# if x is shorter side
				if size_build_x_world < size_build_y_world:
					# building cannot be shorter than the largest side of the building * min ratio
					if height < size_build_y_world * buildings_ratio_z[0]:
						height = size_build_y_world * buildings_ratio_z[0]
					# building cannot be taller than the smallest side of the building * max ratio
					elif height > size_build_x_world * buildings_ratio_z[1]:
						height = size_build_x_world * buildings_ratio_z[1]
				else:
					# building cannot be shorter than the largest side of the building * min ratio
					if height < size_build_x_world * buildings_ratio_z[0]:
						height = size_build_x_world * buildings_ratio_z[0]
					# building cannot be taller than the smallest side of the building * max ratio
					elif height > size_build_y_world * buildings_ratio_z[1]:
						height = size_build_y_world * buildings_ratio_z[1]

				half_size_build_x_world = size_build_x_world / 2
				half_size_build_y_world = size_build_y_world / 2
				# mult = 10
				mult_world_to_grid_space = .1  # *.1 et pas *.01 parce que la grille est tous les 10 mètres en world space
				# half_terrain_world_size = mesh_res_int / 2 * .1
				half_terrain_world_size = mesh_res_int / 2 * 10
				building_corners_pos = (
					(int((pos_x_world - half_size_build_x_world + half_terrain_world_size) * mult_world_to_grid_space),
					 int((pos_y_world - half_size_build_y_world + half_terrain_world_size) * mult_world_to_grid_space)),
					(int((pos_x_world - half_size_build_x_world + half_terrain_world_size) * mult_world_to_grid_space),
					 int((pos_y_world + half_size_build_y_world + half_terrain_world_size) * mult_world_to_grid_space)),
					(int((pos_x_world + half_size_build_x_world + half_terrain_world_size) * mult_world_to_grid_space),
					 int((pos_y_world - half_size_build_y_world + half_terrain_world_size) * mult_world_to_grid_space)),
					(int((pos_x_world + half_size_build_x_world + half_terrain_world_size) * mult_world_to_grid_space),
					 int((pos_y_world + half_size_build_y_world + half_terrain_world_size) * mult_world_to_grid_space)),
				)

				building_on_flat_ground = True
				for corner_pos in building_corners_pos:
					if corner_pos[0] < 0 or corner_pos[1] < 0:
						building_on_flat_ground = False
						break

					try:
						terrain_height_at_corner = verts_np[corner_pos[0], corner_pos[1], 2]
					except IndexError:
						building_on_flat_ground = False
						break
					# if not (.29 < terrain_height_at_corner < .31):
					if not (.29 < terrain_height_at_corner < .31):
						building_on_flat_ground = False
						break
				if not building_on_flat_ground:
					continue

				# pos_z_world = .03
				pos_z_world = 3
				total_verts = len(verts)
				# Faces -------------------------------------------------------------------------------------
				faces.extend([
					(total_verts + 0, total_verts + 1, total_verts + 2, total_verts + 3),  # -y
					(total_verts + 1, total_verts + 6, total_verts + 5, total_verts + 2),  # +x
					(total_verts + 6, total_verts + 7, total_verts + 4, total_verts + 5),  # +y
					(total_verts + 7, total_verts + 0, total_verts + 3, total_verts + 4),  # -x
					(total_verts + 2, total_verts + 5, total_verts + 4, total_verts + 3),  # top face
				])
				# building_color_r = random.uniform(.2, 1)
				# building_color_g = random.uniform(.2, 1)
				# building_color_b = random.uniform(.2, 1)
				building_color_h = min(.1, max(0, random.normalvariate(0, .05)))
				building_color_s = min(.15, max(0, random.normalvariate(0, .15)))
				building_color_v = min(1, max(0, random.normalvariate(1, .5)))
				c = mathutils.Color()
				c.hsv = building_color_h, building_color_s, building_color_v
				# building_color_tuple = building_color_r, building_color_r, building_color_r, 1
				building_color_tuple = c[0], c[1], c[2], 1
				buildings_colors_per_loop.extend([building_color_tuple] * 4 * 5)  # anki
				# Verts pos -------------------------------------------------------------------------------------
				# vert 0 = -x, -y, 0
				# vert 1 = +x, -y, 0
				# vert 2 = +x, -y, +z
				# vert 3 = -x, -y, +z
				# vert 4 = -x, +y, +z
				# vert 5 = +x, +y, +z
				# vert 6 = +x, +y, 0
				# vert 7 = -x, +y, 0
				verts.extend([
					(pos_x_world - half_size_build_x_world, pos_y_world - half_size_build_y_world, pos_z_world - height_below_center),
					(pos_x_world + half_size_build_x_world, pos_y_world - half_size_build_y_world, pos_z_world - height_below_center),
					(pos_x_world + half_size_build_x_world, pos_y_world - half_size_build_y_world, pos_z_world + height),
					(pos_x_world - half_size_build_x_world, pos_y_world - half_size_build_y_world, pos_z_world + height),
					(pos_x_world - half_size_build_x_world, pos_y_world + half_size_build_y_world, pos_z_world + height),
					(pos_x_world + half_size_build_x_world, pos_y_world + half_size_build_y_world, pos_z_world + height),
					(pos_x_world + half_size_build_x_world, pos_y_world + half_size_build_y_world, pos_z_world - height_below_center),
					(pos_x_world - half_size_build_x_world, pos_y_world + half_size_build_y_world, pos_z_world - height_below_center),
				])
				# Verts UVs -------------------------------------------------------------------------------------
				uv_pos_x = random.uniform(-.5, .5)
				uv_pos_y = random.uniform(-.5, .5)
				uv_scale = .1
				uvs_per_face.extend([
					((uv_pos_x + 0, uv_pos_y + 0),
					 (uv_pos_x + size_build_x_world * uv_scale, uv_pos_y + 0),
					 (uv_pos_x + size_build_x_world * uv_scale, uv_pos_y + height * uv_scale),
					 (uv_pos_x + 0, uv_pos_y + height * uv_scale)),
					((uv_pos_x + 0, uv_pos_y + 0),
					 (uv_pos_x + size_build_y_world * uv_scale, uv_pos_y + 0),
					 (uv_pos_x + size_build_y_world * uv_scale, uv_pos_y + height * uv_scale),
					 (uv_pos_x + 0, uv_pos_y + height * uv_scale)),
					((uv_pos_x + 0, uv_pos_y + 0),
					 (uv_pos_x + size_build_x_world * uv_scale, uv_pos_y + 0),
					 (uv_pos_x + size_build_x_world * uv_scale, uv_pos_y + height * uv_scale),
					 (uv_pos_x + 0, uv_pos_y + height * uv_scale)),
					((uv_pos_x + 0, uv_pos_y + 0),
					 (uv_pos_x + size_build_y_world * uv_scale, uv_pos_y + 0),
					 (uv_pos_x + size_build_y_world * uv_scale, uv_pos_y + height * uv_scale),
					 (uv_pos_x + 0, uv_pos_y + height * uv_scale)),
				])
				uvs_per_face.append((
					(uv_pos_x + 0, uv_pos_y + 0),
					(uv_pos_x + size_build_y_world * uv_scale, uv_pos_y + 0),
					(uv_pos_x + size_build_y_world * uv_scale, uv_pos_y + size_build_x_world * uv_scale),
					(uv_pos_x + 0, uv_pos_y + size_build_x_world * uv_scale)
				))
				material_indices_per_face.extend([0] * 4)
				material_indices_per_face.append(1)

			# Create mesh
			city_mesh.from_pydata(verts, [], faces)

			# Apply UVs
			uv_layer_name = "UV"
			city_mesh.uv_layers.new(name=uv_layer_name)
			for polygon_nb, polygon in enumerate(city_mesh.polygons):  # anki
				for vert_nb, loop_index in enumerate(polygon.loop_indices):
					polygon_vertex_uv = uvs_per_face[polygon_nb][vert_nb]
					city_mesh.uv_layers[uv_layer_name].data[loop_index].uv = polygon_vertex_uv

			# set material indices for each face
			max_material_index = -1  # anki
			for polygon_nb, polygon in enumerate(city_mesh.polygons):
				polygon.material_index = material_indices_per_face[polygon_nb]  # anki
				if polygon.material_index > max_material_index:
					max_material_index = polygon.material_index
			bpy.context.view_layer.objects.active = city_ob
			missing_slots = (max_material_index + 1) - len(city_ob.material_slots)
			for materiel_index in range(missing_slots):
				bpy.ops.object.material_slot_add()  # anki
			city_mesh.materials[0] = facade_mat
			city_mesh.materials[1] = roof_mat

			# vertex colors
			vert_color_layer_name = "Buildings colors"
			# print("len(buildings_colors_per_loop)=", len(buildings_colors_per_loop))
			vert_colors_layer = city_mesh.vertex_colors.new(name=vert_color_layer_name)
			for polygon_nb, polygon in enumerate(city_mesh.polygons):
				for loop_index in polygon.loop_indices:
					# vert_colors_layer.data[loop_index].color = 1, 0, 0, 0
					# print("loop_index=", loop_index)
					# try:
					vert_colors_layer.data[loop_index].color = buildings_colors_per_loop[loop_index]
		# except:
		# 	pass
		elif "BETA 2" == city_algo_to_use:
			# BETA 2: fonctionne bien, mais à l'échelle .01
			city_datablock_name = terrain_datablocks_name + " city"
			try:
				city_mesh = bpy.data.meshes[city_datablock_name]
				city_mesh.clear_geometry()
			except KeyError:
				city_mesh = bpy.data.meshes.new(city_datablock_name)
			try:
				city_ob = bpy.data.objects[city_datablock_name]
				city_ob.data = city_mesh
			except KeyError:
				city_ob = bpy.data.objects.new(city_datablock_name, city_mesh)
				context.scene.collection.objects.link(city_ob)
			city_ob.rotation_mode = "XYZ"
			# print(random_terrain_rotation_x90_deg)
			city_ob.rotation_euler = 0, 0, math.radians(-random_terrain_rotation_x90_deg * 90)
			# materials
			try:
				facade_mat = bpy.data.materials["Scatter city façade 6"]
			except:
				with bpy.data.libraries.load(str(utils.get_city_data_dir() / "models" / "library.blend"), link=True) as (data_from, data_to):
					data_to.materials = ["Scatter city façade 6"]
				facade_mat = bpy.data.materials["Scatter city façade 6"]
			try:
				roof_mat = bpy.data.meshes["Scatter city roof"]
			except:
				with bpy.data.libraries.load(str(utils.get_city_data_dir() / "models" / "library.blend"), link=True) as (data_from, data_to):  # anki
					data_to.materials = ["Scatter city roof"]
				roof_mat = bpy.data.materials["Scatter city roof"]
			verts = []
			faces = []
			uvs_per_face = []
			material_indices_per_face = []
			buildings_colors_per_loop = []
			# limite_xy_world_space = int(self.mesh_resolution) * 10 * .01 / 2
			height_below_center = .1
			mesh_res_int = int(self.mesh_resolution)

			for buildingNb in range(int(mesh_res_int / 2) ** 2):
				pos_x_in_heightmap = random.randint(0, mesh_res_int - 1)
				pos_y_in_heightmap = random.randint(0, mesh_res_int - 1)
				pos_x_world = pos_x_in_heightmap * .1 - (mesh_res_int * .1 / 2) + random.uniform(-.5, .5)
				pos_y_world = pos_y_in_heightmap * .1 - (mesh_res_int * .1 / 2) + random.uniform(-.5, .5)
				# terrain_height_at_building_center = verts_np[pos_x_in_heightmap, pos_y_in_heightmap, 2]

				noise_pos = (pos_x_world / 4, pos_y_world / 4, 0)
				noise_value = mathutils.noise.fractal(
					noise_pos,  # position
					2,  # H
					2,  # lacunarity,
					8,  # octaves
					noise_basis="CELLNOISE"
				)
				noise_value += mathutils.noise.fractal(
					noise_pos,  # position
					2,  # H
					2,  # lacunarity,
					8,  # octaves
					noise_basis="BLENDER"
				) * 3
				noise_value -= ((50 - self.scatter_city_buildings_amount_percent) / 100 * 10)
				# noise_value = min(1, noise_value)
				if noise_value <= -1:
					continue

				size_build_x_world = max(.1, random.normalvariate(noise_value * .1, .2))
				size_build_y_world = max(.1, random.normalvariate(noise_value * .1, .2))
				# height = max(.02, (random.normalvariate((noise_value / 3) ** 3, noise_value / 3)).real)
				height = max(.02, (random.normalvariate((noise_value * self.city_buildings_height_modifier / 10) ** 2,
														noise_value * self.city_buildings_height_modifier / 10)).real)

				# min / max ratios
				buildings_ratio_xy = 2
				random_direction = random.choice(['x', 'y'])
				buildings_ratio_z = .25, 20
				if random_direction == 'x':
					if size_build_y_world < size_build_x_world / buildings_ratio_xy:
						size_build_y_world = size_build_x_world / buildings_ratio_xy
					elif size_build_y_world > size_build_x_world * buildings_ratio_xy:
						size_build_y_world = size_build_x_world * buildings_ratio_xy
				else:
					if size_build_x_world < size_build_y_world / buildings_ratio_xy:
						size_build_x_world = size_build_y_world / buildings_ratio_xy
					elif size_build_x_world > size_build_y_world * buildings_ratio_xy:
						size_build_x_world = size_build_y_world * buildings_ratio_xy
				# if x is shorter side
				if size_build_x_world < size_build_y_world:
					# building cannot be shorter than the largest side of the building * min ratio
					if height < size_build_y_world * buildings_ratio_z[0]:
						height = size_build_y_world * buildings_ratio_z[0]
					# building cannot be taller than the smallest side of the building * max ratio
					elif height > size_build_x_world * buildings_ratio_z[1]:
						height = size_build_x_world * buildings_ratio_z[1]
				else:
					# building cannot be shorter than the largest side of the building * min ratio
					if height < size_build_x_world * buildings_ratio_z[0]:
						height = size_build_x_world * buildings_ratio_z[0]
					# building cannot be taller than the smallest side of the building * max ratio
					elif height > size_build_y_world * buildings_ratio_z[1]:
						height = size_build_y_world * buildings_ratio_z[1]

				half_size_build_x_world = size_build_x_world / 2
				half_size_build_y_world = size_build_y_world / 2
				mult = 10
				half_terrain_world_size = mesh_res_int / 2 * .1
				building_corners_pos = (
					(int((pos_x_world - half_size_build_x_world + half_terrain_world_size) * mult),
					 int((pos_y_world - half_size_build_y_world + half_terrain_world_size) * mult)),
					(int((pos_x_world - half_size_build_x_world + half_terrain_world_size) * mult),
					 int((pos_y_world + half_size_build_y_world + half_terrain_world_size) * mult)),
					(int((pos_x_world + half_size_build_x_world + half_terrain_world_size) * mult),
					 int((pos_y_world - half_size_build_y_world + half_terrain_world_size) * mult)),
					(int((pos_x_world + half_size_build_x_world + half_terrain_world_size) * mult),
					 int((pos_y_world + half_size_build_y_world + half_terrain_world_size) * mult)),
				)

				building_on_flat_ground = True
				for corner_pos in building_corners_pos:
					if corner_pos[0] < 0 or corner_pos[1] < 0:
						building_on_flat_ground = False
						break

					try:
						terrain_height_at_corner = verts_np[corner_pos[0], corner_pos[1], 2]
					except IndexError:
						building_on_flat_ground = False
						break
					if not (.29 < terrain_height_at_corner < .31):
						building_on_flat_ground = False
						break
				if not building_on_flat_ground:
					continue

				pos_z_world = .03
				total_verts = len(verts)
				# Faces -------------------------------------------------------------------------------------
				faces.extend([
					(total_verts + 0, total_verts + 1, total_verts + 2, total_verts + 3),  # -y
					(total_verts + 1, total_verts + 6, total_verts + 5, total_verts + 2),  # +x
					(total_verts + 6, total_verts + 7, total_verts + 4, total_verts + 5),  # +y
					(total_verts + 7, total_verts + 0, total_verts + 3, total_verts + 4),  # -x
					(total_verts + 2, total_verts + 5, total_verts + 4, total_verts + 3),  # top face
				])
				# building_color_r = random.uniform(.2, 1)
				# building_color_g = random.uniform(.2, 1)
				# building_color_b = random.uniform(.2, 1)
				building_color_h = min(.1, max(0, random.normalvariate(0, .05)))
				building_color_s = min(.15, max(0, random.normalvariate(0, .15)))
				building_color_v = min(1, max(0, random.normalvariate(1, .5)))
				c = mathutils.Color()
				c.hsv = building_color_h, building_color_s, building_color_v
				# building_color_tuple = building_color_r, building_color_r, building_color_r, 1
				building_color_tuple = c[0], c[1], c[2], 1
				buildings_colors_per_loop.extend([building_color_tuple] * 4 * 5)  # anki
				# Verts pos -------------------------------------------------------------------------------------
				# vert 0 = -x, -y, 0
				# vert 1 = +x, -y, 0
				# vert 2 = +x, -y, +z
				# vert 3 = -x, -y, +z
				# vert 4 = -x, +y, +z
				# vert 5 = +x, +y, +z
				# vert 6 = +x, +y, 0
				# vert 7 = -x, +y, 0
				verts.extend([
					(pos_x_world - half_size_build_x_world, pos_y_world - half_size_build_y_world, pos_z_world - height_below_center),
					(pos_x_world + half_size_build_x_world, pos_y_world - half_size_build_y_world, pos_z_world - height_below_center),
					(pos_x_world + half_size_build_x_world, pos_y_world - half_size_build_y_world, pos_z_world + height),
					(pos_x_world - half_size_build_x_world, pos_y_world - half_size_build_y_world, pos_z_world + height),
					(pos_x_world - half_size_build_x_world, pos_y_world + half_size_build_y_world, pos_z_world + height),
					(pos_x_world + half_size_build_x_world, pos_y_world + half_size_build_y_world, pos_z_world + height),
					(pos_x_world + half_size_build_x_world, pos_y_world + half_size_build_y_world, pos_z_world - height_below_center),
					(pos_x_world - half_size_build_x_world, pos_y_world + half_size_build_y_world, pos_z_world - height_below_center),
				])
				# Verts UVs -------------------------------------------------------------------------------------
				uv_pos_x = random.uniform(-.5, .5)
				uv_pos_y = random.uniform(-.5, .5)
				uv_scale = .1
				uvs_per_face.extend([
					((uv_pos_x + 0, uv_pos_y + 0),
					 (uv_pos_x + size_build_x_world * uv_scale, uv_pos_y + 0),
					 (uv_pos_x + size_build_x_world * uv_scale, uv_pos_y + height * uv_scale),
					 (uv_pos_x + 0, uv_pos_y + height * uv_scale)),
					((uv_pos_x + 0, uv_pos_y + 0),
					 (uv_pos_x + size_build_y_world * uv_scale, uv_pos_y + 0),
					 (uv_pos_x + size_build_y_world * uv_scale, uv_pos_y + height * uv_scale),
					 (uv_pos_x + 0, uv_pos_y + height * uv_scale)),
					((uv_pos_x + 0, uv_pos_y + 0),
					 (uv_pos_x + size_build_x_world * uv_scale, uv_pos_y + 0),
					 (uv_pos_x + size_build_x_world * uv_scale, uv_pos_y + height * uv_scale),
					 (uv_pos_x + 0, uv_pos_y + height * uv_scale)),
					((uv_pos_x + 0, uv_pos_y + 0),
					 (uv_pos_x + size_build_y_world * uv_scale, uv_pos_y + 0),
					 (uv_pos_x + size_build_y_world * uv_scale, uv_pos_y + height * uv_scale),
					 (uv_pos_x + 0, uv_pos_y + height * uv_scale)),
				])
				uvs_per_face.append((
					(uv_pos_x + 0, uv_pos_y + 0),
					(uv_pos_x + size_build_y_world * uv_scale, uv_pos_y + 0),
					(uv_pos_x + size_build_y_world * uv_scale, uv_pos_y + size_build_x_world * uv_scale),
					(uv_pos_x + 0, uv_pos_y + size_build_x_world * uv_scale)
				))
				material_indices_per_face.extend([0] * 4)
				material_indices_per_face.append(1)

			# Create mesh
			city_mesh.from_pydata(verts, [], faces)

			# Apply UVs
			uv_layer_name = "UV"
			city_mesh.uv_layers.new(name=uv_layer_name)
			for polygon_nb, polygon in enumerate(city_mesh.polygons):  # anki
				for vert_nb, loop_index in enumerate(polygon.loop_indices):
					polygon_vertex_uv = uvs_per_face[polygon_nb][vert_nb]
					city_mesh.uv_layers[uv_layer_name].data[loop_index].uv = polygon_vertex_uv

			# set material indices for each face
			max_material_index = -1  # anki
			for polygon_nb, polygon in enumerate(city_mesh.polygons):
				polygon.material_index = material_indices_per_face[polygon_nb]  # anki
				if polygon.material_index > max_material_index:
					max_material_index = polygon.material_index
			bpy.context.view_layer.objects.active = city_ob
			missing_slots = (max_material_index + 1) - len(city_ob.material_slots)
			for materiel_index in range(missing_slots):
				bpy.ops.object.material_slot_add()  # anki
			city_mesh.materials[0] = facade_mat
			city_mesh.materials[1] = roof_mat

			# vertex colors
			vert_color_layer_name = "Buildings colors"
			# print("len(buildings_colors_per_loop)=", len(buildings_colors_per_loop))
			vert_colors_layer = city_mesh.vertex_colors.new(name=vert_color_layer_name)
			for polygon_nb, polygon in enumerate(city_mesh.polygons):
				for loop_index in polygon.loop_indices:
					# vert_colors_layer.data[loop_index].color = 1, 0, 0, 0
					# print("loop_index=", loop_index)
					# try:
					vert_colors_layer.data[loop_index].color = buildings_colors_per_loop[loop_index]
		# except:
		# 	pass

		if self.should_output_console_message: start_time = print_time("Created scatter city", start_time)  # ===================================

		################################ Clouds
		if self.should_add_clouds:
			clouds_datablock_name = self.get_clouds_datablocks_name()
			# Clouds volume datablock
			try:
				clouds_volume = bpy.data.volumes[clouds_datablock_name]
			except KeyError:
				clouds_volume = bpy.data.volumes.new(clouds_datablock_name)
			if int(self.mesh_resolution) <= 512:
				clouds_volume.filepath = str(utils.get_terrains_data_dir() / "Clouds" / "cumulus r6000 v250 vr1143.vdb")
			else:
				clouds_volume.filepath = str(utils.get_terrains_data_dir() / "Clouds" / "cumulus r11000 v500 vr2213.vdb")
			# Clouds object
			try:
				clouds_ob = bpy.data.objects[clouds_datablock_name]
				clouds_ob.data = clouds_volume
			except KeyError:
				clouds_ob = bpy.data.objects.new(clouds_datablock_name, clouds_volume)
				context.scene.collection.objects.link(clouds_ob)
			clouds_ob.parent = terrain_ob
			clouds_ob.rotation_mode = "XYZ"
			clouds_ob.rotation_euler = math.radians(90), 0, 0
			clouds_ob.scale = .1, .1, .1  # .1 car scalé en parent space, et l'objet terrain est en scale .1 déjà
			# Clouds material
			# remove default material
			clouds_volume.materials.clear()
			try:
				clouds_mat = bpy.data.materials["SC Clouds"]
			except KeyError:
				with bpy.data.libraries.load(str(utils.get_terrains_data_dir() / "clouds" / "clouds materials.blend"), link=False) as (data_from, data_to):
					data_to.materials = ["SC Clouds"]
				clouds_mat = bpy.data.materials["SC Clouds"]
			clouds_volume.materials.append(clouds_mat)
			self.update_clouds()
			if self.should_output_console_message: start_time = print_time("Created clouds", start_time)  # ===================================

		# Nettoyage données orphelines
		for img in bpy.data.images:
			if img.users == 0:
				bpy.data.images.remove(img)

		if self.should_output_console_message: print_time("Total time", overall_start_time, long_time_s=1)  # ===================================
		return [DATA_Terrain(mesh_object=terrain_ob)]

	class SC_OT_Randomize_noise_offset(bpy.types.Operator):
		bl_idname = 'sc.terrain_node_randomize_noise_offset'
		bl_description = 'Set a different noise offset randomly'
		bl_label = 'Randomize'
		source_node_path: bpy.props.StringProperty()

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

	class SC_OT_Set_mesh_resolution(bpy.types.Operator):
		bl_idname = 'sc.terrain_node_set_mesh_resolution'
		bl_description = 'Set mesh resolution'
		bl_label = 'M'
		source_node_path: bpy.props.StringProperty()
		should_update_preview: bpy.props.BoolProperty(default=False)
		resolution: bpy.props.IntProperty(min=1)

		def execute(self, context):
			source_node: Terrain1 = eval(self.source_node_path)
			res = source_node.mesh_resolution_final
			if self.should_update_preview:
				res = source_node.mesh_resolution_preview
			res[0] = res[1] = self.resolution
			return {'FINISHED'}

	def get_terrain_datablocks_name(self) -> str:
		# print("id data = ", self.id_data.name)
		return "SC " + self.id_data.name + " " + (self.label if self.label else self.name)

	def get_water_datablocks_name(self) -> str:
		"""Retourne le nom de l'objet et mesh de l'eau pour ce terrain"""
		return self.get_terrain_datablocks_name() + " water"

	def get_population_datablocks_name(self) -> str:
		return self.get_terrain_datablocks_name() + ""

	def get_clouds_datablocks_name(self) -> str:
		return self.get_terrain_datablocks_name() + " clouds"

	def update_terrain_material(self):
		try:
			terrain_material = bpy.data.materials[self.get_terrain_datablocks_name()]
			terrain_material.node_tree.nodes["Mountains texture"].image = bpy.data.images.load(self.ground_material_slopes_texture_filepath)
			terrain_material.node_tree.nodes["City texture"].image = bpy.data.images.load(self.ground_material_flatarea_texture_filepath)
		except KeyError: pass

	def update_water_material(self):
		try:
			water_mat = bpy.data.materials[self.get_water_datablocks_name()]
		except: return

		# Waves 1
		small_waves_noise_node = water_mat.node_tree.nodes["Small waves noise"]
		small_waves_noise_node.inputs["Scale"].default_value = 1 / self.water_material_waves1_scale_m
		small_waves_vertical_strength_node = water_mat.node_tree.nodes["Small waves vertical strength"]
		small_waves_vertical_strength_node.inputs[1].default_value = self.water_material_waves1_strength / 1000

		# Waves 2
		large_waves_noise_node = water_mat.node_tree.nodes["Large waves noise"]
		large_waves_noise_node.inputs["Scale"].default_value = 1 / self.water_material_waves2_scale_m
		large_waves_vertical_strength_node = water_mat.node_tree.nodes["Large waves vertical strength"]
		large_waves_vertical_strength_node.inputs[1].default_value = self.water_material_waves2_strength / 500

		# Light decay
		# self.water_material_deep_blue_depth_m = self.water_material_transparency_distance_m
		water_colors_per_depth_meters_node = water_mat.node_tree.nodes["Water colors per depth meters"]
		# ↓ -.5 parce que Blender ajoute 0.5, pour je ne sais quelle raison, donc il faut compenser
		# water_colors_per_depth_meters_node.color_ramp.elements[1].position = (self.water_material_transparency_distance_m -
		# 																	  (.005 if self.water_material_transparency_distance_m < .1 else 0)) \
		# 																	 * .8
		water_colors_per_depth_meters_node.color_ramp.elements[1].position = (self.water_material_transparency_distance_m) * .8

		# Transparency
		water_volume_node = water_mat.node_tree.nodes["Water volume"]
		# ↓ 7.5 et 60 car la density doit être 7.5 quand la distance de transparence est 60, et *100 à cause des unités dans la scene
		water_volume_node.inputs["Density"].default_value = 7.5 / (self.water_material_transparency_distance_m * 100 / 80)
		water_surface_bsdf_node = water_mat.node_tree.nodes["Water surface bsdf"]
		water_surface_bsdf_node.inputs["Alpha"].default_value = 0.2 + 0.3 * (80 - self.water_material_transparency_distance_m * 100) / 80

		# Wind
		wind_influence_noise_node = water_mat.node_tree.nodes["Wind influence noise"]
		wind_influence_noise_node.inputs["Scale"].default_value = 1 / self.water_material_wind_effect_size_m
		wind_influence_mult = water_mat.node_tree.nodes["Wind influence mult"]
		wind_influence_mult.inputs[1].default_value = 1 / (self.water_material_wind_transition_scale * 2 / 100)
		waves_power_in_calm_wind_node = water_mat.node_tree.nodes["Waves power in calm wind"]
		waves_power_in_calm_wind_node.inputs[1].default_value = self.water_material_wind_calm_zone_waves_strength / 100

		# Water tint
		surface_color_element = water_colors_per_depth_meters_node.color_ramp.elements[0]
		if self.water_material_tint_type == "NORMAL":
			surface_color_element.color = colorsys.hsv_to_rgb(
				.33 + self.water_material_tint_algae_tropical_nordic / 100 * .27,
				self.water_material_tint_strength / 100,
				.8) + (1,)
		elif self.water_material_tint_type == "MUDDY":
			c = mathutils.Color()
			c.hsv = 0.08, \
					.0 + (self.water_material_tint_muddy_strength / 100) * 1.0, \
					.5 + (1 - self.water_material_tint_muddy_strength / 100) * .4
			surface_color_element.color[0] = c.r
			surface_color_element.color[1] = c.g
			surface_color_element.color[2] = c.b

	def update_clouds(self):
		clouds_datablock_name = self.get_clouds_datablocks_name()
		try:
			clouds_ob = bpy.data.objects[clouds_datablock_name]
		except KeyError:
			return
		if self.should_add_clouds:
			clouds_ob.hide_set(not self.should_display_clouds_in_viewport)
			clouds_ob.hide_render = False
			clouds_ob.location = 0, 0, (self.clouds_altitude_meters * 100 - 1000) * .1  # x100 à cause des scene units

			# material update
			mat_clouds = bpy.data.materials["SC Clouds"]
			# altitude
			node_substract_clouds_height = mat_clouds.node_tree.nodes["Subtract clouds height"]
			node_substract_clouds_height.inputs[1].default_value = self.clouds_altitude_meters  # 10 car altitude à 1000 dans le vdb original
			# density
			node_clouds_density_ramp = mat_clouds.node_tree.nodes["Clouds density ramp"]
			c = .07 * self.clouds_bottom_density / 100
			node_clouds_density_ramp.color_ramp.elements[0].color = c, c, c, 1
			c = .1 * self.clouds_top_density / 100
			node_clouds_density_ramp.color_ramp.elements[1].color = c, c, c, 1
			# whiteness
			node_clouds_color_ramp = mat_clouds.node_tree.nodes["Clouds color ramp"]
			c = .3 + .3 * self.clouds_bottom_whiteness / 100
			node_clouds_color_ramp.color_ramp.elements[0].color = c, c, c, 1
			c = .6 + .4 * self.clouds_top_whiteness / 100
			node_clouds_color_ramp.color_ramp.elements[1].color = c, c, c, 1

		else:
			clouds_ob.hide_set(True)
			clouds_ob.hide_render = True

	class SC_OT_choose_texture(bpy.types.Operator, ImportHelper):
		bl_idname = 'sc_op.choose_texture'
		bl_description = 'Open file browser'
		bl_label = 'Choose texture'
		source_node_path: bpy.props.StringProperty()
		# filename_ext = '.html'
		# filename_ext: bpy.props.StringProperty(
		# 	default=".html",
		# )
		filter_glob: bpy.props.StringProperty(
			default="*.jpg;*.png;*.jpeg;",
			options={'HIDDEN'},
			maxlen=255,  # Max internal buffer length, longer would be clamped.
		)

		def execute(self, context):
			print(self.filepath)
			return {"FINISHED"}

	class SC_OT_Create_mesh_and_object(bpy.types.Operator):
		bl_idname = 'sc.terrain_node_create_mesh_and_object'
		bl_description = 'Create all the meshes and objects related to your terrain'
		bl_label = 'Create terrain'
		source_node_path: bpy.props.StringProperty()
		truc: bpy.props.StringProperty()

		@classmethod
		def poll(cls, context):
			try:
				return context.object.mode == 'OBJECT'
			except AttributeError:
				return True

		def execute(self, context):
			source_node: Terrain1 = eval(self.source_node_path)
			source_node.get_terrains()
			return {'FINISHED'}


# self.report({"ERROR"}, "Something isn't right")
# return {"CANCELLED"}

# class SC_OT_Erode(bpy.types.Operator):
# 	bl_idname = 'sc.terrain_node_erode'
# 	bl_description = 'Erode the terrain'
# 	bl_label = 'Erode'
# 	source_node_path: bpy.props.StringProperty()
#
# 	def execute(self, context):
# 		source_node: Terrain1 = eval(self.source_node_path)
# 		bpy.ops.mesh.sc_eroder(
# 			smooth=source_node.should_shade_smooth,
# 			IterRiver=200)
#
# 		# ajoute les vertex colors
# 		ob = bpy.data.objects[source_node.name]
# 		me = ob.data
# 		color_layer = me.vertex_colors.new(name="Erosion flow map").data
# 		flowrate_weight_map = ob.vertex_groups["flowrate"]
# 		for loop in me.loops:
# 			weight = flowrate_weight_map.weight(loop.vertex_index)
# 			color_layer[loop.index].color = weight, weight, weight, 1
#
# 		me.materials.append(bpy.data.materials["Material"])
#
# 		return {'FINISHED'}


class TerrainShapeFromMapNode(bpy.types.Node, Node, DATA_GETTER_NODE_TerrainShapes):
	bl_idname = 'sc_node_aj0j1s891gsrcnlahx36'
	bl_label = 'Terrain shape from map'

	physical_size_meters: bpy.props.FloatVectorProperty(
		name='Physical size',
		description='In meters. The value on the Z axis means the height from 0 to the value entered, where the input map value is 1.'
					'If the map values go below zero, or beyond 1, the terrain shape will go beyond those values too',
		size=3,
		default=(100, 100, 10),
		min=0)

	def sc_init(self, context):
		# self.width = 300
		# self.inputs.new(SOCKET_Map.__name__, 'Height map')
		self.create_input(SOCKET_Map, is_required=True, label='Height map')
		# self.outputs.new(TerrainShapeSocket.__name__, 'Terrain shape')
		self.create_output(SOCKET_Terrain_Shape)

	def sc_draw_buttons(self, context, layout):
		# if len(self.inputs['Height map'].links) <= 0:
		# 	layout.label(text='Height map needed', icon="ERROR")
		# self.ui_display_doc2(layout)
		layout.prop(self, 'physical_size_meters')

	def _get_terrain_shapes_necessary_data(self, *args, **kwargs):
		input_socket_map: SOCKET_Map = self.inputs[0]
		return input_socket_map.get_input_map(*args, **kwargs)

	@Node.get_data_first
	def get_terrain_shapes(self, source_map: DATA_Map, *args, **kwargs):
		# try:
		# 	source_node: MapGetter = self.inputs[0].links[0].from_node
		# 	source_map = source_node.get_map()
		# except:
		# 	source_map = None
		physical_size_meters = self.physical_size_meters[0], self.physical_size_meters[1], self.physical_size_meters[2]
		return [DATA_Terrain_Shape(source_map, physical_size_meters)]


class TerrainGridMeshNode(bpy.types.Node, Node, DATA_GETTER_NODE_SC_Meshes):
	bl_idname = 'sc_node_ue1d6v2aa3zdvy30qw5m'
	bl_label = 'Terrain grid mesh'

	grid_resolution: bpy.props.IntProperty(
		name='Resolution',
		description='How many vertices on each side the mesh grid should have. More means a denser mesh, which will more closely follow the underlying map,'
					'at the cost of a more heavy mesh',
		default=50,
		min=3)

	def sc_init(self, context):
		self.create_input(SOCKET_Terrain_Shape, is_required=True)
		self.create_output(SOCKET_Meshes)

	def sc_draw_buttons(self, context, layout):
		layout.prop(self, 'grid_resolution')

	def _get_sc_meshes_necessary_data(self, *args, **kwargs):
		input_socket_terrain_shapes: SOCKET_Terrain_Shape = self.inputs[0]
		return input_socket_terrain_shapes.get_terrain_shapes()

	@Node.get_data_first
	def get_sc_meshes(self, terrain_shapes: List[DATA_Terrain_Shape], *args, **kwargs):
		startTime = time.time()
		result_meshes: List[DATA_Mesh] = []
		for i, terrain_shape in enumerate(terrain_shapes):
			bl_mesh = bpy.data.meshes.new("Terrain" + str(i))
			mesh_data = DATA_Mesh(bl_mesh)
			nbVertsCôté = self.grid_resolution
			# totalVerts = nbVertsCôté ** 2
			no_vert_actuel = 0

			# CREATE VERTICES + COLOR
			verts = []
			last_progress_display_time = -math.inf
			# mesh_data.colorLayers['Terrain height color'] = vert_colors = []
			for i in range(nbVertsCôté):
				if time.time() >= last_progress_display_time + .2:
					self.afficher_barre_progression(i / nbVertsCôté / 2, time.time() - startTime)
					last_progress_display_time = time.time()
				for j in range(nbVertsCôté):
					vertHeight = terrain_shape.map.get_value(
						i / (nbVertsCôté - 1) - .0,
						j / (nbVertsCôté - 1) - .0) * terrain_shape.physical_size_meters[2]
					verts.append(((i / (nbVertsCôté - 1) - .5) * terrain_shape.physical_size_meters[0],
								  (j / (nbVertsCôté - 1) - .5) * terrain_shape.physical_size_meters[1],
								  vertHeight))
					# vert_colors.append((0, 1, 0, 1))
					no_vert_actuel += 1

			# CREATE FACES

			# start_time = time.time()
			no_face_actuelle = 0
			uvs_per_face = []
			# total_faces = (nbVertsCôté - 1) ** 2
			faces = []
			uv_size_per_face = 1 / (nbVertsCôté - 1)
			for i in range(nbVertsCôté - 1):
				if time.time() >= last_progress_display_time + .2:
					self.afficher_barre_progression(0.5 + i / nbVertsCôté / 2, time.time() - startTime)
					last_progress_display_time = time.time()
				for j in range(nbVertsCôté - 1):
					no_face_actuelle += 1
					mesh_data.faces_material_indices.append(0)
					uvs_per_face.append((
						(i * uv_size_per_face, j * uv_size_per_face),
						((i + 1) * uv_size_per_face, j * uv_size_per_face),
						((i + 1) * uv_size_per_face, (j + 1) * uv_size_per_face),
						(i * uv_size_per_face, (j + 1) * uv_size_per_face),

					))
					faces.append(
						((i + 0) * nbVertsCôté + j,
						 (i + 1) * nbVertsCôté + j,
						 (i + 1) * nbVertsCôté + j + 1,
						 (i + 0) * nbVertsCôté + j + 1))
			bl_mesh.from_pydata(verts, [], faces)

			# UVs
			bl_mesh.uv_layers.new(name="Terrain")
			for polygon_nb, polygon in enumerate(bl_mesh.polygons):
				for vert_nb, loop_index in enumerate(polygon.loop_indices):
					polygon_vertex_uv = uvs_per_face[polygon_nb][vert_nb]
					bl_mesh.uv_layers["Terrain"].data[loop_index].uv = polygon_vertex_uv

			self.afficher_barre_progression(1, time.time() - startTime)
			result_meshes.append(mesh_data)
		return result_meshes
