class_name ThirdPersonCameraRig
extends Node3D

## Third-person orbit/zoom state and Camera3D presentation adapter.
## The rig owns camera input state and local transforms. Character movement,
## terrain, rendering and collision-policy decisions remain external boundaries.

const NO_CAMERA_COLLISION_POLICY_SCRIPT := preload("res://src/scenes/player/no_camera_collision_policy.gd")

## Relative Camera3D child used by this rig.
@export var camera_path: NodePath = NodePath("Camera3D")
## Mouse radians applied per input pixel.
@export var mouse_sensitivity_radians_per_pixel: float = 0.003
## Minimum orbit pitch in radians.
@export var minimum_pitch_radians: float = deg_to_rad(-65.0)
## Maximum orbit pitch in radians.
@export var maximum_pitch_radians: float = deg_to_rad(35.0)
## Pivot height above the character origin in Godot units.
@export var pivot_height_units: float = 1.7
## Initial requested camera distance in Godot units.
@export var initial_distance_units: float = 8.0
## Minimum user-requested zoom distance in Godot units.
@export var minimum_distance_units: float = 2.0
## Maximum user-requested zoom distance in Godot units.
@export var maximum_distance_units: float = 18.0
## Distance change per zoom action in Godot units.
@export var zoom_step_units: float = 1.0
## Camera far plane retained from the renderer sandbox baseline.
@export var camera_far_plane_units: float = 50000.0

var yaw_radians: float:
	get:
		return _yaw_radians

var pitch_radians: float:
	get:
		return _pitch_radians

var requested_distance_units: float:
	get:
		return _requested_distance_units

var resolved_distance_units: float:
	get:
		return _resolved_distance_units

var is_mouse_captured: bool:
	get:
		return _is_mouse_captured

var _character_body: Node3D
var _camera: Camera3D
var _collision_policy: CameraCollisionPolicy
var _yaw_radians := 0.0
var _pitch_radians := deg_to_rad(-18.0)
var _requested_distance_units := 8.0
var _resolved_distance_units := 8.0
var _is_mouse_captured := false


func _ready() -> void:
	_camera = get_node_or_null(camera_path) as Camera3D
	if _camera == null:
		push_error("ThirdPersonCameraRig: Camera3D not found at %s" % camera_path)
	if _collision_policy == null:
		_collision_policy = NO_CAMERA_COLLISION_POLICY_SCRIPT.new()
	_pitch_radians = clampf(_pitch_radians, minimum_pitch_radians, maximum_pitch_radians)
	_requested_distance_units = clampf(
		initial_distance_units,
		minimum_distance_units,
		maximum_distance_units
	)
	if _camera != null:
		_camera.current = true
		_camera.far = camera_far_plane_units
	refresh_camera_transform()


func _physics_process(_delta_seconds: float) -> void:
	refresh_camera_transform()


func _exit_tree() -> void:
	if _is_mouse_captured:
		_is_mouse_captured = false
		Input.mouse_mode = Input.MOUSE_MODE_VISIBLE


## Attaches the character transform rotated by orbit input and captures its initial yaw.
func initialize_for_character(character_body: Node3D) -> void:
	_character_body = character_body
	if _character_body != null:
		_yaw_radians = _character_body.rotation.y
	refresh_camera_transform()


## Synchronizes orbit yaw after keyboard turning changes the character transform.
func synchronize_yaw_from_character() -> void:
	if _character_body != null:
		_yaw_radians = _character_body.rotation.y


## Replaces camera-distance collision behavior and immediately refreshes the camera.
## Passing null is rejected and preserves the current policy.
func set_collision_policy(collision_policy: CameraCollisionPolicy) -> void:
	if collision_policy == null:
		push_error("ThirdPersonCameraRig: collision policy cannot be null")
		return
	_collision_policy = collision_policy
	refresh_camera_transform()


## Applies remappable camera actions and orbit mouse motion.
## Returns true when the event changed or consumed camera state.
func handle_camera_input(event: InputEvent) -> bool:
	if event.is_action_pressed(PlayerInputActions.CAMERA_ROTATE):
		_is_mouse_captured = true
		Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
		return true
	if event.is_action_released(PlayerInputActions.CAMERA_ROTATE):
		_is_mouse_captured = false
		Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
		return true
	if event.is_action_pressed(PlayerInputActions.CAMERA_ZOOM_IN):
		_requested_distance_units = clampf(
			_requested_distance_units - zoom_step_units,
			minimum_distance_units,
			maximum_distance_units
		)
		refresh_camera_transform()
		return true
	if event.is_action_pressed(PlayerInputActions.CAMERA_ZOOM_OUT):
		_requested_distance_units = clampf(
			_requested_distance_units + zoom_step_units,
			minimum_distance_units,
			maximum_distance_units
		)
		refresh_camera_transform()
		return true
	if event.is_action_pressed(PlayerInputActions.RELEASE_CURSOR):
		_is_mouse_captured = false
		Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
		return true
	if _is_mouse_captured and event is InputEventMouseMotion:
		_yaw_radians -= event.relative.x * mouse_sensitivity_radians_per_pixel
		_pitch_radians = clampf(
			_pitch_radians - event.relative.y * mouse_sensitivity_radians_per_pixel,
			minimum_pitch_radians,
			maximum_pitch_radians
		)
		if _character_body != null:
			_character_body.rotation.y = _yaw_radians
		refresh_camera_transform()
		return true
	return false


## Returns the pivot's Godot-world basis for camera-relative sandbox flight.
func godot_world_flight_movement_basis() -> Basis:
	return global_basis


## Applies pivot and Camera3D local transforms using the active collision policy.
func refresh_camera_transform() -> void:
	position = Vector3(0.0, pivot_height_units, 0.0)
	rotation.x = _pitch_radians
	if _camera == null:
		return
	var policy_distance := _collision_policy.resolve_camera_distance(
		self,
		_requested_distance_units
	) if _collision_policy != null else _requested_distance_units
	_resolved_distance_units = clampf(policy_distance, 0.0, _requested_distance_units)
	_camera.position = Vector3(0.0, 0.0, _resolved_distance_units)
	_camera.rotation = Vector3.ZERO
