"""
	Asset: Godot Simple Portal System
	File: advanced_portal_teleport.gd
	Description: An area which teleports a node through the parent node's portal. Checks entry velocity and handles RigidBody3D and CharacterBody3D physics. Can also handle a portal clone of the node if specified.
	Instructions: For detailed documentation, see the README or visit: https://github.com/Donitzo/godot-simple-portal-system
	Repository: https://github.com/Donitzo/godot-simple-portal-system
	License: CC0 License
"""

extends Area3D
class_name AdvancedPortalTeleport

## Checks if the node is moving TOWARDS the portal before teleporting it.
@export var velocity_check:bool = true
## An additional velocity push given to RigidBody3Ds/CharacterBody3D exiting the portal.
@export var exit_push_velocity:float = 0
## Seconds to keep portal clones visible after the node leaves the teleporter.
@export var clone_keep_alive_seconds:float = 0.1

var _parent_portal:Portal

# Info about the nodes currently crossing the portal
var _crossing_nodes:Array = []

func _ready() -> void:
	_parent_portal = get_parent() as Portal
	if _parent_portal == null:
		push_error("The PortalTeleport \"%s\" is not a child of a Portal instance" % name)
	
	connect("area_entered", _on_area_entered)
	connect("area_exited", _on_area_exited)

func _process(delta:float) -> void:
	# Update nodes crossing the portal
	for i in range(_crossing_nodes.size() - 1, -1, -1):
		var crossing_node:Dictionary = _crossing_nodes[i]
	
		if not is_instance_valid(crossing_node.node):
			# Node has been freed, remove crossing_node
			_crossing_nodes.remove_at(i)
			continue
		
		# If the portal has yet to leave the enter portal area, try to teleport it
		if not crossing_node.left and _try_teleport(_crossing_nodes[i]):
			# Switch portals so that the portal clone is placed at the entrance portal instead
			crossing_node.clone_portal = _parent_portal.exit_portal
			crossing_node.left = true

		# Move the clone to the exit portal
		if crossing_node.clone != null and crossing_node.clone_portal != null:
			crossing_node.clone.global_transform = crossing_node.clone_portal.real_to_exit_transform(crossing_node.node.global_transform)

		# If the node has left the enter portal, keep it a bit longer before erasing it
		if crossing_node.left:
			crossing_node.keep_alive_seconds -= delta
		if crossing_node.keep_alive_seconds <= 0:
			# Hide portal clone
			if crossing_node.clone != null:
				crossing_node.clone.visible = false

			_crossing_nodes.remove_at(i)
	
# Try to teleport the crossing node, and return false otherwise
func _try_teleport(crossing_node:Dictionary) -> bool:
	var node:Node3D = crossing_node.node

	# Check if the node is moving towards the portal
	if velocity_check:
		if node is RigidBody3D:
			var local_velocity:Vector3 = _parent_portal.global_transform.basis.inverse() * node.linear_velocity
			if local_velocity.z >= 0:
				return false
		elif node is CharacterBody3D:
			var local_velocity:Vector3 = _parent_portal.global_transform.basis.inverse() * node.velocity
			if local_velocity.z >= 0:
				return false
		else:
			var last_position = crossing_node.position
			crossing_node.position = _parent_portal.to_local(node.global_position)
			if last_position == null or last_position.z <= crossing_node.position.z:
				return false

	# Handle RigidBody3D physics    
	if node is RigidBody3D:
		# Rotate rotation and velocity
		node.linear_velocity = _parent_portal.real_to_exit_direction(node.linear_velocity)
		node.angular_velocity *= _parent_portal.real_to_exit_transform(Transform3D.IDENTITY).basis.inverse()

		# Additional push when exiting the portal
		if exit_push_velocity > 0:
			var exit_forward:Vector3 = _parent_portal.exit_portal.global_transform.basis.z.normalized()
			node.linear_velocity += exit_forward * exit_push_velocity
	
	# Handle CharacterBody3D physics
	elif node is CharacterBody3D:
		# Rotate velocity
		node.velocity = _parent_portal.real_to_exit_direction(node.velocity)

		# Additional push when exiting the portal
		if exit_push_velocity > 0:
			var exit_forward:Vector3 = _parent_portal.exit_portal.global_transform.basis.z.normalized()
			node.velocity += exit_forward * exit_push_velocity

	# Transform the position and orientation
	node.global_transform = _parent_portal.real_to_exit_transform(node.global_transform)
	
	return true

func _on_area_entered(area:Area3D) -> void:
	if area.has_meta("teleportable_root"):
		# The node may not teleport immediately if it's not heading TOWARDS the portal,
		# so we keep a reference to it until it teleports or leaves.
		# This also allows us to hide its portal clone after it leaves.

		var root:Node3D = area.get_node(area.get_meta("teleportable_root"))

		var crossing_node:Dictionary
		if root.has_meta("crossing_node"):
			# Node is crossing another portal, erase it from that portal and start using this one instead
			crossing_node = root.get_meta("crossing_node")
			crossing_node.teleporter._crossing_nodes.erase(crossing_node)
		else:
			# First portal
			var clone:Node3D = area.get_node(area.get_meta("portal_clone")) if area.has_meta("portal_clone") else null
			crossing_node = {
				"node": root, 
				"clone": clone,
				"clone_portal": null,
				"teleporter": null,
				"left": false,
				"keep_alive_seconds": 0.0,
				"position": null,
			}
			root.set_meta("crossing_node", crossing_node)

		crossing_node.clone_portal = _parent_portal
		crossing_node.teleporter = self
		crossing_node.left = false
		crossing_node.keep_alive_seconds = clone_keep_alive_seconds
		crossing_node.position = null

		# Show portal clone if it exists
		if crossing_node.clone != null:
			crossing_node.clone.visible = true

		# Keep track of the node in this portal        
		_crossing_nodes.push_back(crossing_node)
		
		# Try an initial teleport
		if _try_teleport(crossing_node):
			crossing_node.clone_portal = _parent_portal.exit_portal
			crossing_node.left = true

func _on_area_exited(area:Area3D) -> void:
	if area.has_meta("teleportable_root"):
		var root:Node3D = area.get_node(area.get_meta("teleportable_root"))
		var crossing_node:Dictionary = root.get_meta("crossing_node")

		if crossing_node.teleporter == self:
			# The node left the enter portal without teleporting (but don't erase it yet)
			crossing_node.left = true
