class_name ProjectileArc extends Node3D
## Visually project the arc of a projectile through space.
##
## If this node has any children, they will be positioned wherever the projection ends.

## Constant attrition factor for putting collisions
## The actual velocity attrition is dependent on lots of factors
#const PUTT_ATTRITION := 0.75  # rough?
const PUTT_ATTRITION := 0.8325  # green?

## Initial speed of the projectile, in m/s.
## The projectile's initial direction vector is the negative Z direction relative to this node.
@export var initial_speed := 1.0

@export_category("Projection")
## Time between projection steps, in seconds.
@export var time_step := 0.2
## Maximum number of steps to predict before stopping.
@export var max_steps := 50
## Ticks between each projection update. 0 means update every tick.
@export var ticks_per_update := 0
## If enabled, project a linear putt instead of an arcing shot
@export var putt_projection := false

@export_category("Collision")
## Enables collision checking. Projection will end at the point where a collision is detected.
## Uses continuous collision detection.
@export var check_collision := true
## Mask for collision checking.
@export_flags_3d_physics var collision_mask := 1
## Bodies excluded from collision checking.
## This should probably include the ball!
@export var excluded_bodies: Array[CollisionObject3D] = []

var _tick_counter := 0

var _debug_points: Array[Vector3] = []

@onready var polygon: CSGPolygon3D = %Polygon
@onready var path: Path3D = %Path

@onready var gravity: float = ProjectSettings.get_setting("physics/3d/default_gravity")
@onready var gravity_vec: Vector3 = ProjectSettings.get_setting("physics/3d/default_gravity_vector")

@onready var debug_draw: Control = %DebugDraw


func _process(_delta: float) -> void:
	if not visible:
		# Don't bother if we're not visible
		return

	# Short-circuit if we need to wait more ticks
	if _tick_counter > 0:
		_tick_counter -= 1
		return
	_tick_counter = ticks_per_update

	_debug_points = []

	# Rebuild path curve
	path.global_basis = Basis.IDENTITY
	path.curve.clear_points()

	var space := get_world_3d().direct_space_state
	var excluded_rid: Array[RID] = []
	excluded_rid.assign(excluded_bodies.map(func(k: CollisionObject3D) -> RID: return k.get_rid()))

	var pos := global_position
	var vel := -global_basis.z * initial_speed

	for t in range(0, max_steps):
		# TODO: smooth curve with bezier handles
		path.curve.add_point(pos - global_position)

		# Integrate projectile path
		vel += gravity * gravity_vec * time_step
		var next_pos := pos + vel * time_step

		# Collision
		if check_collision:
			var ray_params := PhysicsRayQueryParameters3D.create(
				pos, next_pos, collision_mask, excluded_rid
			)
			var collision := space.intersect_ray(ray_params)
			if collision:
				# Set current position to collision point, so it will be added to the path
				pos = collision["position"]
				_debug_points.append(pos)
				if putt_projection:
					var norm: Vector3 = (collision["normal"] as Vector3).normalized()
					next_pos = pos + norm * 0.05
					vel = PUTT_ATTRITION * (vel - 2 * norm * vel.dot(norm))
				else:
					# End projection!
					break

		pos = next_pos

	# Add terminal point (possibly collision point)
	path.curve.add_point(pos - global_position)

	# Reposition any children
	for n: Node in get_children():
		if n is Node3D and n != path:
			var node_3d: Node3D = n
			node_3d.global_position = pos
			node_3d.global_basis = Basis.IDENTITY

	(%DebugDraw as CanvasItem).queue_redraw()


func _on_visibility_changed() -> void:
	# Force update as soon as visible
	_tick_counter = 0
