# This script is designed to drive gameplay logic based on the number of GPUParticles3D
# that “hit” various static target areas in 3D space. A cloned particle system (rendered 
# in a dedicated SubViewport on layer 20) uses a special testing shader that, for each particle, 
# checks its 3D world-space position against up to eight static target bounding boxes.
#
# The testing shader maps particles that hit a target into a designated horizontal slice of clip 
# space so that the SubViewport (sized 8×1 pixels) accumulates red intensity in each column. Higher 
# red intensity means more particles hit that target.
#
# In this version, the targets are static so their parameters (center, extents, and the inverse of 
# their 3×3 rotation basis) are computed once and sent to the shader. The attractors are shared 
# (set up in the main scene and used by the cloned particle system) rather than cloned.
#
# Other game scripts can connect to the "target_occupancy_changed" signal, which is emitted each frame 
# for every target (0–7) with the measured red intensity (occupancy).

extends Node3D

# Signal emitted when the occupancy of a target changes. Sent every frame.
# occupancy is a float between 0 and 1 corresponding to how many particles are in the target.
signal target_occupancy_changed(target_index: int, occupancy: float)
# Optional debug mesh to visualize the viewport texture
@export var debug_mesh_path: NodePath
var debug_mesh: MeshInstance3D

# Exported node paths.
@export var source_particles_path: NodePath     # The original GPUParticles3D to clone for collision detection.
@export var target_collision_shapes_paths: Array[NodePath] = []  # Array of CollisionShape3D nodes (each with a BoxShape3D).

# Path to the testing shader resource.
const shader = preload("res://scenes/particle_test.gdshader")

# Maximum number of target areas supported.
const MAX_TARGETS: int = 8

# References.
var clone_particles: GPUParticles3D
@onready var sub_viewport: SubViewport = $SubViewport
var target_collision_shapes: Array[CollisionShape3D] = []

func _ready():
	# Get references from the scene.
	var source_particles = get_node(source_particles_path) as GPUParticles3D
	for path in target_collision_shapes_paths:
		var shape = get_node(path) as CollisionShape3D
		target_collision_shapes.append(shape)
	# Pad to MAX_TARGETS if necessary.
	while target_collision_shapes.size() < MAX_TARGETS:
		target_collision_shapes.append(null)

	# Set up debug mesh if path provided
	if not debug_mesh_path.is_empty():
		debug_mesh = get_node(debug_mesh_path)
		if debug_mesh:
			var debug_material = debug_mesh.get_surface_override_material(0)
			debug_material.albedo_texture = sub_viewport.get_texture()
			debug_mesh.set_surface_override_material(0, debug_material)

	# Load the testing shader and assign it to the cloned particle system’s draw pass.
	var shader_material = ShaderMaterial.new()
	shader_material.shader = shader

	# Clone the source particles and set the shader material.
	clone_particles = source_particles.duplicate()
	# diable trails since they add extra meshes.
	clone_particles.trail_enabled = false
	# replace the draw pass with a simple quad mesh.
	clone_particles.draw_pass_1 = QuadMesh.new()
	# and set the special material
	clone_particles.draw_pass_1.surface_set_material(0, shader_material)
	# set visual layer to 20 so it doesn't draw on main camera
	clone_particles.layers = 0
	clone_particles.set_layer_mask_value(20, true)
	# add clone as child of SubViewport
	sub_viewport.add_child(clone_particles)
	
	# Precompute static target parameters and set them in the shader.
	var centers: Array = []      # Array of Vector3 centers.
	var extents: Array = []      # Array of Vector3 half-sizes.
	var inv_bases: Array = []    # Array of mat3 (precomputed inverse bases).
	
	for i in range(MAX_TARGETS):
		if i < target_collision_shapes.size() and target_collision_shapes[i] and (target_collision_shapes[i].shape is BoxShape3D):
			var box: BoxShape3D = target_collision_shapes[i].shape as BoxShape3D
			var gxf: Transform3D = target_collision_shapes[i].global_transform
			print("Target %d:" % i)
			print("  Box extents: ", box.extents)
			print("  Global transform origin: ", gxf.origin)
			print("  Global transform basis: ", gxf.basis)
			centers.append(gxf.origin)
			extents.append(box.extents)
			inv_bases.append(gxf.basis.inverse())  # Precompute the inverse once.
		else:
			centers.append(Vector3.ZERO)
			extents.append(Vector3.ZERO)   # Zero extents means this target is disabled.
			inv_bases.append(Basis.IDENTITY)
	
	shader_material.set_shader_parameter("target_center", centers)
	shader_material.set_shader_parameter("target_extents", extents)
	shader_material.set_shader_parameter("target_inv_basis", inv_bases)

func _process(_delta):
	# Each frame, read back the 8 pixel wide image from the SubViewport.
	var viewport_tex = sub_viewport.get_texture()
	if viewport_tex:
		var img: Image = viewport_tex.get_image()
		if img:
			for x in range(target_collision_shapes.size()):
				var col: Color = img.get_pixel(x, 0)
				var occupancy: float = col.r  # The red channel intensity.
				target_occupancy_changed.emit(x, occupancy)
