@tool
extends Node
var generation_thread: Thread = Thread.new()
#### CHUNK GENERATION ####
@export_category("CHUNK GENERATION")

const CHUNK_SIZE := 256
const CHUNK_BOUNDS := 4

## How detailed the chunk should be. This could be made a const when debug phase is finished
@export_range(4, 256, 4) var chunk_resolution := 32:
	set(new_resolution):
		chunk_resolution = new_resolution
		update_all_mesh()

#### WORLD GENERATION ####
@export_category("WORLD GENERATION")
@export_tool_button("Generate Chunks") var generate_chunk_callable = generate.bind(true)
@export_tool_button("Delete Chunks") var delete_chunk_callable = delete.bind(true)
@export var noise: FastNoiseLite
@export_range(4.0, 128.0, 4.0) var height := 64.0:
	set(new_height):
		height = new_height
		update_all_mesh()

var loaded_chunks := {}# Key: Vector2i, Value: MeshInstance3D


# Called when the node enters the scene tree for the first time. ALSO IN EDITOR
func _ready() -> void:
		if !(Engine.is_editor_hint()):
			generate(true)


func generate(_ForceRegen: bool = false):
	print('Generation starts')
	generate_chunks_around(Vector3(0, 0, 0), _ForceRegen)


func delete(_Force:bool = true):
	loaded_chunks.clear()
	for c in get_children():
		c.queue_free()


func generate_noise(_seed: float):
	noise = FastNoiseLite.new()
	noise.noise_type = FastNoiseLite.TYPE_PERLIN
	noise.seed = _seed
	noise.frequency = 0.01

	if Engine.is_editor_hint():
		update_all_mesh()


func generate_chunk(position: Vector3, _ForceRegen: bool = false):
	WorkerThreadPool.add_task(func():
		var mesh_instance = MeshInstance3D.new()
		update_mesh(mesh_instance, position)
		mesh_instance.position = position
		var chunk_coord = get_chunk_coord(position)
		call_deferred("add_chunk_deferred", mesh_instance, chunk_coord)

	)


func add_chunk_deferred(mesh_instance: MeshInstance3D, chunk_coord: Vector2i):
	add_child(mesh_instance)
	if Engine.is_editor_hint():
		mesh_instance.owner = get_tree().edited_scene_root

	loaded_chunks[chunk_coord] = mesh_instance
	print("Chunk at: ", str(mesh_instance.position), ", coord: ", str(chunk_coord), " loaded")


func get_height(x: float, y: float) -> float:
	if not noise:
		return 0.0

	return noise.get_noise_2d(x, y) * height


func get_normal(x: float, y: float) -> Vector3:
	if not noise:
		return Vector3.UP

	var epsilon := CHUNK_SIZE / chunk_resolution
	var normal := Vector3(
		(get_height(x + epsilon, y) - get_height(x - epsilon, y)) / (2.0 * epsilon),
		1.0,
		(get_height(x, y + epsilon) - get_height(x, y - epsilon)) / (2.0 * epsilon)

	)
	return normal.normalized()


func get_biome(x: float, z: float) -> String:
	if not noise:
		return "default"

	var biome_noise = FastNoiseLite.new()
	biome_noise.noise_type = FastNoiseLite.TYPE_CELLULAR
	biome_noise.seed = noise.seed + 1
	biome_noise.frequency = 0.005
	var value = biome_noise.get_noise_2d(x, z)

	if value < -0.3:
		return "desert"
	elif value < 0.1:
		return "forest"
	elif value < 0.4:
		return "mountain"
	else:
		return "ocean"


func update_all_mesh():
	for chunk_coord in loaded_chunks:
		update_mesh(loaded_chunks[chunk_coord], loaded_chunks[chunk_coord].position)


func update_mesh(chunk_terrain_instance: MeshInstance3D, chunk_pos: Vector3):
	var plane: PlaneMesh = load("res://world_gen/new_plane_mesh.tres").duplicate()
	plane.subdivide_depth = chunk_resolution
	plane.subdivide_width = chunk_resolution
	plane.size = Vector2(CHUNK_SIZE, CHUNK_SIZE)

	var plane_arrays := plane.get_mesh_arrays()
	var vertex_array: PackedVector3Array = plane_arrays[ArrayMesh.ARRAY_VERTEX]
	var normal_array: PackedVector3Array = plane_arrays[ArrayMesh.ARRAY_NORMAL]
	var tangent_array: PackedFloat32Array = plane_arrays[ArrayMesh.ARRAY_TANGENT]

	for i: int in vertex_array.size():
		var vertex := vertex_array[i]
		var world_x = chunk_pos.x + vertex.x
		var world_z = chunk_pos.z + vertex.z
		var normal := Vector3.UP
		var tangent := Vector3.RIGHT

		if noise:
			vertex.y = get_height(world_x, world_z)
			normal = get_normal(world_x, world_z)
			tangent = normal.cross(Vector3.UP)

		vertex_array[i] = vertex
		normal_array[i] = normal
		tangent_array[4 * i] = tangent.x
		tangent_array[4 * i + 1] = tangent.y
		tangent_array[4 * i + 2] = tangent.z
		tangent_array[4 * i + 3] = 1.0

	plane_arrays[ArrayMesh.ARRAY_VERTEX] = vertex_array
	plane_arrays[ArrayMesh.ARRAY_NORMAL] = normal_array
	plane_arrays[ArrayMesh.ARRAY_TANGENT] = tangent_array

	var array_mesh := ArrayMesh.new()
	array_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, plane_arrays)
	#array_mesh.surface_set_material(0, get_biome_mat(chunk_pos))
	chunk_terrain_instance.mesh = array_mesh
	chunk_terrain_instance.create_trimesh_collision()


func get_biome_mat(chunk_pos) -> Material:
	# Apply biome-based material
	var material = StandardMaterial3D.new()
	var biome = get_biome(chunk_pos.x + CHUNK_SIZE / 2.0, chunk_pos.z + CHUNK_SIZE / 2.0)

	match biome:
		"desert":
			material.albedo_color = Color(1.0, 0.8, 0.4)

		"forest":
			material.albedo_color = Color(0.2, 0.6, 0.2)

		"mountain":
			material.albedo_color = Color(0.5, 0.5, 0.5)

		"ocean":
			material.albedo_color = Color(0.0, 0.4, 0.8)

	return material


## Convert position to chunk coordinates
func get_chunk_coord(position: Vector3) -> Vector2i:
	var coord_chunk_x = int(floor(position.x / CHUNK_SIZE))
	var coord_chunk_z = int(floor(position.z / CHUNK_SIZE))
	return Vector2i(coord_chunk_x, coord_chunk_z)


func generate_chunks_around(position: Vector3, ForceRegen: bool = false):
	# Convert position to chunk coordinates
	var position_to_coord := get_chunk_coord(position)
	print("Closest chunk: ", str(position_to_coord))
	var new_chunks := {}

	for x in range(position_to_coord.x - CHUNK_BOUNDS, position_to_coord.x + CHUNK_BOUNDS + 1):
		for z in range(position_to_coord.y - CHUNK_BOUNDS, position_to_coord.y + CHUNK_BOUNDS + 1):
			var chunk_coord := Vector2i(x, z)
			var chunk_position := Vector3(x * CHUNK_SIZE, 0, z * CHUNK_SIZE)
			print("Trying to load chunk at coordinate: ", chunk_coord)

			if ForceRegen and loaded_chunks.has(chunk_coord):
				loaded_chunks[chunk_coord].queue_free()
				loaded_chunks.erase(chunk_coord)

			if not loaded_chunks.has(chunk_coord):
				generate_chunk(chunk_position, ForceRegen)
				new_chunks[chunk_coord] = null # Placeholder until chunk is added
			else:
				new_chunks[chunk_coord] = loaded_chunks[chunk_coord]

	# Unload chunks outside bounds
	for chunk_coord in loaded_chunks:
		if not new_chunks.has(chunk_coord):
			loaded_chunks[chunk_coord].queue_free()
			loaded_chunks.erase(chunk_coord)

	loaded_chunks = new_chunks
