extends CharacterBody3D
class_name BrawlerController

# Stats
@export var speed: float = 5.0
@export var jump_velocity: float = 4.5
@export var mass: float = 4.0

# Spawn
@export var spawn_point: Vector3 = Vector3(0, 4, 0)
@export var death_depth: float = 4.0
@export var respawn_time: float = 4.0

# Dependencies
@onready var input := $Input as BrawlerInput
@onready var rollback_synchronizer := $RollbackSynchronizer as RollbackSynchronizer
@onready var animation_tree := $AnimationTree as AnimationTree
@onready var weapon := $Weapon as BrawlerWeapon
@onready var mesh := $"bomber-guy/rig/Skeleton3D/Cube_008" as MeshInstance3D
@onready var nametag := $Nametag as Label3D
@onready var fall_sound := $"Fall Sound" as PlayRandomStream3D

var player_name: String = "":
	set(p_name):
		if p_name.length() > 24:
			p_name = p_name.substr(0, 21) + "..."
		player_name = p_name
		nametag.text = p_name

var player_id: int = -1
var last_hit_player: BrawlerController
var last_hit_tick: int = -1
var gravity = ProjectSettings.get_setting(&"physics/3d/default_gravity")
var respawn_tick: int = -1
var respawn_count: int = 0

func register_hit(from: BrawlerController):
	if from == self:
		push_error("Player %s (#%s) trying to register hit on themselves!" % [player_name, player_id])
		return

	last_hit_player = from
	last_hit_tick = NetworkRollback.tick if NetworkRollback.is_rollback() else NetworkTime.tick

func shove(motion: Vector3):
	move_and_collide(motion / mass)

func _ready():
	if not input:
		input = $Input

	_snap_to_spawn()

	GameEvents.on_brawler_spawn.emit(self)
	NetworkTime.on_tick.connect(_tick)

	if not player_name:
		player_name = "Nameless Brawler #%s" % [player_id]
	
	# Set player color
	var color = Color.from_hsv((hash(player_id) % 256) / 256.0, 1.0, 1.0)
	var material: StandardMaterial3D = mesh.get_active_material(0)
	material = material.duplicate()
	material.albedo_color = color
	mesh.set_surface_override_material(0, material)

	rollback_synchronizer.set_schema({
		":transform": NetworkSchemas.transform3f32(),
		":velocity": NetworkSchemas.vec3f32(),
		":speed": NetworkSchemas.float32(),
		":mass": NetworkSchemas.float32(),
		
		"Input:movement": NetworkSchemas.vec3f32(),
		"Input:aim": NetworkSchemas.vec3f32()
	})

func _process(delta):
	# Update animation
	# Running
	var movement = Vector3(velocity.x, 0, velocity.z) * speed
	var relative_velocity = quaternion.inverse() * movement
	relative_velocity.y = 0
	relative_velocity /= speed
	relative_velocity = Vector2(relative_velocity.x, relative_velocity.z)
	var animated_velocity = animation_tree.get("parameters/Move/blend_position") as Vector2

	animation_tree.set("parameters/Move/blend_position", animated_velocity.move_toward(relative_velocity, delta / 0.2))
	
	# Float
	_force_update_is_on_floor()
	var animated_float = animation_tree.get("parameters/Float/blend_amount") as float
	var actual_float = 1.0 if not is_on_floor() else 0.0
	animation_tree.set("parameters/Float/blend_amount", move_toward(animated_float, actual_float, delta / 0.2))
	
	# Speed
	animation_tree.set("parameters/MoveScale/scale", speed / 3.75)
	animation_tree.set("parameters/ThrowScale/scale", min(weapon.fire_cooldown / (10. / 24.), 1.0))

func _tick(_delta, tick):
	# Run throw animation if firing
	if weapon.last_fire == tick:
		animation_tree.set("parameters/Throw/request", AnimationNodeOneShot.ONE_SHOT_REQUEST_FIRE)

func _rollback_tick(delta, tick, is_fresh):
	# Respawn
	if tick == respawn_tick:
		_snap_to_spawn()
		velocity = Vector3.ZERO
		last_hit_tick = -1
		
		if is_fresh:
			GameEvents.on_brawler_respawn.emit(self)

	# Skip predictions
	if rollback_synchronizer.is_predicting():
		rollback_synchronizer.ignore_prediction(self)
		return

	# Apply gravity
	_force_update_is_on_floor()
	if not is_on_floor():
		velocity.y -= gravity * delta
	
	# Stick to moving platforms
	var platform_velocity := Vector3.ZERO
	var collision_result := KinematicCollision3D.new()
	if test_move(global_transform, Vector3.DOWN * delta, collision_result):
		var collider := collision_result.get_collider()
		if collider is MovingPlatform:
			var platform := collider as MovingPlatform
			platform_velocity = platform.get_velocity()

	# Jump
	if input.movement.y > 0 and is_on_floor():
		velocity.y = jump_velocity * input.movement.y

	# Movement
	var direction = Vector3(input.movement.x, 0, input.movement.z).normalized()
	if direction:
		velocity.x = direction.x * speed
		velocity.z = direction.z * speed
	else:
		velocity.x = move_toward(velocity.x, 0, speed)
		velocity.z = move_toward(velocity.z, 0, speed)
	
	# Aim
	if input.aim:
		transform = transform.looking_at(position + Vector3(input.aim.x, 0, input.aim.z), Vector3.UP, true).scaled_local(scale)

	# Apply movement
	velocity += platform_velocity
	velocity *= NetworkTime.physics_factor
	move_and_slide()
	velocity /= NetworkTime.physics_factor
	velocity -= platform_velocity
	
	# Death
	if position.y < -death_depth and tick > respawn_tick and is_fresh:
		var respawn_cooldown = respawn_time * NetworkTime.tickrate
		respawn_tick = tick + respawn_cooldown
		respawn_count += 1

		fall_sound.play_random()

		GameEvents.on_brawler_fall.emit(self)

func _exit_tree():
	GameEvents.on_brawler_despawn.emit(self)

func _snap_to_spawn():
	var spawns = get_tree().get_nodes_in_group("Spawn Points")
	var idx = hash(player_id + respawn_count * 39) % spawns.size()
	var spawn = spawns[idx] as Node3D
	
	global_transform = spawn.global_transform

func _force_update_is_on_floor():
	var old_velocity = velocity
	velocity *= 0
	move_and_slide()
	velocity = old_velocity
