# This script is designed to drive gameplay logic based on the number of GPUParticles3D
# that "hit" various static target areas in 3D space. Multiple particle systems can be cloned
# and rendered in a dedicated SubViewport on layer 20. Each 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 systems) rather than cloned.
#
# The script looks for nodes in the "targets" group that are instances of the Target class,
# and directly calls their set_occupancy() method with the measured red intensity.

extends Node3D

# Optional debug mesh to visualize the viewport texture
@export var debug_mesh_path: NodePath
var debug_mesh: MeshInstance3D

# Exported node paths for particle systems to clone
@export var source_particles_paths: Array[NodePath] = []  # Array of GPUParticles3D nodes to clone

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

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

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

func _ready():
	# Find all Target nodes in the "targets" group
	for node in get_tree().get_nodes_in_group("targets"):
		print("Found node in targets group: ", node.name, " of type ", node.get_class())
		if node is Target:
			print("Adding target: ", node.name)
			targets.append(node)
		else:
			print("Skipping non-Target node: ", node.name)
	print("Found ", targets.size(), " valid targets")
	# Ensure we don't exceed MAX_TARGETS
	if targets.size() > MAX_TARGETS:
		push_warning("More than %d targets found, extras will be ignored" % MAX_TARGETS)
		targets.resize(MAX_TARGETS)

	# 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)

	# Create shared shader material
	var shader_material = ShaderMaterial.new()
	shader_material.shader = shader
	
	# Precompute static target parameters and set them in the shader once
	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 < targets.size():
			var collision_shape = targets[i].get_node_or_null("CollisionShape3D") as CollisionShape3D
			if collision_shape and collision_shape.shape is BoxShape3D:
				var box: BoxShape3D = collision_shape.shape
				var gxf: Transform3D = collision_shape.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)
		else:
			centers.append(Vector3.ZERO)
			extents.append(Vector3.ZERO)
			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)

	# Create clones of each particle system
	for path in source_particles_paths:
		var source_particles = get_node(path) as GPUParticles3D
		print("Getting source particles from path: ", path)
		if source_particles:
			print("Found source particles: ", source_particles.name)
		else:
			push_warning("Could not find GPUParticles3D at path: %s" % path)
		if source_particles:
			# Clone the source particles
			var clone = source_particles.duplicate()
			# disable trails since they add extra meshes
			clone.trail_enabled = false
			# replace the draw pass with a simple quad mesh
			clone.draw_pass_1 = QuadMesh.new()
			# and set the shared material
			clone.draw_pass_1.surface_set_material(0, shader_material)
			# set visual layer to 20 so it doesn't draw on main camera
			clone.layers = 0
			clone.set_layer_mask_value(20, true)
			# add clone as child of SubViewport
			sub_viewport.add_child(clone)
			clone_particles.append(clone)

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(targets.size()):
				var col: Color = img.get_pixel(x, 0)
				var occupancy: float = col.r  # The red channel intensity
				targets[x].set_occupancy(occupancy)
