@tool
@icon("../assets/Anchor.svg")
extends Node2D
class_name SS2D_Shape_Anchor

const DEBUG_DRAW_LINE_LENGTH := 128.0

@export var shape_path: NodePath : set = set_shape_path
@export var shape_point_index: int = 0 : set = set_shape_point_index
@export_range (0.0, 1.0) var shape_point_offset: float = 0.0 : set = set_shape_point_offset
@export_range (0, 3.14) var child_rotation: float = 3.14 : set = set_child_rotation
@export var use_shape_scale: bool = false : set = set_use_shape_scale

@export var debug_draw: bool = false : set = set_debug_draw

var cached_shape_transform: Transform2D = Transform2D.IDENTITY
var shape: SS2D_Shape = null


###########
#-SETTERS-#
###########

func set_shape_path(value: NodePath) -> void:
	# Assign path value
	shape_path = value
	set_shape()

	refresh()


func set_shape() -> void:
	# Disconnect old shape
	if shape != null:
		disconnect_shape(shape)

	# Set shape if path is valid and connect
	shape = null
	if has_node(shape_path):
		var new_node: Node = get_node(shape_path)
		if not new_node is SS2D_Shape:
			push_error("Shape Path isn't a valid subtype of SS2D_Shape! Aborting...")
			return
		shape = new_node
		connect_shape(shape)
		shape_point_index = get_shape_index_range(shape, shape_point_index)


func get_shape_index_range(s: SS2D_Shape, idx: int) -> int:
	var point_count: int = s.get_point_array().get_point_count()
	# Subtract 2;
	#   'point_count' is out of bounds; subtract 1
	#   cannot use final idx as starting point_index; subtract another 1
	var final_idx: int = point_count - 2
	if idx < 0:
		idx = final_idx
	idx = idx % (final_idx + 1)
	return idx


func set_shape_point_index(value: int) -> void:
	if value == shape_point_index:
		return

	if shape == null:
		shape_point_index = value
		return

	shape_point_index = get_shape_index_range(shape, value)
	refresh()


func set_shape_point_offset(value: float) -> void:
	shape_point_offset = value
	refresh()


func set_use_shape_scale(value: bool) -> void:
	use_shape_scale = value
	refresh()


func set_child_rotation(value: float) -> void:
	child_rotation = value
	refresh()


func set_debug_draw(v: bool) -> void:
	debug_draw = v
	refresh()


##########
#-EVENTS-#
##########
func _process(_delta: float) -> void:
	if shape == null:
		set_shape()
		return
	if shape.is_queued_for_deletion():
		return
	if shape.get_global_transform() != cached_shape_transform:
		cached_shape_transform = shape.get_global_transform()
		refresh()


func _monitored_node_leaving() -> void:
	pass


func _handle_point_change() -> void:
	refresh()


#########
#LOGIC-#
#########
func _cubic_bezier(p0: Vector2, p1: Vector2, p2: Vector2, p3: Vector2, t: float) -> Vector2:
	var q0 := p0.lerp(p1, t)
	var q1 := p1.lerp(p2, t)
	var q2 := p2.lerp(p3, t)

	var r0 := q0.lerp(q1, t)
	var r1 := q1.lerp(q2, t)

	var s := r0.lerp(r1, t)
	return s


func disconnect_shape(s: SS2D_Shape) -> void:
	s.disconnect("points_modified", self._handle_point_change)
	s.disconnect("tree_exiting", self._monitored_node_leaving)


func connect_shape(s: SS2D_Shape) -> void:
	s.connect("points_modified", self._handle_point_change)
	s.connect("tree_exiting", self._monitored_node_leaving)


func refresh() -> void:
	if shape == null:
		return
	if not is_instance_valid(shape):
		return
	if shape.is_queued_for_deletion():
		disconnect_shape(shape)
		shape = null
		return

	var pa := shape.get_point_array()
	var pt_a_index: int = shape_point_index
	var pt_b_index: int = shape_point_index + 1
	var pt_a_key: int = pa.get_point_key_at_index(pt_a_index)
	var pt_b_key: int = pa.get_point_key_at_index(pt_b_index)

	var pt_a: Vector2 = shape.global_transform * pa.get_point_position(pt_a_key)
	var pt_b: Vector2 = shape.global_transform * pa.get_point_position(pt_b_key)

	var pt_a_handle: Vector2
	var pt_b_handle: Vector2

	var n_pt: Vector2
	var n_pt_a: Vector2
	var n_pt_b: Vector2

	var angle := 0.0

	pt_a_handle = shape.global_transform * (
		pa.get_point_position(pt_a_key) + pa.get_point_out(pt_a_key)
	)
	pt_b_handle = shape.global_transform * (
		pa.get_point_position(pt_b_key) + pa.get_point_in(pt_b_key)
	)

	# If this segment uses no bezier curve, use linear interpolation instead
	if pt_a_handle != pt_a or pt_b_handle != pt_b:
		n_pt = _cubic_bezier(pt_a, pt_a_handle, pt_b_handle, pt_b, shape_point_offset)
	else:
		n_pt = pt_a.lerp(pt_b, shape_point_offset)

	n_pt_a = _cubic_bezier(
		pt_a, pt_a_handle, pt_b_handle, pt_b, clampf(shape_point_offset - 0.1, 0.0, 1.0)
	)
	n_pt_b = _cubic_bezier(
		pt_a, pt_a_handle, pt_b_handle, pt_b, clampf(shape_point_offset + 0.1, 0.0, 1.0)
	)

	angle = atan2(n_pt_a.y - n_pt_b.y, n_pt_a.x - n_pt_b.x)

	self.global_transform = Transform2D(angle + child_rotation, n_pt)

	if use_shape_scale:
		self.scale = shape.scale

	queue_redraw()


func _draw() -> void:
	if Engine.is_editor_hint() and debug_draw:
		draw_line(Vector2.ZERO, Vector2(0, -DEBUG_DRAW_LINE_LENGTH), self.modulate)
