class_name MapCamera2D
extends Camera2D
## A node that adds mouse, keyboard and gesture zooming, panning and dragging to [Camera2D].

## Zoom speed: multiplies [member Camera2D.zoom] each mouse wheel scroll (set to 1 to disable zooming).
@export_range(0.1, 10) var zoom_factor := 1.25
## Minimum [member Camera2D.zoom].
@export_range(0.01, 100) var zoom_min := 0.1
## Maximum [member Camera2D.zoom].
@export_range(0.01, 100) var zoom_max := 10.0
## If [code]true[/code], [member MapCamera2D.zoom_min] is effectively increased (up to [member MapCamera2D.zoom_max]) to stay within limits.
@export var zoom_limited := true
## If [code]true[/code], mouse zooming is done relative to the cursor (instead of to the center of the screen).
@export var zoom_relative := true
## If [code]true[/code], zooming can also be done with the plus and minus keys.
@export var zoom_keyboard := true

## Pan speed: adds to [member Camera2D.offset] while the cursor is near the viewport's edges (set to 0 to disable panning).
@export_range(0, 10000) var pan_speed := 250.0
## Maximum number of pixels away from the viewport's edges for the cursor to be considered near.
@export_range(0, 1000) var pan_margin := 25.0
## If [code]true[/code], panning can also be done with the arrow keys (and space bar for centering).
@export var pan_keyboard := true

## If [code]true[/code], the map can be dragged while holding the left mouse button.
@export var drag := true
## Slide after dragging: multiplies the final drag movement each second (set to 0 to stop immediately).
@export_range(0, 1) var drag_inertia := 0.1

var _tween_offset
var _tween_zoom
var _pan_direction: set = _set_pan_direction
var _pan_direction_mouse = Vector2.ZERO
var _dragging = false
var _drag_movement = Vector2()

@onready var _target_zoom = zoom

func _ready():
	_pan_direction = Vector2.ZERO
	
	change_zoom()
	
	get_viewport().size_changed.connect(change_zoom)

func _process(delta):
	if _drag_movement == Vector2.ZERO:
		clamp_offset(_pan_direction * pan_speed * delta / zoom)
	else:
		_drag_movement *= drag_inertia ** delta
		
		clamp_offset(-_drag_movement / zoom)
		
		if _drag_movement.length_squared() < 0.01:
			set_process(false)
			set_physics_process(false)

func _physics_process(delta):
	_process(delta)

func _unhandled_input(event):
	if event is InputEventMagnifyGesture:
		change_zoom(1 + ((zoom_factor if zoom_factor > 1 else 1 / zoom_factor) - 1) * (event.factor - 1) * 2.5)
	elif event is InputEventPanGesture:
		change_zoom(1 + (1 / zoom_factor - 1) * event.delta.y / 7.5)
	elif event is InputEventMouseButton:
		if event.pressed:
			match event.button_index:
				MOUSE_BUTTON_WHEEL_UP:
					change_zoom(zoom_factor)
				MOUSE_BUTTON_WHEEL_DOWN:
					change_zoom(1 / zoom_factor)
				MOUSE_BUTTON_LEFT:
					if drag:
						Input.set_default_cursor_shape(Input.CURSOR_DRAG) # delete to disable drag cursor
						
						_dragging = true
						_drag_movement = Vector2()
						
						set_process(false)
						set_physics_process(false)
		elif event.button_index == MOUSE_BUTTON_LEFT and _dragging:
			Input.set_default_cursor_shape(Input.CURSOR_ARROW)
			
			_dragging = false
			
			if _drag_movement != Vector2.ZERO || _pan_direction != Vector2.ZERO:
				set_process(process_callback == CAMERA2D_PROCESS_IDLE)
				set_physics_process(process_callback == CAMERA2D_PROCESS_PHYSICS)
	elif event is InputEventMouseMotion:
		if _pan_direction_mouse != Vector2.ZERO:
			_pan_direction -= _pan_direction_mouse
		
		_pan_direction_mouse = Vector2()
		
		if _dragging:
			if _tween_offset != null:
				_tween_offset.kill()
			
			clamp_offset(-event.relative / zoom)
			
			_drag_movement = event.relative
		elif pan_margin > 0:
			var camera_size = get_viewport_rect().size
			
			if event.position.x < pan_margin:
				_pan_direction_mouse.x -= 1
			
			if event.position.x >= camera_size.x - pan_margin:
				_pan_direction_mouse.x += 1
			
			if event.position.y < pan_margin:
				_pan_direction_mouse.y -= 1
			
			if event.position.y >= camera_size.y - pan_margin:
				_pan_direction_mouse.y += 1
		
		if _pan_direction_mouse != Vector2.ZERO:
			_pan_direction += _pan_direction_mouse
	elif event is InputEventKey:
		if zoom_keyboard && event.pressed:
			match event.keycode:
				KEY_MINUS:
					change_zoom(zoom_factor if zoom_factor < 1 else 1 / zoom_factor, false)
				KEY_EQUAL:
					change_zoom(zoom_factor if zoom_factor > 1 else 1 / zoom_factor, false)
		
		if pan_keyboard && !event.echo:
			match event.keycode:
				KEY_LEFT:
					_pan_direction -= Vector2(1 if event.pressed else -1, 0)
				KEY_RIGHT:
					_pan_direction += Vector2(1 if event.pressed else -1, 0)
				KEY_UP:
					_pan_direction -= Vector2(0, 1 if event.pressed else -1)
				KEY_DOWN:
					_pan_direction += Vector2(0, 1 if event.pressed else -1)
				KEY_SPACE: # delete to disable keyboard centering
					if event.pressed:
						if _tween_offset != null:
							_tween_offset.kill()
						
						offset = Vector2.ZERO

func _set_pan_direction(value):
	_pan_direction = value
	
	if _pan_direction == Vector2.ZERO || _dragging:
		set_process(false)
		set_physics_process(false)
	elif pan_speed > 0:
		set_process(process_callback == CAMERA2D_PROCESS_IDLE)
		set_physics_process(process_callback == CAMERA2D_PROCESS_PHYSICS)
		
		_drag_movement = Vector2()
		
		if _tween_offset != null:
			_tween_offset.kill()

## After changing the node's global position, set [code]offset = offset[/code] then call this to stay within limits.
func clamp_offset(relative := Vector2()):
	var camera_size = get_viewport_rect().size / zoom
	var camera_rect = Rect2(get_screen_center_position() + relative - camera_size / 2, camera_size)
	
	if camera_rect.position.x < limit_left:
		_drag_movement.x = 0
		relative.x += limit_left - camera_rect.position.x
		camera_rect.end.x += limit_left - camera_rect.position.x
	
	if camera_rect.end.x > limit_right:
		_drag_movement.x = 0
		relative.x -= camera_rect.end.x - limit_right
	
	if camera_rect.end.y > limit_bottom:
		_drag_movement.y = 0
		relative.y -= camera_rect.end.y - limit_bottom
		camera_rect.position.y -= camera_rect.end.y - limit_bottom
	
	if camera_rect.position.y < limit_top:
		_drag_movement.y = 0
		relative.y += limit_top - camera_rect.position.y
	
	if relative != Vector2.ZERO:
		offset += relative

## After changing the node's limits, call this without arguments to stay within limits.
func change_zoom(factor = null, with_cursor = true):
	var limited_zoom_min = zoom_min
	
	if zoom_limited:
		var min_zoom_within_limits = get_viewport_rect().size / Vector2(limit_right - limit_left, limit_bottom - limit_top)
		
		limited_zoom_min = min(max(zoom_min, min_zoom_within_limits.x, min_zoom_within_limits.y), zoom_max)
	
	if factor != null:
		if factor < 1:
			if _target_zoom.x < limited_zoom_min || is_equal_approx(_target_zoom.x, limited_zoom_min):
				return
			
			if _target_zoom.y < limited_zoom_min || is_equal_approx(_target_zoom.y, limited_zoom_min):
				return
		elif factor > 1:
			if _target_zoom.x > zoom_max || is_equal_approx(_target_zoom.x, zoom_max):
				return
			
			if _target_zoom.y > zoom_max || is_equal_approx(_target_zoom.y, zoom_max):
				return
		else:
			return
		
		_target_zoom *= factor
	
	var clamped_zoom = _target_zoom
	
	clamped_zoom *= max(1, limited_zoom_min / _target_zoom.x, limited_zoom_min / _target_zoom.y)
	clamped_zoom *= min(1, zoom_max / _target_zoom.x, zoom_max / _target_zoom.y)
	
	if factor == null:
		_set_zoom_level(clamped_zoom)
		
		_target_zoom = zoom
	elif position_smoothing_enabled && position_smoothing_speed > 0:
		if zoom_relative && with_cursor && !is_processing() && !is_physics_processing():
			var relative_position = get_global_mouse_position() - global_position - offset
			var relative = relative_position - relative_position * zoom / clamped_zoom
			
			if _tween_offset != null:
				_tween_offset.kill()
			
			_tween_offset = create_tween().set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT).set_process_mode(process_callback as Tween.TweenProcessMode)
			_tween_offset.tween_property(self, 'offset', offset + relative, 2.5 / position_smoothing_speed)
		
		if _tween_zoom != null:
			_tween_zoom.kill()
		
		_tween_zoom = create_tween().set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT).set_process_mode(process_callback as Tween.TweenProcessMode)
		_tween_zoom.tween_method(func(value): _set_zoom_level(Vector2.ONE / value), Vector2.ONE / zoom, Vector2.ONE / clamped_zoom, 2.5 / position_smoothing_speed)
	else:
		if zoom_relative && with_cursor:
			var relative_position = get_global_mouse_position() - global_position - offset
			var relative = relative_position - relative_position * zoom / clamped_zoom
			
			zoom = clamped_zoom
			
			clamp_offset(relative)
		else:
			_set_zoom_level(clamped_zoom)

func _set_zoom_level(value):
	zoom = value
	
	clamp_offset()
