extends Node3D
var rd: RenderingDevice
var shader_file1: Resource
var shader_file2: Resource
var shader_spirv1: RDShaderSPIRV
var shader_spirv2: RDShaderSPIRV
var shader_pass1: RID
var shader_pass2: RID
var start_time := Time.get_ticks_msec() / 1000.0
var color = Color.CORAL
@export var iout_surface_points = []

var pipeline1
var pipeline2
var buffer
var surface_buffer
var normal_buffer
var uv_buffer
var idx_buffer
var counter_buffer
var chunk_position_buffer
var params_buffer

func create_device(world_size: int):
	
	rd = RenderingServer.create_local_rendering_device()

	# 1. Load Shaders
	shader_file1 = load("res://SurfaceNetsWorld/compute_surface_points.glsl")
	shader_pass1 = rd.shader_create_from_spirv(shader_file1.get_spirv())
	shader_file2 = load("res://SurfaceNetsWorld/sdf_mesh_generation.glsl")
	shader_pass2 = rd.shader_create_from_spirv(shader_file2.get_spirv())

	# 2. Create Pipelines
	pipeline1 = rd.compute_pipeline_create(shader_pass1)
	pipeline2 = rd.compute_pipeline_create(shader_pass2)

	# 3. Pre-allocate Buffers (assuming world_size is constant)
	# add 1 to look at adjacent voxels for chunk boundaries
	var total = (world_size + 2) ** 3

	# We create them once with empty/zero data of the correct size
	buffer = rd.storage_buffer_create(total * 4) 
	surface_buffer = rd.storage_buffer_create(total * 3 * 4)
	normal_buffer = rd.storage_buffer_create(total * 3 * 4)
	uv_buffer = rd.storage_buffer_create(total * 2 * 4)
	idx_buffer = rd.storage_buffer_create(total * 6 * 4)
	counter_buffer = rd.storage_buffer_create(4)
	chunk_position_buffer = rd.storage_buffer_create(16) # vec4
	params_buffer = rd.uniform_buffer_create(16) # world_size, threshold, time, etc

func compute_mesh(world_size: int, threshold: float, chunk_pos: Vector3) -> ArrayMesh:
	world_size += 2
	
	# 1. Update existing buffers with NEW data for THIS chunk
	var chunk_pos_data = PackedFloat32Array([chunk_pos.x, chunk_pos.y, chunk_pos.z, 0.0]).to_byte_array()
	rd.buffer_update(chunk_position_buffer, 0, chunk_pos_data.size(), chunk_pos_data)

	# Reset the counter to 0 for the new chunk
	var counter_reset = PackedInt32Array([0]).to_byte_array()
	rd.buffer_update(counter_buffer, 0, 4, counter_reset)
	
	# Chunk position (offset)
	var chunk_position_peer := PackedFloat32Array()
	chunk_position_peer.resize(4)
	chunk_position_peer.set(0, chunk_pos.x)
	chunk_position_peer.set(1, chunk_pos.y)
	chunk_position_peer.set(2, chunk_pos.z)
	chunk_position_peer.set(3, 0.0)
	var chunk_position_bytes := chunk_position_peer.to_byte_array()
	rd.buffer_update(chunk_position_buffer, 0, chunk_position_bytes.size(), chunk_position_bytes)
	
	var u_time := Time.get_ticks_msec() / 1000.0 - start_time
	var peer := StreamPeerBuffer.new()
	peer.put_32(world_size)
	peer.put_float(threshold)
	peer.put_float(u_time)
	peer.put_32(0)
	var uniform_params_bytes := peer.data_array
	rd.buffer_update(params_buffer, 0, uniform_params_bytes.size(), uniform_params_bytes)

	# Create a uniform to assign the buffer to the rendering device
	var uniform_buf := RDUniform.new()
	uniform_buf.uniform_type = RenderingDevice.UNIFORM_TYPE_STORAGE_BUFFER
	uniform_buf.binding = 0 # this needs to match the "binding" in our shader file
	uniform_buf.add_id(buffer)
	
	# Create a uniform to assign the buffer to the rendering device
	var surface_uniform_buf := RDUniform.new()
	surface_uniform_buf.uniform_type = RenderingDevice.UNIFORM_TYPE_STORAGE_BUFFER
	surface_uniform_buf.binding = 2
	surface_uniform_buf.add_id(surface_buffer)
	
	var normal_uniform = RDUniform.new()
	normal_uniform.uniform_type = RenderingDevice.UNIFORM_TYPE_STORAGE_BUFFER
	normal_uniform.binding = 3
	normal_uniform.add_id(normal_buffer)

	var uv_uniform = RDUniform.new()
	uv_uniform.uniform_type = RenderingDevice.UNIFORM_TYPE_STORAGE_BUFFER
	uv_uniform.binding = 4
	uv_uniform.add_id(uv_buffer)
	
	var idx_uniform := RDUniform.new()
	idx_uniform.uniform_type = RenderingDevice.UNIFORM_TYPE_STORAGE_BUFFER
	idx_uniform.binding = 5
	idx_uniform.add_id(idx_buffer)
	
	var counter_uniform := RDUniform.new()
	counter_uniform.uniform_type = RenderingDevice.UNIFORM_TYPE_STORAGE_BUFFER
	counter_uniform.binding = 6
	counter_uniform.add_id(counter_buffer)
	
	var chunk_position_uniform := RDUniform.new()
	chunk_position_uniform.uniform_type = RenderingDevice.UNIFORM_TYPE_STORAGE_BUFFER
	chunk_position_uniform.binding = 7
	chunk_position_uniform.add_id(chunk_position_buffer)
	
	

	var uniform_params := RDUniform.new()
	uniform_params.uniform_type = RenderingDevice.UNIFORM_TYPE_UNIFORM_BUFFER
	uniform_params.binding = 1
	uniform_params.add_id(params_buffer)

	var uniform_set1 := rd.uniform_set_create([uniform_buf, uniform_params, surface_uniform_buf, normal_uniform, uv_uniform, idx_uniform, counter_uniform, chunk_position_uniform], shader_pass1, 0) # the last parameter (the 0) needs to match the "set" in our shader file
	var uniform_set2 := rd.uniform_set_create([uniform_buf, uniform_params, surface_uniform_buf, normal_uniform, uv_uniform, idx_uniform, counter_uniform], shader_pass2, 0)
	
	var dispatch_count = int(ceil(world_size / 4.0))
	
	# 1. Dispatch PASS 1 (Calculate Points)
	var pipeline1 := rd.compute_pipeline_create(shader_pass1) # Points only
	var compute_list = rd.compute_list_begin()
	rd.compute_list_bind_compute_pipeline(compute_list, pipeline1)
	rd.compute_list_bind_uniform_set(compute_list, uniform_set1, 0)
	rd.compute_list_dispatch(compute_list, dispatch_count, dispatch_count, dispatch_count)
	rd.compute_list_end()

	# 2. Dispatch PASS 2 (Generate Indices)
	var pipeline2 := rd.compute_pipeline_create(shader_pass2) # Indices only
	compute_list = rd.compute_list_begin()
	rd.compute_list_bind_compute_pipeline(compute_list, pipeline2)
	rd.compute_list_bind_uniform_set(compute_list, uniform_set2, 0)
	rd.compute_list_dispatch(compute_list, dispatch_count, dispatch_count, dispatch_count)
	rd.compute_list_end()
	
	# Submit to GPU and wait for sync
	rd.submit()
	rd.sync()
	# Read back the data from the buffer
	var out_verts = rd.buffer_get_data(surface_buffer).to_float32_array()
	var out_norms = rd.buffer_get_data(normal_buffer).to_float32_array()
	var out_indices = rd.buffer_get_data(idx_buffer).to_int32_array()
	var final_count = rd.buffer_get_data(counter_buffer).to_int32_array()[0]
	var out_surface_points = rd.buffer_get_data(surface_buffer).to_float32_array()
	
	# 5. Build the Mesh
	var mesh = ArrayMesh.new()
	var arrays = []
	arrays.resize(Mesh.ARRAY_MAX)

	# We need to reshape the flat float array into Vector3s
	var verts := PackedVector3Array()
	var normals := PackedVector3Array()
	# Instead of blindly appending every voxel, we check if the voxel was "active"
		# Or, even better, map the original voxel indices to new packed indices
	var active_map = {}
	var packed_verts := PackedVector3Array()
	var packed_normals := PackedVector3Array()
	var packed_indices := PackedInt32Array()

	var offset = Vector3(world_size / 2.0, world_size / 2.0, world_size / 2.0)
	var final_indices = out_indices.slice(0, final_count)

	for old_idx in final_indices:
		var v_base = old_idx * 3

		# 1. Skip if the shader marked this as an empty voxel
		if out_verts[v_base] < -0.5:
			continue 

		if not active_map.has(old_idx):
			active_map[old_idx] = packed_verts.size()
			# 2. Re-center the vertex so the mesh isn't floating in the corner
			var pos = Vector3(out_verts[v_base], out_verts[v_base+1], out_verts[v_base+2]) - offset
			packed_verts.append(pos)
			packed_normals.append(Vector3(out_norms[v_base], out_norms[v_base+1], out_norms[v_base+2]))

		packed_indices.append(active_map[old_idx])

	iout_surface_points = out_surface_points
	if packed_verts.size() > 0:
		arrays[Mesh.ARRAY_VERTEX] = packed_verts
		arrays[Mesh.ARRAY_NORMAL] = packed_normals
		arrays[Mesh.ARRAY_INDEX] = packed_indices
		mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
	
	return mesh

func build_sample_dict(world_size: int, flat_buffer: PackedFloat32Array) -> Dictionary:
	var dict := {}
	var total = world_size * world_size * world_size

	for idx in total:
		var voxel_x = idx % world_size
		var voxel_y = (idx / world_size) % world_size
		var voxel_z = idx / (world_size * world_size)

		var voxel_id = Vector3i(voxel_x, voxel_y, voxel_z)
		var distance = flat_buffer[idx]

		dict[voxel_id] = distance

	return dict
	
func build_surface_dict(world_size: int, flat_buffer: PackedFloat32Array) -> Dictionary:
	var dict := {}
	var total = world_size * world_size * world_size

	for idx in total:
		var base = idx * 3
		var x = flat_buffer[base]
		var y = flat_buffer[base + 1]
		var z = flat_buffer[base + 2]

		var voxel_x = idx % world_size
		var voxel_y = (idx / world_size) % world_size
		var voxel_z = idx / (world_size * world_size)

		var voxel_id = Vector3i(voxel_x, voxel_y, voxel_z)
		var surface_pos = Vector3(x, y, z)

		dict[voxel_id] = surface_pos

	return dict

func _exit_tree():
	# If the rendering device wasn't initialized, we have nothing to free
	if not rd:
		return

	# 1. Free Shader and Pipeline RIDs
	# Pipelines depend on shaders, so free them first
	if pipeline1.is_valid():
		rd.free_rid(pipeline1)
	if pipeline2.is_valid():
		rd.free_rid(pipeline2)

	if shader_pass1.is_valid():
		rd.free_rid(shader_pass1)
	if shader_pass2.is_valid():
		rd.free_rid(shader_pass2)

	# 2. Free Buffer RIDs
	# These are the actual memory allocations on the VRAM
	var buffers_to_free = [
		buffer, 
		surface_buffer, 
		normal_buffer, 
		uv_buffer, 
		idx_buffer, 
		counter_buffer, 
		chunk_position_buffer, 
		params_buffer
	]
	
	for b_rid in buffers_to_free:
		if b_rid.is_valid():
			rd.free_rid(b_rid)

	# 3. Finalize the Rendering Device
	# This tells Godot we are done with this local device entirely
	rd.free()
	rd = null
