extends Node3D

@export var body_path: NodePath = NodePath("Body")
@export var left_pupil_path: NodePath = NodePath("Body/HeadPivot/EyeLeft/Pupil")
@export var right_pupil_path: NodePath = NodePath("Body/HeadPivot/EyeRight/Pupil")
@export var camera_path: NodePath
@export var look_origin_path: NodePath = NodePath("Body/HeadPivot")
@export var look_reference_path: NodePath = NodePath("Body")
@export var look_target_path: NodePath = NodePath("")
@export var follow_enabled: bool = true
@export var follow_speed: float = 2.8
@export var follow_acceleration: float = 8.0
@export var follow_start_distance: float = 3.5
@export var follow_stop_distance: float = 1.9
@export var follow_turn_speed: float = 8.0
@export var observed_stop_dot: float = 0.45
@export var require_line_of_sight: bool = false
@export var lock_vertical: bool = true
@export var vertical_unlock_height: float = 0.6
@export var vertical_lock_smooth_speed: float = 6.0
@export var vertical_lock_hold_time: float = 0.3
@export var max_look_angle_deg: float = 90.0
@export var eye_return_speed: float = 0.2
@export var max_offset: float = 0.08
@export var side_eye_boost: float = 1.4
@export var head_path: NodePath = NodePath("Body/HeadPivot")
@export var head_turn_speed: float = 16.0
@export var head_max_yaw_deg: float = 55.0
@export var head_max_pitch_deg: float = 22.0

var _left_pupil: Node3D
var _right_pupil: Node3D
var _body: RigidBody3D
var _left_base: Vector3
var _right_base: Vector3
var _camera: Camera3D
var _look_origin: Node3D
var _head: Node3D
var _head_base_rot: Vector3
var _vertical_lock_factor: float = 1.0
var _vertical_hold_timer: float = 0.0
var _look_reference: Node3D
var _look_target: Node3D
var _follow_velocity := Vector3.ZERO


func _ready() -> void:
	_body = get_node_or_null(body_path) as RigidBody3D
	_left_pupil = get_node_or_null(left_pupil_path) as Node3D
	_right_pupil = get_node_or_null(right_pupil_path) as Node3D
	if _left_pupil:
		_left_base = _left_pupil.position
	if _right_pupil:
		_right_base = _right_pupil.position
	_camera = _resolve_camera()
	_look_origin = get_node_or_null(look_origin_path) as Node3D
	_look_reference = get_node_or_null(look_reference_path) as Node3D
	_look_target = get_node_or_null(look_target_path) as Node3D
	_head = get_node_or_null(head_path) as Node3D
	if _head:
		_head_base_rot = _head.rotation
	if _body:
		_body.top_level = false
		_body.freeze = true
		_body.sleeping = true
		_body.contact_monitor = true
		_body.max_contacts_reported = 8


func _physics_process(delta: float) -> void:
	_update_follow(delta)
	_sync_body_to_root()
	_update_pupils()


func _process(_delta: float) -> void:
	_sync_body_to_root()
	_update_pupils()


func _update_pupils() -> void:
	if _look_target == null and look_target_path != NodePath(""):
		_look_target = get_node_or_null(look_target_path) as Node3D
	if _look_target == null:
		var viewport_cam := get_viewport().get_camera_3d()
		if viewport_cam != null and viewport_cam != _camera:
			_camera = viewport_cam
		elif _camera == null or not _camera.is_inside_tree():
			_camera = _resolve_camera()
	if _look_target == null and _camera == null:
		return
	var origin := _look_origin
	if origin == null:
		origin = self
	var target := _look_target.global_position if _look_target != null else _camera.global_position
	var dir_world := target - origin.global_position
	if dir_world.length_squared() <= 0.0001:
		return
	dir_world = dir_world.normalized()
	var reference := _look_reference if _look_reference != null else origin
	var forward := -reference.global_basis.z
	var min_dot := cos(deg_to_rad(max_look_angle_deg))
	var can_look := dir_world.dot(forward) >= min_dot
	if not can_look:
		var delta := get_process_delta_time()
		if _left_pupil:
			var target_left := Vector3(_left_base.x, _left_base.y, _left_base.z)
			var pos_left := _left_pupil.position
			pos_left.x = move_toward(pos_left.x, target_left.x, eye_return_speed * delta)
			pos_left.y = target_left.y
			pos_left.z = move_toward(pos_left.z, target_left.z, eye_return_speed * delta)
			_left_pupil.position = pos_left
		if _right_pupil:
			var target_right := Vector3(_right_base.x, _right_base.y, _right_base.z)
			var pos_right := _right_pupil.position
			pos_right.x = move_toward(pos_right.x, target_right.x, eye_return_speed * delta)
			pos_right.y = target_right.y
			pos_right.z = move_toward(pos_right.z, target_right.z, eye_return_speed * delta)
			_right_pupil.position = pos_right
		if _head:
			_head.rotation.x = _head_base_rot.x
			_head.rotation.y = lerp_angle(_head.rotation.y, _head_base_rot.y, head_turn_speed * delta)
		return
	if lock_vertical:
		var origin_y := origin.global_position.y
		var target_offset := target.y - origin_y
		var is_above := target_offset > vertical_unlock_height
		if is_above:
			_vertical_hold_timer = vertical_lock_hold_time
		else:
			_vertical_hold_timer = max(0.0, _vertical_hold_timer - get_process_delta_time())
		if is_above:
			_vertical_lock_factor = 1.0
		else:
			var unlock := 1.0 if _vertical_hold_timer > 0.0 else 0.0
			_vertical_lock_factor = move_toward(_vertical_lock_factor, unlock, vertical_lock_smooth_speed * get_process_delta_time())
		target.y = lerp(origin_y, target.y, _vertical_lock_factor)
		dir_world = target - origin.global_position
		if dir_world.length_squared() <= 0.0001:
			return
		dir_world = dir_world.normalized()
	if _left_pupil:
		_update_eye(_left_pupil, _left_base, dir_world)
	if _right_pupil:
		_update_eye(_right_pupil, _right_base, dir_world)
	if _head:
		_update_head(dir_world)


func _update_follow(delta: float) -> void:
	if _body == null:
		return
	if not follow_enabled:
		_slow_to_stop(delta)
		return
	if _look_target == null and look_target_path != NodePath(""):
		_look_target = get_node_or_null(look_target_path) as Node3D
	if _look_target == null:
		_slow_to_stop(delta)
		return
	if _look_target.has_method("is_in_vehicle") and bool(_look_target.call("is_in_vehicle")):
		_slow_to_stop(delta)
		return
	if _is_observed():
		_slow_to_stop(delta)
		return

	var to_target := _look_target.global_position - _body.global_position
	to_target.y = 0.0
	var distance := to_target.length()
	if distance <= 0.001:
		_slow_to_stop(delta)
		return

	var desired_velocity := Vector3.ZERO
	if distance > follow_start_distance:
		desired_velocity = to_target.normalized() * follow_speed
	elif distance <= follow_stop_distance:
		desired_velocity = Vector3.ZERO
	else:
		var blend := inverse_lerp(follow_stop_distance, follow_start_distance, distance)
		desired_velocity = to_target.normalized() * (follow_speed * blend)

	_follow_velocity = _follow_velocity.move_toward(desired_velocity, follow_acceleration * delta)
	global_position += _follow_velocity * delta

	if _follow_velocity.length_squared() > 0.0001:
		var facing_dir := _follow_velocity.normalized()
		var target_yaw := atan2(-facing_dir.x, -facing_dir.z)
		rotation.y = lerp_angle(rotation.y, target_yaw, follow_turn_speed * delta)


func _slow_to_stop(delta: float) -> void:
	_follow_velocity = _follow_velocity.move_toward(Vector3.ZERO, follow_acceleration * delta)
	global_position += _follow_velocity * delta


func _sync_body_to_root() -> void:
	if _body == null:
		return
	_body.position = Vector3.ZERO
	_body.rotation = Vector3.ZERO


func _is_observed() -> bool:
	var observer_origin := Vector3.ZERO
	var observer_forward := Vector3.ZERO
	if _look_target != null and _look_target.has_method("get_look_origin") and _look_target.has_method("get_look_direction"):
		observer_origin = _look_target.call("get_look_origin")
		observer_forward = _look_target.call("get_look_direction")
	else:
		var active_camera := _resolve_active_camera()
		if active_camera == null:
			return false
		observer_origin = active_camera.global_position
		observer_forward = -active_camera.global_basis.z

	if observer_forward.length_squared() <= 0.0001:
		return false
	observer_forward = observer_forward.normalized()
	var to_bot := _body.global_position - observer_origin
	if to_bot.length_squared() <= 0.0001:
		return true
	var dir_to_bot := to_bot.normalized()
	if observer_forward.dot(dir_to_bot) < observed_stop_dot:
		return false
	if not require_line_of_sight:
		return true
	var query := PhysicsRayQueryParameters3D.create(observer_origin, _body.global_position)
	query.exclude = [_body.get_rid()]
	if _look_target is CollisionObject3D:
		query.exclude.append((_look_target as CollisionObject3D).get_rid())
	var hit := get_world_3d().direct_space_state.intersect_ray(query)
	return hit.is_empty()


func _resolve_active_camera() -> Camera3D:
	if _camera != null and _camera.is_inside_tree() and _camera.current:
		return _camera
	var viewport_cam := get_viewport().get_camera_3d()
	if viewport_cam != null:
		_camera = viewport_cam
		return viewport_cam
	if _camera == null or not _camera.is_inside_tree():
		_camera = _resolve_camera()
	return _camera


func _resolve_camera() -> Camera3D:
	if camera_path != NodePath(""):
		var from_path := get_node_or_null(camera_path) as Camera3D
		if from_path:
			return from_path
	var viewport_cam := get_viewport().get_camera_3d()
	if viewport_cam:
		return viewport_cam
	var by_name := get_tree().get_root().find_child("Camera3D", true, false) as Camera3D
	return by_name


func _update_eye(eye: Node3D, base_pos: Vector3, dir_world: Vector3) -> void:
	var parent := eye.get_parent() as Node3D
	if parent == null:
		return
	var dir_local := parent.global_basis.inverse() * dir_world
	var flat := Vector2(dir_local.x, dir_local.y)
	flat.x *= side_eye_boost
	if flat.length() > 1.0:
		flat = flat.normalized()
	var offset := Vector3(flat.x, flat.y, 0.0) * max_offset
	eye.position = base_pos + offset


func _update_head(dir_world: Vector3) -> void:
	var parent := _head.get_parent() as Node3D
	if parent == null:
		return
	var dir_local := parent.global_basis.inverse() * dir_world
	var yaw := atan2(-dir_local.x, -dir_local.z)
	var pitch := atan2(dir_local.y, -dir_local.z)
	yaw = clamp(yaw, deg_to_rad(-head_max_yaw_deg), deg_to_rad(head_max_yaw_deg))
	pitch = clamp(pitch, deg_to_rad(-head_max_pitch_deg), deg_to_rad(head_max_pitch_deg))
	var target := Vector3(_head_base_rot.x + pitch, _head_base_rot.y + yaw, _head_base_rot.z)
	_head.rotation.x = lerp_angle(_head.rotation.x, target.x, head_turn_speed * get_process_delta_time())
	_head.rotation.y = lerp_angle(_head.rotation.y, target.y, head_turn_speed * get_process_delta_time())
