class_name ShotSetup extends Node3D

signal finished(source: ShotSetup)

enum Phase {
	AIM,
	POWER_ADJUST,
	CURVE_ADJUST,
	DOWNSWING,
	SHOT,
	SHOT_RESET,
	FINISHED,
	DEAD,
}

const PITCH_MIN := deg_to_rad(-60.0)
const PITCH_MAX := deg_to_rad(-5.0)
const ZOOM_LENGTH := 0.1
const ZOOM_ELEVATION_RATIO := 0.3166

const ZOOM_MIN := 1.0
const ZOOM_MAX := 12.0

const ARROW_ACCELERATION := 8.0
const PLAYER_ACCELERATION := 2.0

const FREE_CAM_GIMBAL_TWEEN_TIME := 0.2
const FREE_CAM_RETURN_TIME := 0.618
const BALL_RETURN_TIME := 0.618
const CAMERA_SNAP_TIME := 0.3

const WASTED_BALL_RETURN_DELAY := 3.5

## Shots above this threshold trigger a "big power" effect
const BIG_POWER_THRESHOLD := 0.7

## Amount of life lost when landing in water
const WATER_DAMAGE := 10.0

## Angle of influence that shot curve has, in radians
const CURVE_INFLUENCE := PI / 16

## Just enough to make things interesting!
const SHOT_OFFSET_X_FACTOR := 20.0
const SHOT_OFFSET_Z_FACTOR := 2.0 / 45.0

## Impulse offset multiplier due to curve, in meters
const CURVE_FACTOR := 0.002

## Maximum absolute curve for the "nice shot" animation to play
const NICE_THRESHOLD := 0.2

## Force by which explosions knock the ball away
const EXPLOSIVE_FORCE_FACTOR := 0.12

## In Driving Range mode, the ball can be retrieved in the shot phase.
@export var driving_range := false

## Initially-selected club
@export var initial_club_type: Club.Type = Club.Type.DRIVER

## Initially-selected ball type
@export var initial_ball: GameBall.Type = GameBall.Type.BASIC

@export_category("Shot Parameters")
@export var base_power := 2.5
@export var base_curve := 0.0

@export_category("Debug")
## When enabled, ignore curve meter and hit a perfect shot every time.
@export var perfect_aim := false
## Keep projection visible
@export var keep_projection := false

var player: WorldPlayer

var control_disabled := false

var reset_enabled := false:
	set(value):
		if value != reset_enabled:
			if value:
				hud.show_reset_prompt()
			else:
				hud.hide_reset_prompt()
		reset_enabled = value

var phase: Phase = Phase.FINISHED:
	set(value):
		if value != phase:
			_on_phase_change(value)
		phase = value

var hud: ShotHUD

var club_type: Club.Type:
	set = _set_club_type

var club: Club:
	get:
		return player.get_club(club_type) if player else null

var ball_type: GameBall.Type:
	set(value):
		if value != ball_type:
			if hud:
				hud.ball_selector.value = value
			ball_point.spawn_ball(value)
		ball_type = value

var shot_power: float:
	set(value):
		hud.power_bar.value = value
	get:
		return hud.power_bar.value

var shot_curve: float:
	set(value):
		hud.curve_bar.value = value
	get:
		return hud.curve_bar.value

var game_ball: GameBall:
	get:
		return ball_point.ball

var _free_camera: FreeCamera
var _returning_free_camera := false
var _restart_queued := false

var _tracking_camera: OrbitalCamera

@onready var direction: Node3D = %Direction
@onready var elevation: Node3D = %Elevation
@onready var pitch: Node3D = %Pitch
@onready var zoom: Node3D = %Zoom
@onready var camera: Camera3D = %Camera

@onready var player_pivot: Node3D = %PlayerPivot
@onready var player_offset: Node3D = %PlayerOffset
# TODO: genericize for selectable characters
@onready var character: CharacterController = $PlayerPivot/PlayerOffset/GfolfGirl

@onready var shot_animation: AnimationPlayer = %ShotAnimation
@onready var shot_sfx: ShotSFX = %ShotSFX

@onready var death_animation: AnimationPlayer = %DeathAnimation

@onready var arrow: Node3D = %Arrow
@onready var arrow_pivot: Node3D = %ArrowPivot
@onready var arrow_animation: AnimationPlayer = %ArrowAnimation
@onready var shot_projection: ProjectileArc = %ShotProjection

@onready var ball_point: BallPoint = %BallPoint

@onready var drive_arrow: Node3D = %DriveArrow
@onready var wedge_arrow: Node3D = %WedgeArrow
@onready var iron_arrow: Node3D = %IronArrow
@onready var putt_arrow: Node3D = %PuttArrow

@onready var downswing_timer: Timer = %DownswingTimer
@onready var ball_return_timer: Timer = %BallReturnTimer
@onready var reset_prompt_timer: Timer = %ResetPromptTimer

@onready var explosion_animation: AnimationPlayer = %ExplosionAnimation
@onready var player_label: Label3D = %PlayerLabel

@onready var camera_distance := zoom.position.z:
	set = _set_camera_distance

@onready var world: World = get_tree().get_first_node_in_group(World.group)
@onready var game: Game = get_tree().get_first_node_in_group(Game.group)

@onready var _target_rotation := Vector2(pitch.rotation.x, direction.rotation.y)

static var scene := preload("res://src/player/shot_setup/shot_setup.tscn")


func _ready() -> void:
	if player:
		player.on_life_changed.connect(_on_life_changed)

		# Create & set up HUD
		ball_type = initial_ball
		club_type = initial_club_type

		hud = ShotHUD.create(player)
		world.ui.add_player_hud(hud)
		character.set_color(player.color)

		# Set up player 3D label
		player_label.text = player.name
		player_label.modulate = player.color
		player_label.outline_modulate = ColorTools.get_bg_color(player.color)

	#_on_phase_change(phase)


func _on_tree_exiting() -> void:
	if is_instance_valid(_tracking_camera):
		_tracking_camera.queue_free()
	if is_instance_valid(_free_camera):
		_free_camera.queue_free()
	hud.queue_free()


func _set_camera_distance(value: float) -> void:
	var tween := get_tree().create_tween()
	tween.tween_property(zoom, "position:z", value, ZOOM_LENGTH).set_trans(Tween.TRANS_SINE)
	tween.set_parallel()
	(
		tween
		. tween_property(elevation, "position:y", value * ZOOM_ELEVATION_RATIO, ZOOM_LENGTH)
		. set_trans(Tween.TRANS_SINE)
	)
	camera_distance = value


func _unhandled_input(event: InputEvent) -> void:
	if event is InputEventMouseButton:
		Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
	elif event is InputEventMouseMotion:
		if Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
			camera_motion((event as InputEventMouseMotion).relative)


func camera_motion(motion: Vector2) -> void:
	if not control_disabled and phase == Phase.AIM:
		# Can only control camera while aiming
		_target_rotation.y = _target_rotation.y - deg_to_rad(motion.x * Game.settings.x_sensitivity)
		_target_rotation.x = clampf(
			(
				_target_rotation.x
				- (
					deg_to_rad(motion.y * Game.settings.y_sensitivity)
					* (-1 if Game.settings.invert_pitch else 1)
				)
			),
			PITCH_MIN,
			PITCH_MAX
		)


## Return this instance to the AIM phase the next time we process while FINISHED.
func queue_restart() -> void:
	_restart_queued = true


func is_active() -> bool:
	return phase != Phase.FINISHED


func is_shot_good() -> bool:
	return abs(shot_curve) <= NICE_THRESHOLD


func finish_downswing() -> void:
	phase = Phase.SHOT


func get_shot_impulse(meter_pct: float) -> Vector3:
	# Basic direction of the shot
	var shot_vec := -direction.global_basis.rotated(direction.global_basis.x, club.angle).z
	# Curve rotates shot direction around the Y-axis
	var curved_shot := shot_vec.rotated(Vector3.UP, shot_curve * CURVE_INFLUENCE * shot_power)
	# Various factors attenuate shot power
	return curved_shot * club.power * base_power * meter_pct


func take_shot() -> void:
	# Impact screenshake
	if game:
		if shot_power > BIG_POWER_THRESHOLD:
			var shake_intensity: float = (
				10.0 * (shot_power - BIG_POWER_THRESHOLD) / (1.0 - BIG_POWER_THRESHOLD)
			)
			game.viewport.screen_shake(shake_intensity, 1.0)

	print_debug("WHACK!\nPower: ", shot_power, "\nCurve: ", shot_curve)

	# Momentary impulse applied to the ball
	var impulse := get_shot_impulse(shot_power)
	print_debug("Shot impulse: ", impulse, "; ", impulse.length(), " N*s")

	# Curve the curve
	var curve := shot_curve * absf(shot_curve) * CURVE_FACTOR

	# Position where the ball is hit (imparts spin)
	var offset := (
		direction.global_basis.x.normalized() * -curve * game_ball.radius * SHOT_OFFSET_X_FACTOR
	)
	print_debug("Curve offset magnitude: ", offset.length())
	offset += (
		direction.global_basis.z.normalized()
		* game_ball.radius
		* game_ball.radius
		* SHOT_OFFSET_Z_FACTOR
	)
	print_debug("Shot offset: ", offset, "; ", offset.length(), " m")

	if game_ball:
		game_ball.magnetic = club_type == Club.Type.IRON
		game_ball.freeze = false
		game_ball.apply_impulse(impulse, offset)

	reset_prompt_timer.start()

	# Play SFX
	shot_sfx.play_shot_sfx(club_type, is_shot_good(), shot_power)

	# Use a ball if a limited type is selected
	if player.get_balls(ball_type) > 0:
		player.mutate_balls(ball_type, -1)


## Make the shot projection widget visible, with animated transition
func _show_shot_projection() -> void:
	if not game_ball:
		return

	shot_projection.set_ball(game_ball)
	shot_projection.putt_projection = club_type == Club.Type.PUTTER
	shot_projection.initial_speed = 1
	shot_projection.basis = Basis.from_euler(Vector3(club.angle, 0, 0))
	var shot_speed := get_shot_impulse(1.0).length() / game_ball.mass
	var tween := get_tree().create_tween()
	tween.tween_property(shot_projection, "initial_speed", shot_speed, CAMERA_SNAP_TIME).set_trans(
		Tween.TRANS_QUAD
	)
	shot_projection.show()


func insert_free_cam() -> void:
	arrow_animation.play("hide")
	_show_shot_projection()
	hud.hide_hud()
	_free_camera = FreeCamera.create(camera)
	add_sibling(_free_camera)
	# Un-gimbal-lock ourselves: quickly tween Z rotation to 0
	var tween := get_tree().create_tween()
	tween.tween_property(_free_camera, "rotation:z", 0, FREE_CAM_GIMBAL_TWEEN_TIME)
	control_disabled = true
	camera.current = false


func return_free_cam() -> void:
	# TODO alter shot aim based on free camera selection
	arrow_animation.play("show")

	if not keep_projection:
		shot_projection.hide()

	hud.show_hud()
	_free_camera.queue_free()
	_free_camera = null
	control_disabled = false
	_returning_free_camera = false
	camera.current = true


func return_ball() -> void:
	if not game_ball:
		return

	game_ball.freeze = true
	var tween := get_tree().create_tween()
	(
		tween
		. tween_property(
			game_ball,
			"global_transform",
			ball_point.global_transform,
			BALL_RETURN_TIME,
		)
		. set_trans(Tween.TRANS_SINE)
	)
	tween.tween_callback(end_shot_track)


func travel_to_ball() -> void:
	if not game_ball:
		return

	game_ball.freeze = true
	global_position = game_ball.global_position

	# Re-orient to the ball's last contact normal if there is one.
	# Normally this will just be Vector3.UP or something close to it.
	direction.rotation.y = 0
	_target_rotation.y = 0
	global_basis = game_ball.get_reoriented_basis()

	# Adjust position downward to account for ball radius
	global_position -= global_basis.y.normalized() * game_ball.radius

	ball_point.snap()


func start_shot_track() -> void:
	if not game_ball:
		return

	if phase == Phase.SHOT:
		_tracking_camera = OrbitalCamera.create(game_ball)
		_tracking_camera.rotation.y = randf_range(0.0, TAU)
		add_sibling(_tracking_camera)
		_tracking_camera.global_transform = ball_point.global_transform


func end_shot_track() -> void:
	camera.make_current()
	if is_instance_valid(_tracking_camera):
		_tracking_camera.queue_free()
	if phase in [Phase.SHOT, Phase.SHOT_RESET]:
		phase = Phase.FINISHED


func start_death() -> void:
	print_debug("starting death sequence")
	world.ui.play_death_sequence()
	death_animation.play("death")
	get_tree().paused = true


func finish_death() -> void:
	print_debug("finishing death sequence")
	get_tree().paused = false
	player.die()
	queue_free()


func _set_club_type(new_club_type: Club.Type) -> void:
	if new_club_type == club_type:
		return

	var new_club := player.get_club(new_club_type)
	if not new_club:
		# `new_club` will be null if player has no club in the given slot
		# TODO play bonk
		return

	# Hide all arrows
	# TODO animate?
	drive_arrow.hide()
	wedge_arrow.hide()
	iron_arrow.hide()
	putt_arrow.hide()
	if hud:
		hud.club_selector.value = new_club_type
	# TODO club change animation
	character.hold_right(new_club.get_model())
	match new_club_type:
		Club.Type.DRIVER:
			drive_arrow.show()
		Club.Type.PUTTER:
			putt_arrow.show()
		Club.Type.WEDGE:
			wedge_arrow.show()
		Club.Type.IRON:
			iron_arrow.show()
		Club.Type.SPECIAL:
			# TODO figure this out
			pass
		_:
			print_debug("Not sure how to equip club type: ", new_club)

	club_type = new_club_type


## Called immediately before `phase` is mutated.
func _on_phase_change(new_phase: Phase) -> void:
	reset_prompt_timer.stop()
	reset_enabled = false

	match new_phase:
		Phase.AIM:
			hud.show_hud()
			if not arrow.visible:
				arrow_animation.play("show")
			camera.make_current()
			hud.power_bar.hide()
			hud.curve_bar.hide()

			character.reset()
		Phase.POWER_ADJUST:
			hud.curve_bar.hide()

			hud.power_bar.show()
			hud.reset_power_bar()  # Reset if needed
		Phase.CURVE_ADJUST:
			hud.curve_bar.show()
			hud.start_curve_bar()
		Phase.DOWNSWING:
			hud.gauge_flourish()
			character.downswing()
			downswing_timer.start()
		Phase.SHOT:
			hud.power_bar.hide()
			hud.curve_bar.hide()

			if perfect_aim:
				shot_curve = 0.0

			if is_shot_good():
				hud.play_nice_animation()

			if not driving_range:
				shot_animation.play("shoot")
			arrow_animation.play("hide")
			hud.hide_hud()

			take_shot()
		Phase.FINISHED:
			hud.power_bar.hide()
			hud.curve_bar.hide()
			hud.hide_hud()
			finished.emit(self)


func _on_game_ball_changed(ball: GameBall) -> void:
	var z_offset := 0.0
	if ball:
		ball.entered_water.connect(_on_ball_entered_water)
		ball.sleeping_state_changed.connect(_on_ball_sleeping_state_changed)
		z_offset = ball.radius
	player_offset.position.z = z_offset


func _process(delta: float) -> void:
	# REMOVEME
	if Input.is_action_just_pressed("ui_menu"):
		print("Debugging...")

	## Visual updates
	# Rotation
	direction.rotation.y = lerp_angle(
		direction.rotation.y, _target_rotation.y, delta * Game.settings.x_acceleration
	)
	pitch.rotation.x = lerp_angle(
		pitch.rotation.x, _target_rotation.x, delta * Game.settings.y_acceleration
	)

	# Arrow lags behind camera control
	arrow_pivot.rotation.y = lerp_angle(
		arrow_pivot.rotation.y, _target_rotation.y, delta * ARROW_ACCELERATION
	)
	# Player lags further behind
	player_pivot.rotation.y = lerp_angle(
		player_pivot.rotation.y, _target_rotation.y, delta * PLAYER_ACCELERATION
	)

	## Input Handling
	if control_disabled:
		if Input.is_action_just_pressed("camera_cancel"):
			if is_instance_valid(_free_camera) and not _returning_free_camera:
				_returning_free_camera = true
				var tween := get_tree().create_tween()
				(
					tween
					. tween_property(
						_free_camera,
						"global_transform",
						camera.global_transform,
						FREE_CAM_RETURN_TIME
					)
					. set_trans(Tween.TRANS_SINE)
				)
				tween.tween_callback(return_free_cam)
		return

	match phase:
		Phase.AIM:
			# Camera zoom
			if Input.is_action_just_pressed("shot_zoom_in"):
				camera_distance = max(camera_distance - 1.0, ZOOM_MIN)
			if Input.is_action_just_pressed("shot_zoom_out"):
				camera_distance = min(camera_distance + 1.0, ZOOM_MAX)

			# Club select
			if Input.is_action_just_pressed("select_driver"):
				club_type = Club.Type.DRIVER
			if Input.is_action_just_pressed("select_iron"):
				club_type = Club.Type.IRON
			if Input.is_action_just_pressed("select_wedge"):
				club_type = Club.Type.WEDGE
			if Input.is_action_just_pressed("select_special"):
				club_type = Club.Type.SPECIAL
			if Input.is_action_just_pressed("select_putter"):
				club_type = Club.Type.PUTTER

			# Ball select
			if Input.is_action_just_pressed("ball_next"):
				ball_type = player.next_ball(ball_type)
			if Input.is_action_just_pressed("ball_previous"):
				ball_type = player.prev_ball(ball_type)

			# Switch to free cam
			if (
				Input.is_action_just_pressed("camera_back")
				or Input.is_action_just_pressed("camera_forward")
				or Input.is_action_just_pressed("camera_left")
				or Input.is_action_just_pressed("camera_right")
			):
				insert_free_cam()

			# Advance to next phase
			if Input.is_action_just_pressed("shot_accept"):
				if player.get_balls(ball_type) != 0:
					# Check that player has enough of the selected ball (<0 means unlimited)
					phase = Phase.POWER_ADJUST
				# TODO play UI bonk if player doesn't have balls (lmao)
		Phase.POWER_ADJUST:
			if Input.is_action_just_pressed("shot_accept"):
				# TODO set power gauge parameters if needed
				character.start_upswing()
				hud.start_power_bar()
			if Input.is_action_just_pressed("shot_cancel"):
				hud.reset_power_bar()
				phase = Phase.AIM
			if Input.is_action_just_released("shot_accept") and shot_power > 0:
				hud.stop_power_bar()
				phase = Phase.CURVE_ADJUST
		Phase.CURVE_ADJUST:
			if Input.is_action_just_pressed("shot_cancel"):
				hud.reset_curve_bar()
				phase = Phase.POWER_ADJUST
			if Input.is_action_just_pressed("shot_accept"):
				hud.stop_curve_bar()
				phase = Phase.DOWNSWING
		Phase.SHOT:
			if reset_enabled and Input.is_action_just_pressed("shot_reset"):
				phase = Phase.SHOT_RESET
				reset_enabled = false
				return_ball()

			if Input.is_action_just_pressed("activate_ball"):
				game_ball.activate_ability()

			if driving_range and Input.is_action_just_pressed("shot_accept"):
				phase = Phase.AIM
				return_ball()
		Phase.FINISHED:
			if _restart_queued:
				_restart_queued = false
				phase = Phase.AIM
		Phase.DEAD:
			start_death()


func _on_ball_sleeping_state_changed() -> void:
	if game_ball.sleeping and phase == Phase.SHOT:
		travel_to_ball()
		end_shot_track()


func _on_ball_entered_water() -> void:
	# Should only be possible during SHOT phase, but let's check just to be sure...
	if phase == Phase.SHOT:
		phase = Phase.SHOT_RESET
		game_ball.freeze = true
		hud.play_wasted_animation()
		player.life -= WATER_DAMAGE
		ball_return_timer.start(WASTED_BALL_RETURN_DELAY)


func _on_ball_return_timer_timeout() -> void:
	return_ball()


func _on_hitbox_ball_collision(ball: GameBall) -> void:
	# TODO play animation
	var damage := ball.get_damage()
	player.life -= damage
	game.viewport.hit_lag_small()
	game.viewport.screen_shake(damage, 0.4)
	explosion_animation.play("explode")
	# TODO kinematic collisions

	# Knock other ball away
	var explosion_impulse := (
		(
			(ball.global_position - global_position).normalized() * damage
			+ global_basis.y.normalized()
		)
		* EXPLOSIVE_FORCE_FACTOR
	)
	ball.apply_central_impulse(explosion_impulse)


func _on_life_changed(new_value: float) -> void:
	if new_value <= 0:
		# No action is taken till the next process phase
		# This is because we need to be in the main thread to run the death sequence!
		phase = Phase.DEAD


func _on_reset_prompt_timer_timeout() -> void:
	reset_enabled = true


## Create a new instance for the given player.
static func create(_player: WorldPlayer) -> ShotSetup:
	var instance: ShotSetup = ShotSetup.scene.instantiate()
	instance.player = _player
	return instance