extends Node3D

signal matador_killed
signal all_defeated

const MATADOR := preload("res://Matador.tscn")

var _spawn_count: int = 1
var _alive: int = 0
var _kills: int = 0
var _resolved: bool = false

# Flat (x,z) positions of the most recent batch spawned. A fresh wave keeps at least
# _SPAWN_MIN_SEP away from these so it never lands on top of where the last one did.
const _SPAWN_MIN_SEP: float = 3.5
var _last_spawn_positions: Array[Vector3] = []


func _ready() -> void:
	add_to_group(&"matador_spawn")
	_spawn_count = maxi(1, int(DP.f("mat_spawn_count")))
	# Deferred so it's safe from _ready and matches the previous spawn timing.
	_spawn.call_deferred()


func _spawn() -> void:
	# Bull and matadors start at opposite ends of a random diameter, so they always
	# begin as far apart as the arena allows (edge-to-edge across the centre).
	var arena_r := DP.f("arena_spawn_radius")
	var bull_angle := randf() * TAU
	_place_bull(bull_angle, arena_r)
	var placed: Array[Vector3] = _spread_positions(_spawn_count, bull_angle + PI, arena_r)
	for pos: Vector3 in placed:
		_spawn_at(pos)
	_last_spawn_positions = placed


func _place_bull(angle: float, arena_r: float) -> void:
	var bull := get_tree().get_first_node_in_group(&"player") as Node3D
	if not bull:
		return
	bull.global_position = Vector3(
		cos(angle) * arena_r, bull.global_position.y, sin(angle) * arena_r
	)


# Positions for the matadors, spread across the far side (centred on `center_angle`)
# so every matador stays near the point diametrically opposite the bull. Each slot gets
# its own angular band and a staggered radius, then rejection-samples inside that band so
# no two matadors land closer than _SPAWN_MIN_SEP — otherwise they pile up on top of
# each other on spawn. The arc widens with the crowd so a big wave doesn't get cramped.
func _spread_positions(count: int, center_angle: float, arena_r: float) -> Array[Vector3]:
	var positions: Array[Vector3] = []
	if count <= 1:
		positions.append(Vector3(cos(center_angle) * arena_r, 1.0, sin(center_angle) * arena_r))
		return positions
	var arc := clampf(TAU * 0.33 + float(count) * 0.25, TAU * 0.33, TAU * 0.92)
	var slot := arc / float(count)
	var inner := maxf(2.5, arena_r * 0.5)
	for i: int in count:
		# Give each matador its own angular slot for an even fan, but sample the radius
		# freely across the whole far-side band so the rejection test can push crowded
		# neighbours onto a different ring instead of piling them at one distance.
		var base_angle := center_angle - arc * 0.5 + slot * (float(i) + 0.5)
		var pos := Vector3(cos(base_angle) * arena_r, 1.0, sin(base_angle) * arena_r)
		for attempt: int in 16:
			var angle := base_angle + randf_range(-0.5, 0.5) * slot
			var radius := randf_range(inner, arena_r)
			pos = Vector3(cos(angle) * radius, 1.0, sin(angle) * radius)
			if _clear_of(pos, positions, _SPAWN_MIN_SEP):
				break
		positions.append(pos)
	return positions


func _spawn_at(pos: Vector3, generation: int = 0) -> void:
	var m: Node3D = MATADOR.instantiate()
	m.position = pos
	m.set_meta(&"generation", generation)
	get_parent().add_child(m)
	m.killed.connect(_on_matador_killed.bind(m))
	_alive += 1


# A slain matador is replaced by mat_split_factor fresh ones on the spot (a hydra),
# so the horde doubles each generation, up to mat_split_generations deep. The bull
# wins only when a whole generation dies out without splitting — i.e. once the spawns
# stop and the last body drops.
func _on_matador_killed(dead: Node3D) -> void:
	matador_killed.emit()
	_alive -= 1
	_kills += 1
	_split(dead)
	var win_kills := int(DP.f("mat_win_kills"))
	var reached_quota := win_kills > 0 and _kills >= win_kills
	if (reached_quota or _alive <= 0) and not _resolved:
		_resolved = true
		all_defeated.emit()


func _split(dead: Node3D) -> void:
	var count := int(DP.f("mat_split_factor"))
	var generation := int(dead.get_meta(&"generation", 0))
	if count < 2 or generation >= int(DP.f("mat_split_generations")):
		return
	var arena_r := DP.f("arena_spawn_radius")
	var origin := dead.global_position
	origin.y = 0.0
	var wave: Array[Vector3] = []
	for i: int in count:
		var pos := _pick_spawn(origin, arena_r, wave)
		wave.append(pos)
		_spawn_at(pos, generation + 1)
	_last_spawn_positions = wave


# Rejection-sample a spawn point ringed around `origin`, pushing the ring outward on
# each failed attempt until it clears both the previous wave (_last_spawn_positions)
# and the spots already taken this wave — so a fresh wave never lands where the last
# one did. Falls back to the last candidate if the arena is too cramped to satisfy it.
func _pick_spawn(origin: Vector3, arena_r: float, taken: Array[Vector3]) -> Vector3:
	var pos := origin
	for attempt: int in 12:
		var angle := randf() * TAU
		var r := randf_range(2.0, 3.5) + float(attempt) * 0.4
		pos = origin + Vector3(cos(angle) * r, 0.0, sin(angle) * r)
		var flat := Vector2(pos.x, pos.z)
		if flat.length() > arena_r:
			flat = flat.normalized() * arena_r
			pos = Vector3(flat.x, 0.0, flat.y)
		if _clear_of(pos, _last_spawn_positions, _SPAWN_MIN_SEP) \
				and _clear_of(pos, taken, _SPAWN_MIN_SEP):
			break
	pos.y = 1.0
	return pos


func _clear_of(pos: Vector3, others: Array[Vector3], min_sep: float) -> bool:
	for o: Vector3 in others:
		if Vector2(pos.x - o.x, pos.z - o.z).length() < min_sep:
			return false
	return true
