@tool
class_name XRToolsMovementGrapple
extends XRToolsMovementProvider


## XR Tools Movement Provider for Grapple Movement
##
## This script provide simple grapple based movement - "bat hook" style
## where the player flings a rope to the target and swings on it.
## This script works with the [XRToolsPlayerBody] attached to the players
## [XROrigin3D].


## Signal emitted when grapple starts
signal grapple_started()

## Signal emitted when grapple finishes
signal grapple_finished()


## Grapple state
enum GrappleState {
	IDLE,			## Grapple is idle
	FIRED,			## Grapple is fired
	WINCHING,		## Grapple is winching
}


# Default grapple collision mask of 1-5 (world)
const DEFAULT_COLLISION_MASK := 0b0000_0000_0000_0000_0000_0000_0001_1111

# Default grapple enable mask of 5:grapple-target
const DEFAULT_ENABLE_MASK := 0b0000_0000_0000_0000_0000_0000_0001_0000


## Movement provider order
@export var order : int = 20

## Grapple length - use to adjust maximum distance for possible grapple hooking.
@export var grapple_length : float = 15.0

## Grapple collision mask
@export_flags_3d_physics var grapple_collision_mask : int = DEFAULT_COLLISION_MASK:
	set = _set_grapple_collision_mask

## Grapple enable mask
@export_flags_3d_physics var grapple_enable_mask : int = DEFAULT_ENABLE_MASK

## Impulse speed applied to the player on first grapple
@export var impulse_speed : float = 10.0

## Winch speed applied to the player while the grapple is held
@export var winch_speed : float = 2.0

## Probably need to add export variables for line size, maybe line material at
## some point so dev does not need to make children editable to do this.
## For now, right click on grapple node and make children editable to edit these
## facets.
@export var rope_width : float = 0.02

## Air friction while grappling
@export var friction : float = 0.1

## Grapple button (triggers grappling movement).  Be sure this button does not
## conflict with other functions.
@export var grapple_button_action : String = "trigger_click"

# Hook related variables
var hook_object : Node3D = null
var hook_local := Vector3(0,0,0)
var hook_point := Vector3(0,0,0)

# Grapple button state
var _grapple_button := false

# Get line creation nodes
@onready var _line_helper : Node3D = $LineHelper
@onready var _line : CSGCylinder3D = $LineHelper/Line

# Get Controller node - consider way to universalize this if user wanted to
# attach this to a gun instead of player's hand.  Could consider variable to
# select controller instead.
@onready var _controller := XRHelpers.get_xr_controller(self)

# Get Raycast node
@onready var _grapple_raycast : RayCast3D = $Grapple_RayCast

# Get Grapple Target Node
@onready var _grapple_target : Node3D = $Grapple_Target


# Add support for is_xr_class on XRTools classes
func is_xr_class(name : String) -> bool:
	return name == "XRToolsMovementGrapple" or super(name)


# Function run when node is added to scene
func _ready():
	# In Godot 4 we must now manually call our super class ready function
	super()

	# Skip if running in the editor
	if Engine.is_editor_hint():
		return

	# Ensure grapple length is valid
	var min_hook_length := 1.5 * XRServer.world_scale
	if grapple_length < min_hook_length:
		grapple_length = min_hook_length

	# Set ray-cast
	_grapple_raycast.target_position = Vector3(0, 0, -grapple_length) * XRServer.world_scale
	_grapple_raycast.collision_mask = grapple_collision_mask

	# Deal with line
	_line.radius = rope_width
	_line.hide()


# Update grapple display objects
func _process(_delta: float):
	# Skip if running in the editor
	if Engine.is_editor_hint():
		return

	# Update grapple line
	if is_active:
		var line_length := (hook_point - _controller.global_transform.origin).length()
		_line_helper.look_at(hook_point, Vector3.UP)
		_line.height = line_length
		_line.position.z = line_length / -2
		_line.visible = true
	else:
		_line.visible = false

	# Update grapple target
	if enabled and !is_active and _is_raycast_valid():
		_grapple_target.global_transform.origin  = _grapple_raycast.get_collision_point()
		_grapple_target.global_transform = _grapple_target.global_transform.orthonormalized()
		_grapple_target.visible = true
	else:
		_grapple_target.visible = false


# Perform grapple movement
func physics_movement(delta: float, player_body: XRToolsPlayerBody, disabled: bool):
	# Disable if requested
	if disabled or !enabled or !_controller.get_is_active():
		_set_grappling(false)
		return

	# Update grapple button
	var old_grapple_button := _grapple_button
	_grapple_button = _controller.is_button_pressed(grapple_button_action)

	# Enable/disable grappling
	var do_impulse := false
	if is_active and !_grapple_button:
		_set_grappling(false)
	elif _grapple_button and !old_grapple_button and _is_raycast_valid():
		hook_object = _grapple_raycast.get_collider()
		hook_point = _grapple_raycast.get_collision_point()
		hook_local = hook_point * hook_object.global_transform
		do_impulse = true
		_set_grappling(true)

	# Skip if not grappling
	if !is_active:
		return

	# Get hook direction
	hook_point = hook_object.global_transform * hook_local
	var hook_vector := hook_point - _controller.global_transform.origin
	var hook_length := hook_vector.length()
	var hook_direction := hook_vector / hook_length

	# Apply gravity
	player_body.velocity += player_body.gravity * delta

	# Select the grapple speed
	var speed := impulse_speed if do_impulse else winch_speed
	if hook_length < 1.0:
		speed = 0.0

	# Ensure velocity is at least winch_speed towards hook
	var vdot = player_body.velocity.dot(hook_direction)
	if vdot < speed:
		player_body.velocity += hook_direction * (speed - vdot)

	# Scale down velocity
	player_body.velocity *= 1.0 - friction * delta

	# Perform exclusive movement as we have dealt with gravity
	player_body.velocity = player_body.move_body(player_body.velocity)
	return true


# Called when the grapple collision mask has been modified
func _set_grapple_collision_mask(new_value: int) -> void:
	grapple_collision_mask = new_value
	if is_inside_tree() and _grapple_raycast:
		_grapple_raycast.collision_mask = new_value


# Set the grappling state and fire any signals
func _set_grappling(active: bool) -> void:
	# Skip if no change
	if active == is_active:
		return

	# Update the is_active flag
	is_active = active;

	# Report transition
	if is_active:
		emit_signal("grapple_started")
	else:
		emit_signal("grapple_finished")


# Test if the raycast is striking a valid target
func _is_raycast_valid() -> bool:
	# Fail if raycast not colliding
	if not _grapple_raycast.is_colliding():
		return false

	# Get the target of the raycast
	var target : CollisionObject3D = _grapple_raycast.get_collider()

	# Check tartget layer
	return true if target.collision_layer & grapple_enable_mask else false


# This method verifies the movement provider has a valid configuration.
func _get_configuration_warnings() -> PackedStringArray:
	var warnings := super()

	# Check the controller node
	if !XRHelpers.get_xr_controller(self):
		warnings.append("This node must be within a branch of an XRController3D node")

	# Return warnings
	return warnings