extends Node
class_name NetworkTickrateHandshake

## Internal class to manage the tickrate handshake.
##
## Whenever a new peer joins, they exchange their configured tickrate with the 
## host. If the tickrate mismatches, a warning is emitted by default, as this is
## assumed to be a developer mistake.
## [br][br]
## However, if this is expected, different actions can be configured.

## Emit a warning on tickrate mismatch
const WARN := 0

## Disconnect peer on tickrate mismatch[br]
## This is enforced by the host.
const DISCONNECT := 1

## Adjust tickrate to the host's on mismatch
const ADJUST := 2

## Emit [signal on_tickrate_mismatch] on mismatch[br]
## This is emitted on both host and client.
const SIGNAL := 3

## Configures what happens on a tickrate mismatch.[br]
## Defaults to [constant WARN], based on project settings.
var mismatch_action: int = ProjectSettings.get_setting(&"netfox/time/tickrate_mismatch_action", WARN)

static var _logger := NetfoxLogger._for_netfox("NetworkTickrateHandshake")

## Emitted when a tickrate mismatch is encountered, and [member mismatch_action] is set to 
## [constant SIGNAL].
signal on_tickrate_mismatch(peer: int, tickrate: int)

## Run the tickrate handshake.
## [br][br]
## This will connect to signals, so that every new peer receives tickrate info
## from the host.
## [br][br]
## Called by [_NetworkTime], no need to call manually.
func run() -> void:
	if _is_authority():
		# Broadcast tickrate
		_submit_tickrate.rpc(NetworkTime.tickrate)
		
		# Submit tickrate to anyone joining
		multiplayer.peer_connected.connect(_handle_new_peer)
	else:
		# Submit tickrate to host
		_submit_tickrate.rpc_id(1, NetworkTime.tickrate)

## Stop the tickrate handshake.
## [br][br]
## Called by [_NetworkTime], no need to call manually.
func stop() -> void:
	if multiplayer.peer_connected.is_connected(_handle_new_peer):
		multiplayer.peer_connected.disconnect(_handle_new_peer)

func _ready() -> void:
	name = "NetworkTickrateHandshake"

func _handle_new_peer(peer: int) -> void:
	if _is_authority():
		_submit_tickrate.rpc_id(peer, NetworkTime.tickrate)

func _handle_tickrate_mismatch(peer: int, tickrate: int) -> void:
	match mismatch_action:
		WARN:
			_logger.warning(
				"Local tickrate %dtps differs from tickrate of peer #%d at %dtps! " +
				"Make sure that tickrates are correctly configured in the Project settings! " +
				"See netfox/Time/Tickrate.", [
					NetworkTime.tickrate, peer, tickrate
				])
		DISCONNECT:
			if _is_authority():
				_logger.warning("Peer #%d's tickrate of %dtps differs from expected %dtps! Disconnecting.", [
					peer, tickrate, NetworkTime.tickrate
				])
				multiplayer.multiplayer_peer.disconnect_peer(peer)
		ADJUST:
			if not _is_authority():
				_logger.info("Local tickrate %dtps differs from tickrate of host at %dtps! Adjusting.", [
					NetworkTime.tickrate, tickrate
				])
				# TODO: Make tickrate mutable at user's digression
				NetworkTime._tickrate = tickrate
		SIGNAL:
			on_tickrate_mismatch.emit(peer, tickrate)

func _is_authority() -> bool:
	# HACK: This method is here to ease testing; pretending to be a client is messy from a unit test
	return multiplayer.is_server()

@rpc("any_peer", "reliable", "call_remote")
func _submit_tickrate(tickrate: int) -> void:
	var sender := multiplayer.get_remote_sender_id()
	_logger.debug("Received tickrate %d from peer %d", [tickrate, sender])

	if tickrate != NetworkTime.tickrate:
		_handle_tickrate_mismatch(sender, tickrate)
