class_name GameplayCamera extends Camera3D

@export var target: Node3D
@export var target_distance: float = 5.0
@export var target_min_distance: float = 2.0
@export var target_max_distance: float = 10.0
@export var target_decentering_factor: float = 0.4
@export var follow_lerp_speed: float = 0.999
@export var turn_lerp_speed: float = 0.999
@export var fov_lerp_speed: float = 0.999
@export var clip_margin: float = 0.5
@export var gimbal_margin: float = 0.75
@export var player_control := false
@export var look_speed: float = 1.0
@export var look_lerp_speed: float = 0.99
@export var zoom_speed: float = 4.0
@export var mouse_sensitivity: float = 0.03125
@export var mouse_wheel_sensitivity: float = 5.0
@export var analog_sensitivity: float = 1.0
@export var analog_zoom_sensitivity: float = 1.0

var target_direction := Vector3.ZERO
var look_impetus := Vector3.ZERO
var look_velocity := Vector3.ZERO

@onready var _raycast_object := PhysicsRayQueryParameters3D.new()

func _ready() -> void:
	snap_to_target()

func _update_mouse_mode() -> void:
	if player_control && !Engine.is_embedded_in_editor():
		Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
	else:
		Input.mouse_mode = Input.MOUSE_MODE_VISIBLE

func _unhandled_input(event: InputEvent) -> void:
	if !player_control || Engine.is_embedded_in_editor(): return
	var move_event := event as InputEventMouseMotion
	var button_event := event as InputEventMouseButton
	if move_event:
		look_impetus.x -= move_event.relative.x*mouse_sensitivity
		look_impetus.y += move_event.relative.y*mouse_sensitivity
	elif button_event:
		match button_event.button_index:
			MOUSE_BUTTON_WHEEL_UP:
				look_impetus.z -= mouse_wheel_sensitivity
			MOUSE_BUTTON_WHEEL_DOWN:
				look_impetus.z += mouse_wheel_sensitivity

func _handle_analog_input() -> void:
	if !player_control: return
	look_impetus.x -= Input.get_axis(&'look_left', &'look_right')
	look_impetus.y += Input.get_axis(&'look_down', &'look_up')
	look_impetus.z += Input.get_axis(&'zoom_in', &'zoom_out')

func _respond_to_impetus(delta: float) -> void:
	var gck: float = _gimbal_check()
	if (
		(look_impetus.y > 0.0 and gck < -gimbal_margin) ||
		(look_impetus.y < 0.0 and gck > gimbal_margin)
	):
		look_impetus.y = 0.0
	var desired_look_velocity := Vector3(
		look_impetus.x*look_speed*target_distance,
		look_impetus.y*look_speed*target_distance,
		look_impetus.z*zoom_speed
	)
	look_impetus = look_impetus.limit_length()
	look_velocity = lerp(look_velocity, desired_look_velocity, look_lerp_speed)
	_try_go(
		global_position +
		global_basis.x*look_velocity.x*delta +
		global_basis.y*look_velocity.y*delta
	)
	target_distance = clamp(
		target_distance + look_velocity.z*delta,
		target_min_distance, target_max_distance
	)
	look_impetus = Vector3.ZERO

func _try_go(where: Vector3) -> void:
	_raycast_object.from = global_position
	_raycast_object.to = where + (
		(where - global_position).normalized()*clip_margin
	)
	var raycast_results := (
		get_world_3d().direct_space_state.intersect_ray(_raycast_object)
	)
	if raycast_results.is_empty():
		global_position = where
	else:
		global_position = raycast_results.position + (
			raycast_results.normal*clip_margin
		)

func snap_to_target() -> void:
	if !target: return
	target_direction = (
		-target.global_basis.z - target.global_basis.y/2.0
	).normalized()
	_follow_target(0.0, true)

func _gimbal_check() -> float:
	return target_direction.dot(target.global_basis.y)

func _follow_target(
	delta: float,
	snap: bool = false
) -> void:
	if !target: return
	var velocity := Vector3.ZERO
	var top_speed: float = 30.0
	if target is RigidBody3D:
		velocity = (target as RigidBody3D).linear_velocity
		if target is Character:
			top_speed = (target as Character).get_top_speed()
	var decentering: float = target_distance*target_decentering_factor
	var target_position := (
		target.global_position + decentering*target.global_basis.y
	)
	var target_offset := target_position - global_position
	if !snap:
		target_direction = target_offset.normalized()
	var desired_position := target_position - target_distance*target_direction
	_try_go(desired_position if snap else lerp(
		global_position, desired_position,
		1.0 - (1.0 - follow_lerp_speed)**delta
	))
	if abs(_gimbal_check()) <= lerp(gimbal_margin, 1.0, 0.5):
		var desired_basis := Basis.looking_at(
			target_offset,
			target.global_basis.y
		)
		global_basis = desired_basis if snap else global_basis.slerp(
			desired_basis,
			1.0 - (1.0 - turn_lerp_speed)**delta
		)
	var desired_fov: float = min(
		75.0 + 15.0*velocity.project(global_basis.z).length()/top_speed,
		179.0
	)
	fov = desired_fov if snap else lerp(
		fov, desired_fov,
		1.0 - (1.0 - fov_lerp_speed)**delta
	)

func _physics_process(delta: float) -> void:
	_update_mouse_mode()
	_handle_analog_input()
	_respond_to_impetus(delta)
	_follow_target(delta)
