class_name GrappleGun
extends Node2D

signal grappled(grapple_point:Vector2)
signal retrieved(sp:Sprite2D)

@export var shoot_speed:float = 600.0
@export var retract_speed:float = 800.0
@export var gun_hook:Sprite2D
@export var hook_marker:Marker2D
@export var line:Line2D

var sm:FuncMachine

var hook_bullet:Sprite2D

@onready var grapple_detector: Area2D = $Hook/GrappleDetector
@onready var launch: AudioStreamPlayer2D = $Launch
@onready var retracted: AudioStreamPlayer2D = $Retracted
@onready var stick: AudioStreamPlayer2D = $Stick

## Returns  normalized direction to current grapple. If no grapple, returns Vector2.ZERO
var dir_to_grapple:Vector2:
	get:
		if hook_bullet:
			return global_position.direction_to(hook_bullet.global_position)
		return Vector2.ZERO

## returns true if the grapple is attached to something
var is_attached:
	get: return is_attached

var is_ready:bool:
	get: return is_ready

var gun_dir:Vector2:
	get:
		return Vector2.from_angle(global_rotation - PI / 2.0).normalized()

var is_retracted:bool:
	get:
		if hook_bullet == null:
			return true
		elif hook_bullet.global_position.distance_squared_to(hook_marker.global_position) < 1024:
			return true
		return false

## If true then the grapple gun can fire
var can_fire:bool = true:
	get: return can_fire
	
var grapple_collisions:int = 0

func _ready():
	grapple_detector.body_entered.connect(func(body): 
		grapple_collisions += 1 
		can_fire = false)
	grapple_detector.body_exited.connect(func(body):
		grapple_collisions = max( 0, grapple_collisions - 1)
		can_fire = grapple_collisions == 0)
	
	sm = FuncMachine.new()
	sm.add(_ready_enter, sm._update_empty, _ready_exit)
	sm.add(_shoot_enter, _shoot_update)
	sm.add(_retract_enter, _retract_update, _retract_exit)
	sm.add(_stuck_enter, _stuck_update)
	
	sm.start(_ready)

func _process(delta: float) -> void:
	sm.update(delta)

func fire() -> bool:
	if can_fire && sm.current_state_name == "ready":
		sm.change(_shoot_enter)
		return true
	return false

func retract() -> bool:
	if sm.current_state_name == "shoot" || sm.current_state_name == "stuck":
		sm.change(_retract_enter)
		return true
	return false

func reset():
	if sm.current_state_name != "ready":
		sm.change(_ready_enter)

func _shoot_enter():
	launch.play()
	gun_hook.visible = false
	line.visible = true
	create_hook()
	await get_tree().process_frame
	
func _shoot_update(delta:float):
	hook_bullet.global_position += -hook_bullet.transform.y * shoot_speed * delta
	line.set_point_position(1, line.to_local(hook_bullet.global_position))

func _retract_enter():
	retracted.play()
	line.visible = true
	is_attached = false

func _retract_update(delta):
	hook_bullet.global_position += hook_bullet.global_position.direction_to(hook_marker.global_position) * retract_speed * delta
	line.set_point_position(1, line.to_local(hook_bullet.global_position))
	if is_retracted:
		hook_bullet.global_position = hook_marker.global_position
		hook_bullet.global_rotation = hook_marker.global_rotation
		sm.change(_ready_enter)

func _retract_exit():
	if hook_bullet.get_child_count() > 0:
		var goodies:Array = U.get_children(hook_bullet, Sprite2D)
		for i in range(goodies.size() - 1, - 1, -1):
			var ch:Sprite2D = goodies[i]
			hook_bullet.remove_child(ch)
			retrieved.emit(ch)
			ch.queue_free()
	
func _stuck_enter():
	stick.play()
	is_attached = true

func _stuck_update(delta:float):
	line.set_point_position(1, line.to_local(hook_bullet.global_position))
	
func _ready_enter():
	if hook_bullet:
		hook_bullet.queue_free()
		hook_bullet = null
	is_ready = true
	is_attached = false
	gun_hook.visible = true
	line.visible = false

func _ready_exit():
	is_ready = false

func create_hook():
	if hook_bullet:
		hook_bullet.queue_free()
		
	hook_bullet = gun_hook.duplicate()
	var n = get_node_or_null("../../../../")
	if n:
		n.add_child.call_deferred(hook_bullet)
	else:
		get_tree().get_root().add_child.call_deferred(hook_bullet)
	hook_bullet.global_position = hook_marker.global_position
	hook_bullet.global_rotation = hook_marker.global_rotation
	hook_bullet.visible = true
	
	var detector:Area2D = hook_bullet.get_node("GrappleDetector")
	#detector.set_deferred("monitoring", true)
	#detector.set_deferred("monitorable", true)
	detector.body_entered.connect(_ongrapple)
	detector.area_entered.connect(_on_passed_area)

func _on_passed_area(area:Area2D):
		var b:Bolt = area.owner
		if b:
			b.queue_free()
			var s:Sprite2D = Sprite2D.new()
			s.texture = b.sprite.texture
			hook_bullet.add_child(s)
			s.global_position = b.global_position
			s.global_rotation = b.global_rotation
		if sm.current_state_name != "retract":
			sm.change(_retract_enter)
	
func _ongrapple(body:Node2D):	
	if sm.current_state_name != "stuck" && sm.current_state_name != "retract":
		sm.change(_stuck_enter)
		grappled.emit(hook_bullet.global_position)

func _notification(what):
	if what == NOTIFICATION_PREDELETE && hook_bullet:
		hook_bullet.queue_free()
		hook_bullet = null
		
