extends Node
class_name _RollbackSimulationServer

## Runs gameplay simulation during rollback
##
## Rollback involves restoring the game state to an earlier point in time and
## re-running game logic from there. This class tracks which nodes participate
## in rollback, which of them need to be actually simulated, and runs the
## simulation itself.
## [br][br]
## Node simulation honors scene tree order. Processing order and physics
## processing order are [b]not[/b] considered.
## [br][br]
## To participate in rollback, call [method register] using the method to call
## to simulate the node. Only one [Callable] can be actively registered per
## node.

var _history_server: _NetworkHistoryServer

var _callbacks := {}		# node to callback
var _simulated_ticks := {}	# node to array of ticks

var _input_graph := _Graph.new() # Links inputs to objects controlled by them

# Currently simulated object
var _current_object: Object = null
# Predicted nodes for next simulation
var _predicted_nodes := _Set.new()

var _group := StringName("__nf_rollback_sim" + str(get_instance_id()))

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

## Register a [param]callback[/param] to run as part of the rollback loop
func register(callback: Callable) -> void:
	if not is_instance_valid(callback.get_object()):
		_logger.error("Trying to register callback that belongs to an invalid object!")
		return

	assert(callback.get_object() is Node, "Only nodes supported for now!")

	_callbacks[callback.get_object()] = callback

## Deregister a [param]callback[/param] from the rollback loop
func deregister(callback: Callable) -> void:
	if not callback or not callback.is_valid(): return

	var object := callback.get_object()

	if not is_instance_valid(object): return
	if _callbacks[object] != callback: return

	_callbacks.erase(object)
	_input_graph.erase(object)
	_simulated_ticks.erase(object)

## Deregister a [param]node[/param] from the rollback loop
func deregister_node(node: Node) -> void:
	if _callbacks.has(node):
		deregister(_callbacks[node])
	_input_graph.erase(node)

## Register [param]input[/param] as providing input for [param]node[/param]
func register_rollback_input_for(node: Node, input: Node) -> void:
	_input_graph.link(input, node)

## Deregister [param]input[/param] from providing input for [param]node[/param]
func deregister_rollback_input(node: Node, input: Node) -> void:
	_input_graph.unlink(input, node)

## Return true if the currently simulated node is being predicted
func is_predicting_current() -> bool:
	if not _current_object or not is_instance_valid(_current_object):
		return false
	return _predicted_nodes.has(_current_object)

## Get the currently simulated object
func get_simulated_object() -> Object:
	return _current_object

## Simulate a tick
func simulate(delta: float, tick: int) -> void:
	_current_object = null

	var input_snapshot := NetworkHistoryServer._get_rollback_input_snapshot(tick)
	var state_snapshot := NetworkHistoryServer._get_rollback_state_snapshot(tick)
	var nodes := _get_nodes_to_simulate(input_snapshot)
	_predicted_nodes.clear()

	# Sort based on SceneTree order
	for node in nodes:
		node.add_to_group(_group)
	nodes = get_tree().get_nodes_in_group(_group)

	# Determine predicted nodes
	for node in _callbacks.keys():
		if _is_predicting(input_snapshot, node):
			_predicted_nodes.add(node)

	# Run callbacks and clear group
	for node in nodes:
		_current_object = node

		var callback := _callbacks[node] as Callable
		var is_fresh := _is_tick_fresh_for(node, tick)
		callback.call(delta, tick, is_fresh)
		node.remove_from_group(_group)

		_current_object = null
		_set_tick_simulated_for(node, tick)

	# Metrics
	NetworkPerformance.push_rollback_nodes_simulated(nodes.size())

func _get_nodes_to_simulate(input_snapshot: _Snapshot) -> Array[Node]:
	var result: Array[Node] = []
	if not input_snapshot:
		return []

	var tick := input_snapshot.tick
	for node in _callbacks.keys():
		var inputs := [] as Array[Node]
		inputs.assign(_input_graph.get_linked_to(node))

		if inputs.is_empty():
			# Node has no input, simulate it
			result.append(node)
			continue

		if NetworkRollback.is_mutated(node, tick):
			# Node is mutated, must simulate
			result.append(node)
			continue

		if not input_snapshot.has_subjects(inputs, true):
			# We don't have input for node, don't simulate
			continue

		result.append(node)

	return result

func _is_predicting(input_snapshot: _Snapshot, node: Node) -> bool:
	var input_nodes := [] as Array[Node]
	input_nodes.assign(_input_graph.get_linked_to(node))

	var is_owned := node.is_multiplayer_authority()
	var is_inputless := input_nodes.is_empty()
	var has_input := false

	if not is_inputless and input_snapshot:
		has_input = input_snapshot.has_subjects(input_nodes, true)

	if not is_owned and has_input:
		# We don't own the node, but we own input for it - not (input) predicting
		return false
	if not is_owned:
		# We don't own the node, so we can only guess - i.e. predict
		return true
	if is_owned and is_inputless:
		# We own the node, node doesn't depend on input, we're sure
		return false
	if is_owned and not has_input:
		# We own the node, node depends on input, we don't have data for input - predict
		return true
	# We own the node and we have data for node's input - we're sure
	return false

func _is_tick_fresh_for(node: Node, tick: int) -> bool:
	if not _simulated_ticks.has(node):
		return true
	var ticks := _simulated_ticks.get(node) as Array[int]
	return not ticks.has(tick)

func _set_tick_simulated_for(node: Node, tick: int) -> void:
	if not _simulated_ticks.has(node):
		_simulated_ticks[node] = [tick] as Array[int]
	else:
		_simulated_ticks[node].append(tick)

func _trim_ticks_simulated(beginning: int) -> void:
	for object in _simulated_ticks:
		_simulated_ticks[object] = _simulated_ticks[object]\
			.filter(func(tick): return tick >= beginning)

func _get_controlled_by(input: Node) -> Array[Node]:
	var result := [] as Array[Node]
	result.assign(_input_graph.get_linked_from(input))
	return result

func _get_inputs_of(node: Node) -> Array[Node]:
	var result := [] as Array[Node]
	result.assign(_input_graph.get_linked_to(node))
	return result

func _init(p_history_server: _NetworkHistoryServer = null):
	_history_server = p_history_server

func _ready():
	# Ensure dependencies
	if not _history_server: _history_server = NetworkHistoryServer
