extends CharacterBody3D

# Hoof positions in bull_v10 local space — dust emitter offsets
const HOOF_OFFSETS: Array = [
	Vector3(-0.30, -0.25, -0.50),
	Vector3( 0.30, -0.25, -0.50),
	Vector3(-0.30, -0.25,  0.60),
	Vector3( 0.30, -0.25,  0.60),
]

@onready var camera_pivot: Node3D = $Camera3D
@onready var cube_guy:     Node3D = $bull_v10

var _hoof_emitters: Array[CPUParticles3D] = []
var _legs: Node

var _kick_timer: float        = 0.0
var _was_on_floor: bool       = false
var _pre_slide_vel_y: float   = 0.0
var _charge_dir: Vector3      = Vector3.ZERO
var _both_held: bool          = false
var _thrust_left_charge: float  = 0.0
var _thrust_right_charge: float = 0.0
var _charge_was_full: bool    = false

var _charge_overlay: ColorRect            = null
var _charge_ready_emitter: CPUParticles3D = null

enum DustState { NONE, WALK, CHARGE }
var _dust_state: DustState = DustState.NONE


func _ready() -> void:
	add_to_group(&"player")
	_setup_hoof_dust()
	_setup_legs()
	_setup_charge_indicators()
	DP.any_changed.connect(_on_dp_changed)


func _setup_legs() -> void:
	_legs = Node.new()
	_legs.set_script(load("res://bull_legs.gd"))
	add_child(_legs)
	_legs.call(&"setup", self, cube_guy)


func _setup_hoof_dust() -> 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.10
	sphere.height = 0.20
	sphere.radial_segments = 4
	sphere.rings = 2
	sphere.material = mat

	var ramp := Gradient.new()
	ramp.set_color(0, Color(0.80, 0.69, 0.46, 1.0))
	ramp.set_color(1, Color(0.80, 0.69, 0.46, 0.0))

	var scale_curve := Curve.new()
	scale_curve.add_point(Vector2(0.0, 0.2))
	scale_curve.add_point(Vector2(0.4, 1.0))
	scale_curve.add_point(Vector2(1.0, 0.0))

	for offset: Vector3 in HOOF_OFFSETS:
		var p := CPUParticles3D.new()
		p.position = offset
		p.emitting = false
		p.amount = 12
		p.lifetime = DP.f("dust_lifetime")
		p.explosiveness = DP.f("dust_explosiveness")
		p.randomness = 0.6
		p.local_coords = false
		p.direction = Vector3(0.0, 1.0, 0.0)
		p.spread = DP.f("dust_spread")
		p.gravity = Vector3(0.0, DP.f("dust_gravity_y"), 0.0)
		p.initial_velocity_min = DP.f("walk_vel_min")
		p.initial_velocity_max = DP.f("walk_vel_max")
		p.scale_amount_min = DP.f("walk_scale_min")
		p.scale_amount_max = DP.f("walk_scale_max")
		p.mesh = sphere
		p.color_ramp = ramp
		p.scale_amount_curve = scale_curve
		cube_guy.add_child(p)
		_hoof_emitters.append(p)


func _setup_charge_indicators() -> 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

	# Screen overlay — red tint while charge-ahead (both buttons) is active
	var ui := CanvasLayer.new()
	add_child(ui)
	_charge_overlay = ColorRect.new()
	_charge_overlay.color = Color(0.7, 0.05, 0.0, 0.0)
	_charge_overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE
	_charge_overlay.set_anchors_preset(Control.PRESET_FULL_RECT)
	ui.add_child(_charge_overlay)

	# Orange spark burst when single-button charge hits max
	var burst_ramp := Gradient.new()
	burst_ramp.set_color(0, Color(1.0, 0.55, 0.0, 1.0))
	burst_ramp.set_color(1, Color(1.0, 0.15, 0.0, 0.0))

	var burst_scale := Curve.new()
	burst_scale.add_point(Vector2(0.0, 1.0))
	burst_scale.add_point(Vector2(1.0, 0.0))

	_charge_ready_emitter = CPUParticles3D.new()
	_charge_ready_emitter.position = Vector3(0.0, 0.3, 0.0)
	_charge_ready_emitter.emitting = false
	_charge_ready_emitter.one_shot = true
	_charge_ready_emitter.amount = 16
	_charge_ready_emitter.lifetime = 0.5
	_charge_ready_emitter.explosiveness = 1.0
	_charge_ready_emitter.randomness = 0.4
	_charge_ready_emitter.local_coords = false
	_charge_ready_emitter.direction = Vector3.UP
	_charge_ready_emitter.spread = 120.0
	_charge_ready_emitter.initial_velocity_min = 2.5
	_charge_ready_emitter.initial_velocity_max = 5.0
	_charge_ready_emitter.scale_amount_min = 0.2
	_charge_ready_emitter.scale_amount_max = 0.5
	_charge_ready_emitter.mesh = sphere
	_charge_ready_emitter.color_ramp = burst_ramp
	_charge_ready_emitter.scale_amount_curve = burst_scale
	cube_guy.add_child(_charge_ready_emitter)


func _set_dust_state(new_state: DustState) -> void:
	if new_state == _dust_state:
		return
	_dust_state = new_state
	for p: CPUParticles3D in _hoof_emitters:
		p.lifetime = DP.f("dust_lifetime")
		p.explosiveness = DP.f("dust_explosiveness")
		p.spread = DP.f("dust_spread")
		p.gravity = Vector3(0.0, DP.f("dust_gravity_y"), 0.0)
		match new_state:
			DustState.NONE:
				p.emitting = false
			DustState.WALK:
				p.scale_amount_min = DP.f("walk_scale_min")
				p.scale_amount_max = DP.f("walk_scale_max")
				p.initial_velocity_min = DP.f("walk_vel_min")
				p.initial_velocity_max = DP.f("walk_vel_max")
				p.emitting = true
			DustState.CHARGE:
				p.scale_amount_min = DP.f("charge_scale_min")
				p.scale_amount_max = DP.f("charge_scale_max")
				p.initial_velocity_min = DP.f("charge_vel_min")
				p.initial_velocity_max = DP.f("charge_vel_max")
				p.emitting = true


func _on_dp_changed(_key: String, _val: Variant) -> void:
	if _hoof_emitters.is_empty() or _dust_state == DustState.NONE:
		return
	var prev := _dust_state
	_dust_state = DustState.NONE
	_set_dust_state(prev)




func _apply_thrust(dir: Vector3) -> void:
	var impulse := DP.f("thrust_impulse")
	velocity.x += dir.x * impulse
	velocity.z += dir.z * impulse
	if is_on_floor():
		velocity.y += DP.f("thrust_vertical")
	var flat := Vector2(velocity.x, velocity.z)
	var spd  := flat.length()
	if spd > DP.f("max_speed"):
		var s := DP.f("max_speed") / spd
		velocity.x *= s
		velocity.z *= s


func _apply_kick(direction: Vector3, charging: bool) -> void:
	var base := DP.f("kick_impulse") * (DP.f("charge_kick_mult") if charging else 1.0)

	# Random force variance — each kick punches a different amount
	var force := base * randf_range(1.0 - DP.f("kick_force_var"), 1.0 + DP.f("kick_force_var"))

	# Random angle spread — slight mis-kick like a real ball
	var spread := deg_to_rad(randf_range(-DP.f("kick_spread"), DP.f("kick_spread")))
	var kick_dir := direction.rotated(Vector3.UP, spread)

	velocity.x += kick_dir.x * force
	velocity.z += kick_dir.z * force

	# Vertical arc — only when grounded so arcs don't chain infinitely in the air
	if is_on_floor():
		velocity.y += DP.f("kick_vertical") * randf_range(0.75, 1.25)

	# Cap immediately after kick so charge doesn't overshoot by too much
	var flat := Vector2(velocity.x, velocity.z)
	var spd  := flat.length()
	var cap  := DP.f("max_speed") * (DP.f("charge_kick_mult") if charging else 1.0)
	if spd > cap:
		var s := cap / spd
		velocity.x *= s
		velocity.z *= s


func _physics_process(delta: float) -> void:
	# ── Gravity & jump ────────────────────────────────────────────────────────
	if not is_on_floor():
		velocity += get_gravity() * delta

	if Input.is_action_just_pressed(&"jump") and is_on_floor():
		velocity.y = DP.f("jump_velocity")

	# ── Camera-relative input direction ───────────────────────────────────────
	var input_dir    := Input.get_vector("move_left", "move_right", "move_forward", "move_back")
	var cam_basis    := camera_pivot.global_transform.basis
	var flat_forward := -Vector3(cam_basis.z.x, 0.0, cam_basis.z.z).normalized()
	var flat_right   :=  Vector3(cam_basis.x.x, 0.0, cam_basis.x.z).normalized()
	var direction    := (flat_forward * -input_dir.y + flat_right * input_dir.x).normalized()

	var charging := Input.is_action_pressed(&"charge")
	var on_floor  := is_on_floor()

	# ── Mouse button actions ──────────────────────────────────────────────────
	#   Both held    → charge: lock direction, ramp speed, lower head
	#   Left only    → thrust NE (forward-right diagonal)
	#   Right only   → thrust NW (forward-left diagonal)
	var left_held  := Input.is_action_pressed(&"thrust_left")
	var right_held := Input.is_action_pressed(&"thrust_right")
	var both_held  := left_held and right_held

	if both_held and not _both_held:
		var raw := cube_guy.global_transform.basis.z
		_charge_dir = Vector3(raw.x, 0.0, raw.z).normalized()

	if both_held:
		_thrust_left_charge  = 0.0
		_thrust_right_charge = 0.0
		velocity.x += _charge_dir.x * DP.f("bull_charge_accel") * delta
		velocity.z += _charge_dir.z * DP.f("bull_charge_accel") * delta
		var flat_c := Vector2(velocity.x, velocity.z)
		var spd_c  := flat_c.length()
		if spd_c > DP.f("bull_charge_max_speed"):
			velocity.x *= DP.f("bull_charge_max_speed") / spd_c
			velocity.z *= DP.f("bull_charge_max_speed") / spd_c
		_legs.set(&"charge_pitch_target", DP.f("charge_head_pitch"))
	else:
		_legs.set(&"charge_pitch_target", 0.0)

		if left_held and not right_held:
			_thrust_left_charge = minf(_thrust_left_charge + delta, DP.f("thrust_charge_time"))
		elif Input.is_action_just_released(&"thrust_left") and _thrust_left_charge > 0.0:
			var t     := _thrust_left_charge / DP.f("thrust_charge_time")
			var angle := lerpf(deg_to_rad(DP.f("thrust_min_angle")), deg_to_rad(DP.f("thrust_max_angle")), t)
			var raw   := cube_guy.global_transform.basis.z
			_apply_thrust(Vector3(raw.x, 0.0, raw.z).normalized().rotated(Vector3.UP, -angle))
			_thrust_left_charge = 0.0

		if right_held and not left_held:
			_thrust_right_charge = minf(_thrust_right_charge + delta, DP.f("thrust_charge_time"))
		elif Input.is_action_just_released(&"thrust_right") and _thrust_right_charge > 0.0:
			var t     := _thrust_right_charge / DP.f("thrust_charge_time")
			var angle := lerpf(deg_to_rad(DP.f("thrust_min_angle")), deg_to_rad(DP.f("thrust_max_angle")), t)
			var raw   := cube_guy.global_transform.basis.z
			_apply_thrust(Vector3(raw.x, 0.0, raw.z).normalized().rotated(Vector3.UP, angle))
			_thrust_right_charge = 0.0

	# Fire spark burst the frame either single-button charge first hits max
	var max_charge  := DP.f("thrust_charge_time")
	var either_full := _thrust_left_charge >= max_charge or _thrust_right_charge >= max_charge
	if either_full and not _charge_was_full:
		_charge_ready_emitter.restart()
	_charge_was_full = either_full

	# Fade red overlay in while charge-ahead, out when released
	var target_alpha := 0.30 if both_held else 0.0
	var oc := _charge_overlay.color
	_charge_overlay.color = Color(oc.r, oc.g, oc.b, lerpf(oc.a, target_alpha, delta * 10.0))

	_both_held = both_held

	# Reset kick timer on landing so the first kick after a bounce is immediate
	if on_floor and not _was_on_floor and direction:
		_kick_timer = 0.0
	_was_on_floor = on_floor

	# ── Periodic kick ─────────────────────────────────────────────────────────
	_kick_timer = maxf(0.0, _kick_timer - delta)
	if direction and _kick_timer <= 0.0 and not both_held:
		_apply_kick(direction, charging)
		_kick_timer = DP.f("kick_interval")

	# ── Rolling friction (exponential, frame-rate independent) ────────────────
	var retain := pow(DP.f("roll_damping"), delta)
	velocity.x *= retain
	velocity.z *= retain

	# ── Visual rotation follows velocity direction ─────────────────────────────
	var flat_speed := Vector2(velocity.x, velocity.z).length()
	if flat_speed > 0.4:
		var target_angle := atan2(velocity.x, velocity.z)
		cube_guy.rotation.y = lerp_angle(
			cube_guy.rotation.y, target_angle, delta * DP.f("visual_turn_speed"))

	_pre_slide_vel_y = velocity.y
	move_and_slide()

	# ── Bounce on landing ─────────────────────────────────────────────────────
	if is_on_floor() and not _was_on_floor:
		var impact := -_pre_slide_vel_y
		if impact > DP.f("bounce_min_impact"):
			velocity.y = impact * DP.f("bounce_restitution")

	# ── Dust ──────────────────────────────────────────────────────────────────
	if flat_speed > DP.f("dust_charge_spd") and on_floor:
		_set_dust_state(DustState.CHARGE)
	elif flat_speed > DP.f("dust_walk_spd") and on_floor:
		_set_dust_state(DustState.WALK)
	else:
		_set_dust_state(DustState.NONE)
