## ClientPrediction — Client-side prediction & reconciliation for networked FPS.
##
## Architecture (tick loop):
##   1. on_before_tick()  → captures a local-world Snapshot into a 64-entry
##      ring buffer BEFORE input is applied.
##   2. The character controller runs its normal local movement (predicted
##      simulation — instant feedback).
##   3. on_after_tick()   → sends the raw input to the server via ENet
##      channel 0 and advances the local tick counter.
##   4. When a server state snapshot arrives, the controller reconciles:
##      a) Compares predicted state with authoritative state at that tick.
##      b) If mismatch detected → emits state_mispredicted(delta).
##      c) Rewinds to confirmed state, re-applies all unconfirmed inputs,
##         then emits reconciled().
##
## The same node also supports INTERPOLATION MODE for remote players
## (those not owned by this peer). See set_interpolate_mode().
##
## Server-side input queue:
##   This script also contains server-side logic for receiving client
##   inputs and making them available to GameServer via consume_pending_inputs().
##
## Dependencies:
##   - NetworkManager autoload (for RPC relay)
##   - Snapshot (data container)
##   - FPSCharacterController (linked via setup())
extends Node

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
const BUFFER_SIZE: int = 64

# ---------------------------------------------------------------------------
# Signals
# ---------------------------------------------------------------------------
## Emitted when a misprediction is detected during reconciliation.
## delta_position: Vector3 — the position difference (server - predicted).
signal state_mispredicted(delta_position: Vector3)

## Emitted after successful reconciliation (state corrected).
signal reconciled()

# ---------------------------------------------------------------------------
# State — Mode
# ---------------------------------------------------------------------------
## If true, this instance is actively predicting (client-side local player).
var prediction_enabled: bool = false

## If true, this instance is interpolating a remote player's position.
var interpolate_mode: bool = false

# ---------------------------------------------------------------------------
# State — Prediction (local player)
# ---------------------------------------------------------------------------
## Local tick counter, incremented each physics tick.
var local_tick: int = 0

## Ring buffer of snapshots taken BEFORE input each tick.
## Indexed by tick % BUFFER_SIZE.
var _snapshot_buffer: Array[Snapshot] = []

## Pending (unconfirmed) inputs, keyed by local_tick.
## Each value is a Dictionary matching the FPSCharacterController input format.
var _pending_inputs: Dictionary = {}

## Last confirmed snapshot received from the server.
var _last_confirmed_snapshot: Snapshot = null

## Tick of the last confirmed snapshot.
var _last_confirmed_tick: int = -1

# ---------------------------------------------------------------------------
# State — Interpolation (remote players)
# ---------------------------------------------------------------------------
var _prev_snapshot: Snapshot = null
var _next_snapshot: Snapshot = null
var _interp_fraction: float = 0.0

# ---------------------------------------------------------------------------
# References
# ---------------------------------------------------------------------------
var _controller: FPSCharacterController = null

# ---------------------------------------------------------------------------
# Lifecycle
# ---------------------------------------------------------------------------
func _init() -> void:
	_snapshot_buffer.resize(BUFFER_SIZE)

func _ready() -> void:
	if not multiplayer.is_server() and NetworkManager:
		# Client instances: listen for authoritative server state.
		NetworkManager.server_state_received.connect(_on_client_state_received)

# ---------------------------------------------------------------------------
# Public API — setup
# ---------------------------------------------------------------------------

## Link this prediction node to its FPSCharacterController.
func setup(controller: FPSCharacterController) -> void:
	_controller = controller
	if not multiplayer.is_server():
		prediction_enabled = true
		print("[ClientPrediction] Prediction active for entity %d" % controller.entity_id)

## Switch to interpolation mode (for remote player representations).
func set_interpolate_mode(enabled: bool) -> void:
	interpolate_mode = enabled
	if enabled:
		prediction_enabled = false

# ---------------------------------------------------------------------------
# Prediction hooks (called by FPSCharacterController._physics_process)
# ---------------------------------------------------------------------------

## MUST be called at the start of _physics_process, BEFORE any input
## processing or movement. Captures the current character state into
## the snapshot ring buffer.
func on_before_tick() -> void:
	if _controller == null or interpolate_mode or not prediction_enabled:
		return

	var snap: Snapshot = _capture_snapshot(local_tick)
	_snapshot_buffer[local_tick % BUFFER_SIZE] = snap
	_pending_inputs[local_tick] = null  # placeholder, filled by on_after_tick

## MUST be called at the end of _physics_process, AFTER local movement
## and input processing. Stores the applied input, sends it to the
## server via RPC, and advances the local tick.
func on_after_tick(input_dict: Dictionary) -> void:
	if _controller == null or interpolate_mode or not prediction_enabled:
		return

	# Store the input for potential reconciliation.
	_pending_inputs[local_tick] = input_dict.duplicate()

	# Send input to the server via NetworkManager RPC (ENet channel 0).
	if NetworkManager and NetworkManager.has_method(&"send_client_input"):
		NetworkManager.send_client_input.rpc_id(1, local_tick, input_dict)

	local_tick += 1

# ---------------------------------------------------------------------------
# Snapshot capture
# ---------------------------------------------------------------------------
func _capture_snapshot(tick: int) -> Snapshot:
	var s = Snapshot.new()
	s.timestamp = tick
	s.position = _controller.global_position
	s.rotation = _controller.global_transform.basis.get_rotation_quaternion()
	s.velocity = _controller.velocity
	s.grounded = _controller.is_on_floor()
	return s

# ---------------------------------------------------------------------------
# Server state handling (called when authoritative state arrives)
# ---------------------------------------------------------------------------

## Handle a server state snapshot: detect misprediction and reconcile.
func on_server_state(server_snapshot: Snapshot) -> void:
	if prediction_enabled:
		_reconcile_from_server(server_snapshot)
	elif interpolate_mode:
		_store_interp_snapshot(server_snapshot)

func _reconcile_from_server(server_snapshot: Snapshot) -> void:
	var server_tick: int = server_snapshot.timestamp

	# Ignore stale / out-of-order state.
	if server_tick <= _last_confirmed_tick:
		return

	# 1. Retrieve our predicted snapshot for this tick.
	var predicted: Snapshot = _snapshot_buffer[server_tick % BUFFER_SIZE]

	if predicted != null and predicted.timestamp == server_tick:
		# 2. Compare predicted vs authoritative state.
		var delta: Vector3 = server_snapshot.position - predicted.position
		if delta.length_squared() > 0.0001:
			state_mispredicted.emit(delta)

	# 3. Update last confirmed state.
	_last_confirmed_snapshot = server_snapshot
	_last_confirmed_tick = server_tick

	# 4. Rewind and re-predict.
	_reconcile(server_snapshot, server_tick)

## Core reconciliation: rewind to the confirmed server state, then
## re-apply every unconfirmed input that was sent after that tick.
func _reconcile(confirmed: Snapshot, confirmed_tick: int) -> void:
	if _controller == null:
		return

	# Collect pending ticks that are strictly after the confirmed tick.
	var pending_ticks: Array[int] = []
	for tick in _pending_inputs.keys():
		if tick > confirmed_tick:
			pending_ticks.append(tick)
	pending_ticks.sort()

	if pending_ticks.is_empty():
		# Nothing to re-apply — snap directly to confirmed state.
		_apply_snapshot(confirmed)
		reconciled.emit()
		return

	# Rewind to the confirmed state.
	_apply_snapshot(confirmed)

	# Re-apply every pending input in tick order.
	var tick_delta: float = 1.0 / 128.0  # matches the 128 Hz tick rate
	for tick in pending_ticks:
		var input_dict: Dictionary = _pending_inputs.get(tick, {})
		if input_dict.is_empty():
			continue
		_simulate_input(input_dict, tick_delta)

	reconciled.emit()

## Apply a snapshot directly to the character controller (rewind step).
func _apply_snapshot(snap: Snapshot) -> void:
	if _controller == null:
		return

	_controller.global_position = snap.position
	_controller.global_transform.basis = Basis(snap.rotation)
	_controller.velocity = snap.velocity

## Re-simulate a single tick of input on the character controller.
## Uses the controller's own _move_local() for identical physics.
## NOTE: crouch and sprint state are not re-applied during reconciliation
## because they are frame-state toggles; the server state already accounts
## for them. Only movement direction and jump matter for position correction.
func _simulate_input(input_dict: Dictionary, delta: float) -> void:
	if _controller == null:
		return

	var move_dir: Vector3 = input_dict.get("move_direction", Vector3.ZERO)
	var jump: bool = input_dict.get("jump", false)
	var yaw_deg: float = input_dict.get("look_yaw", 0.0)

	# Re-apply look yaw so movement is oriented correctly.
	_controller.rotation.y = deg_to_rad(yaw_deg)

	# Re-run local movement (this calls move_and_slide internally).
	_controller._move_local(move_dir, delta, jump)

# ---------------------------------------------------------------------------
# Remote player interpolation
# ---------------------------------------------------------------------------

func _store_interp_snapshot(snap: Snapshot) -> void:
	_prev_snapshot = _next_snapshot
	_next_snapshot = snap
	_interp_fraction = 0.0

## Advance interpolation for a remote player. Call each _process frame.
func advance_interpolation(delta: float) -> void:
	if not interpolate_mode or _controller == null:
		return
	if _prev_snapshot == null or _next_snapshot == null:
		return

	_interp_fraction += delta * 128.0  # tick rate
	if _interp_fraction >= 1.0:
		_apply_snapshot(_next_snapshot)
		_prev_snapshot = _next_snapshot
		_next_snapshot = null
		return

	# Smooth interpolation between snapshots.
	_controller.global_position = _prev_snapshot.position.lerp(
		_next_snapshot.position, _interp_fraction
	)
	_controller.global_transform.basis = Basis(_prev_snapshot.rotation).slerp(
		Basis(_next_snapshot.rotation), _interp_fraction
	)
	_controller.velocity = _prev_snapshot.velocity.lerp(
		_next_snapshot.velocity, _interp_fraction
	)

# ---------------------------------------------------------------------------
# Client-side state reception
# ---------------------------------------------------------------------------

## Client-side: called when NetworkManager emits server_state_received.
## Converts the raw dict into a Snapshot and passes it to the
## reconciliation pipeline.
func _on_client_state_received(entity_id: int, snapshot_dict: Dictionary) -> void:
	# Route to the correct prediction controller based on entity_id.
	if _controller == null or _controller.entity_id != entity_id:
		return

	var snap: Snapshot = Snapshot.from_dict(snapshot_dict)
	on_server_state(snap)


# ---------------------------------------------------------------------------
# Utility
# ---------------------------------------------------------------------------
## Reset all prediction state (call on respawn / round restart).
func reset() -> void:
	_snapshot_buffer = []
	_snapshot_buffer.resize(BUFFER_SIZE)
	_pending_inputs.clear()
	_last_confirmed_snapshot = null
	_last_confirmed_tick = -1
	local_tick = 0
	_prev_snapshot = null
	_next_snapshot = null
	_interp_fraction = 0.0
