extends CharacterBody3D

# WANDER  → bull outside flee range, roaming freely
# FLEE    → bull within range, matador backs away (raycast-steered around walls)
# BRACE   → charge detected: plant feet, face the bull (cite phase)
# SIDESTEP→ bull commits: sharp lateral step to let it pass (pase phase)
# ATTACK  → post-dodge or mid-range: chase and strike
# ROLL    → hit by bull but roll chance fires: survive and recover
# RAGDOLL → hit by bull, no roll chance
enum State { WANDER, FLEE, BRACE, SIDESTEP, ATTACK, ROLL, RAGDOLL }

signal killed

var _state:         State                   = State.WANDER
var _skeleton:      Skeleton3D              = null
var _sim:           PhysicalBoneSimulator3D = null
var _anim_player:   AnimationPlayer         = null
var _wander_target: Vector3                 = Vector3.ZERO
var _idle_timer:    float                   = 0.0
var _bull:          CharacterBody3D         = null
var _step_dir:      Vector3                 = Vector3.ZERO
var _brace_timer:   float                   = 0.0
var _step_timer:    float                   = 0.0
var _dodge_cd:      float                   = 0.0
var _will_dodge:    bool                    = true
var _attack_timer:  float                   = 0.0
var _roll_timer:    float                   = 0.0
var _roll_dir:      Vector3                 = Vector3.ZERO
var _steer_dir:     Vector3                 = Vector3.ZERO
var _steer_cd:      float                   = 0.0
var _ole_player:    AudioStreamPlayer       = null
var _death_player:  AudioStreamPlayer       = null
var _blood_burst:   CPUParticles3D          = null

@onready var _mesh:      Node3D           = $matador2
@onready var _hit_area:  Area3D           = $HitArea
@onready var _body_col:  CollisionShape3D = $CollisionShape3D

const _ANIM_RUN:    StringName = &"Run"
const _ANIM_IDLE:   StringName = &"Taunt"
const _ANIM_ATTACK: StringName = &"Attack"
const _ANIM_ROLL:   StringName = &"Roll"

# Candidate steering offsets (radians) tried in order when a wall is ahead
const _STEER_ANGLES: Array    = [0.0, -0.35, 0.35, -0.7, 0.7, -1.1, 1.1, PI]
const _STEER_LOOKAHEAD: float = 3.5   # longer = turns before hitting wall
const _STEER_INTERVAL:  float = 0.12  # recompute direction at most ~8×/sec


func _ready() -> void:
	add_to_group(&"matador")
	_skeleton = _find_skeleton(_mesh)
	_anim_player = _find_anim_player(_mesh)
	if _skeleton:
		_sim = _setup_physical_bones()
	if _anim_player:
		for anim in [_ANIM_RUN, _ANIM_IDLE, _ANIM_ATTACK, _ANIM_ROLL]:
			_ensure_loop(anim)
		if not _anim_player.has_animation(_ANIM_RUN):
			push_warning("Matador: expected animations not found. Available: %s" %
				str(_anim_player.get_animation_list()))
		_anim_player.play(_ANIM_IDLE)
	_hit_area.body_entered.connect(_on_body_entered)
	_pick_wander_target()
	_setup_blood_burst()
	if ResourceLoader.exists("res://sounds/ole.ogg"):
		_ole_player = AudioStreamPlayer.new()
		_ole_player.stream = load("res://sounds/ole.ogg")
		_ole_player.volume_db = 0.0
		add_child(_ole_player)
	if ResourceLoader.exists("res://sounds/matador_death.ogg"):
		_death_player = AudioStreamPlayer.new()
		_death_player.stream = load("res://sounds/matador_death.ogg")
		_death_player.volume_db = 2.0
		add_child(_death_player)


func _physics_process(delta: float) -> void:
	if _bull == null:
		var players := get_tree().get_nodes_in_group(&"player")
		if not players.is_empty():
			_bull = players[0] as CharacterBody3D

	if not is_on_floor():
		velocity += get_gravity() * delta

	_dodge_cd = maxf(0.0, _dodge_cd - delta)

	if _state != State.RAGDOLL and _bull != null:
		_update_ai_state()

	match _state:
		State.WANDER:   _tick_wander(delta)
		State.FLEE:     _tick_flee(delta)
		State.BRACE:    _tick_brace(delta)
		State.SIDESTEP: _tick_sidestep(delta)
		State.ATTACK:   _tick_attack(delta)
		State.ROLL:     _tick_roll(delta)
		State.RAGDOLL:  _tick_ragdoll(delta)


func _update_ai_state() -> void:
	var dist := global_position.distance_to(_bull.global_position)
	match _state:
		State.WANDER:
			if dist < DP.f("mat_flee_range"):
				_state = State.FLEE
			elif _dodge_cd <= 0.0 and dist < DP.f("mat_attack_range") and not _is_charge_incoming():
				_steer_cd = 0.0
				_start_attack()
		State.FLEE:
			if _dodge_cd <= 0.0 and (dist < DP.f("mat_commit_dist") or _is_charge_incoming()):
				_start_brace()
			elif dist > DP.f("mat_flee_range") * 1.3:
				_state = State.WANDER
				_steer_cd = 0.0
				_pick_wander_target()
		State.BRACE:
			pass  # brace timer drives transition
		State.SIDESTEP:
			pass  # step timer drives transition
		State.ATTACK:
			if _attack_timer <= 0.0 or dist > DP.f("mat_attack_range") * 1.5:
				_state = State.FLEE
		State.ROLL:
			pass  # roll timer drives transition


# Bull is moving fast and aimed within ~30° of the matador
func _is_charge_incoming() -> bool:
	var bull_flat := Vector3(_bull.velocity.x, 0.0, _bull.velocity.z)
	if bull_flat.length() < DP.f("mat_charge_speed"):
		return false
	var to_me := (global_position - _bull.global_position)
	to_me.y = 0.0
	if to_me.length() > DP.f("mat_brace_range"):
		return false
	return bull_flat.normalized().dot(to_me.normalized()) > 0.85


# Returns a wall-clear direction, recomputed at most every _STEER_INTERVAL seconds.
# Randomises among available clear angles so juke direction is unpredictable.
func _steer_clear(desired_dir: Vector3, delta: float) -> Vector3:
	_steer_cd -= delta
	if _steer_cd > 0.0 and _steer_dir != Vector3.ZERO:
		return _steer_dir
	_steer_cd = _STEER_INTERVAL
	var space := get_world_3d().direct_space_state
	var origin := global_position + Vector3.UP * 0.6
	var params := PhysicsRayQueryParameters3D.new()
	params.exclude = [get_rid()]
	params.collision_mask = 1
	# Try straight ahead first; if blocked, pick randomly from the side candidates.
	params.from = origin
	params.to = origin + desired_dir * _STEER_LOOKAHEAD
	if space.intersect_ray(params).is_empty():
		_steer_dir = desired_dir
		return desired_dir
	var candidates: Array = [-0.35, 0.35, -0.7, 0.7, -1.1, 1.1, PI]
	candidates.shuffle()
	for angle: float in candidates:
		var dir := desired_dir.rotated(Vector3.UP, angle)
		params.from = origin
		params.to = origin + dir * _STEER_LOOKAHEAD
		if space.intersect_ray(params).is_empty():
			_steer_dir = dir
			return dir
	_steer_dir = desired_dir
	return desired_dir


func _tick_wander(delta: float) -> void:
	if _idle_timer > 0.0:
		_idle_timer -= delta
		velocity.x = move_toward(velocity.x, 0.0, 10.0 * delta)
		velocity.z = move_toward(velocity.z, 0.0, 10.0 * delta)
		_play_anim(_ANIM_IDLE)
		move_and_slide()
		return

	var dx: float = _wander_target.x - global_position.x
	var dz: float = _wander_target.z - global_position.z
	if Vector2(dx, dz).length() < 0.8:
		_idle_timer = randf_range(DP.f("mat_idle_min"), DP.f("mat_idle_max"))
		_pick_wander_target()
	else:
		var dir := _steer_clear(Vector3(dx, 0.0, dz).normalized(), delta)
		var spd  := DP.f("mat_walk_speed")
		velocity.x = move_toward(velocity.x, dir.x * spd, spd * 8.0 * delta)
		velocity.z = move_toward(velocity.z, dir.z * spd, spd * 8.0 * delta)
		_mesh.rotation.y = lerp_angle(
			_mesh.rotation.y, atan2(velocity.x, velocity.z), delta * 8.0)
		_play_anim(_ANIM_RUN)
	move_and_slide()


func _tick_flee(delta: float) -> void:
	var away := (global_position - _bull.global_position)
	away.y = 0.0
	if away.length() < 0.01:
		away = Vector3(randf() - 0.5, 0.0, randf() - 0.5)
	var dir := _steer_clear(away.normalized(), delta)
	var spd := DP.f("mat_flee_speed")
	velocity.x = move_toward(velocity.x, dir.x * spd, spd * 8.0 * delta)
	velocity.z = move_toward(velocity.z, dir.z * spd, spd * 8.0 * delta)
	_mesh.rotation.y = lerp_angle(_mesh.rotation.y, atan2(velocity.x, velocity.z), delta * 8.0)
	_play_anim(_ANIM_RUN)
	move_and_slide()


# Cite phase: plant feet, face the bull — wait for it to commit
func _start_brace() -> void:
	_steer_cd = 0.0
	_state = State.BRACE
	var bull_flat := Vector3(_bull.velocity.x, 0.0, _bull.velocity.z)
	var charge_dir: Vector3
	if bull_flat.length() > 1.0:
		charge_dir = bull_flat.normalized()
	else:
		charge_dir = (_bull.global_position - global_position)
		charge_dir.y = 0.0
		charge_dir = charge_dir.normalized()
	_will_dodge = randf() < DP.f("mat_dodge_chance")
	var side := 1.0 if randf() > 0.5 else -1.0
	_step_dir = charge_dir.rotated(Vector3.UP, PI * 0.5 * side)
	_brace_timer = DP.f("mat_brace_duration")


func _tick_brace(delta: float) -> void:
	velocity.x = move_toward(velocity.x, 0.0, 18.0 * delta)
	velocity.z = move_toward(velocity.z, 0.0, 18.0 * delta)
	var to_bull := (_bull.global_position - global_position)
	to_bull.y = 0.0
	if to_bull.length() > 0.1:
		_mesh.rotation.y = lerp_angle(
			_mesh.rotation.y, atan2(to_bull.x, to_bull.z), delta * 14.0)
	_play_anim(_ANIM_ATTACK)
	move_and_slide()

	_brace_timer -= delta
	var dist := global_position.distance_to(_bull.global_position)
	if dist < DP.f("mat_commit_dist") or _brace_timer <= 0.0:
		if _will_dodge:
			_state = State.SIDESTEP
			_step_timer = DP.f("mat_step_duration")
			if _ole_player:
				_ole_player.pitch_scale = randf_range(0.88, 1.12)
				_ole_player.play()
		else:
			_state = State.FLEE
			_dodge_cd = DP.f("mat_dodge_cooldown")


# Pase phase: sharp lateral step, then immediately exploit with an attack
func _tick_sidestep(delta: float) -> void:
	_step_timer -= delta
	if _step_timer <= 0.0:
		_start_attack()
		return
	var spd := DP.f("mat_step_speed")
	velocity.x = _step_dir.x * spd
	velocity.z = _step_dir.z * spd
	_mesh.rotation.y = lerp_angle(_mesh.rotation.y, atan2(velocity.x, velocity.z), delta * 20.0)
	_play_anim(_ANIM_ROLL)
	move_and_slide()


func _start_attack() -> void:
	_state = State.ATTACK
	_attack_timer = DP.f("mat_attack_duration")
	_dodge_cd = DP.f("mat_dodge_cooldown")


func _tick_attack(delta: float) -> void:
	_attack_timer -= delta
	var to_bull := (_bull.global_position - global_position)
	to_bull.y = 0.0
	var dist := to_bull.length()

	if dist > 0.1:
		_mesh.rotation.y = lerp_angle(
			_mesh.rotation.y, atan2(to_bull.x, to_bull.z), delta * 10.0)

	if dist > DP.f("mat_attack_min_dist"):
		var dir := _steer_clear(to_bull.normalized(), delta)
		var spd := DP.f("mat_attack_speed")
		velocity.x = move_toward(velocity.x, dir.x * spd, spd * 8.0 * delta)
		velocity.z = move_toward(velocity.z, dir.z * spd, spd * 8.0 * delta)
	else:
		velocity.x = move_toward(velocity.x, 0.0, 14.0 * delta)
		velocity.z = move_toward(velocity.z, 0.0, 14.0 * delta)

	_play_anim(_ANIM_ATTACK)
	move_and_slide()


func _enter_roll(hit_dir: Vector3) -> void:
	_state = State.ROLL
	_roll_timer = DP.f("mat_roll_duration")
	hit_dir.y = 0.0
	var side := 1.0 if randf() > 0.5 else -1.0
	_roll_dir = hit_dir.normalized().rotated(Vector3.UP, PI * 0.5 * side)


func _tick_roll(delta: float) -> void:
	_roll_timer -= delta
	if _roll_timer <= 0.0:
		_state = State.FLEE
		_dodge_cd = DP.f("mat_dodge_cooldown")
		return
	var spd := DP.f("mat_step_speed")
	velocity.x = _roll_dir.x * spd
	velocity.z = _roll_dir.z * spd
	_mesh.rotation.y = lerp_angle(_mesh.rotation.y, atan2(velocity.x, velocity.z), delta * 20.0)
	_play_anim(_ANIM_ROLL)
	move_and_slide()


func _tick_ragdoll(_delta: float) -> void:
	velocity.x = 0.0
	velocity.z = 0.0
	move_and_slide()


func apply_ability_hit(hit_dir: Vector3, strength: float) -> void:
	if _state == State.RAGDOLL or _state == State.ROLL:
		return
	if _bull == null:
		var players := get_tree().get_nodes_in_group(&"player")
		if not players.is_empty():
			_bull = players[0] as CharacterBody3D
	_enter_ragdoll(hit_dir, strength)


func _on_body_entered(body: Node3D) -> void:
	if _state == State.RAGDOLL or _state == State.ROLL:
		return
	if not body.is_in_group(&"player"):
		return
	var player := body as CharacterBody3D
	if player.velocity.length() < DP.f("mat_hit_threshold"):
		return
	var flat_vel := Vector3(player.velocity.x, 0.0, player.velocity.z)
	var hit_dir  := flat_vel.normalized() if flat_vel.length() > 0.5 else \
		(global_position - player.global_position).normalized()

	if randf() < DP.f("mat_roll_chance"):
		_enter_roll(hit_dir)
	else:
		_enter_ragdoll(hit_dir, player.velocity.length())


func _enter_ragdoll(hit_dir: Vector3, bull_speed: float) -> void:
	_state = State.RAGDOLL
	killed.emit()
	var cleanup_timer := get_tree().create_timer(4.0)
	cleanup_timer.timeout.connect(func() -> void:
		if is_instance_valid(self):
			queue_free()
	)
	hit_dir.y = 0.0
	hit_dir = hit_dir.normalized()
	var throw_dir := (hit_dir + Vector3(0.0, 0.5, 0.0)).normalized()

	_spawn_blood_burst(hit_dir)
	if _death_player:
		_death_player.pitch_scale = randf_range(0.9, 1.1)
		_death_player.play()

	if _bull != null:
		var cam := _bull.get_node_or_null("Camera3D")
		if cam:
			cam.call(&"trigger_hit", clampf(bull_speed / 15.0, 0.4, 1.0))

	velocity = Vector3.ZERO
	collision_layer = 0

	if _anim_player:
		_anim_player.pause()

	if not _sim:
		return
	for child: Node in _sim.get_children():
		if not (child is PhysicalBone3D):
			continue
		var pb := child as PhysicalBone3D
		var bone_idx := _skeleton.find_bone(pb.bone_name)
		if bone_idx >= 0:
			pb.global_transform = _skeleton.global_transform * _skeleton.get_bone_global_pose(bone_idx)
	_sim.active = true
	_sim.physical_bones_start_simulation()
	var strength := DP.f("mat_ragdoll_impulse") * clampf(bull_speed / 15.0, 0.4, 1.3)
	var right := throw_dir.cross(Vector3.UP).normalized()
	for child: Node in _sim.get_children():
		if not (child is PhysicalBone3D):
			continue
		var pb := child as PhysicalBone3D
		var impulse := Vector3.ZERO
		match pb.bone_name:
			"COG", "matador":
				impulse = throw_dir * strength
			"chest":
				impulse = (throw_dir + Vector3.UP * 0.3).normalized() * strength * 0.9
			"head":
				impulse = (throw_dir + Vector3.UP * 0.5).normalized() * strength * 0.7
			"leg_L":
				impulse = (throw_dir * 0.4 + right * 0.6 + Vector3.UP * 0.4).normalized() * strength * 0.6
			"leg_R":
				impulse = (throw_dir * 0.4 - right * 0.6 + Vector3.UP * 0.4).normalized() * strength * 0.6
			"arm_L":
				impulse = (throw_dir * 0.3 + right * 0.8 + Vector3.UP * 0.5).normalized() * strength * 0.5
			"arm_R":
				impulse = (throw_dir * 0.3 - right * 0.8 + Vector3.UP * 0.5).normalized() * strength * 0.5
			_:
				continue
		var noise := Vector3(randf_range(-0.15, 0.15), randf_range(-0.08, 0.15), randf_range(-0.15, 0.15))
		pb.apply_central_impulse(impulse + noise * strength * 0.2)


# ── Blood burst ───────────────────────────────────────────────────────────────

func _setup_blood_burst() -> void:
	var mat := StandardMaterial3D.new()
	mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
	mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
	mat.vertex_color_use_as_albedo = true
	mat.cull_mode = BaseMaterial3D.CULL_DISABLED

	var sphere := SphereMesh.new()
	sphere.radius = 0.07
	sphere.height = 0.14
	sphere.radial_segments = 4
	sphere.rings = 2
	sphere.material = mat

	var ramp := Gradient.new()
	ramp.set_color(0, Color(1.0, 0.02, 0.02, 1.0))
	ramp.set_color(1, Color(0.25, 0.0, 0.0, 0.0))

	var scale_curve := Curve.new()
	scale_curve.add_point(Vector2(0.0, 1.0))
	scale_curve.add_point(Vector2(0.6, 0.5))
	scale_curve.add_point(Vector2(1.0, 0.0))

	_blood_burst = CPUParticles3D.new()
	_blood_burst.mesh               = sphere
	_blood_burst.color_ramp         = ramp
	_blood_burst.scale_amount_curve = scale_curve
	_blood_burst.one_shot           = true
	_blood_burst.explosiveness      = 0.9
	_blood_burst.amount             = 25
	_blood_burst.lifetime           = 1.1
	_blood_burst.randomness         = 0.4
	_blood_burst.local_coords       = false
	_blood_burst.spread             = 80.0
	_blood_burst.gravity            = Vector3(0.0, -7.0, 0.0)
	_blood_burst.initial_velocity_min = 4.0
	_blood_burst.initial_velocity_max = 11.0
	_blood_burst.scale_amount_min   = 0.5
	_blood_burst.scale_amount_max   = 1.6
	_blood_burst.emitting           = false
	add_child(_blood_burst)


func _spawn_blood_burst(hit_dir: Vector3) -> void:
	_blood_burst.global_position = global_position + Vector3(0.0, 0.9, 0.0)
	_blood_burst.direction = (hit_dir * 0.6 + Vector3.UP * 0.4).normalized()
	_blood_burst.restart()


# ── Animation helpers ─────────────────────────────────────────────────────────

func _ensure_loop(anim_name: StringName) -> void:
	if _anim_player.has_animation(anim_name):
		var anim := _anim_player.get_animation(anim_name)
		anim.loop_mode = Animation.LOOP_LINEAR


func _play_anim(anim_name: StringName) -> void:
	if _anim_player == null:
		return
	if _anim_player.current_animation == anim_name:
		return
	_anim_player.play(anim_name)


func _pick_wander_target() -> void:
	# 35% chance: pick a point orbiting close to the bull for a daring pass
	if _bull != null and randf() < 0.35:
		var angle := randf() * TAU
		var r     := randf_range(1.5, 4.0)
		var bull_flat := _bull.global_position
		bull_flat.y = 0.0
		_wander_target = bull_flat + Vector3(cos(angle) * r, 0.0, sin(angle) * r)
		return
	var radius := DP.f("mat_wander_radius")
	var angle  := randf() * TAU
	var dist   := randf_range(2.0, radius)
	_wander_target = Vector3(cos(angle) * dist, 0.0, sin(angle) * dist)


# ── Ragdoll setup ─────────────────────────────────────────────────────────────

func _setup_physical_bones() -> PhysicalBoneSimulator3D:
	for child: Node in _skeleton.get_children():
		if child is PhysicalBoneSimulator3D:
			(child as PhysicalBoneSimulator3D).active = false
			return child as PhysicalBoneSimulator3D

	var sim := PhysicalBoneSimulator3D.new()
	sim.active = false
	_skeleton.add_child(sim)

	const BONES: Array = [
		["matador",       0.18],
		["COG",           0.12],
		["chest",         0.13, 0.30],
		["head",          0.14],
		["collarbone_L",  0.06, 0.18],
		["collarbone_R",  0.06, 0.18],
		["arm_L",         0.07, 0.22],
		["arm_R",         0.07, 0.22],
		["forearm_L",     0.06, 0.22],
		["forearm_R",     0.06, 0.22],
		["hand_L",        0.05, 0.12],
		["hand_R",        0.05, 0.12],
		["leg_L",         0.10, 0.32],
		["leg_R",         0.10, 0.32],
		["shin_L",        0.08, 0.28],
		["shin_R",        0.08, 0.28],
		["foot_L",        0.07, 0.18],
		["foot_R",        0.07, 0.18],
	]

	for entry: Array in BONES:
		var bname: String = entry[0]
		if _skeleton.find_bone(bname) == -1:
			continue
		var pb := PhysicalBone3D.new()
		pb.bone_name        = bname
		pb.joint_type       = PhysicalBone3D.JOINT_TYPE_PIN
		pb.mass             = 0.4
		pb.linear_damp      = 0.1
		pb.angular_damp     = 0.2
		pb.collision_layer  = 1
		pb.collision_mask   = 1
		var cs    := CollisionShape3D.new()
		var use_capsule: bool = entry.size() >= 3
		if use_capsule:
			var cap    := CapsuleShape3D.new()
			cap.radius  = entry[1]
			cap.height  = entry[2]
			cs.shape    = cap
		else:
			var sph    := SphereShape3D.new()
			sph.radius  = entry[1]
			cs.shape    = sph
		pb.add_child(cs)
		sim.add_child(pb)

	return sim


# ── Utility ───────────────────────────────────────────────────────────────────

func _find_skeleton(node: Node) -> Skeleton3D:
	if node is Skeleton3D:
		return node as Skeleton3D
	for child: Node in node.get_children():
		var r := _find_skeleton(child)
		if r:
			return r
	return null


func _find_anim_player(node: Node) -> AnimationPlayer:
	if node is AnimationPlayer:
		return node as AnimationPlayer
	for child: Node in node.get_children():
		var r := _find_anim_player(child)
		if r:
			return r
	return null
