extends Node2D

const MAP_SCENE: PackedScene = preload("res://world/maps/map_01.tscn")
const SHIP_SCENE: PackedScene = preload("res://ships/ship.tscn")
const BULLET_SCENE: PackedScene = preload("res://ships/bullet.tscn")

@onready var _map_container: Node2D = $MapContainer
@onready var _players: Node2D = $Players
@onready var _spawner: MultiplayerSpawner = $MultiplayerSpawner
@onready var _bullets: Node2D = $Bullets
@onready var _bullet_spawner: MultiplayerSpawner = $BulletSpawner
@onready var _team_select: TeamSelect = $TeamSelect

# Server-authoritative: the 2 races offered for this match, decided once and
# shared by every player — see TeamSelect._ready(), which either reads this
# directly (server's own instance) or requests it over RPC (remote clients).
var _offered_race_ids: Array = []


func _ready() -> void:
	var map := MAP_SCENE.instantiate()
	_map_container.add_child(map)
	var background := map.get_node_or_null("Background") as Control
	if background:
		GameConfig.world_bounds = Rect2(background.position, background.size)
	_build_tile_collisions(map)

	_spawner.spawn_function = _spawn_ship
	_bullet_spawner.spawn_function = _spawn_bullet
	NetworkManager.peer_joined.connect(_on_peer_joined)
	NetworkManager.peer_left.connect(_on_peer_left)

	if multiplayer.is_server():
		spawn_peer(multiplayer.get_unique_id())
		for peer_id in multiplayer.get_peers():
			spawn_peer(peer_id)


# Picks the match's offered races once and caches them, so every caller
# (the server's own TeamSelect and every remote client's request) converges
# on the same pair regardless of call order. Also kicks off bot fill for
# those races — see bots/bot_manager.gd. Both factions are always offered
# (only order is randomized, for spawn-side variety).
func decide_offered_races() -> Array:
	if _offered_race_ids.is_empty():
		var ids: Array = TeamSelect.RACES.map(func(race): return race.id)
		ids.shuffle()
		_offered_race_ids = ids
		# Deferred: this can run from TeamSelect._ready(), which (as World's
		# child) fires before World's own _ready() — before _players/_spawner
		# are assigned. Deferring runs it after the whole scene's _ready
		# cascade finishes, once World is actually spawn-ready.
		BotManager.call_deferred("on_match_start", self, _offered_race_ids)
	return _offered_race_ids


@rpc("any_peer", "call_remote", "reliable")
func request_offered_races() -> void:
	if not multiplayer.is_server():
		return
	var sender_id := multiplayer.get_remote_sender_id()
	_deliver_offered_races.rpc_id(sender_id, decide_offered_races())


@rpc("authority", "call_remote", "reliable")
func _deliver_offered_races(race_ids: Array) -> void:
	_team_select.offer_races(race_ids)


# Spawns (or returns the already-spawned) ship for a given peer_id — real
# (positive) or bot (negative, see bots/bot_manager.gd). Replicates to every
# client automatically via MultiplayerSpawner.
func spawn_peer(peer_id: int) -> Node:
	if _players.has_node(str(peer_id)):
		return _players.get_node(str(peer_id))
	return _spawner.spawn(peer_id)


func despawn_peer(peer_id: int) -> void:
	var ship := _players.get_node_or_null(str(peer_id))
	if ship:
		# Stops _server_tick() from emitting any further _receive_state
		# broadcasts for this ship the instant despawn is requested — without
		# this, a state packet already in flight on the unreliable channel can
		# still reach a client after that client has already processed this
		# node's despawn replication, causing a "Node not found" RPC error.
		ship.set_physics_process(false)
		ship.queue_free()


func _spawn_ship(peer_id: int) -> Node:
	var ship := SHIP_SCENE.instantiate()
	ship.name = str(peer_id)
	ship.peer_id = peer_id
	return ship


# Called by a Ship node running on the server when its authoritative fire
# cooldown allows a shot. Replicated to every peer via the bullet spawner.
# damage is decided by the firing ship's role (see
# ship_movement.gd's _server_process_shoot()), not a flat global.
func request_bullet_spawn(source_peer_id: int, pos: Vector2, vel: Vector2, damage: int) -> void:
	if not multiplayer.is_server():
		return
	_bullet_spawner.spawn({"peer_id": source_peer_id, "pos": pos, "vel": vel, "damage": damage})


func _spawn_bullet(data: Dictionary) -> Node:
	var bullet := BULLET_SCENE.instantiate()
	bullet.global_position = data.pos
	bullet.velocity = data.vel
	bullet.source_peer_id = data.peer_id
	bullet.damage = data.damage
	_play_shoot_sound(data.peer_id, data.pos)
	return bullet


# Runs once per peer (this spawn_function itself is what MultiplayerSpawner
# replicates, so every client fires this locally, not just the server) —
# each peer's own ship's Camera2D is that peer's audio listener, so
# AudioStreamPlayer2D's max_distance naturally limits who hears the shot to
# ships within GameConfig.ship_shoot_sound_max_distance, with no need to
# track which peers are "in range" server-side.
func _play_shoot_sound(source_peer_id: int, pos: Vector2) -> void:
	var sound_path: String = PlayerRegistry.get_info(source_peer_id).get("ship_sound_path", "")
	if sound_path.is_empty():
		return
	var player := AudioStreamPlayer2D.new()
	player.stream = load(sound_path)
	player.global_position = pos
	player.max_distance = GameConfig.ship_shoot_sound_max_distance
	player.finished.connect(player.queue_free)
	add_child(player)
	player.play()


func _on_peer_joined(peer_id: int) -> void:
	if multiplayer.is_server():
		spawn_peer(peer_id)


func _on_peer_left(peer_id: int) -> void:
	if not multiplayer.is_server():
		return
	despawn_peer(peer_id)


# Tiles have no collision shapes of their own, so spawn one StaticBody2D per
# occupied cell. Asteroids get round hitboxes to roughly match their sprites.
# Walls only bounce the ship; asteroids also deal impact damage.
func _build_tile_collisions(map: Node) -> void:
	for layer in map.find_children("*", "TileMapLayer", true, false):
		var is_asteroid := layer.name == "Asteroids"
		var group := "environment_hazard" if is_asteroid else "environment_wall"
		_add_layer_colliders(layer, is_asteroid, group)


func _add_layer_colliders(layer: TileMapLayer, use_circle: bool, group: String) -> void:
	var tile_size := Vector2(layer.tile_set.tile_size)
	for cell in layer.get_used_cells():
		var body := StaticBody2D.new()
		body.position = layer.map_to_local(cell)
		body.add_to_group(group)
		var shape := CollisionShape2D.new()
		if use_circle:
			var circle := CircleShape2D.new()
			circle.radius = min(tile_size.x, tile_size.y) * 0.5
			shape.shape = circle
		else:
			var rect := RectangleShape2D.new()
			rect.size = tile_size
			shape.shape = rect
		body.add_child(shape)
		layer.add_child(body)
