class_name PlayerInputSource
extends RefCounted

## Converts remappable Godot Input Map actions into immutable [MoveIntent] values.
## This adapter owns no movement state and may be replaced independently of the
## local movement controller.

const RENDER_SANDBOX_PROFILE_ID := &"RenderSandbox"
const BLIZZLIKE_335_PROFILE_ID := &"Blizzlike335"

var input_profile_id: StringName:
	get:
		return _input_profile_id

var _input_profile_id: StringName


## Selects one supported action layout. Unknown profiles fail closed to Blizzlike335.
func _init(requested_profile_id: StringName = BLIZZLIKE_335_PROFILE_ID) -> void:
	_input_profile_id = (
		RENDER_SANDBOX_PROFILE_ID
		if requested_profile_id == RENDER_SANDBOX_PROFILE_ID
		else BLIZZLIKE_335_PROFILE_ID
	)


## Samples the current Input singleton state for one physics tick.
func sample_move_intent() -> MoveIntent:
	if _input_profile_id == RENDER_SANDBOX_PROFILE_ID:
		return compose_move_intent(
			Input.get_action_strength(PlayerInputActions.MOVE_FORWARD),
			Input.get_action_strength(PlayerInputActions.MOVE_BACKWARD),
			Input.get_action_strength(PlayerInputActions.STRAFE_LEFT),
			Input.get_action_strength(PlayerInputActions.STRAFE_RIGHT),
			Input.get_action_strength(PlayerInputActions.DEBUG_FLY_UP),
			Input.get_action_strength(PlayerInputActions.DEBUG_FLY_DOWN),
			Input.is_action_pressed(PlayerInputActions.DEBUG_SPRINT)
		)
	return compose_blizzlike_335_move_intent(
		Input.get_action_strength(PlayerInputActions.MOVE_FORWARD),
		Input.get_action_strength(PlayerInputActions.MOVE_BACKWARD),
		Input.get_action_strength(PlayerInputActions.BLIZZLIKE_STRAFE_LEFT),
		Input.get_action_strength(PlayerInputActions.BLIZZLIKE_STRAFE_RIGHT),
		Input.get_action_strength(PlayerInputActions.TURN_LEFT),
		Input.get_action_strength(PlayerInputActions.TURN_RIGHT),
		Input.is_action_pressed(PlayerInputActions.CAMERA_ROTATE)
	)


## Builds the compatibility layout: Q/E always strafe; A/D turn unless
## right-mouse camera rotation is active, when A/D also strafe. Default bindings
## come from build 12340; conditional routing is reference-implementation evidence.
static func compose_blizzlike_335_move_intent(
		forward_strength: float,
		backward_strength: float,
		strafe_left_strength: float,
		strafe_right_strength: float,
		turn_left_strength: float,
		turn_right_strength: float,
		camera_rotate_active: bool
) -> MoveIntent:
	var effective_strafe_left := strafe_left_strength
	var effective_strafe_right := strafe_right_strength
	var turn_axis := clampf(turn_right_strength, 0.0, 1.0) - clampf(turn_left_strength, 0.0, 1.0)
	if camera_rotate_active:
		effective_strafe_left += turn_left_strength
		effective_strafe_right += turn_right_strength
		turn_axis = 0.0
	var move_intent := compose_move_intent(
		forward_strength,
		backward_strength,
		effective_strafe_left,
		effective_strafe_right,
		0.0,
		0.0,
		false
	)
	return MoveIntent.new(
		move_intent.forward_axis,
		move_intent.strafe_axis,
		0.0,
		false,
		turn_axis
	)


## Builds a normalized sandbox intent from action strengths for deterministic tests and
## alternative input adapters. Strengths outside `[0, 1]` are clamped.
static func compose_move_intent(
		forward_strength: float,
		backward_strength: float,
		strafe_left_strength: float,
		strafe_right_strength: float,
		fly_up_strength: float,
		fly_down_strength: float,
		sprint_requested: bool
) -> MoveIntent:
	var planar_axis := Vector2(
		clampf(strafe_right_strength, 0.0, 1.0) - clampf(strafe_left_strength, 0.0, 1.0),
		clampf(forward_strength, 0.0, 1.0) - clampf(backward_strength, 0.0, 1.0)
	)
	if planar_axis.length_squared() > 1.0:
		planar_axis = planar_axis.normalized()
	var vertical_axis := clampf(fly_up_strength, 0.0, 1.0) - clampf(fly_down_strength, 0.0, 1.0)
	return MoveIntent.new(
		planar_axis.y,
		planar_axis.x,
		vertical_axis,
		sprint_requested
	)
