extends Node

# Server-relayed chat. "All" messages reach every peer in the match; "team"
# messages only reach peers who share the sender's chosen race (races double
# as the match's two teams — see TeamSelect/PlayerRegistry).

signal message_received(sender_name: String, sender_race: int, team_only: bool, text: String)

const MAX_MESSAGE_LENGTH := 140

# Virtual system sender for join/leave announcements below. Not a real peer,
# so chat_box.gd special-cases this name to render it distinctly from player
# chat instead of trying to look up a race color for it.
const ADMIN_NAME := "Admin"


func _ready() -> void:
	# PlayerRegistry's join/removal state is already replicated identically to
	# every peer (loadout relay + ENet's own disconnect broadcast), so these
	# fire in sync on every client already — no extra RPC needed to announce
	# them, same as send_chat's local echo below needing no round trip.
	PlayerRegistry.player_joined.connect(_on_player_joined)
	PlayerRegistry.player_removed.connect(_on_player_removed)


# Bots (negative peer_id) are matchmaking padding, not real players — the
# race-selection headcount already excludes them for the same reason (see
# team_select.gd), so admin chat doesn't announce bot fill churn either.
func _on_player_joined(peer_id: int) -> void:
	if peer_id < 0:
		return
	var player_name: String = PlayerRegistry.get_info(peer_id).get("name", "A player")
	_announce("%s has joined the match." % player_name)


func _on_player_removed(peer_id: int, info: Dictionary) -> void:
	if peer_id < 0:
		return
	_announce("%s has left the match." % info.get("name", "A player"))


func _announce(text: String) -> void:
	message_received.emit(ADMIN_NAME, 0, false, text)


func send_chat(raw_text: String, team_only: bool) -> void:
	var text := raw_text.strip_edges().left(MAX_MESSAGE_LENGTH)
	if text.is_empty():
		return

	# Echo to ourselves immediately — no need to wait on a round trip to see
	# our own message, and this sidesteps relying on "call_local" local
	# execution semantics for a targeted rpc_id() call (unlike a broadcast
	# .rpc(), those do NOT also run locally on the caller in Godot).
	var local_id := multiplayer.get_unique_id()
	var local_info := PlayerRegistry.get_info(local_id)
	message_received.emit(local_info.get("name", "Player"), local_info.get("race", 0), team_only, text)

	if multiplayer.is_server():
		_relay(local_id, text, team_only)
	else:
		# Must target the server explicitly (id 1), not a bare broadcast
		# .rpc(): ENetMultiplayerPeer's default server_relay transparently
		# forwards a client's broadcast RPC to every other client too, which
		# would let every peer run this unfiltered — silently leaking "team"
		# chat to the other team before the server ever gets a chance to
		# filter it.
		submit_chat.rpc_id(1, text, team_only)


# Client -> server only. The server is the sole authority on who a "team"
# message is allowed to reach.
@rpc("any_peer", "call_remote", "reliable")
func submit_chat(text: String, team_only: bool) -> void:
	if not multiplayer.is_server():
		return
	_relay(multiplayer.get_remote_sender_id(), text, team_only)


func _relay(sender_id: int, raw_text: String, team_only: bool) -> void:
	var text := raw_text.strip_edges().left(MAX_MESSAGE_LENGTH)
	if text.is_empty():
		return

	var sender_info := PlayerRegistry.get_info(sender_id)
	var sender_name: String = sender_info.get("name", "Player")
	var sender_race: int = sender_info.get("race", 0)

	for peer_id in multiplayer.get_peers():
		if peer_id != sender_id and _should_receive(peer_id, sender_race, team_only):
			_receive_chat.rpc_id(peer_id, sender_name, sender_race, team_only, text)


@rpc("authority", "call_remote", "reliable")
func _receive_chat(sender_name: String, sender_race: int, team_only: bool, text: String) -> void:
	message_received.emit(sender_name, sender_race, team_only, text)


func _should_receive(receiver_id: int, sender_race: int, team_only: bool) -> bool:
	if not team_only:
		return true
	return PlayerRegistry.get_info(receiver_id).get("race", -1) == sender_race
