## FpsCamera — first-person camera view effects for tactical FPS.
##
## Manages view bobbing (head bob from movement), weapon sway, and FOV kick.
## The parent FPSCharacterController handles mouse look (yaw/pitch).
##
## Place as a Camera3D child of FPSCharacterController.
class_name FpsCamera
extends Camera3D

# ---------------------------------------------------------------------------
# Exports
# ---------------------------------------------------------------------------

## Enable view bobbing (head bob when walking/running).
@export var view_bob_enabled: bool = true

## How much the camera bobs horizontally (units).
@export_range(0.0, 0.5) var bob_amplitude_h: float = 0.04

## How much the camera bobs vertically.
@export_range(0.0, 0.5) var bob_amplitude_v: float = 0.04

## Frequency multiplier for bobbing (higher = faster).
@export_range(0.5, 5.0) var bob_frequency: float = 2.5

## Sprint bob multiplier (more aggressive).
@export_range(1.0, 3.0) var bob_sprint_mult: float = 1.6

## Crouch bob multiplier (less movement).
@export_range(0.1, 1.0) var bob_crouch_mult: float = 0.4

## FOV kick on sprint (tactical FOV increase).
@export_range(0.0, 10.0) var sprint_fov_kick: float = 3.0

## FOV kick when firing weapon.
@export_range(-5.0, 10.0) var shoot_fov_kick: float = 0.5

## How fast FOV recovers.
@export var fov_recovery_speed: float = 6.0

## Enable weapon sway (subtle rotation from movement).
@export var weapon_sway_enabled: bool = true

## Maximum weapon sway rotation (degrees).
@export_range(0.0, 5.0) var sway_max_rotation: float = 1.5

## Sway responsiveness (higher = snappier).
@export_range(1.0, 20.0) var sway_response: float = 8.0

# ---------------------------------------------------------------------------
# Internal
# ---------------------------------------------------------------------------

## Reference to parent controller.
var _controller: FPSCharacterController = null

## Bob time accumulator.
var _bob_time: float = 0.0

## Current bob offset.
var _bob_offset: Vector2 = Vector2.ZERO

## Current weapon node (child named "Weapon" or first MeshInstance3D).
var _weapon: Node3D = null

## Weapon rest position (local transform when no sway).
var _weapon_rest: Transform3D

## FOV state.
var _base_fov: float = 75.0
var _current_fov_offset: float = 0.0

## Sway state (used as both current value and interpolation target).
var _sway_current: Vector2 = Vector2.ZERO
var _sway_target: Vector2 = Vector2.ZERO

## Base eye Y set by parent controller's crouch logic (for additive bob).
var _base_eye_y: float = 0.0

# ---------------------------------------------------------------------------
# Lifecycle
# ---------------------------------------------------------------------------

func _ready() -> void:
	_controller = get_parent() as FPSCharacterController
	if _controller == null:
		push_warning("FpsCamera: Parent is not an FPSCharacterController. View features disabled.")

	_base_fov = fov

	# Find weapon node (for sway)
	for child in get_children():
		if child is Node3D:
			_weapon = child
			_weapon_rest = child.transform
			break


func _process(delta: float) -> void:
	if _controller == null:
		return

	# Capture the eye height set by parent controller's crouch logic
	# before we apply view bob offsets (which must be additive).
	_base_eye_y = position.y

	# FOV management
	_update_fov(delta)

	# View bobbing
	if view_bob_enabled:
		_update_bob(delta)

	# Weapon sway
	if weapon_sway_enabled and _weapon:
		_update_sway(delta)


# ---------------------------------------------------------------------------
# FOV
# ---------------------------------------------------------------------------

func _update_fov(delta: float) -> void:
	var target_offset: float = 0.0

	# Sprint FOV kick — sustained while sprinting (not just on transition)
	if _controller.is_sprinting():
		target_offset = sprint_fov_kick

	# Shoot FOV kick is triggered externally via trigger_shoot_fov()
	# and decays toward the sprint/walk target naturally via move_toward.
	_current_fov_offset = move_toward(_current_fov_offset, target_offset, fov_recovery_speed * delta)
	fov = _base_fov + _current_fov_offset


# ---------------------------------------------------------------------------
# View bobbing
# ---------------------------------------------------------------------------

func _update_bob(delta: float) -> void:
	var crouch_amount: float = _controller.get_crouch_amount()
	var sprinting: bool = _controller.is_sprinting()

	# Calculate bob speed multiplier from controller state
	var speed_mult: float = 1.0
	if sprinting:
		speed_mult = bob_sprint_mult
	elif crouch_amount > 0.0:
		speed_mult = lerpf(1.0, bob_crouch_mult, crouch_amount)

	if speed_mult < 0.01:
		# Stationary — reset to zero
		_bob_offset = _bob_offset.move_toward(Vector2.ZERO, delta * 10.0)
	else:
		_bob_time += delta * bob_frequency * speed_mult
		_bob_offset = Vector2(
			sin(_bob_time * 2.0) * bob_amplitude_h * speed_mult,
			sin(_bob_time) * bob_amplitude_v * speed_mult
		)

	# Apply bob to camera position (relative to parent's eye offset)
	position.x = _bob_offset.x
	# Bob is additive on Y so it does NOT overwrite the crouch eye height
	# set by the parent FPSCharacterController._update_crouch().
	position.y = _base_eye_y + _bob_offset.y


# ---------------------------------------------------------------------------
# Weapon sway
# ---------------------------------------------------------------------------

func _update_sway(delta: float) -> void:
	# Sway follows mouse movement indirectly via controller yaw/pitch changes
	# Use velocity for physics-based sway: recentre over time
	var vel: Vector3 = _controller.velocity
	var sway_h := clamp(vel.x * 0.003, -sway_max_rotation, sway_max_rotation)
	var sway_v := clamp(vel.z * 0.003, -sway_max_rotation, sway_max_rotation)
	# Add a tiny amount from pitch/yaw delta (not mouse, from movement direction change)
	_sway_target = Vector2(
		move_toward(_sway_target.x, sway_h, sway_response * delta),
		move_toward(_sway_target.y, sway_v, sway_response * delta)
	)

	# Apply to weapon node as small rotation offsets
	if _weapon:
		var target := Transform3D(
			Basis.from_euler(Vector3(
				deg_to_rad(_sway_target.y),
				deg_to_rad(_sway_target.x),
				0.0
			)),
			_weapon_rest.origin
		)
		_weapon.transform = _weapon.transform.interpolate_with(target, sway_response * delta * 0.5)


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------

## Trigger a shoot FOV kick. Call from weapon script.
func trigger_shoot_fov() -> void:
	_current_fov_offset += shoot_fov_kick


## Reset view to resting state (for respawn).
func reset_view() -> void:
	_bob_time = 0.0
	_bob_offset = Vector2.ZERO
	_current_fov_offset = 0.0
	_sway_current = Vector2.ZERO
	_sway_target = Vector2.ZERO
	fov = _base_fov
	if _weapon:
		_weapon.transform = _weapon_rest


## Get the current bob offset for external effects.
func get_bob_offset() -> Vector2:
	return _bob_offset
