extends CharacterBody3D

const COORDINATE_MAPPER_SCRIPT := preload("res://src/domain/coordinates/coordinate_mapper.gd")
const GODOT_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/godot_world_position.gd")
const ADT_TILE_COORDINATE_SCRIPT := preload("res://src/domain/coordinates/adt_tile_coordinate.gd")
const ADT_TILE_LOCAL_POSITION_SCRIPT := preload("res://src/domain/coordinates/adt_tile_local_position.gd")
const PLAYER_INPUT_SOURCE_SCRIPT := preload("res://src/gameplay/input/player_input_source.gd")
const PLAYER_INPUT_ACTIONS_SCRIPT := preload("res://src/gameplay/input/player_input_actions.gd")
const LOCAL_PLAYER_MOVEMENT_CONTROLLER_SCRIPT := preload("res://src/gameplay/movement/local_player_movement_controller.gd")
const PLAYER_MOVEMENT_CAPABILITIES_SCRIPT := preload("res://src/gameplay/movement/player_movement_capabilities.gd")
const ADT_TERRAIN_QUERY_SCRIPT := preload("res://src/gameplay/terrain/adt_terrain_query.gd")

@export var extracted_dir: String = "res://data/extracted"
@export var map_name: String = "Azeroth"
@export var spawn_tile_x: int = 42
@export var spawn_tile_y: int = 28
@export var spawn_at_tile_center: bool = true

@export var run_speed: float = 7.0
@export var backward_speed: float = 4.5
@export var strafe_speed: float = 4.5
@export var sprint_multiplier: float = 6.0
@export var flight_vertical_speed: float = 7.0
@export var acceleration: float = 28.0
## TrinityCore 3.3.5 player MOVE_TURN_RATE default, in radians per second.
@export var keyboard_turn_speed_radians_per_second: float = 3.141594
## Local composition profile. RenderSandbox preserves debug sprint/free flight;
## Blizzlike335 rejects them but does not yet claim complete movement parity.
@export_enum("RenderSandbox", "Blizzlike335") var movement_profile_id: String = "RenderSandbox"
@export var ground_offset: float = 0.05
@export var ground_snap_speed: float = 24.0

@export var camera_pivot_path: NodePath = NodePath("CameraPivot")
@export var visual_path: NodePath = NodePath("Visual")
@export var animation_presenter_path: NodePath = NodePath("Visual/AnimationPresenter")

var _camera_rig: ThirdPersonCameraRig
var _appearance_presenter: CharacterAppearancePresenter
var _animation_presenter: CharacterAnimationPresenter
var _player_input_source: PlayerInputSource
var _local_movement_controller: LocalPlayerMovementController
var _terrain_query: TerrainQuery


## Replaces the ground-height backend without changing player movement or presentation.
## Passing null is a composition error and leaves the current query unchanged.
func set_terrain_query(terrain_query: TerrainQuery) -> void:
	if terrain_query == null:
		push_error("ThirdPersonWowController: terrain query cannot be null")
		return
	_terrain_query = terrain_query


func _ready() -> void:
	_player_input_source = PLAYER_INPUT_SOURCE_SCRIPT.new(StringName(movement_profile_id))
	if _terrain_query == null:
		_terrain_query = ADT_TERRAIN_QUERY_SCRIPT.new(extracted_dir, map_name)
	_local_movement_controller = LOCAL_PLAYER_MOVEMENT_CONTROLLER_SCRIPT.new(
		run_speed,
		backward_speed,
		strafe_speed,
		flight_vertical_speed,
		acceleration,
		sprint_multiplier,
		PLAYER_MOVEMENT_CAPABILITIES_SCRIPT.for_profile_id(StringName(movement_profile_id)),
		keyboard_turn_speed_radians_per_second
	)
	_camera_rig = get_node_or_null(camera_pivot_path) as ThirdPersonCameraRig
	_appearance_presenter = get_node_or_null(visual_path) as CharacterAppearancePresenter
	_animation_presenter = get_node_or_null(animation_presenter_path) as CharacterAnimationPresenter
	if _camera_rig == null:
		push_error("ThirdPersonWowController: ThirdPersonCameraRig not found at %s" % camera_pivot_path)
	else:
		_camera_rig.initialize_for_character(self)
	if _appearance_presenter == null:
		push_error("ThirdPersonWowController: CharacterAppearancePresenter not found at %s" % visual_path)
	else:
		_appearance_presenter.character_appearance_ready.connect(_on_character_appearance_ready)
		_appearance_presenter.load_character_appearance(extracted_dir)
	if _animation_presenter == null:
		push_error("ThirdPersonWowController: CharacterAnimationPresenter not found at %s" % animation_presenter_path)
	if spawn_at_tile_center:
		var spawn_tile = ADT_TILE_COORDINATE_SCRIPT.new(spawn_tile_x, spawn_tile_y)
		var half_tile_size := COORDINATE_MAPPER_SCRIPT.ADT_TILE_SIZE_YARDS * 0.5
		var spawn_local = ADT_TILE_LOCAL_POSITION_SCRIPT.new(half_tile_size, global_position.y, half_tile_size)
		var spawn_position = COORDINATE_MAPPER_SCRIPT.adt_tile_local_to_godot(spawn_tile, spawn_local)
		global_position = Vector3(spawn_position.x_units, spawn_position.y_units, spawn_position.z_units)
	var spawn_ground_sample := _sample_ground_height(global_position)
	if spawn_ground_sample.is_available:
		global_position.y = spawn_ground_sample.height_units + ground_offset


func _unhandled_input(event: InputEvent) -> void:
	if _camera_rig != null:
		_camera_rig.handle_camera_input(event)
	if event.is_action_pressed(PLAYER_INPUT_ACTIONS_SCRIPT.DEBUG_TOGGLE_FLIGHT) and not event.is_echo():
		_local_movement_controller.toggle_sandbox_flight()


func _physics_process(delta: float) -> void:
	var move_intent := _player_input_source.sample_move_intent()
	var yaw_delta_radians := _local_movement_controller.calculate_yaw_delta_radians(
		move_intent,
		delta
	)
	if not is_zero_approx(yaw_delta_radians):
		rotation.y += yaw_delta_radians
		if _camera_rig != null:
			_camera_rig.synchronize_yaw_from_character()
	var godot_world_movement_basis := global_basis
	if _local_movement_controller.is_flight_enabled and _camera_rig != null:
		godot_world_movement_basis = _camera_rig.godot_world_flight_movement_basis()
	global_position += _local_movement_controller.advance(move_intent, godot_world_movement_basis, delta)

	if not _local_movement_controller.is_flight_enabled:
		var ground_sample := _sample_ground_height(global_position)
		if ground_sample.is_available:
			var target_y := ground_sample.height_units + ground_offset
			global_position.y = lerpf(global_position.y, target_y, clampf(ground_snap_speed * delta, 0.0, 1.0))

	var movement_velocity := _local_movement_controller.godot_world_velocity_units_per_second
	var horizontal_motion := Vector2(movement_velocity.x, movement_velocity.z)
	if _appearance_presenter != null and horizontal_motion.length_squared() > 0.01:
		_appearance_presenter.global_rotation.y = atan2(-movement_velocity.x, -movement_velocity.z)

	if _animation_presenter != null:
		_animation_presenter.present_locomotion(horizontal_motion.length_squared() > 0.04)


func _on_character_appearance_ready(character_root: Node3D) -> void:
	if _animation_presenter != null:
		_animation_presenter.bind_character_root(character_root)


func _sample_ground_height(godot_world_position: Vector3) -> TerrainGroundSample:
	return _terrain_query.sample_ground_height(
		GODOT_WORLD_POSITION_SCRIPT.new(
			godot_world_position.x,
			godot_world_position.y,
			godot_world_position.z
		)
	)
