class_name CameraController
extends Camera2D

## Whether to use pixel snap.
@export var pixel_snap: bool = true
## Whether camera movement should be smooth.
@export var enable_smoothing: bool = true

## The current target being followed.
var _active_target: CameraTarget

var _previous_pixel_snap_delta := Vector2.ZERO
## The current camera velocity, for smooth damping.
var _current_velocity := Vector2.ZERO
## The current exact position of the camera.
var _current_position: Vector2


func _ready() -> void:
	# Add to group so we can look it up later.
	add_to_group("camera_controllers")

## Critically damped spring, based on Game Programming Gems 4 Chapter 1.10. https://archive.org/details/game-programming-gems-4/page/95/mode/2up
## Returns a 2-tuple of [next_position, next_velocity].
func smooth_damp(current: float, target: float, current_velocity: float, smooth_time: float, max_speed: float, delta: float) -> Array[float]:
	smooth_time = max(smooth_time, 0.0001)
	var omega := 2.0 / smooth_time

	var x := omega * delta
	var x_exp := 1.0 / (1.0 + x + 0.48 * x * x + 0.235 * x * x * x)
	var change := current - target
	var original_target := target

	# Clamp max speed.
	var max_change := max_speed * smooth_time
	change = clamp(change, -max_change, max_change)
	target = current - change

	var temp := (current_velocity + omega * change) * delta
	current_velocity = (current_velocity - omega * temp) * x_exp
	var output := target + (change + temp) * x_exp

	# Prevent overshooting.
	if (original_target - current > 0.0) == (output > original_target):
		output = original_target
		current_velocity = (output - original_target) / delta

	return [output, current_velocity]

func _unhandled_input(_event: InputEvent) -> void:
	if Input.is_action_just_pressed("debug_camera_1"):
		pixel_snap = not pixel_snap
		print("Camera pixel snap: ", pixel_snap)
	if Input.is_action_just_pressed("debug_camera_2"):
		enable_smoothing = not enable_smoothing
		print("Camera smoothing: ", enable_smoothing)

# It's important that the camera position gets updated in _process instead of _physics_process,
# since it needs to be dependent on frame rate.
func _process(delta: float) -> void:
	# Update position.
	var next_position: Vector2
	var target: Vector2 = _active_target.global_position

	if enable_smoothing:
		# Handle target movement with smooth_damp.
		# Replace this with any smooth follow / lerp of your choosing, but be careful to use `delta`
		# properly -- improper use can result in jitter. Don't do lerp(current, target, delta).
		var res_x := smooth_damp(_current_position.x, target.x, _current_velocity.x, 0.2, INF, delta)
		var res_y := smooth_damp(_current_position.y, target.y, _current_velocity.y, 0.2, INF, delta)
		next_position.x = res_x[0]
		next_position.y = res_y[0]
		_current_velocity.x = res_x[1]
		_current_velocity.y = res_y[1]
	else:
		# Set next camera position to the exact target.
		next_position = target

	_current_position = next_position

	if pixel_snap:
		# IMPORTANT: perform pixel snap so that the camera movement doesn't interfere with the
		# sub-pixel "smooth movement" we do in the shader.
		var snapped_position = (next_position + Vector2(0.5, 0.5)).floor()
		_previous_pixel_snap_delta = snapped_position - next_position
		next_position = snapped_position
	else:
		_previous_pixel_snap_delta = Vector2.ZERO

	# Set the camera position.
	global_position = next_position
	# IMPORTANT: Work around godot bug where camera doesn't update immediately: https://github.com/godotengine/godot/issues/74203
	force_update_scroll()

## Returns the position delta between the pixel-snapped position and the actual controlled camera position.
## If pixel snap is off, returns Zero.
func get_pixel_snap_delta() -> Vector2:
	return _previous_pixel_snap_delta

## Registers a camera target to follow.
func register_target(target: CameraTarget) -> void:
	assert(not _active_target)
	_active_target = target
	_current_position = _active_target.global_position
