extends RefCounted

# One acquisition per attack. "Enemy" is relative to the caster's faction.
const Timeline = preload("res://scripts/core/attack_timeline.gd")
const MODES := ["aim", "self", "forward", "nearest_enemy", "farthest_enemy", "random_enemy", "lowest_hp_enemy", "highest_hp_enemy", "lowest_hp_ratio_enemy", "highest_hp_ratio_enemy", "random_point", "random_direction"]

static func acquire(actor: Node2D, pattern: Dictionary, direction: Vector2, aim: Vector2) -> Dictionary:
	var mode := str(pattern.get("target_mode", "aim"))
	var radius := float(pattern.get("target_range", 0.0))
	if radius <= 0.0:
		radius = maxf(float(pattern.get("trigger_range", 0.0)), float(Timeline.of(pattern).get("hit_reach", 44.0)))
	var state := {"mode": mode, "point": aim, "radius": radius}
	var forward := direction.normalized() if direction.length_squared() > 0.001 else Vector2.DOWN
	if mode == "aim":
		return state
	if mode == "self":
		state.point = actor.global_position
	elif mode == "forward":
		state.point = actor.global_position + forward * radius
	elif mode in ["random_point", "random_direction"]:
		var distance := radius * sqrt(randf()) if mode == "random_point" else radius
		state.point = actor.global_position + Vector2.RIGHT.rotated(randf() * TAU) * distance
	else:
		var candidates: Array[Node2D] = []
		var group := "enemy" if actor.is_in_group("player") else "player"
		for node in GameState.scoped_nodes(actor, group):
			if node is Node2D and node != actor and _alive(node) and actor.global_position.distance_to(node.global_position) <= radius:
				candidates.append(node)
		if candidates.is_empty():
			var fallback := str(pattern.get("target_fallback", "aim"))
			state.mode = fallback
			if fallback == "self":
				state.point = actor.global_position
			elif fallback == "forward":
				state.point = actor.global_position + forward * radius
			return state
		candidates.sort_custom(func(a: Node2D, b: Node2D) -> bool:
			var av := _score(a, actor, mode)
			var bv := _score(b, actor, mode)
			return a.get_instance_id() < b.get_instance_id() if is_equal_approx(av, bv) else av < bv)
		var chosen: Node2D = candidates.pick_random() if mode == "random_enemy" else candidates[0]
		state["node"] = weakref(chosen)
		state.point = chosen.global_position
	return state

static func _alive(node: Node) -> bool:
	return is_instance_valid(node) and not node.is_queued_for_deletion() and node.get("active") != false and (node.get("health") == null or float(node.get("health")) > 0.0)

static func _score(node: Node2D, actor: Node2D, mode: String) -> float:
	if mode in ["nearest_enemy", "farthest_enemy", "random_enemy"]:
		return actor.global_position.distance_squared_to(node.global_position) * (-1.0 if mode == "farthest_enemy" else 1.0)
	var hp := float(node.get("health")) if node.get("health") != null else 0.0
	if mode in ["lowest_hp_ratio_enemy", "highest_hp_ratio_enemy"]:
		var maximum := float(node.call("get_max_health")) if node.has_method("get_max_health") else float(node.get("max_health"))
		hp /= maxf(1.0, maximum)
	return hp * (-1.0 if mode in ["highest_hp_enemy", "highest_hp_ratio_enemy"] else 1.0)

static func destination(state: Dictionary, actor: Node2D, aim: Vector2) -> Vector2:
	if state.is_empty() or state.get("mode", "aim") == "aim":
		return aim
	if state.get("mode") == "self":
		return actor.global_position
	if state.has("node"):
		var node = state.node.get_ref()
		if node != null and _alive(node) and actor.global_position.distance_to(node.global_position) <= float(state.radius):
			state.point = node.global_position
		else:
			state.erase("node") # Lost targets freeze, never jump to a replacement.
	return state.point

static func follow(origin: Vector2, destination_point: Vector2, zone: Dictionary, delta: float) -> Vector2:
	if bool(zone.get("locked", false)):
		return origin
	return origin.move_toward(destination_point, maxf(0.0, float(zone.get("tracking_speed", 0.0))) * maxf(0.0, delta))
