class_name Player
extends CharacterBody3D

const FishInventoryType = preload("res://inventory/fish_inventory.gd")
const CollectionLogType = preload("res://collection/collection_log.gd")
const FishCatchType = preload("res://fish/fish_catch.gd")
const FishSaleServiceType = preload("res://economy/fish_sale_service.gd")
const PlayerWalletType = preload("res://economy/player_wallet.gd")
const PlayerBagType = preload("res://inventory/player_bag.gd")
const PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
const PlayerFishingUpgradesType = preload(
	"res://progression/player_fishing_upgrades.gd"
)
const PlayerItemEffectsType = preload(
	"res://progression/player_item_effects.gd"
)
const PlayerCoolerCapacityType = preload(
	"res://progression/player_cooler_capacity.gd"
)

class ShowcaseCameraSnapshot:
	extends RefCounted

	var yaw_transform: Transform3D
	var yaw_rotation: Vector3
	var pitch_rotation: Vector3
	var spring_length: float
	var target_zoom: float
	var camera_input_enabled: bool
	var camera_dragging: bool


@export_category("Movement")
@export var walk_speed: float = 5.0
@export var sprint_speed: float = 8.0
@export var sneak_speed: float = 2.0
@export var slow_walk_speed: float = 3.25
@export_range(0.1, 20.0, 0.1) var jump_velocity: float = 4.6
@export_range(0.1, 5.0, 0.05) var upward_gravity_multiplier: float = 1.35
@export_range(0.1, 5.0, 0.05) var fall_gravity_multiplier: float = 2.1
@export_range(0.1, 3.0, 0.05) var body_center_height: float = 0.9
@export var body_rotation_speed: float = 12.0

@export_category("Control")
@export var local_control_enabled: bool = true

@export_category("Fishing Stats")
@export_range(0.01, 2.0, 0.01) var reel_speed: float = 0.32
@export_range(1, 100, 1) var click_power: int = 1

@export_category("Showcase")
@export_range(0.05, 1.0, 0.01) var showcase_turn_duration: float = 0.28
@export_range(0.05, 1.0, 0.01) var showcase_restore_duration: float = 0.24
@export_range(0.05, 1.5, 0.01) var showcase_camera_transition_duration: float = 0.42
@export_range(-180.0, 180.0, 1.0) var showcase_camera_yaw_offset: float = 180.0
@export_range(-80.0, 80.0, 1.0) var showcase_camera_pitch: float = -8.0
@export_range(1.0, 10.0, 0.1) var showcase_camera_zoom_distance: float = 3.6
@export_range(0.5, 3.0, 0.05) var showcase_camera_target_height: float = 1.45
@export_range(0.01, 1.0, 0.01) var catch_presentation_base_scale: float = 0.10

@export_category("Camera")
@export var mouse_sensitivity: float = 0.005
@export var controller_camera_speed: float = 2.5
@export_range(0.0, 1.0, 0.01) var controller_camera_deadzone: float = 0.2
@export var invert_camera_y: bool = false
@export_range(-89.0, 0.0, 0.5) var minimum_pitch_degrees: float = -65.0
@export_range(0.0, 89.0, 0.5) var maximum_pitch_degrees: float = 45.0
@export var minimum_zoom: float = 2.0
@export var maximum_zoom: float = 8.0
@export var zoom_step: float = 0.75
@export var zoom_smoothing: float = 12.0

@onready var _visuals: Node3D = %Visuals
@onready var _camera_yaw: Node3D = %CameraYaw
@onready var _camera_pitch: Node3D = %CameraPitch
@onready var _spring_arm: SpringArm3D = %SpringArm3D
@onready var _camera: Camera3D = %Camera3D
@onready var inventory: FishInventoryType = %Inventory
@onready var collection_log: CollectionLogType = %CollectionLog
@onready var wallet: PlayerWalletType = %Wallet
@onready var fish_sale_service: FishSaleServiceType = %FishSaleService
@onready var bag: PlayerBagType = %Bag
@onready var hotbar: PlayerHotbarType = %Hotbar
@onready var fishing_upgrades: PlayerFishingUpgradesType = %FishingUpgrades
@onready var item_effects: PlayerItemEffectsType = %ItemEffects
@onready var cooler_capacity: PlayerCoolerCapacityType = %CoolerCapacity
@onready var _cast_origin: Marker3D = %CastOrigin
@onready var _fishing_rod: Node3D = %FishingRod
@onready var _fishing_rod_tip: Marker3D = %FishingRodTip
@onready var _catch_display: Node3D = %CatchDisplay
@onready var _catch_sprite: Sprite3D = %CatchSprite

var _gravity: float = float(ProjectSettings.get_setting("physics/3d/default_gravity"))
var _camera_dragging: bool = false
var _camera_input_enabled: bool = true
var _movement_enabled: bool = true
var _water_recovery_active: bool = false
var _target_zoom: float = 5.0
var _showcase_rod_visibility: bool = true
var _showcase_rod_state_stored: bool = false
var _showcase_visual_rotation: Vector3
var _showcase_visual_rotation_stored: bool = false
var _showcase_turn_tween: Tween
var _showcase_restore_tween: Tween
var _showcase_camera_tween: Tween
var _showcase_camera_snapshot: ShowcaseCameraSnapshot
var _showcase_restore_generation: int = 0
var _network_peer_id: int = 0
var _network_authoritative_simulation: bool = false
var _network_interpolation_enabled: bool = false
var _network_axis: Vector2 = Vector2.ZERO
var _network_camera_yaw: float = 0.0
var _network_jump_pending: bool = false
var _network_sprint: bool = false
var _network_sneak: bool = false
var _network_slow_walk: bool = false
var _last_network_input_sequence: int = 0
var _network_target_position: Vector3
var _network_target_velocity: Vector3
var _network_target_visual_yaw: float = 0.0
var _network_snapshot_ready: bool = false


func _ready() -> void:
	_target_zoom = clampf(_spring_arm.spring_length, minimum_zoom, maximum_zoom)
	_spring_arm.spring_length = _target_zoom
	_camera.current = local_control_enabled


func _physics_process(delta: float) -> void:
	if _network_interpolation_enabled:
		_update_network_interpolation(delta)
		return
	if _water_recovery_active:
		velocity = Vector3.ZERO
		return

	if not is_on_floor():
		var gravity_multiplier: float = (
			upward_gravity_multiplier
			if velocity.y > 0.0
			else fall_gravity_multiplier
		)
		velocity.y -= _gravity * gravity_multiplier * delta
	elif _movement_enabled and (
		(
			local_control_enabled
			and Input.is_action_just_pressed("jump")
		)
		or (
			_network_authoritative_simulation
			and _network_jump_pending
		)
	):
		velocity.y = jump_velocity
	_network_jump_pending = false

	if not _movement_enabled:
		velocity.x = 0.0
		velocity.z = 0.0
		move_and_slide()
		return

	var input_vector: Vector2
	var camera_basis: Basis
	if local_control_enabled:
		input_vector = Input.get_vector(
			"move_left",
			"move_right",
			"move_forward",
			"move_backward"
		)
		camera_basis = _camera_yaw.global_basis
	elif _network_authoritative_simulation:
		input_vector = _network_axis
		camera_basis = Basis(Vector3.UP, _network_camera_yaw)
	else:
		velocity.x = 0.0
		velocity.z = 0.0
		return
	var move_direction: Vector3 = camera_basis.x * input_vector.x + camera_basis.z * input_vector.y
	move_direction.y = 0.0
	var input_strength: float = minf(input_vector.length(), 1.0)
	move_direction = move_direction.normalized()

	var speed: float = _get_network_aware_speed()
	if item_effects != null:
		speed *= item_effects.get_movement_multiplier()
	velocity.x = move_direction.x * speed * input_strength
	velocity.z = move_direction.z * speed * input_strength

	if not move_direction.is_zero_approx():
		var target_rotation: float = atan2(-move_direction.x, -move_direction.z)
		_visuals.rotation.y = lerp_angle(
			_visuals.rotation.y,
			target_rotation,
			1.0 - exp(-body_rotation_speed * delta)
		)

	move_and_slide()


func _process(delta: float) -> void:
	if not local_control_enabled:
		return

	if (
		_camera_input_enabled
		and _camera_dragging
		and not Input.is_action_pressed("camera_drag")
	):
		_camera_dragging = false

	if _camera_input_enabled:
		var stick: Vector2 = Vector2(
			Input.get_joy_axis(0, JOY_AXIS_RIGHT_X),
			Input.get_joy_axis(0, JOY_AXIS_RIGHT_Y)
		)
		if stick.length() > controller_camera_deadzone:
			var adjusted_strength: float = (
				(stick.length() - controller_camera_deadzone)
				/ (1.0 - controller_camera_deadzone)
			)
			_rotate_camera(
				stick.normalized()
				* adjusted_strength
				* controller_camera_speed
				* delta
			)

		var zoom_weight: float = 1.0 - exp(-zoom_smoothing * delta)
		_spring_arm.spring_length = lerpf(
			_spring_arm.spring_length,
			_target_zoom,
			zoom_weight
		)


func _unhandled_input(event: InputEvent) -> void:
	if not local_control_enabled or not _camera_input_enabled:
		return

	if event.is_action("camera_drag"):
		_camera_dragging = event.is_pressed()
		get_viewport().set_input_as_handled()
		return

	if event is InputEventMouseMotion and _camera_dragging:
		_rotate_camera(event.relative * mouse_sensitivity)
		get_viewport().set_input_as_handled()
		return

	if (
		event is InputEventMouseButton
		and event.shift_pressed
		and event.is_action_pressed("camera_zoom_in")
	):
		_set_target_zoom(_target_zoom - zoom_step)
		get_viewport().set_input_as_handled()
	elif (
		event is InputEventMouseButton
		and event.shift_pressed
		and event.is_action_pressed("camera_zoom_out")
	):
		_set_target_zoom(_target_zoom + zoom_step)
		get_viewport().set_input_as_handled()


func _get_current_speed() -> float:
	if Input.is_action_pressed("sneak"):
		return sneak_speed
	if Input.is_action_pressed("slow_walk"):
		return slow_walk_speed
	if Input.is_action_pressed("sprint"):
		return sprint_speed
	return walk_speed


func _get_network_aware_speed() -> float:
	if local_control_enabled:
		return _get_current_speed()
	if _network_sneak:
		return sneak_speed
	if _network_slow_walk:
		return slow_walk_speed
	if _network_sprint:
		return sprint_speed
	return walk_speed


func _rotate_camera(delta_rotation: Vector2) -> void:
	_camera_yaw.rotation.y -= delta_rotation.x
	var vertical_direction: float = -1.0 if invert_camera_y else 1.0
	_camera_pitch.rotation.x = clampf(
		_camera_pitch.rotation.x - delta_rotation.y * vertical_direction,
		deg_to_rad(minimum_pitch_degrees),
		deg_to_rad(maximum_pitch_degrees)
	)


func _set_target_zoom(value: float) -> void:
	_target_zoom = clampf(value, minimum_zoom, maximum_zoom)


func set_local_control(enabled: bool) -> void:
	local_control_enabled = enabled
	if is_node_ready():
		_camera.current = enabled
	if not enabled:
		_camera_dragging = false


func set_network_peer_id(peer_id: int) -> void:
	_network_peer_id = peer_id


func get_network_peer_id() -> int:
	return _network_peer_id


func configure_network_remote(authoritative_simulation: bool) -> void:
	set_local_control(false)
	_network_authoritative_simulation = authoritative_simulation
	_network_interpolation_enabled = not authoritative_simulation
	_network_snapshot_ready = false
	_camera.current = false


func capture_network_input(sequence: int) -> Dictionary:
	var axis: Vector2 = Input.get_vector(
		"move_left",
		"move_right",
		"move_forward",
		"move_backward"
	)
	return {
		"sequence": sequence,
		"axis": [axis.x, axis.y],
		"camera_yaw": _camera_yaw.global_rotation.y,
		"jump": Input.is_action_just_pressed("jump"),
		"sprint": Input.is_action_pressed("sprint"),
		"sneak": Input.is_action_pressed("sneak"),
		"slow_walk": Input.is_action_pressed("slow_walk"),
	}


func apply_authoritative_network_input(data: Dictionary) -> void:
	var sequence: int = int(data.get("sequence", 0))
	if sequence <= _last_network_input_sequence:
		return
	var axis: Array = data.get("axis", [])
	if axis.size() != 2:
		return
	_last_network_input_sequence = sequence
	_network_axis = Vector2(float(axis[0]), float(axis[1])).limit_length(1.0)
	_network_camera_yaw = float(data.get("camera_yaw", 0.0))
	_network_jump_pending = bool(data.get("jump", false))
	_network_sprint = bool(data.get("sprint", false))
	_network_sneak = bool(data.get("sneak", false))
	_network_slow_walk = bool(data.get("slow_walk", false))


func make_network_snapshot(peer_id: int) -> Dictionary:
	return {
		"peer_id": peer_id,
		"acknowledged_input": _last_network_input_sequence,
		"position": [global_position.x, global_position.y, global_position.z],
		"velocity": [velocity.x, velocity.y, velocity.z],
		"visual_yaw": _visuals.rotation.y,
		"grounded": is_on_floor(),
	}


func push_network_snapshot(snapshot: Dictionary) -> void:
	var parsed: Dictionary = _parse_network_snapshot(snapshot)
	if parsed.is_empty():
		return
	_network_target_position = parsed["position"]
	_network_target_velocity = parsed["velocity"]
	_network_target_visual_yaw = parsed["visual_yaw"]
	if not _network_snapshot_ready:
		global_position = _network_target_position
		velocity = _network_target_velocity
		_visuals.rotation.y = _network_target_visual_yaw
	_network_snapshot_ready = true


func apply_local_prediction_correction(snapshot: Dictionary) -> void:
	var parsed: Dictionary = _parse_network_snapshot(snapshot)
	if parsed.is_empty():
		return
	var authoritative_position: Vector3 = parsed["position"]
	var error_distance: float = global_position.distance_to(
		authoritative_position
	)
	if error_distance > 2.0:
		global_position = authoritative_position
	elif error_distance > 0.05:
		global_position = global_position.lerp(authoritative_position, 0.18)


func apply_network_teleport(snapshot: Dictionary) -> void:
	var parsed: Dictionary = _parse_network_snapshot(snapshot)
	if parsed.is_empty():
		return
	global_position = parsed["position"]
	velocity = parsed["velocity"]
	_visuals.rotation.y = parsed["visual_yaw"]
	_network_target_position = global_position
	_network_target_velocity = velocity
	_network_target_visual_yaw = _visuals.rotation.y
	_network_snapshot_ready = true


func _parse_network_snapshot(snapshot: Dictionary) -> Dictionary:
	var position: Variant = snapshot.get("position")
	var network_velocity: Variant = snapshot.get("velocity")
	if (
		typeof(position) != TYPE_ARRAY
		or typeof(network_velocity) != TYPE_ARRAY
		or position.size() != 3
		or network_velocity.size() != 3
	):
		return {}
	var parsed_position := Vector3(
		float(position[0]),
		float(position[1]),
		float(position[2])
	)
	var parsed_velocity := Vector3(
		float(network_velocity[0]),
		float(network_velocity[1]),
		float(network_velocity[2])
	)
	var visual_yaw: float = float(snapshot.get("visual_yaw", 0.0))
	if (
		not parsed_position.is_finite()
		or not parsed_velocity.is_finite()
		or not is_finite(visual_yaw)
	):
		return {}
	return {
		"position": parsed_position,
		"velocity": parsed_velocity,
		"visual_yaw": visual_yaw,
	}


func _update_network_interpolation(delta: float) -> void:
	if not _network_snapshot_ready:
		return
	var position_weight: float = 1.0 - exp(-12.0 * delta)
	global_position = global_position.lerp(
		_network_target_position,
		position_weight
	)
	velocity = _network_target_velocity
	_visuals.rotation.y = lerp_angle(
		_visuals.rotation.y,
		_network_target_visual_yaw,
		1.0 - exp(-14.0 * delta)
	)


func is_local_control_enabled() -> bool:
	return local_control_enabled


func set_movement_enabled(enabled: bool) -> void:
	_movement_enabled = enabled
	if not enabled:
		velocity.x = 0.0
		velocity.z = 0.0


func is_movement_enabled() -> bool:
	return _movement_enabled


func set_camera_input_enabled(enabled: bool) -> void:
	_camera_input_enabled = enabled
	if not enabled:
		_camera_dragging = false


func apply_camera_settings(
	new_mouse_sensitivity: float,
	new_controller_sensitivity: float,
	invert_vertical: bool,
) -> void:
	mouse_sensitivity = clampf(new_mouse_sensitivity, 0.001, 0.012)
	controller_camera_speed = clampf(
		new_controller_sensitivity,
		0.5,
		5.0
	)
	invert_camera_y = invert_vertical


func is_camera_input_enabled() -> bool:
	return _camera_input_enabled


func set_water_recovery_active(active: bool) -> void:
	_water_recovery_active = active
	velocity = Vector3.ZERO


func prepare_for_water_recovery() -> void:
	_restore_gameplay_presentation_for_recovery()


func restore_gameplay_orientation_after_recovery() -> void:
	_restore_gameplay_presentation_for_recovery()


func _restore_gameplay_presentation_for_recovery() -> void:
	# Recovery owns presentation teardown. Ending immediately kills every
	# showcase tween, invalidates its callbacks, and restores the gameplay yaw.
	end_catch_showcase(Callable(), true)
	var gameplay_rotation: Vector3 = _visuals.rotation
	gameplay_rotation.x = 0.0
	gameplay_rotation.z = 0.0
	_visuals.rotation = gameplay_rotation


func get_body_center_position() -> Vector3:
	return global_position + Vector3.UP * body_center_height


func get_body_center_height() -> float:
	return body_center_height


func get_fishing_rod_tip() -> Marker3D:
	return _fishing_rod_tip


func get_fishing_rod() -> Node3D:
	return _fishing_rod


func set_active_item_is_rod(active_is_rod: bool) -> void:
	if _showcase_rod_state_stored:
		_showcase_rod_visibility = active_is_rod
	else:
		_fishing_rod.visible = active_is_rod


func get_cast_origin_position() -> Vector3:
	return _cast_origin.global_position


func begin_catch_showcase(fish_catch: FishCatchType) -> void:
	if fish_catch == null or not fish_catch.is_valid():
		return
	_kill_showcase_camera_tween()
	_kill_showcase_restore_tween()
	_capture_showcase_camera_snapshot()
	if not _showcase_visual_rotation_stored:
		_showcase_visual_rotation = _visuals.rotation
		_showcase_visual_rotation_stored = true
	var showcase_camera_position: Vector3 = _begin_showcase_camera_transition()
	_turn_showcase_toward_position(showcase_camera_position)
	if not _showcase_rod_state_stored:
		_showcase_rod_visibility = _fishing_rod.visible
		_showcase_rod_state_stored = true
	_fishing_rod.visible = false
	_catch_sprite.texture = fish_catch.fish.display_texture
	_catch_display.scale = (
		Vector3.ONE
		* fish_catch.display_scale
		* catch_presentation_base_scale
	)
	_catch_display.visible = _catch_sprite.texture != null


func end_catch_showcase(
	restored_callback: Callable = Callable(),
	immediate: bool = false,
) -> void:
	_kill_showcase_turn_tween()
	_kill_showcase_camera_tween()
	_kill_showcase_restore_tween()
	_catch_display.visible = false
	_catch_display.scale = Vector3.ONE
	_catch_sprite.texture = null
	if _showcase_rod_state_stored:
		_fishing_rod.visible = _showcase_rod_visibility
	_showcase_rod_visibility = true
	_showcase_rod_state_stored = false
	if (
		not _showcase_visual_rotation_stored
		and _showcase_camera_snapshot == null
	):
		if restored_callback.is_valid():
			restored_callback.call()
		return
	if immediate:
		_complete_showcase_restore(restored_callback)
		return

	var current_rotation: Vector3 = _visuals.rotation
	var restore_target: Vector3 = _showcase_visual_rotation
	restore_target.y = (
		current_rotation.y
		+ wrapf(
			_showcase_visual_rotation.y - current_rotation.y,
			-PI,
			PI
		)
	)
	var restore_generation: int = _showcase_restore_generation
	_showcase_restore_tween = create_tween()
	_showcase_restore_tween.set_parallel(true)
	_showcase_restore_tween.set_trans(Tween.TRANS_QUAD)
	_showcase_restore_tween.set_ease(Tween.EASE_IN_OUT)
	if _showcase_visual_rotation_stored:
		_showcase_restore_tween.tween_property(
			_visuals,
			"rotation",
			restore_target,
			maxf(showcase_restore_duration, 0.05)
		)
	if _showcase_camera_snapshot != null:
		var camera_yaw_target: Vector3 = (
			_showcase_camera_snapshot.yaw_rotation
		)
		camera_yaw_target.y = (
			_camera_yaw.rotation.y
			+ wrapf(
				camera_yaw_target.y - _camera_yaw.rotation.y,
				-PI,
				PI
			)
		)
		var camera_duration: float = maxf(
			showcase_camera_transition_duration,
			0.05
		)
		_showcase_restore_tween.tween_property(
			_camera_yaw,
			"rotation",
			camera_yaw_target,
			camera_duration
		)
		_showcase_restore_tween.tween_property(
			_camera_yaw,
			"position",
			_showcase_camera_snapshot.yaw_transform.origin,
			camera_duration
		)
		_showcase_restore_tween.tween_property(
			_camera_pitch,
			"rotation",
			_showcase_camera_snapshot.pitch_rotation,
			camera_duration
		)
		_showcase_restore_tween.tween_property(
			_spring_arm,
			"spring_length",
			_showcase_camera_snapshot.spring_length,
			camera_duration
		)
	_showcase_restore_tween.finished.connect(
		_on_showcase_restore_finished.bind(
			restore_generation,
			restored_callback
		)
	)


func _turn_showcase_toward_position(target_position: Vector3) -> void:
	_kill_showcase_turn_tween()
	var camera_direction: Vector3 = (
		target_position - _visuals.global_position
	)
	camera_direction.y = 0.0
	if camera_direction.is_zero_approx():
		return
	camera_direction = camera_direction.normalized()
	var target_world_yaw: float = atan2(
		-camera_direction.x,
		-camera_direction.z
	)
	var target_local_yaw: float = target_world_yaw - global_rotation.y
	var current_yaw: float = _visuals.rotation.y
	var shortest_target_yaw: float = (
		current_yaw
		+ wrapf(target_local_yaw - current_yaw, -PI, PI)
	)
	_showcase_turn_tween = create_tween()
	_showcase_turn_tween.set_trans(Tween.TRANS_QUAD)
	_showcase_turn_tween.set_ease(Tween.EASE_IN_OUT)
	_showcase_turn_tween.tween_property(
		_visuals,
		"rotation:y",
		shortest_target_yaw,
		maxf(showcase_turn_duration, 0.05)
	)
	_showcase_turn_tween.finished.connect(_on_showcase_turn_finished)


func _kill_showcase_turn_tween() -> void:
	if _showcase_turn_tween != null and _showcase_turn_tween.is_valid():
		_showcase_turn_tween.kill()
	_showcase_turn_tween = null


func _kill_showcase_restore_tween() -> void:
	if _showcase_restore_tween != null and _showcase_restore_tween.is_valid():
		_showcase_restore_tween.kill()
	_showcase_restore_tween = null
	_showcase_restore_generation += 1


func _kill_showcase_camera_tween() -> void:
	if _showcase_camera_tween != null and _showcase_camera_tween.is_valid():
		_showcase_camera_tween.kill()
	_showcase_camera_tween = null


func _on_showcase_turn_finished() -> void:
	_showcase_turn_tween = null


func _on_showcase_restore_finished(
	restore_generation: int,
	restored_callback: Callable,
) -> void:
	if restore_generation != _showcase_restore_generation:
		return
	_showcase_restore_tween = null
	_complete_showcase_restore(restored_callback)


func _complete_showcase_restore(
	restored_callback: Callable,
) -> void:
	if _showcase_visual_rotation_stored:
		_visuals.rotation = _showcase_visual_rotation
	_showcase_visual_rotation = Vector3.ZERO
	_showcase_visual_rotation_stored = false
	if _showcase_camera_snapshot != null:
		_camera_yaw.transform = _showcase_camera_snapshot.yaw_transform
		_camera_pitch.rotation = _showcase_camera_snapshot.pitch_rotation
		_spring_arm.spring_length = _showcase_camera_snapshot.spring_length
		_target_zoom = _showcase_camera_snapshot.target_zoom
		_camera_input_enabled = (
			_showcase_camera_snapshot.camera_input_enabled
		)
		_camera_dragging = _showcase_camera_snapshot.camera_dragging
	_showcase_camera_snapshot = null
	if restored_callback.is_valid():
		restored_callback.call()


func _capture_showcase_camera_snapshot() -> void:
	if _showcase_camera_snapshot != null:
		return
	_showcase_camera_snapshot = ShowcaseCameraSnapshot.new()
	_showcase_camera_snapshot.yaw_transform = _camera_yaw.transform
	_showcase_camera_snapshot.yaw_rotation = _camera_yaw.rotation
	_showcase_camera_snapshot.pitch_rotation = _camera_pitch.rotation
	_showcase_camera_snapshot.spring_length = _spring_arm.spring_length
	_showcase_camera_snapshot.target_zoom = _target_zoom
	_showcase_camera_snapshot.camera_input_enabled = _camera_input_enabled
	_showcase_camera_snapshot.camera_dragging = _camera_dragging
	_camera_input_enabled = false
	_camera_dragging = false


func _begin_showcase_camera_transition() -> Vector3:
	var target_world_yaw: float = (
		_visuals.global_rotation.y
		+ deg_to_rad(showcase_camera_yaw_offset)
	)
	var target_local_yaw: float = target_world_yaw - global_rotation.y
	var shortest_target_yaw: float = (
		_camera_yaw.rotation.y
		+ wrapf(
			target_local_yaw - _camera_yaw.rotation.y,
			-PI,
			PI
		)
	)
	var target_pitch: float = deg_to_rad(showcase_camera_pitch)
	var target_position: Vector3 = _camera_yaw.position
	target_position.y = showcase_camera_target_height
	var target_zoom: float = maxf(showcase_camera_zoom_distance, 0.1)
	var camera_duration: float = maxf(
		showcase_camera_transition_duration,
		0.05
	)
	_showcase_camera_tween = create_tween()
	_showcase_camera_tween.set_parallel(true)
	_showcase_camera_tween.set_trans(Tween.TRANS_QUAD)
	_showcase_camera_tween.set_ease(Tween.EASE_IN_OUT)
	_showcase_camera_tween.tween_property(
		_camera_yaw,
		"rotation:y",
		shortest_target_yaw,
		camera_duration
	)
	_showcase_camera_tween.tween_property(
		_camera_yaw,
		"position:y",
		target_position.y,
		camera_duration
	)
	_showcase_camera_tween.tween_property(
		_camera_pitch,
		"rotation:x",
		target_pitch,
		camera_duration
	)
	_showcase_camera_tween.tween_property(
		_spring_arm,
		"spring_length",
		target_zoom,
		camera_duration
	)

	var horizontal_offset: Vector3 = (
		Basis(Vector3.UP, target_world_yaw).z * target_zoom
	)
	return global_position + target_position + horizontal_offset


func get_facing_direction() -> Vector3:
	var facing: Vector3 = -_visuals.global_basis.z
	facing.y = 0.0
	if facing.is_zero_approx():
		facing = -global_basis.z
		facing.y = 0.0
	if facing.is_zero_approx():
		return Vector3.FORWARD
	return facing.normalized()
