extends Node3D

const CHUNK_TYPE_BIOME: int = 0
const CHUNK_TYPE_STRAIGHT_TRACK: int = 1
const CHUNK_TYPE_CURVED_TRACK: int = 2

const CHUNK_GENERATION_FRAME_BUDGET_USEC: int = 2000 #2 ms/frame to generate chunks
const CHUNK_CLEANUP_FRAME_BUDGET_USEC: int = 1000 #1 ms/frame per cleanup
const LAMPPOST_WIRE_FRAME_BUDGET_USEC: int = 1000 #1 ms/frame per i fili dei lampioni
#(about 4 ms o work for frame)
#Se compaiono chunk troppo lentamente davanti al treno:
#aumentare generazione a 3000 o 4000
#Se ci sono micro-scatti:
#abbassare generazione a 1000 o 1500
#Se i fili appaiono in ritardo:
#aumentare wire a 1500 o 2000
#Se il cleanup causa scatti:
#abbassare cleanup a 500

const MAX_CHUNK_UNIQUENESS: int = 5
const RAILWAY_SCENE_DIRECTORY: String = "res://tgcc/chunk/railway/scene"
const RIVER_SIDE_ORDER: Array[String] = ["north", "est", "south", "west"]
const RIVER_DIRECTIONS: Dictionary = {
	"north": Vector2(0.0, -1.0),
	"est": Vector2(1.0, 0.0),
	"south": Vector2(0.0, 1.0),
	"west": Vector2(-1.0, 0.0),
}
const RIVER_NEIGHBOUR_OFFSETS: Dictionary = {
	"north": Vector2i(0, -1),
	"est": Vector2i(1, 0),
	"south": Vector2i(0, 1),
	"west": Vector2i(-1, 0),
}

@export_group("Rails")
@export var rail_path: Path3D #rail path

@export_group("Biomes")
@export var biome_list: Array[Biome] #list of scenes for the biome

@export_group("Grid and Area")
@export var chunk_size: float = 20.0	#size of a cell; default 20 (global position is defined dividing x and z for this value) 
@export var eye_line: int = 3 #cells to be considerated around the train (e.g. 3 => a square of cells from -3 to 3 around train cell)
@export var district_scale: float = 0.05 #noise value to distribuite biome; low values create large area, high values create biome with more variants

@export_group("Lamppost")
@export var lamppost_wire_material: ShaderMaterial 
@export_range(0.01, 1.0) var wire_thickness: float = 0.05
@export var lamppost_dist_factor: int = 10 #max distance of connections

var board: Dictionary = {}
var last_pos_train: Vector2i = Vector2i(999999, 999999) 
var noise_generator: FastNoiseLite 
var altitude_generator: FastNoiseLite
var wire_connections: Dictionary = {}
var chunk_candidate_cache: Dictionary = {} #node cache (metadata)
var prop_candidate_cache: Dictionary = {}
var pending_generation_cells: Array[Vector2i] = []
var pending_cleanup_cells: Array[Vector2i] = []
var pending_wire_cells: Array[Vector2i] = []
var pending_generation_cursor: int = 0
var pending_cleanup_cursor: int = 0
var pending_wire_cursor: int = 0
var pending_wire_lookup: Dictionary = {}
var rail_chunk_catalogue: Dictionary = {} #rails chunk list

var manual_biome: Biome = null

func _ready() -> void:
	if biome_list.is_empty() or rail_path == null: return

	#connect events
	UIEvents.update_rail_chunks.connect(_update_rail_chunks)
	
	#noise generator for biome and altitute
	noise_generator = FastNoiseLite.new()
	noise_generator.noise_type = FastNoiseLite.TYPE_PERLIN
	noise_generator.seed = randi() 
	noise_generator.frequency = district_scale 
	
	altitude_generator = FastNoiseLite.new()
	altitude_generator.noise_type = FastNoiseLite.TYPE_PERLIN
	altitude_generator.seed = randi() 
	altitude_generator.frequency = district_scale * 0.5 
	
	#fill cache with available chunk
	_warm_chunk_candidate_cache()
	_warm_rail_chunk_catalogue()
	#set unique pieces
	_update_set_pieces()

func _update_set_pieces() -> void:
	var set_pieces = get_tree().get_nodes_in_group("set_pieces")
	
	print("🔍 Found ", set_pieces.size(), " in 'set_pieces' group.")
	
	for sp in set_pieces:
		if not "exclusive_biome" in sp or sp.exclusive_biome == "":
			print("⚠️ Set Piece '", sp.name, "' not have exclusive biome -> always visible.")
			continue 

		var local_biome = ""
		if manual_biome != null:
			local_biome = manual_biome.name
		else:
			var grid_x = roundi(sp.global_position.x / chunk_size)
			var grid_z = roundi(sp.global_position.z / chunk_size)
			local_biome = _get_procedural_biome_name(noise_generator.get_noise_2d(grid_x, grid_z))

		print("Analyze: '", sp.name, "' | Need: [", sp.exclusive_biome, "] | Current biome is: [", local_biome, "]")			
		if local_biome == sp.exclusive_biome:
			sp.show()
			sp.process_mode = Node.PROCESS_MODE_INHERIT
			print("   ✅ Temple is visible.")
		else:
			sp.hide()
			sp.process_mode = Node.PROCESS_MODE_DISABLED 
			print("   ❌ Temple is an invisible ghost.")
	
func _destroy_and_regenrate_world() -> void:
	_warm_chunk_candidate_cache()
	_clear_pending_world_work()

	#Turn off old temples
	_update_set_pieces() 
	
	#Destroy all models
	for pos in board.keys():
		var cella = board[pos]
		if not _is_persistent_obstacle(cella):
			if cella.has("node") and is_instance_valid(cella["node"]):
				cella["node"].queue_free()
				
	#Clean board and wire connections
	board.clear()
	wire_connections.clear()
		
	#Create train
	last_pos_train = Vector2i(999999, 999999)
	if rail_path != null and rail_path.train_instance != null:
		var train_pos = rail_path.train_instance.global_position
		var current_pos = Vector2i(roundi(train_pos.x / chunk_size), roundi(train_pos.z / chunk_size))
		_refresh_world(current_pos)

func _process(_delta: float) -> void:
	#based on time budgets the queues are evaluated by frame and not all together
	_drain_cleanup_queue()
	_drain_generation_queue()
	_drain_wire_queue()

func _physics_process(_delta: float) -> void:
	if rail_path == null or rail_path.train_instance == null: return
		
	var train_pos = rail_path.train_instance.global_position
	var grid_x = roundi(train_pos.x / chunk_size)
	var grid_z = roundi(train_pos.z / chunk_size)
	var current_pos = Vector2i(grid_x, grid_z)
	
	if current_pos != last_pos_train:
		last_pos_train = current_pos
		_refresh_world(current_pos)

func collect_all_chunkinfo(root: Node, list: Array[Node]) -> void:
	if root == null: return
	
	if root.has_method("get_rotated_data") or "have_lamppost" in root:
		list.append(root)
		
	for child in root.get_children():
		collect_all_chunkinfo(child, list)

func collect_all_propinfo(root: Node, list: Array[Node]) -> void:
	if root == null: return

	if "available_props" in root:
		list.append(root)

	for child in root.get_children():
		collect_all_propinfo(child, list)

func _warm_chunk_candidate_cache() -> void:
	var unique_scenes: Dictionary = {}

	for biome in biome_list:
		if biome == null:
			continue
		for scene in biome.available_chunks:
			if scene == null:
				continue
			unique_scenes[_get_chunk_scene_cache_key(scene)] = scene

	if manual_biome != null:
		for scene in manual_biome.available_chunks:
			if scene == null:
				continue
			unique_scenes[_get_chunk_scene_cache_key(scene)] = scene

	for scene in unique_scenes.values():
		_get_chunk_scene_metadata(scene)

func _warm_rail_chunk_catalogue() -> void:
	rail_chunk_catalogue.clear()

	var directory := DirAccess.open(RAILWAY_SCENE_DIRECTORY)
	if directory == null:
		return

	directory.list_dir_begin()
	var file_name := directory.get_next()
	while file_name != "":
		if not directory.current_is_dir() and file_name.ends_with(".tscn"):
			var scene_path := "%s/%s" % [RAILWAY_SCENE_DIRECTORY, file_name]
			var scene := load(scene_path) as PackedScene
			if scene != null:
				var chunk_type = _get_chunk_type_from_scene(scene)
				if chunk_type == CHUNK_TYPE_STRAIGHT_TRACK or chunk_type == CHUNK_TYPE_CURVED_TRACK:
					if not rail_chunk_catalogue.has(chunk_type):
						rail_chunk_catalogue[chunk_type] = []
					rail_chunk_catalogue[chunk_type].append(scene)
		file_name = directory.get_next()
	directory.list_dir_end()

func _get_chunk_scene_cache_key(scene: PackedScene) -> String:
	if scene == null:
		return ""
	if scene.resource_path != "":
		return scene.resource_path
	return "scene_%s" % scene.get_instance_id()

func _get_chunk_scene_metadata(scene: PackedScene) -> Dictionary:
	if scene == null:
		return {}

	var key = _get_chunk_scene_cache_key(scene)
	if chunk_candidate_cache.has(key):
		return chunk_candidate_cache[key]

	var preview_chunk = scene.instantiate()
	var info_list: Array[Node] = []
	collect_all_chunkinfo(preview_chunk, info_list)
	var info_node = info_list[0] if info_list.size() > 0 else null

	if info_node == null or not info_node.has_method("get_rotated_data"):
		preview_chunk.queue_free()
		chunk_candidate_cache[key] = {}
		return {}

	var info_path: NodePath = NodePath(".")
	if info_node != preview_chunk:
		info_path = preview_chunk.get_path_to(info_node)

	var rotations = []
	for rot in range(4):
		rotations.append({
			"rotation": rot,
			"data": info_node.get_rotated_data(rot)
		})

	var metadata = {
		"info_path": info_path,
		"rotations": rotations,
		"has_lamppost": "have_lamppost" in info_node and info_node.have_lamppost,
		"uniqueness": _get_chunk_uniqueness_from_info(info_node),
	}
	preview_chunk.queue_free()
	chunk_candidate_cache[key] = metadata
	return metadata

func _get_cached_chunk_info(instance: Node, scene: PackedScene) -> Node:
	var metadata = _get_chunk_scene_metadata(scene)
	if metadata.is_empty():
		return null

	var info_path: NodePath = metadata["info_path"]
	if String(info_path) == ".":
		return instance
	return instance.get_node_or_null(info_path)

func _get_chunk_type_from_scene(scene: PackedScene) -> int:
	if scene == null:
		return CHUNK_TYPE_BIOME

	var preview_chunk = scene.instantiate()
	var info_list: Array[Node] = []
	collect_all_chunkinfo(preview_chunk, info_list)
	var info_node = info_list[0] if info_list.size() > 0 else null
	var chunk_type = CHUNK_TYPE_BIOME
	if info_node != null and "chunk_type" in info_node:
		chunk_type = info_node.chunk_type
	preview_chunk.queue_free()
	return chunk_type

func _clear_pending_world_work() -> void:
	pending_generation_cells.clear()
	pending_cleanup_cells.clear()
	pending_wire_cells.clear()
	pending_generation_cursor = 0
	pending_cleanup_cursor = 0
	pending_wire_cursor = 0
	pending_wire_lookup.clear()

func _collect_replaceable_rail_chunks(root: Node, result: Array[Node3D]) -> void:
	if root == null:
		return

	if root is Node3D:
		var node_3d := root as Node3D
		if node_3d.scene_file_path.begins_with(RAILWAY_SCENE_DIRECTORY):
			var info_list: Array[Node] = []
			collect_all_chunkinfo(node_3d, info_list)
			var info_node = info_list[0] if info_list.size() > 0 else null
			if info_node != null and "chunk_type" in info_node:
				var chunk_type = info_node.chunk_type
				if chunk_type == CHUNK_TYPE_STRAIGHT_TRACK or chunk_type == CHUNK_TYPE_CURVED_TRACK:
					result.append(node_3d)
					return

	for child in root.get_children():
		_collect_replaceable_rail_chunks(child, result)

func _pick_replacement_rail_scene(chunk_type: int, current_scene_path: String) -> PackedScene:
	var candidates: Array = rail_chunk_catalogue.get(chunk_type, [])
	if candidates.is_empty():
		return null

	var alternatives: Array[PackedScene] = []
	for candidate in candidates:
		var scene := candidate as PackedScene
		if scene != null and scene.resource_path != current_scene_path:
			alternatives.append(scene)

	if not alternatives.is_empty():
		return alternatives.pick_random()
	return candidates.pick_random() as PackedScene

func _replace_rail_chunk_instance(chunk_root: Node3D) -> bool:
	if chunk_root == null or not is_instance_valid(chunk_root):
		return false

	var info_list: Array[Node] = []
	collect_all_chunkinfo(chunk_root, info_list)
	var info_node = info_list[0] if info_list.size() > 0 else null
	if info_node == null or not "chunk_type" in info_node:
		return false

	var replacement_scene = _pick_replacement_rail_scene(info_node.chunk_type, chunk_root.scene_file_path)
	if replacement_scene == null:
		return false

	var chunk_parent = chunk_root.get_parent()
	var replacement = replacement_scene.instantiate() as Node3D
	if chunk_parent == null or replacement == null:
		return false

	var chunk_index = chunk_root.get_index()
	var chunk_name = chunk_root.name
	chunk_root.name = "%s_old" % chunk_name

	replacement.transform = chunk_root.transform
	chunk_parent.add_child(replacement)
	chunk_parent.move_child(replacement, chunk_index)
	replacement.name = chunk_name
	replacement.owner = chunk_root.owner
	chunk_root.queue_free()
	return true

#rebuild the scene catalogue from res://tgcc/chunk/railway/scene,
#than serach on the current scene which chunks can be changed and for each one chose a new scene (the same kind)
func _refresh_rail_chunks() -> void:
	print("update rail chunks")
	_warm_rail_chunk_catalogue()

	var current_scene: Node = get_tree().current_scene
	if current_scene == null:
		return

	var replaceable_chunks: Array[Node3D] = []
	_collect_replaceable_rail_chunks(current_scene, replaceable_chunks)

	var replaced_count = 0
	for chunk_root in replaceable_chunks:
		if _replace_rail_chunk_instance(chunk_root):
			replaced_count += 1

	if replaced_count > 0:
		await get_tree().process_frame
		_destroy_and_regenrate_world()

func _update_rail_chunks() -> void:
	call_deferred("_refresh_rail_chunks")

func _refresh_world(center: Vector2i) -> void:
	_rebuild_generation_queue(center)
	_rebuild_cleanup_queue(center)

#check cells around the train by rings:
#first the center, then borders with distance = 1, the distance = 2 and so to eye_line.
#Use maxi(abs(x), abs(z)) to know the border of the ring. Cells closest to the train have more priority
func _rebuild_generation_queue(center: Vector2i) -> void:
	pending_generation_cells.clear()
	pending_generation_cursor = 0

	for radius in range(eye_line + 1):
		for x in range(-radius, radius + 1):
			for z in range(-radius, radius + 1):
				if maxi(abs(x), abs(z)) != radius:
					continue

				var grid_pos = center + Vector2i(x, z)
				if board.has(grid_pos):
					continue
				pending_generation_cells.append(grid_pos)

#For each cells add to cleanup queue the cells too far
#Use eye_line plus a margin value to hide chunks not instantly when go out of the eye line
#Persistent obstacle cells are not deleted (rails or set_pieces)
func _rebuild_cleanup_queue(center: Vector2i) -> void:
	pending_cleanup_cells.clear()
	pending_cleanup_cursor = 0
	var safe_margin = 2

	for grid_pos in board.keys():
		var cell = board[grid_pos]
		if _is_persistent_obstacle(cell):
			continue

		var dist_x = abs(grid_pos.x - center.x)
		var dist_z = abs(grid_pos.y - center.y)
		if dist_x > eye_line + safe_margin or dist_z > eye_line + safe_margin:
			pending_cleanup_cells.append(grid_pos)

#Check if a queue can continue to work based on time budget setted
func _queue_has_frame_budget(start_usec: int, budget_usec: int, processed: int) -> bool:
	return processed == 0 or Time.get_ticks_usec() - start_usec < budget_usec

func _drain_generation_queue() -> void:
	var processed: int = 0
	var start_usec: int = Time.get_ticks_usec()
	while pending_generation_cursor < pending_generation_cells.size() and _queue_has_frame_budget(start_usec, CHUNK_GENERATION_FRAME_BUDGET_USEC, processed):
		var grid_pos: Vector2i = pending_generation_cells[pending_generation_cursor]
		pending_generation_cursor += 1
		if board.has(grid_pos):
			continue

		var is_obstacle = _register_cell_with_ray(grid_pos)
		if not is_obstacle:
			_add_compatible_biome(grid_pos)
		processed += 1

	if pending_generation_cursor >= pending_generation_cells.size():
		pending_generation_cells.clear()
		pending_generation_cursor = 0

func _drain_cleanup_queue() -> void:
	var processed: int = 0
	var start_usec: int = Time.get_ticks_usec()
	while pending_cleanup_cursor < pending_cleanup_cells.size() and _queue_has_frame_budget(start_usec, CHUNK_CLEANUP_FRAME_BUDGET_USEC, processed):
		var grid_pos: Vector2i = pending_cleanup_cells[pending_cleanup_cursor]
		pending_cleanup_cursor += 1
		if not board.has(grid_pos):
			continue

		var cell = board[grid_pos]
		if _is_persistent_obstacle(cell):
			continue

		if cell.has("node") and is_instance_valid(cell["node"]):
			cell["node"].queue_free()
		board.erase(grid_pos)
		processed += 1

	if pending_cleanup_cursor >= pending_cleanup_cells.size():
		pending_cleanup_cells.clear()
		pending_cleanup_cursor = 0

func _queue_lamppost_wire_connection(grid_pos: Vector2i) -> void:
	if pending_wire_lookup.has(grid_pos):
		return
	pending_wire_lookup[grid_pos] = true
	pending_wire_cells.append(grid_pos)

func _drain_wire_queue() -> void:
	var processed: int = 0
	var start_usec: int = Time.get_ticks_usec()
	while pending_wire_cursor < pending_wire_cells.size() and _queue_has_frame_budget(start_usec, LAMPPOST_WIRE_FRAME_BUDGET_USEC, processed):
		var grid_pos: Vector2i = pending_wire_cells[pending_wire_cursor]
		pending_wire_cursor += 1
		pending_wire_lookup.erase(grid_pos)

		if not board.has(grid_pos):
			continue
		if not board[grid_pos].get("have_lamppost", false):
			continue

		_connect_lamppost_wires(grid_pos)
		processed += 1

	if pending_wire_cursor >= pending_wire_cells.size():
		pending_wire_cells.clear()
		pending_wire_cursor = 0

func _generate_pieces_around_train(center: Vector2i) -> void:
	for x in range(-eye_line, eye_line + 1):
		for z in range(-eye_line, eye_line + 1):
			var grid_pos = center + Vector2i(x, z)

			if not board.has(grid_pos):
				var ce_obstacle = _register_cell_with_ray(grid_pos)
				if not ce_obstacle:
					_add_compatible_biome(grid_pos)

#Decice which catalogue of chunks use for a cell. If manual_biome is set use always it
#Otherwise read noise_generator.get_noise_2d to give an index for biome_list
func _choose_catalogue_by_cell(grid_pos: Vector2i) -> Array[PackedScene]:
	if manual_biome != null:
		return manual_biome.available_chunks
		
	var value = noise_generator.get_noise_2d(grid_pos.x, grid_pos.y)
	var normalized_value = (value + 1.0) / 2.0 
	var indice = clamp(int(normalized_value * biome_list.size()), 0, biome_list.size() - 1)
	return biome_list[indice].available_chunks

func _get_procedural_biome_name(value: float) -> String:
	if manual_biome != null:
		return manual_biome.name
		
	var normalized_value = (value + 1.0) / 2.0
	var index = clamp(int(normalized_value * biome_list.size()), 0, biome_list.size() - 1)
	return biome_list[index].name

func _get_chunk_uniqueness_from_info(info_node: Node) -> int:
	if info_node != null and "uniqueness" in info_node:
		return clampi(info_node.uniqueness, 0, MAX_CHUNK_UNIQUENESS)
	return 0

func _get_prop_uniqueness_from_info(info_node: Node) -> int:
	if info_node != null and "uniqueness" in info_node:
		return clampi(info_node.uniqueness, 0, MAX_CHUNK_UNIQUENESS)
	return -1

func _is_persistent_obstacle(cell: Dictionary) -> bool:
	return cell.get("type", "") == "obstacle" and cell.get("persistent", true)

func _has_nearby_unique_chunk(grid_pos: Vector2i, uniqueness: int) -> bool:
	if uniqueness <= 0:
		return false

	for nearby_pos in board.keys():
		var nearby_uniqueness = int(board[nearby_pos].get("uniqueness", 0))
		if nearby_uniqueness <= 0:
			continue

		var min_distance = maxi(uniqueness, nearby_uniqueness)
		var dist_x = abs(nearby_pos.x - grid_pos.x)
		var dist_z = abs(nearby_pos.y - grid_pos.y)
		if dist_x <= min_distance and dist_z <= min_distance:
			return true
	return false

func _get_uniqueness_pick_weight(uniqueness: int) -> float:
	return 1.0 / pow(float(uniqueness + 1), 2.0)

func _pick_weighted_candidate(candidates: Array) -> Dictionary:
	var total_weight = 0.0
	for candidate in candidates:
		total_weight += candidate.weight

	if total_weight <= 0.0:
		return candidates.pick_random()

	var target_weight = randf() * total_weight
	var current_weight = 0.0
	for candidate in candidates:
		current_weight += candidate.weight
		if current_weight >= target_weight:
			return candidate
	return candidates.back()

func _pick_backup_scene(zone_catalogue: Array[PackedScene], grid_pos: Vector2i) -> PackedScene:
	for scene in zone_catalogue:
		var metadata = _get_chunk_scene_metadata(scene)
		var uniqueness = int(metadata.get("uniqueness", 0))
		if not _has_nearby_unique_chunk(grid_pos, uniqueness):
			return scene
	return zone_catalogue[0]

func _get_prop_scene_cache_key(scene: PackedScene) -> String:
	if scene == null:
		return ""
	if scene.resource_path != "":
		return scene.resource_path
	return "prop_scene_%s" % scene.get_instance_id()

func _get_prop_scene_uniqueness(scene: PackedScene) -> int:
	if scene == null:
		return -1

	var key = _get_prop_scene_cache_key(scene)
	if prop_candidate_cache.has(key):
		return prop_candidate_cache[key]

	var preview_prop = scene.instantiate()
	var uniqueness = _get_prop_uniqueness_from_info(preview_prop)
	if uniqueness == -1:
		var prop_info_list: Array[Node] = []
		collect_all_propinfo(preview_prop, prop_info_list)
		if not prop_info_list.is_empty():
			uniqueness = _get_prop_uniqueness_from_info(prop_info_list[0])
	preview_prop.queue_free()

	prop_candidate_cache[key] = uniqueness
	return uniqueness

func _get_marker_prop_uniqueness(marker: Node) -> int:
	var uniqueness = _get_prop_uniqueness_from_info(marker)
	if uniqueness == -1:
		return 0
	return uniqueness

func _pick_weighted_prop_scene(marker: Node) -> PackedScene:
	var candidates = []
	var fallback_uniqueness = _get_marker_prop_uniqueness(marker)

	for prop_scene in marker.available_props:
		if prop_scene == null:
			continue

		var uniqueness = _get_prop_scene_uniqueness(prop_scene)
		if uniqueness == -1:
			uniqueness = fallback_uniqueness

		candidates.append({
			"scene": prop_scene,
			"weight": _get_uniqueness_pick_weight(uniqueness),
			"uniqueness": uniqueness
		})

	if candidates.is_empty():
		return null
	return _pick_weighted_candidate(candidates).scene

func _spawn_props_for_chunk(root: Node) -> void:
	var prop_markers: Array[Node] = []
	collect_all_propinfo(root, prop_markers)

	for marker in prop_markers:
		_spawn_prop_for_marker(marker)

func _spawn_prop_for_marker(marker: Node) -> void:
	if marker.available_props.is_empty():
		return

	var prop_scene = _pick_weighted_prop_scene(marker)
	if prop_scene == null:
		return

	var prop_instance = prop_scene.instantiate()
	var prop_node = prop_instance as Node3D
	if prop_node == null:
		prop_instance.queue_free()
		return

	marker.add_child(prop_node)
	prop_node.transform = Transform3D.IDENTITY

#Using a vertical raycast from the top to the bottom at the center of the cell
#If there is a collision search for a new chunk node
func _register_cell_with_ray(grid_pos: Vector2i) -> bool:
	if board.has(grid_pos): return true 
	
	var world_pos = Vector3(grid_pos.x * chunk_size, 0, grid_pos.y * chunk_size)
	var space_state = get_world_3d().direct_space_state
	var ray_origin = Vector3(world_pos.x, 50.0, world_pos.z)
	var ray_end = Vector3(world_pos.x, -10.0, world_pos.z)
	
	var query = PhysicsRayQueryParameters3D.create(ray_origin, ray_end)
	var result = space_state.intersect_ray(query)
	
	if result.size() > 0:
		var collider = result["collider"]
		var root_chunk = collider
		var info_list: Array[Node] = []

		#Find parent node
		while root_chunk != null and root_chunk != get_tree().root:
			collect_all_chunkinfo(root_chunk, info_list)
			if info_list.size() > 0:
				break
			root_chunk = root_chunk.get_parent()

		var right_info_node = null

		#Find closest chunk to the ray
		if info_list.size() > 0:
			var min_distance = 999999.0
			var center_cell_pos = Vector3(world_pos.x, 0, world_pos.z)

			for info in info_list:
				if info is Node3D:
					var pos_info = Vector3(info.global_position.x, 0, info.global_position.z)
					var dist = center_cell_pos.distance_to(pos_info)
					
					if dist < min_distance:
						min_distance = dist
						right_info_node = info
						
		#If no node is found use first element
		if right_info_node == null and info_list.size() > 0:
			right_info_node = info_list[0]

		var exit_found = {"north": false, "est": false, "south": false, "west": false}
		var river_exit_found = {"north": false, "est": false, "south": false, "west": false}
		var height_found = {"north": 0, "est": 0, "south": 0, "west": 0}
		var have_lamppost = false
		var uniqueness = 0
		
		#Get node info
		if right_info_node != null:
			if right_info_node.has_method("get_rotated_data"):
				var rotation_steps = roundi(rad_to_deg(right_info_node.global_rotation.y) / -90.0)
				var data = right_info_node.get_rotated_data(rotation_steps)
				exit_found = data["connections"]
				river_exit_found = data["river_connections"]
				height_found = data["heights"]

			if "have_lamppost" in right_info_node:
				have_lamppost = right_info_node.have_lamppost

			uniqueness = _get_chunk_uniqueness_from_info(right_info_node)

		#Add piece to the grid
		board[grid_pos] = {
			"type": "obstacle", 
			"exit": exit_found, 
			"river_exit": river_exit_found,
			"heights": height_found, 
			"node": root_chunk,  
			"info": right_info_node,     
			"have_lamppost": have_lamppost,
			"uniqueness": uniqueness,
			"persistent": true
		}
		
		if have_lamppost:
			_queue_lamppost_wire_connection(grid_pos)
			
		return true 
	return false 

#For the cell to fill check the contrains for the four neighbors:
#If the neighbor at north have and exit on south then new chunk should have exit to north (the same for east and ovest)
#Contrains: 1 -> connection is mandatory; 0 -> no connection; -1 -> no contrain because there is no neighbor or it already exits
#When generator know the contrains take all chunks and try all different rotations
#A candidate is valid only if all contrains are correct
func _add_compatible_biome(grid_pos: Vector2i) -> void:
	var req_conn_north = _needed_connection(grid_pos + Vector2i(0, -1), "south")
	var req_conn_est = _needed_connection(grid_pos + Vector2i(1, 0), "west")
	var req_conn_south = _needed_connection(grid_pos + Vector2i(0, 1), "north")
	var req_conn_west = _needed_connection(grid_pos + Vector2i(-1, 0), "est")
	var req_river_north = _needed_river_connection(grid_pos + Vector2i(0, -1), "south")
	var req_river_est = _needed_river_connection(grid_pos + Vector2i(1, 0), "west")
	var req_river_south = _needed_river_connection(grid_pos + Vector2i(0, 1), "north")
	var req_river_west = _needed_river_connection(grid_pos + Vector2i(-1, 0), "est")
	
	var req_height_north = _needed_heights(grid_pos + Vector2i(0, -1), "south")
	var req_height_est = _needed_heights(grid_pos + Vector2i(1, 0), "west")
	var req_height_south = _needed_heights(grid_pos + Vector2i(0, 1), "north")
	var req_height_west = _needed_heights(grid_pos + Vector2i(-1, 0), "est")
	
	var height_noise = altitude_generator.get_noise_2d(grid_pos.x, grid_pos.y)
	var height_target = clamp(roundi((height_noise + 1.0) / 2.0), 0, 1)
	
	var zone_catalogue = _choose_catalogue_by_cell(grid_pos)
	var valid_candidates = []
	
	for scene in zone_catalogue:
		var metadata = _get_chunk_scene_metadata(scene)
		if metadata.is_empty():
			continue

		for candidate in metadata["rotations"]:
			var rot = candidate["rotation"]
			var data = candidate["data"]
			var u_conn = data["connections"]
			var u_river_conn = data["river_connections"]
			var u_height = data["heights"]
			var uniqueness = int(metadata.get("uniqueness", 0))
			if _has_nearby_unique_chunk(grid_pos, uniqueness):
				continue
			
			var match_conn_n = (req_conn_north == -1) or ((req_conn_north == 1) == u_conn["north"])
			var match_conn_e = (req_conn_est == -1)  or ((req_conn_est == 1) == u_conn["est"])
			var match_conn_s = (req_conn_south == -1)  or ((req_conn_south == 1) == u_conn["south"])
			var match_conn_o = (req_conn_west == -1) or ((req_conn_west == 1) == u_conn["west"])
			var match_river_n = (req_river_north == -1) or ((req_river_north == 1) == u_river_conn["north"])
			var match_river_e = (req_river_est == -1) or ((req_river_est == 1) == u_river_conn["est"])
			var match_river_s = (req_river_south == -1) or ((req_river_south == 1) == u_river_conn["south"])
			var match_river_o = (req_river_west == -1) or ((req_river_west == 1) == u_river_conn["west"])
			
			var match_alt_n = (req_height_north == -1) or (req_height_north == u_height["north"])
			var match_alt_e = (req_height_est == -1)  or (req_height_est == u_height["est"])
			var match_alt_s = (req_height_south == -1)  or (req_height_south == u_height["south"])
			var match_alt_o = (req_height_west == -1) or (req_height_west == u_height["west"])
			
			var diff_n = abs(u_height["north"] - height_target) if req_height_north == -1 else 0
			var diff_e = abs(u_height["est"] - height_target) if req_height_est == -1 else 0
			var diff_s = abs(u_height["south"] - height_target) if req_height_south == -1 else 0
			var diff_o = abs(u_height["west"] - height_target) if req_height_west == -1 else 0
			var excessive_jumps = diff_n > 1 or diff_e > 1 or diff_s > 1 or diff_o > 1
			
			if match_conn_n and match_conn_e and match_conn_s and match_conn_o and match_river_n and match_river_e and match_river_s and match_river_o and match_alt_n and match_alt_e and match_alt_s and match_alt_o and not excessive_jumps:
				var score = 0
				if req_height_north == -1 and u_height["north"] == height_target: score += 1
				if req_height_est == -1 and u_height["est"] == height_target: score += 1
				if req_height_south == -1 and u_height["south"] == height_target: score += 1
				if req_height_west == -1 and u_height["west"] == height_target: score += 1
				
				valid_candidates.append({
					"scene": scene,
					"rotation": rot,
					"data": data,
					"score": score,
					"weight": _get_uniqueness_pick_weight(uniqueness),
					"uniqueness": uniqueness
				})
		
	if valid_candidates.size() > 0:
		var max_score = -1
		for c in valid_candidates:
			if c.score > max_score: max_score = c.score
				
		var best_candidate = []
		for c in valid_candidates:
			if c.score == max_score: best_candidate.append(c)
				
		var choise = _pick_weighted_candidate(best_candidate)
		var new_chunk = choise.scene.instantiate()
		new_chunk.position = Vector3(grid_pos.x * chunk_size, 0, grid_pos.y * chunk_size)
		new_chunk.rotation.y = choise.rotation * (-PI / 2.0)
		add_child(new_chunk)
		_spawn_props_for_chunk(new_chunk)
		
		var info_new = _get_cached_chunk_info(new_chunk, choise.scene)
		
		var have_lamppost = false
		if info_new != null and "have_lamppost" in info_new:
			have_lamppost = info_new.have_lamppost

		var river_flow_direction = _calculate_river_flow_direction(grid_pos, choise.data["river_connections"])
		_apply_river_flow_direction(new_chunk, river_flow_direction)
			
		board[grid_pos] = {
			"type": "biome", 
			"exit": choise.data["connections"], 
			"river_exit": choise.data["river_connections"],
			"river_flow_direction": river_flow_direction,
			"heights": choise.data["heights"], 
			"node": new_chunk, 
			"info": info_new,
			"have_lamppost": have_lamppost,
			"uniqueness": choise.uniqueness
		}
		
		if have_lamppost:
			_queue_lamppost_wire_connection(grid_pos)

	else:	
		var backup_scene = _pick_backup_scene(zone_catalogue, grid_pos)
		var backup = backup_scene.instantiate()
		backup.position = Vector3(grid_pos.x * chunk_size, 0, grid_pos.y * chunk_size)
		add_child(backup)
		_spawn_props_for_chunk(backup)
		
		var info_backup = _get_cached_chunk_info(backup, backup_scene)
		var backup_uniqueness = _get_chunk_uniqueness_from_info(info_backup)
	
		var safe_heights = {
			"north": req_height_north if req_height_north != -1 else height_target,
			"est": req_height_est if req_height_est != -1 else height_target,
			"south": req_height_south if req_height_south != -1 else height_target,
			"west": req_height_west if req_height_west != -1 else height_target
		}
		board[grid_pos] = {
			"type": "biome", 
			"exit": {"north":false, "est":false, "south":false, "west":false}, 
			"river_exit": {"north":false, "est":false, "south":false, "west":false},
			"river_flow_direction": Vector2.ZERO,
			"heights": safe_heights, 
			"node": backup,
			"info": info_backup,
			"have_lamppost": false,
			"uniqueness": backup_uniqueness
		}

#Check if a neighbors have a river direction: if yes try to continue it,
#otherwise create a direction based on connections
func _calculate_river_flow_direction(grid_pos: Vector2i, river_connections: Dictionary) -> Vector2:
	var connected_sides: Array[String] = []
	for side in RIVER_SIDE_ORDER:
		if river_connections.has(side) and river_connections[side]:
			connected_sides.append(side)

	if connected_sides.is_empty():
		return Vector2.ZERO

	var neighbour_flow = _get_connected_neighbour_river_flow(grid_pos, connected_sides)
	if not neighbour_flow.is_zero_approx():
		var continued_flow = _continue_river_flow_from_neighbour(grid_pos, connected_sides, neighbour_flow)
		if not continued_flow.is_zero_approx():
			return continued_flow.normalized()

	var default_flow = _get_default_river_flow(connected_sides)
	if default_flow.is_zero_approx():
		return Vector2.ZERO
	return default_flow.normalized()

func _get_connected_neighbour_river_flow(grid_pos: Vector2i, connected_sides: Array[String]) -> Vector2:
	for side in connected_sides:
		var neighbour_pos: Vector2i = grid_pos + RIVER_NEIGHBOUR_OFFSETS[side]
		if not board.has(neighbour_pos):
			continue
		var neighbour = board[neighbour_pos]
		if not neighbour.has("river_flow_direction"):
			continue
		var neighbour_flow: Vector2 = neighbour["river_flow_direction"]
		if not neighbour_flow.is_zero_approx():
			return neighbour_flow.normalized()
	return Vector2.ZERO

func _continue_river_flow_from_neighbour(grid_pos: Vector2i, connected_sides: Array[String], neighbour_flow: Vector2) -> Vector2:
	for side in connected_sides:
		var neighbour_pos: Vector2i = grid_pos + RIVER_NEIGHBOUR_OFFSETS[side]
		if not board.has(neighbour_pos):
			continue
		var neighbour = board[neighbour_pos]
		if not neighbour.has("river_flow_direction"):
			continue
		var side_direction: Vector2 = RIVER_DIRECTIONS[side]
		var neighbour_direction: Vector2 = neighbour["river_flow_direction"]
		if neighbour_direction.is_zero_approx():
			continue
		neighbour_direction = neighbour_direction.normalized()
		var other_sides = connected_sides.duplicate()
		other_sides.erase(side)
		if other_sides.is_empty():
			return neighbour_direction

		var other_direction = _get_average_river_side_direction(other_sides)
		if neighbour_direction.dot(-side_direction) > 0.25:
			return other_direction - side_direction
		if neighbour_direction.dot(side_direction) > 0.25:
			return side_direction - other_direction

	var default_flow = _get_default_river_flow(connected_sides)
	if default_flow.dot(neighbour_flow) < 0.0:
		return -default_flow
	return default_flow

func _get_default_river_flow(connected_sides: Array[String]) -> Vector2:
	if connected_sides.size() == 1:
		return RIVER_DIRECTIONS[connected_sides[0]]

	if connected_sides.has("north") and connected_sides.has("south"):
		return Vector2(0.0, 1.0)
	if connected_sides.has("est") and connected_sides.has("west"):
		return Vector2(1.0, 0.0)

	var start_direction: Vector2 = RIVER_DIRECTIONS[connected_sides[0]]
	var end_direction = _get_average_river_side_direction(connected_sides.slice(1))
	return end_direction - start_direction

func _get_average_river_side_direction(sides: Array[String]) -> Vector2:
	var direction := Vector2.ZERO
	for side in sides:
		direction += RIVER_DIRECTIONS[side]
	if direction.is_zero_approx():
		return Vector2.ZERO
	return direction / float(sides.size())

func _apply_river_flow_direction(root: Node, flow_direction: Vector2) -> void:
	if flow_direction.is_zero_approx():
		return
	_set_river_flow_direction_recursive(root, flow_direction.normalized())

func _set_river_flow_direction_recursive(node: Node, flow_direction: Vector2) -> void:
	if node is MeshInstance3D and node.name.begins_with("Water_F"):
		var mesh_instance := node as MeshInstance3D
		mesh_instance.set_instance_shader_parameter("river_flow_direction", flow_direction)

	for child in node.get_children():
		_set_river_flow_direction_recursive(child, flow_direction)

func _needed_connection(near_pos: Vector2i, side_needed: String) -> int:
	if not board.has(near_pos): 
		_register_cell_with_ray(near_pos)
	if board.has(near_pos) and board[near_pos].has("exit"):
		return 1 if board[near_pos]["exit"][side_needed] else 0 
	return -1 

func _needed_river_connection(near_pos: Vector2i, side_needed: String) -> int:
	if not board.has(near_pos): 
		_register_cell_with_ray(near_pos)
	if board.has(near_pos) and board[near_pos].has("river_exit"):
		return 1 if board[near_pos]["river_exit"][side_needed] else 0
	return -1

func _needed_heights(near_pos: Vector2i, side_needed: String) -> int:
	if board.has(near_pos) and board[near_pos].has("heights"):
		return board[near_pos]["heights"][side_needed]
	return -1 
			
func _get_lampposts(info_node: Node, side: String) -> Array[Node3D]:
	var lampposts: Array[Node3D] = []
	if side == "sx":
		if "connection_left" in info_node and info_node.connection_left != null:
			for p in info_node.connection_left: 
				if is_instance_valid(p): lampposts.append(p)
	else:
		if "connection_right" in info_node and info_node.connection_right != null:
			for p in info_node.connection_right: 
				if is_instance_valid(p): lampposts.append(p)
	return lampposts

func _connect_lamppost_wires(new_board_pos: Vector2i) -> void:
	var new_cell = board[new_board_pos]
	var new_root = new_cell["node"]
	var new_info = new_cell["info"]
	
	if new_info == null: return
	if new_info is Node3D: new_info.force_update_transform()

	var new_sx = _get_lampposts(new_info, "sx")
	var new_dx = _get_lampposts(new_info, "dx")
	if new_sx.is_empty() or new_dx.is_empty(): return
	
	var new_id = new_root.get_instance_id()
	if not wire_connections.has(new_id): wire_connections[new_id] = 0

	if wire_connections[new_id] >= 2: return

	var p_sx_my_best = null; var p_dx_my_best = null
	var p_sx_your_best = null; var p_dx_your_best = null
	var best_closest_to_root = null
	var best_distance = 999999.0
	var ray_research = 6
	
	for x in range(-ray_research, ray_research + 1):
		for z in range(-ray_research, ray_research + 1):
			if x == 0 and z == 0: continue
			
			var closest_post = new_board_pos + Vector2i(x, z)
			if board.has(closest_post) and board[closest_post].get("have_lamppost", false):
				var closest_root = board[closest_post]["node"]
				var closest_info = board[closest_post]["info"]
				
				if not is_instance_valid(closest_root) or closest_root == new_root or closest_info == null: continue
				
				var closest_id = closest_root.get_instance_id()
				var closest_connections = wire_connections.get(closest_id, 0)
				
				if closest_connections >= 2: continue
				
				if closest_info is Node3D: closest_info.force_update_transform()
				var closest_sx = _get_lampposts(closest_info, "sx")
				var closest_dx = _get_lampposts(closest_info, "dx")
				if closest_sx.is_empty() or closest_dx.is_empty(): continue
				
				for i_m in range(new_sx.size()):
					for i_t in range(closest_sx.size()):
						var my_c = (new_sx[i_m].global_position + new_dx[i_m].global_position) / 2.0
						var your_c = (closest_sx[i_t].global_position + closest_dx[i_t].global_position) / 2.0
						var dist = my_c.distance_to(your_c)
						
						if dist < best_distance:
							best_distance = dist
							best_closest_to_root = closest_root
							p_sx_my_best = new_sx[i_m]
							p_dx_my_best = new_dx[i_m]
							p_sx_your_best = closest_sx[i_t]
							p_dx_your_best = closest_dx[i_t]

	var max_dist = chunk_size * lamppost_dist_factor
	
	if best_closest_to_root != null and best_distance < max_dist:
		var dist_streight = p_sx_my_best.global_position.distance_to(p_sx_your_best.global_position) + p_dx_my_best.global_position.distance_to(p_dx_your_best.global_position)
		var dist_cross = p_sx_my_best.global_position.distance_to(p_dx_your_best.global_position) + p_dx_my_best.global_position.distance_to(p_sx_your_best.global_position)

		if dist_streight <= dist_cross:
			_draw_parable(p_sx_my_best.global_position, p_sx_your_best.global_position, new_root)
			_draw_parable(p_dx_my_best.global_position, p_dx_your_best.global_position, new_root)
		else:
			_draw_parable(p_sx_my_best.global_position, p_dx_your_best.global_position, new_root)
			_draw_parable(p_dx_my_best.global_position, p_sx_your_best.global_position, new_root)

		wire_connections[new_id] += 1
		var closest_id = best_closest_to_root.get_instance_id()
		if not wire_connections.has(closest_id): wire_connections[closest_id] = 0
		wire_connections[closest_id] += 1

#draw lamppost wires
func _draw_parable(p1: Vector3, p2: Vector3, parent: Node3D) -> void:
	var segments = 15
	var lowering = 1.5 
	var curve_points = []
	var sag_factors = []
	
	for i in range(segments + 1):
		var t = float(i) / float(segments)
		var global_post = p1.lerp(p2, t)
		var gravity = lowering * (1.0 - pow(2.0 * t - 1.0, 2.0))
		global_post.y -= gravity
		
		curve_points.append(parent.to_local(global_post))
		sag_factors.append(sin(t * PI))

	var st = SurfaceTool.new()
	st.begin(Mesh.PRIMITIVE_TRIANGLES)
	
	var lati = 4 
	var ray = wire_thickness / 2.0
	
	for i in range(curve_points.size() - 1):
		var p_current = curve_points[i]
		var p_next = curve_points[i+1]
		var sag_currnet = sag_factors[i]
		var sag_next = sag_factors[i+1]
		
		var dir = (p_next - p_current).normalized()
		var up = Vector3.UP
		var right = dir.cross(up).normalized()
		if right.length_squared() < 0.01:
			right = dir.cross(Vector3.RIGHT).normalized()
		up = right.cross(dir).normalized()
		
		for s in range(lati):
			var ang1 = (float(s) / lati) * TAU
			var ang2 = (float((s + 1) % lati) / lati) * TAU
			var offset1 = (right * cos(ang1) + up * sin(ang1)) * ray
			var offset2 = (right * cos(ang2) + up * sin(ang2)) * ray
			
			st.set_color(Color(sag_currnet, 0, 0, 1))
			st.add_vertex(p_current + offset1)
			st.set_color(Color(sag_currnet, 0, 0, 1))
			st.add_vertex(p_current + offset2)
			st.set_color(Color(sag_next, 0, 0, 1))
			st.add_vertex(p_next + offset1)
			
			st.set_color(Color(sag_currnet, 0, 0, 1))
			st.add_vertex(p_current + offset2)
			st.set_color(Color(sag_next, 0, 0, 1))
			st.add_vertex(p_next + offset2)
			st.set_color(Color(sag_next, 0, 0, 1))
			st.add_vertex(p_next + offset1)
			
	var wire_node = MeshInstance3D.new()
	wire_node.mesh = st.commit()
	if lamppost_wire_material: wire_node.material_override = lamppost_wire_material
	parent.add_child(wire_node)
