extends Node
class_name _NetworkRollback

## Orchestrates the rollback loop.
##
## @tutorial(NetworkRollback Guide): https://foxssake.github.io/netfox/latest/netfox/guides/network-rollback/
## @tutorial(Modifying objects during rollback): https://foxssake.github.io/netfox/latest/netfox/tutorials/modifying-objects-during-rollback/

## Whether rollback is enabled.
var enabled: bool = ProjectSettings.get_setting(&"netfox/rollback/enabled", true)

## Whether diff states are enabled.
## [br][br]
## Diff states send only the state properties that have changed.
var enable_diff_states: bool = ProjectSettings.get_setting(&"netfox/rollback/enable_diff_states", true)

## How many ticks to store as history.
## [br][br]
## The larger the history limit, the further we can roll back into the past,
## thus the more latency we can manage. The drawback is, with higher history
## limit comes more history data stored, thus higher memory usage.
## [br][br]
## Rollback won't go further than this limit, regardless of inputs received.
## [br][br]
## [i]read-only[/i], you can change this in the project settings
var history_limit: int:
	get:
		return _history_limit
	set(v):
		push_error("Trying to set read-only variable history_limit")

## The earliest tick that history is retained for.
## [br][br]
## Determined by [member history_limit].
## [br][br]
## [i]read-only[/i]
var history_start: int:
	get:
		return maxi(0, NetworkTime.tick - history_limit)
	set(v):
		push_error("Trying to set read-only variable history_start")

## Offset into the past for display, in ticks.
## [br][br]
## After the rollback, we have the option to not display the absolute latest
## state of the game, but let's say the state two frames ago ( offset = 2 ).
## This can help with hiding latency, by giving more time for an up-to-date
## state to arrive before we try to display it.
## [br][br]
## [i]read-only[/i], you can change this in the project settings
var display_offset: int:
	get:
		return _display_offset
	set(v):
		push_error("Trying to set read-only variable display_offset")

## The currently displayed tick.
## [br][br]
## This is the current tick as returned by [member _NetworkTime.tick], minus
## the [member display_offset]. By configuring the [member display_offset], a
## past tick may be displayed to the player, so that updates from the server
## have slightly more time to arrive, masking latency.
## [br][br]
## [i]read-only[/i]
var display_tick: int:
	get:
		if enabled:
			return maxi(0, NetworkTime.tick - NetworkRollback.display_offset)
		else:
			return NetworkTime.tick
	set(v):
		push_error("Trying to set read-only variable display_tick")

## Offset into the future to submit inputs, in ticks.
##
## By submitting inputs into the future, they don't happen instantly, but with
## some delay. This can help hiding latency - even if input takes some time to
## arrive, it will still be up to date, as it was timestamped into the future.
## This only works if the input delay is greater than the network latency.
## [br][br]
## In cases where the latency is greater than the input delay, this can still
## reduce the amount of resimulated frames, resulting in less compute.
## [br][br]
## [b]Note:[/b] the [code]is_fresh[/code] parameter may not work as expected
## with input latency higher than network latency.
## [br][br]
## [i]read-only[/i], you can change this in the project settings
var input_delay: int:
	get:
		return _input_delay
	set(v):
		push_error("Trying to set read-only variable input_delay")

## How many previous input frames to send along with the current one.
## [br][br]
## As inputs are sent over an unreliable channel, packets may get lost or appear
## out of order. To mitigate packet loss, we send the current and previous n
## ticks of input data. This way, even if the input for a given tick gets lost
## in transmission, the next (n-1) packets will contain the data for it.
## [br][br]
## [i]read-only[/i], you can change this in the project settings
var input_redundancy: int:
	get:
		return max(1, _input_redundancy)
	set(v):
		push_error("Trying to set read-only variable input_redundancy")

## The current [i]rollback[/i] tick.
## [br][br]
## Note that this is different from [member _NetworkTime.tick], and only makes
## sense in the context of a rollback loop.
var tick: int:
	get:
		return _tick
	set(v):
		push_error("Trying to set read-only variable tick")

## Event emitted before running the network rollback loop.
signal before_loop()

## Event emitted in preparation of each rollback tick.
## [br][br]
## Handlers should apply the state and input corresponding to the given tick.
signal on_prepare_tick(tick: int)

## Event emitted after preparing each rollback tick.
## [br][br]
## Handlers may process the prepared tick, e.g. modulating the input by its age
## to implement input prediction.
signal after_prepare_tick(tick: int)

## Event emitted to process the given rollback tick.
## [br][br]
## Handlers should check if they *need* to resimulate the given tick, and if so,
## generate the next state based on the current data ( applied in the prepare
## tick phase ).
signal on_process_tick(tick: int)

## Event emitted after the given rollback tick was processed.
signal after_process_tick(tick: int)

## Event emitted to record the given rollback tick.
## [br][br]
## By this time, the tick is advanced from the simulation, handlers should save
## their resulting states for the given tick.
signal on_record_tick(tick: int)

## Event emitted after running the network rollback loop.
signal after_loop()

# Settings
var _history_limit: int = ProjectSettings.get_setting(&"netfox/rollback/history_limit", 64)
var _display_offset: int = ProjectSettings.get_setting(&"netfox/rollback/display_offset", 0)
var _input_delay: int = ProjectSettings.get_setting(&"netfox/rollback/input_delay", 0)
var _input_redundancy: int = ProjectSettings.get_setting(&"netfox/rollback/input_redundancy", 3)

# Timing
var _tick: int = 0
var _resim_from: int

var _rollback_from: int = -1
var _rollback_to: int = -1
var _rollback_stage: String = ""

# Resim + mutations
var _is_rollback: bool = false
var _simulated_nodes: _Set = _Set.new()
var _mutated_nodes: Dictionary = {}

var _earliest_input := -1
var _earliest_state := -1

const _STAGE_BEFORE := "B"
const _STAGE_PREPARE := "P"
const _STAGE_SIMULATE := "S"
const _STAGE_RECORD := "R"
const _STAGE_AFTER := "A"

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

## Submit the resimulation start tick for the current loop.
##
## This is used to determine the resimulation range during each loop.
func notify_resimulation_start(tick: int) -> void:
	_resim_from = min(_resim_from, tick)

## Submit node for simulation.
##
## This is used mostly internally by [RollbackSynchronizer]. The idea is to
## submit each affected node while preparing the tick, and then run only the
## nodes that need to be resimulated.
func notify_simulated(node: Node) -> void:
	_simulated_nodes.add(node)


## Check if node was submitted for simulation.
##
## This is used mostly internally by [RollbackSynchronizer]. The idea is to
## submit each affected node while preparing the tick, and then use
## [member is_simulated] to run only the nodes that need to be resimulated.
func is_simulated(node: Node) -> bool:
	return _simulated_nodes.has(node)

## Check if a network rollback is currently active.
func is_rollback() -> bool:
	return _is_rollback

## Checks if a given object is rollback-aware, i.e. has the
## [code]_rollback_tick[/code] method implemented.
##
## This is used by [RollbackSynchronizer] to see if it should simulate the
## given object during rollback.
func is_rollback_aware(what: Object) -> bool:
	return what.has_method(&"_rollback_tick")

## Calls the [code]_rollback_tick[/code] method on the target, running its
## simulation for the given rollback tick.
## [br][br]
## This is used by [RollbackSynchronizer] to resimulate ticks during rollback.
## While the [code]_rollback_tick[/code] method could be called directly as
## well, this method exists to future-proof the code a bit, so the method name
## is not repeated all over the place.
## [br][br]
## [i]Note:[/i] Make sure to check if the target is rollback-aware, because if
## it's not, this method will run into an error.
func process_rollback(target: Object, delta: float, p_tick: int, is_fresh: bool) -> void:
	target._rollback_tick(delta, p_tick, is_fresh)

## Marks the target object as mutated.
## [br][br]
## Mutated objects will be re-recorded for the specified tick, and resimulated
## from the given tick onwards.
## [br][br]
## For special cases, you can specify the tick when the mutation happened. Since
## it defaults to the current rollback [member tick], this parameter rarely
## needs to be specified.
## [br][br]
## Note that registering a mutation into the past will yield a warning.
## [br][br]
## @experimental: The Mutations API is experimental!
func mutate(target: Object, p_tick: int = tick) -> void:
	_mutated_nodes[target] = mini(p_tick, _mutated_nodes.get(target, p_tick))

	if is_rollback() and p_tick < tick:
		_logger.warning(
			"Trying to mutate object %s in the past, for tick %d!",
			[target, p_tick]
		)

## Check whether the target object was mutated in or after the given tick via
## [method mutate].
## [br][br]
## @experimental: The Mutations API is experimental!
func is_mutated(target: Object, p_tick: int = tick) -> bool:
	if _mutated_nodes.has(target):
		return p_tick >= _mutated_nodes.get(target)
	else:
		return false

## Check whether the target object was mutated specifically in the given tick
## via [method mutate].
## [br][br]
## @experimental: The Mutations API is experimental!
func is_just_mutated(target: Object, p_tick: int = tick) -> bool:
	if _mutated_nodes.has(target):
		return _mutated_nodes.get(target) == p_tick
	else:
		return false

## Register that a node has submitted its input for a specific tick
## @deprecated
func register_rollback_input_submission(_node: Node, _tick: int) -> void:
	pass

## Get the latest input tick submitted for a specific node
## [br][br]
## Returns [code]-1[/code] if no input was submitted for the node, ever.
func get_latest_input_tick(node: Node) -> int:
	var input_nodes := RollbackSimulationServer._get_inputs_of(node)
	var reference_tick := NetworkTime.tick

	return NetworkHistoryServer.get_latest_input_for(input_nodes, reference_tick)

## Check if a node has submitted input for a specific tick (or later)
func has_input_for_tick(node: Node, tick: int) -> bool:
	var latest_input := get_latest_input_tick(node)
	return latest_input != -1 and latest_input >= tick

## Free all input submission data for a node
## [br][br]
## Use this once the node is freed.
## @deprecated
func free_input_submission_data_for(_node: Node) -> void:
	pass

func _get_rollback_method(object: Object) -> Callable:
	return object._rollback_tick

func _ready():
	NetfoxLogger.register_tag(_get_rollback_tag)
	NetworkTime.after_tick_loop.connect(_rollback)
	NetworkTime.after_tick.connect(func(_dt, tick):
		NetworkHistoryServer._record_rollback_input(tick + input_delay)
		NetworkSynchronizationServer._synchronize_input(tick + input_delay)
	)

	NetworkSynchronizationServer._on_input.connect(_handle_input)
	NetworkSynchronizationServer._on_state.connect(_handle_state)

func _exit_tree():
	NetfoxLogger.free_tag(_get_rollback_tag)

func _get_rollback_tag() -> String:
	if _is_rollback:
		return "%s@%d|%d>%d" % [_rollback_stage, _tick, _rollback_from, _rollback_to]
	else:
		return "_"

func _rollback() -> void:
	if not enabled:
		return

	# Ask all rewindables to submit their earliest inputs
	_resim_from = NetworkTime.tick
	before_loop.emit()

	# Figure out where to start rollback from
	var range_source = "notif"
	if _earliest_input >= 0 and _earliest_input <= _resim_from:
		range_source = "earliest input"
		_resim_from = _earliest_input
	if _earliest_state >= 0 and _earliest_state <= _resim_from:
		range_source = "latest state"
		_resim_from = _earliest_state
	_resim_from = mini(_resim_from, NetworkTime.tick - 1)
	_logger.trace("Simulating range @%d>@%d using %s", [_resim_from, NetworkTime.tick, range_source])

	# Only set _is_rollback *after* emitting before_loop
	_is_rollback = true
	_rollback_stage = _STAGE_BEFORE

	# from = Earliest input amongst all rewindables
	var from := _resim_from

	# to = Current tick
	var to := NetworkTime.tick

	# Limit number of rollback ticks
	if to - from > history_limit:
		_logger.warning(
			"Trying to run rollback for ticks %d to %d, past the history limit of %d",
			[from, to, history_limit]
		)
		from = NetworkTime.tick - history_limit

	_earliest_input = -1
	_earliest_state = -1

	# for tick in from .. to:
	_rollback_from = from
	_rollback_to = to
	for tick in range(from, to):
		_tick = tick
		_simulated_nodes.clear()

		# Prepare state
		#	Done individually by Rewindables ( usually Rollback Synchronizers )
		#	Restore input and state for tick
		_rollback_stage = _STAGE_PREPARE
		on_prepare_tick.emit(tick)
		NetworkHistoryServer._restore_rollback_input(tick)
		NetworkHistoryServer._restore_rollback_state(tick)
		after_prepare_tick.emit(tick)

		# Simulate rollback tick
		#	Method call on rewindables
		#	Rollback synchronizers go through each node they manage
		#	If current tick is in node's range, tick
		#		If authority: Latest input >= tick >= Latest state
		#		If not: Latest input >= tick >= Earliest input
		_rollback_stage = _STAGE_SIMULATE
		on_process_tick.emit(tick)
		RollbackSimulationServer.simulate(NetworkTime.ticktime, tick)
		after_process_tick.emit(tick)

		# Record state for tick + 1
		_rollback_stage = _STAGE_RECORD
		on_record_tick.emit(tick + 1)
		NetworkHistoryServer._record_rollback_state(tick + 1)
		NetworkSynchronizationServer._synchronize_state(tick + 1)

	# Restore display state
	_rollback_stage = _STAGE_AFTER
	after_loop.emit()
	NetworkHistoryServer._restore_rollback_state(display_tick)
	RollbackSimulationServer._trim_ticks_simulated(history_start)

	# Cleanup
	_mutated_nodes.clear()
	_is_rollback = false

func _handle_input(snapshot: _Snapshot):
	if snapshot.is_empty():
		return
	if _earliest_input < 0 or snapshot.tick < _earliest_input:
		_logger.trace("Ingested input @%d, earliest @%d->@%d", [snapshot.tick, _earliest_input, snapshot.tick])
		_earliest_input = snapshot.tick
	else:
		_logger.trace("Ingested input @%d, earliest @%d->@%d", [snapshot.tick, _earliest_input, _earliest_input])

func _handle_state(snapshot: _Snapshot):
	if snapshot.is_empty():
		return
	if _earliest_state < 0 or snapshot.tick < _earliest_state:
		_logger.trace("Ingested state @%d, latest @%d->@%d", [snapshot.tick, _earliest_state, snapshot.tick])
		_earliest_state = snapshot.tick
	else:
		_logger.trace("Ingested state @%d, latest @%d->@%d", [snapshot.tick, _earliest_state, _earliest_state])

# Insight 1:
#	state(x) = simulate(state(x - 1), input(x - 1))
#	state(x + 1) = simulate(state(x), input(x))
# Insight 2:
#	Server is authorative over all state, client over its own input, i.e.
#	Server broadcasts state
#	Client sends input to server
# Flow:
#	Clients send in their inputs
#	Server simulates frames from earliest input to current
#	Server broadcasts simulated frames
#	Clients receive authorative states
#	Clients simulate local frames
