class_name GameBall extends RigidBody3D
## Base class for all gfolf balls

## Fired as soon as this ball enters a water hazard
signal entered_water

## Types of game balls
enum Type {
	NONE,
	BASIC,
	PLASMA,
	BRICK,
	BEACH,
	POWER,
}

const TERRAIN_DAMPING_EPSILON := 1e-6
const MAGNUS_EPSILON := 1e-3

## If enabled, ball ability cooldown is only reset at end of shot.
@export var once_per_shot_ability := false

## Angular damping while in air
@export var air_damping := 0.0
## Angular damping while in collision with rough terrain
@export var rough_damping := 8.0

#@export var fluid_density := 1.225
#@export var lift_coefficient := 0.05
#@export var radius := 0.05
## Coefficient of angular velocity influence on linear velocity
## This is approximately 1/2 * rho * C_L * pi * r^2
## where `rho` is the fluid density of the medium, or 1.225 for air at sea level,
## and `C_L` is the lift coefficient which for our purposes is 0.05,
## and `r` is the radius of the ball, which is variable.
## NOTE: Rather than use r^2 we use r * a constant 0.05
#@export var magnus_coefficient := 0.00024
@export var magnus_coefficient := 0.00481056

## Causes the ball to stick to surfaces
@export var magnetic := false

## Base damage inflicted on impact with a player
@export var base_damage := 15.0

## Scaling factor for additional force-based damage
@export var damage_force_scale := 0.01

## Approximate average radius, for physics & positioning purposes
@export var radius := 0.05

var current_gravity: Vector3

var player: WorldPlayer

var _last_contact_normal: Vector3 = Vector3.UP
var _position_on_last_wake: Vector3
var _awake := false
var _ability_triggered := false
var _zones: Array[BallZone] = []

@onready var ability_cooldown: Timer = %AbilityCooldown
@onready var manual_sleep_timer: Timer = %ManualSleepTimer
@onready var sfx: BallSFX = %SFX
@onready var effects: BallParticleEffects = %ParticleEffects

@onready var normal_physics: PhysicsMaterial = preload(
	"res://src/equipment/balls/physics_ball/normal_physics.tres"
)


## Should this ball stick to surfaces, rather than bounce?
func is_sticky() -> bool:
	return magnetic


## Called by a water area when this ball enters it
func enter_water() -> void:
	entered_water.emit()


## Activate this ball's ability, if there is one.
func activate_ability() -> void:
	if once_per_shot_ability:
		if not _ability_triggered:
			_ability_triggered = true
			_activate_ability()
		# TODO bonk
	elif ability_cooldown.is_stopped():
		_activate_ability()
		ability_cooldown.start()
	# TODO bonk


func _activate_ability() -> void:
	# Implmemented by derived type
	pass


func get_damage() -> float:
	print("velocity: ", linear_velocity.length())
	return base_damage + linear_velocity.length_squared() * damage_force_scale


func _total_terrain_angular_damping() -> float:
	return _zones.reduce(
		func(a: float, b: BallZone) -> float: return a + b.terrain_angular_damping, 0.0
	)


func _magnus_force() -> Vector3:
	return magnus_coefficient * radius * angular_velocity.cross(linear_velocity)


func _integrate_forces(state: PhysicsDirectBodyState3D) -> void:
	current_gravity = state.total_gravity
	if not _awake:
		# Triggered on first frame after waking
		_awake = true
		_ability_triggered = false
		_position_on_last_wake = global_position
		_last_contact_normal = Vector3.UP
		# TODO something's fucky here... I think this gets called once after the ball sleeps

	var damping := air_damping
	if state.get_contact_count():
		constant_force = Vector3.ZERO

		# We want the contact normal which minimizes the angle to the up vector
		var min_dot := -1.0
		for i: int in range(state.get_contact_count()):
			var norm := state.get_contact_local_normal(i)
			var dot := norm.dot(Vector3.UP)
			if dot > min_dot:
				min_dot = dot
				_last_contact_normal = norm

		damping = _total_terrain_angular_damping()
		if damping <= TERRAIN_DAMPING_EPSILON:
			damping = rough_damping
	angular_damp = damping


func _physics_process(_delta: float) -> void:
	# Simulate magnus effect
	var magnus := _magnus_force()
	if magnus.length_squared() > MAGNUS_EPSILON:
		apply_central_force(magnus)


func enter_zone(zone: BallZone) -> void:
	_zones.push_back(zone)

	if zone.water_hazard:
		entered_water.emit()


func exit_zone(zone: BallZone) -> void:
	_zones.erase(zone)


func get_reoriented_basis() -> Basis:
	var up := _last_contact_normal.normalized()
	var forward := (_position_on_last_wake - global_position).normalized()
	var right := up.cross(forward).normalized()
	forward = right.cross(up)  # orthonormalize
	return Basis(right, up, forward)


func _on_sleeping_state_changed() -> void:
	print("SLEEPING STATE: ", sleeping)
	if sleeping:
		# Trigger to reassign on wake
		_awake = false


func _on_collision(body: Node) -> void:
	if is_sticky():
		# Freeze physics as soon as we hit something
		freeze = true
		manual_sleep_timer.start()

	var terrain: Terrain.Type
	if body is Terrain3D:
		terrain = Terrain.at_position(global_position, body as Terrain3D)
	elif body is CSGShape3D:
		terrain = Terrain.from_physical_layer((body as CSGShape3D).collision_layer)
	elif body is CollisionObject3D:
		terrain = Terrain.from_physical_layer((body as CollisionObject3D).collision_layer)
	else:
		print_debug("COLLIDER: ", body)

	if terrain:
		sfx.play_sfx(terrain)
		effects.play_effect(terrain)


func _fire_sleep_signal() -> void:
	sleeping = true
	sleeping_state_changed.emit()
