extends RefCounted

## NEO-22 WASD wall scrape / floor seam tick (extracted from [code]player.gd[/code] for
## [kbd]gdlint[/kbd] [code]max-file-lines[/code]). Owns coyote + scrape feet state; calls back into
## the [CharacterBody3D] player for physics and walk helpers.

const LOCOMOTION_FLOOR_COYOTE_TICKS: int = 8
const LOCOMOTION_WALL_SCRAPE_PROBE_HOLD_VY_MAX: float = -1.05
const LOCOMOTION_WALL_SCRAPE_SNAP_FALLBACK: float = 0.12
const LOCOMOTION_SCRAPE_FEET_LOCK_LERP: float = 0.2
const LOCOMOTION_SCRAPE_FEET_CORRECT_MAX: float = 0.008
const LOCOMOTION_SCRAPE_FEET_ERR_GATE: float = 0.075
const LOCOMOTION_SCRAPE_RAY_FEET_ABSORB_MAX: float = 0.1
const LOCOMOTION_SCRAPE_RAY_PROBE_Y_LERP: float = 0.42
const LOCOMOTION_SCRAPE_RAY_FEET_MEDIAN_SPAN: float = 0.055
const LOCOMOTION_SCRAPE_WISH_LINE_MICRO_PERP_M: float = 0.032
## Sentinel when no walkable floor hit (tests compare to this).
const FLOOR_RAY_FEET_INVALID: float = -1.0e9
## Must match [code]player.gd[/code] walk continuation probe (same ray math as
## [method _walk_ray_hit_up_floor]).
const _WALK_SUPPORT_PROBE_DEPTH: float = 0.32
const _WALK_SUPPORT_PROBE_MIN_UP_DOT: float = 0.42
const _WALK_CONTINUATION_MAX_BELOW_FEET: float = 0.108

var _player: CharacterBody3D
var _cap_half: float
var _cap_radius: float
var _move_speed: float
var _floor_snap_moving: float

var _floor_coyote: int = 0
var _scrape_feet_lock_valid: bool = false
var _scrape_feet_y_lock: float = 0.0
var _scrape_probe_y_ema_valid: bool = false
var _scrape_probe_y_ema: float = 0.0


## Pure: walk continuation rules after a downward feet probe hit (same thresholds as
## [method _floor_ray_feet_y]).
static func evaluate_floor_ray_hit_y(
	feet_y: float,
	hit_y: float,
	normal: Vector3,
	continuation_max_below_feet: float,
	min_floor_up_dot: float
) -> float:
	if hit_y < feet_y - continuation_max_below_feet:
		return FLOOR_RAY_FEET_INVALID
	if normal.dot(Vector3.UP) <= min_floor_up_dot:
		return FLOOR_RAY_FEET_INVALID
	return hit_y


## Pure: median of one or more feet Y samples (sorted); empty → [constant FLOOR_RAY_FEET_INVALID].
static func median_feet_y_from_samples(samples: Array[float]) -> float:
	if samples.is_empty():
		return FLOOR_RAY_FEET_INVALID
	var y_vals: Array[float] = samples.duplicate()
	y_vals.sort()
	var mid: int = (y_vals.size() - 1) >> 1
	return y_vals[mid]


## Pure: XZ after micro-slip projection onto the wish line (or unchanged if out of band).
static func xz_after_micro_slip_projection(
	anchor_xz: Vector2, player_xz: Vector2, wish_xz: Vector2, perp_limit_m: float
) -> Vector2:
	var w2s: float = wish_xz.length_squared()
	if w2s < 1e-12:
		return player_xz
	var w2n: Vector2 = wish_xz / sqrt(w2s)
	var along: float = (player_xz - anchor_xz).dot(w2n)
	var p2_on: Vector2 = anchor_xz + w2n * along
	var perp_len: float = (player_xz - p2_on).length()
	if perp_len > perp_limit_m or perp_len <= 1e-7:
		return player_xz
	return Vector2(p2_on.x, p2_on.y)


## Pure: non-negative wish-aligned horizontal speed, capped at [param move_speed].
static func horizontal_velocity_aligned_to_wish(
	wish_xz: Vector2, vel_xz: Vector2, move_speed: float
) -> Vector2:
	if wish_xz.length_squared() < 1e-10:
		return vel_xz
	var w2: Vector2 = wish_xz.normalized()
	var along: float = vel_xz.dot(w2)
	if along < 0.0:
		along = 0.0
	along = minf(move_speed, along)
	return Vector2(w2.x * along, w2.y * along)


func _init(
	player: CharacterBody3D,
	cap_half: float,
	cap_radius: float,
	move_speed: float,
	floor_snap_moving: float
) -> void:
	_player = player
	_cap_half = cap_half
	_cap_radius = cap_radius
	_move_speed = move_speed
	_floor_snap_moving = floor_snap_moving


func reset_scrape_feet_lock() -> void:
	_scrape_feet_lock_valid = false
	_scrape_probe_y_ema_valid = false


func reset_for_nav_or_snap() -> void:
	_floor_coyote = 0
	reset_scrape_feet_lock()


func _feet_y() -> float:
	return _player.global_position.y - _cap_half - _cap_radius


func _has_scrape_vertical_contact() -> bool:
	if _player.is_on_wall():
		return true
	for i: int in _player.get_slide_collision_count():
		var n: Vector3 = _player.get_slide_collision(i).get_normal()
		if n.y < 0.48 and n.y > -0.22:
			return true
	return false


func _project_global_xz_on_wish_line_if_micro_slip(anchor_xz: Vector2, wish_xz: Vector2) -> void:
	var p2: Vector2 = Vector2(_player.global_position.x, _player.global_position.z)
	var new_xz: Vector2 = xz_after_micro_slip_projection(
		anchor_xz, p2, wish_xz, LOCOMOTION_SCRAPE_WISH_LINE_MICRO_PERP_M
	)
	if new_xz.is_equal_approx(p2):
		return
	_player.global_position.x = new_xz.x
	_player.global_position.z = new_xz.y


func _align_velocity_xz_to_wish() -> void:
	var wish: Vector3 = _player._locomotion_wish_world_xz
	var w2 := Vector2(wish.x, wish.z)
	var v2 := Vector2(_player.velocity.x, _player.velocity.z)
	var out: Vector2 = horizontal_velocity_aligned_to_wish(w2, v2, _move_speed)
	_player.velocity.x = out.x
	_player.velocity.z = out.y


func _floor_ray_feet_y(
	space: PhysicsDirectSpaceState3D, anchor_feet_xz: Vector2, off_xz: Vector2
) -> float:
	var feet_y: float = _player.global_position.y - _cap_half - _cap_radius
	var feet_x: float = anchor_feet_xz.x + off_xz.x
	var feet_z: float = anchor_feet_xz.y + off_xz.y
	var probe_from := Vector3(feet_x, feet_y + 0.04, feet_z)
	var probe_end := Vector3(feet_x, feet_y - _WALK_SUPPORT_PROBE_DEPTH, feet_z)
	var q := PhysicsRayQueryParameters3D.create(probe_from, probe_end)
	q.collision_mask = _player.collision_mask
	q.exclude = [_player.get_rid()]
	var res: Dictionary = space.intersect_ray(q)
	if res.is_empty() or not res.has("normal") or not res.has("position"):
		return FLOOR_RAY_FEET_INVALID
	var hit_y: float = (res["position"] as Vector3).y
	var n: Vector3 = res["normal"]
	return evaluate_floor_ray_hit_y(
		feet_y, hit_y, n, _WALK_CONTINUATION_MAX_BELOW_FEET, _WALK_SUPPORT_PROBE_MIN_UP_DOT
	)


func _floor_ray_feet_y_median(
	space: PhysicsDirectSpaceState3D, anchor_feet_xz: Vector2, wish_probe: Vector2
) -> float:
	var offs: Array[Vector2] = [Vector2.ZERO]
	var ws2: float = wish_probe.length_squared()
	if ws2 > 1e-10:
		var wp: Vector2 = wish_probe / sqrt(ws2)
		var perp: Vector2 = Vector2(-wp.y, wp.x) * LOCOMOTION_SCRAPE_RAY_FEET_MEDIAN_SPAN
		offs.append(perp)
		offs.append(-perp)
	var y_vals: Array[float] = []
	for k: int in range(offs.size()):
		var y_one: float = _floor_ray_feet_y(space, anchor_feet_xz, offs[k])
		if y_one != FLOOR_RAY_FEET_INVALID:
			y_vals.append(y_one)
	return median_feet_y_from_samples(y_vals)


func run_wasd(delta: float) -> void:
	if _player._has_walk_goal:
		_player.clear_nav_goal()
	_player._idle_anchor_active = false
	_player.floor_block_on_wall = true
	var scrape_ray_anchor_feet_xz := Vector2(_player.global_position.x, _player.global_position.z)
	var feet_y: float = _feet_y()
	var wish: Vector3 = _player._locomotion_wish_world_xz
	_player.velocity.x = wish.x * _move_speed
	_player.velocity.z = wish.z * _move_speed
	var wish_probe: Vector2 = Vector2(wish.x, wish.z)
	var wish_probe_active: bool = wish_probe.length_squared() > 1e-10
	var locomotion_probe_floor: bool = (
		wish_probe_active and _player._walk_has_close_floor_probe_below(wish_probe)
	)
	# Slide normals reflect the **previous** tick until [method CharacterBody3D.move_and_slide] runs.
	var scrape_vertical_pre_slide: bool = _has_scrape_vertical_contact()
	var scrape_probe_hold: bool = (
		locomotion_probe_floor
		and scrape_vertical_pre_slide
		and _player.velocity.y >= LOCOMOTION_WALL_SCRAPE_PROBE_HOLD_VY_MAX
	)
	if _player.is_on_floor():
		_floor_coyote = LOCOMOTION_FLOOR_COYOTE_TICKS
	elif scrape_probe_hold:
		_floor_coyote = LOCOMOTION_FLOOR_COYOTE_TICKS
	else:
		_floor_coyote = maxi(0, _floor_coyote - 1)
	var locomotion_grounded_y: bool = _player.is_on_floor() or _floor_coyote > 0
	var wish_active_scrape_vertical_pre: bool = wish_probe_active and scrape_vertical_pre_slide
	var scrape_micro_slip_xz: bool = locomotion_grounded_y and wish_active_scrape_vertical_pre
	if locomotion_grounded_y:
		_player.velocity.y = 0.0
	else:
		_player._apply_walk_air_gravity(delta, feet_y, Vector2.ZERO)
	_player._walk_clip_horizontal_velocity_against_vertical_contacts(wish)
	_player.floor_snap_length = _floor_snap_moving
	if locomotion_grounded_y and wish_active_scrape_vertical_pre:
		if locomotion_probe_floor:
			_player.floor_snap_length = 0.0
		else:
			_player.floor_snap_length = minf(
				_floor_snap_moving, LOCOMOTION_WALL_SCRAPE_SNAP_FALLBACK
			)
	_player.move_and_slide()
	if scrape_micro_slip_xz:
		_project_global_xz_on_wish_line_if_micro_slip(scrape_ray_anchor_feet_xz, wish_probe)
	var feet_after: float = _feet_y()
	var scrape_vertical_post_slide: bool = _has_scrape_vertical_contact()
	var on_floor_post_slide: bool = _player.is_on_floor()
	var post_slide_wish_floor_scrape: bool = (
		wish_probe_active and on_floor_post_slide and scrape_vertical_post_slide
	)
	if post_slide_wish_floor_scrape:
		var ray_absorbed: bool = false
		if locomotion_probe_floor:
			var w3d := _player.get_world_3d()
			if w3d != null:
				var probe_feet_y: float = _floor_ray_feet_y_median(
					w3d.direct_space_state, scrape_ray_anchor_feet_xz, wish_probe
				)
				if probe_feet_y != FLOOR_RAY_FEET_INVALID:
					var raw_absorb_dy: float = probe_feet_y - feet_after
					var absorb_dy: float = raw_absorb_dy
					if absf(raw_absorb_dy) > LOCOMOTION_SCRAPE_RAY_FEET_ABSORB_MAX:
						_scrape_probe_y_ema_valid = false
					else:
						if _scrape_probe_y_ema_valid:
							_scrape_probe_y_ema = lerpf(
								_scrape_probe_y_ema,
								probe_feet_y,
								LOCOMOTION_SCRAPE_RAY_PROBE_Y_LERP
							)
						else:
							_scrape_probe_y_ema = probe_feet_y
							_scrape_probe_y_ema_valid = true
						absorb_dy = _scrape_probe_y_ema - feet_after
					if (
						absf(absorb_dy) <= LOCOMOTION_SCRAPE_RAY_FEET_ABSORB_MAX
						and absf(absorb_dy) > 1e-5
					):
						_player.global_position.y += absorb_dy
						feet_after = _feet_y()
						_scrape_feet_y_lock = feet_after
						_scrape_feet_lock_valid = true
						ray_absorbed = true
		if not ray_absorbed:
			if not _scrape_feet_lock_valid:
				_scrape_feet_y_lock = feet_after
				_scrape_feet_lock_valid = true
			else:
				if absf(_scrape_feet_y_lock - feet_after) > LOCOMOTION_SCRAPE_FEET_ERR_GATE:
					_scrape_feet_y_lock = feet_after
				else:
					_scrape_feet_y_lock = lerpf(
						_scrape_feet_y_lock, feet_after, LOCOMOTION_SCRAPE_FEET_LOCK_LERP
					)
				var pull_feet: float = _scrape_feet_y_lock - feet_after
				var step_y: float = clampf(
					pull_feet,
					-LOCOMOTION_SCRAPE_FEET_CORRECT_MAX,
					LOCOMOTION_SCRAPE_FEET_CORRECT_MAX
				)
				if absf(step_y) > 1e-6:
					_player.global_position.y += step_y
		if scrape_micro_slip_xz:
			_project_global_xz_on_wish_line_if_micro_slip(scrape_ray_anchor_feet_xz, wish_probe)
	else:
		reset_scrape_feet_lock()
	if post_slide_wish_floor_scrape:
		if locomotion_probe_floor:
			_align_velocity_xz_to_wish()
		if locomotion_grounded_y:
			_player.velocity.y = 0.0
	_player._snap_capsule_upright()
	_player._debug_trace_transform("physics")
