class_name PawnController extends CharacterBody3D

enum State {
	NORMAL,
	KNOCKDOWN,
	KNOCKUP,
	FLUNG,
	BOUND,
	DEAD
}

const trap_template = preload("res://templates/trap.tscn")
const remove_trap_modal = preload("res://templates/remove_trap_modal.tscn")
const disarm_trap_modal = preload("res://templates/disarm_trap_modal.tscn")

@export var speed : float = 10
@onready var body : PawnBody = $PawnBody
@onready var input : PawnInput = $PawnInput
@onready var data : PawnLevelData = $Data
@onready var trap_sound : AudioStreamPlayer3D = $TrapSound
@onready var detonate_sound : AudioStreamPlayer3D = $DetonateSound
@onready var detect_sound : AudioStreamPlayer3D = $DetectSound
@onready var fling_sound : AudioStreamPlayer3D = $FlingSound
@onready var crash_sound : AudioStreamPlayer3D = $CrashSound
@onready var reload_sound : AudioStreamPlayer3D = $PawnBody/ReloadSound
@onready var detect_icon : Sprite3D = $DetectIcon

var id : int = 1

@export var state : State
var button_actions : Dictionary[int, String]
var current_square : Vector3i
var facing : Vector3
@export var detecting : bool = false
var detect_squares : Dictionary[Vector3i, bool] = {}
var detect_tween : Tween = null
var fling_direction : Vector3
var fling_speed : float
var struggle_timer : float
var struggle_angle : float

var poison_strength : int = 0
var poison_time_remaining : float = 0
var poison_pulse_timer : float

var melee_range : float = 3.0
var ranged_range : float = 6
var attack_timer : float = 0
var melee_recovery_time : float = .75
var ranged_recovery_time : float = .2
var ranged_reload_time : float = 1
var projectile_speed : float = 10.0
var projectile_damage : int = 4
var ammo = 5
var max_ammo = 5
var combat_target
var meleeing : bool = false

var input_locked : bool = false
var action_tween : Tween = null


var camera : PawnCamera = null

var modal = null

signal trap_cycled(trap_index : int)
signal trap_quantity_changed(trap_index : int, quantity : int)
signal trap_list_changed(traps)
signal health_changed(current : int, max : int)
signal harmed()
signal poison_status_changed(poisoned : bool)
signal combat_target_changed(melee : bool)
signal ammo_changed(current : int, max : int)
signal struggling(value : float)

func _physics_process(delta: float) -> void:
	if attack_timer > 0:
		attack_timer -= delta
	check_attack_target()
	
	update_poison(delta)
	
	if !update_struggle(delta):
		return
	
	var dir = input.dir
	
	if input_locked or modal != null:
		dir = Vector3.ZERO
	
	var can_fall : bool = false
	var moving : bool = false
	match(state):
		State.FLUNG:
			can_fall = true
			moving = true
			dir = fling_direction
			body.look_at(body.global_position - dir)
			
			can_fall = true
			velocity = fling_speed * dir
			#Raycast for a wall
			var space_state = get_world_3d().direct_space_state
			# use global coordinates, not local to node
			var start = global_position + Vector3(0,0.25,0)
			var end = start + velocity.normalized() * 0.25 + velocity * 2 * delta
			var query = PhysicsRayQueryParameters3D.create(start, end, 1|2, [self])
			var result = space_state.intersect_ray(query)
			if result:
				var opp : PawnController = result.collider as PawnController
				if opp:
					opp.knockdown(fling_direction)
					opp.hurt(10)
				knockdown(-fling_direction)
				hurt(10)
				moving = false
		State.NORMAL:
			can_fall = true
			if dir.length_squared() > 0:
				moving = true
				facing = dir.normalized()
				body.look_at(body.global_position - dir)
				velocity = speed * dir
				if is_poisoned():
					velocity *= 0.5
				if detecting:
					velocity *= .33
		State.KNOCKUP:
			if is_on_floor():
				knockdown(facing)
			else:
				moving = true
				can_fall = true
		State.KNOCKDOWN,State.BOUND,State.DEAD:
			return
		

	if can_fall:
		var down = 0
		if !is_on_floor():
			velocity.y = velocity.y + get_gravity().y * delta
			moving = true
	
	if moving:
		move_and_slide()
		
	if detecting:
		update_detect_region.rpc(true)
	
	#Deal with the rest of the buttons
	if !input_locked:
		if modal:
			var buttons = [
				"lay trap",
				"detect",
				"attack",
				"detonate"
			]
			for button in buttons:
				if input.is_action_just_pressed(button):
					modal.button_pressed(button)
		elif state == State.NORMAL:
			update_actions()

func attack() -> void:
	if attack_timer > 0:
		return
	if meleeing:
		body.play_animation.rpc("melee")
		attack_timer = melee_recovery_time
	else:
		if ammo <= 0:
			reload_sound.play()
			ammo = max_ammo
			attack_timer = ranged_reload_time
			ammo_changed.emit(ammo, max_ammo)
			return
		
		ammo-=1
		ammo_changed.emit(ammo, max_ammo)
		attack_timer = ranged_recovery_time
		body.play_animation("shoot")
		if combat_target != null:
			var v = body.global_position.direction_to(combat_target.global_position)
			v.y = 0
			body.look_at(body.global_position - v)


func update_actions() -> void:
	if input.is_action_just_pressed("left cycle trap"):
		cycle_active_trap(-1)
	if input.is_action_just_pressed("right cycle trap"):
		cycle_active_trap(1)
	if input.is_action_just_pressed("detonate"):
		detonate()
	elif input.is_action_just_pressed("detect"):
		start_detecting()
	elif input.is_action_just_released("detect"):
		stop_detecting()
	elif !detecting and input.is_action_just_pressed("lay trap"):
		try_lay_trap()
	elif input.is_action_pressed("attack"):
		attack()

func update_struggle(delta : float) -> bool:
	if struggle_timer > 0:
		struggle_timer -= delta
		var snapped_angle = round(input.dir.angle_to(Vector3(1,0,0)) * 4 / PI) * PI * .25
		if abs(snapped_angle - struggle_angle) >= PI * .25:
			struggle_angle = snapped_angle
			struggle_timer -= 2 * delta
		struggling.emit(struggle_timer)
		if struggle_timer <= 0:
			hurt(1)
			knockdown(facing)
		else:
			return false
	return true

func update_poison(delta : float) -> void:
	if is_poisoned():
		poison_time_remaining -= delta
		poison_pulse_timer += delta
		if poison_pulse_timer >= 1.0:
			poison_pulse_timer -= 1.0
			hurt(poison_strength)
		if poison_time_remaining <= 0:
			var pshader : ShaderMaterial = body.material.next_pass
			pshader.set_shader_parameter("strength", 0)
			poison_status_changed.emit(false)

func fire_ranged() -> void:
	var shot = body.projectile_template.instantiate()
	var tdir : Vector3 = Vector3.ZERO
	shot.speed = projectile_speed
	tdir = body.ranged_point.global_position.direction_to(combat_target.global_position) if combat_target else facing
	shot.direction = tdir
	shot.damage = projectile_damage
	shot.position = body.ranged_point.global_position
	Game.level.add_projectile(shot)
	
func check_attack_target() -> void:
	var ranged_closest = null
	var ranged_d_sq = 99999999
	var melee_closest = null
	var melee_d_sq = 99999999
	var ranged_sq = ranged_range * ranged_range
	var melee_sq = melee_range * melee_range
	var space_state = get_world_3d().direct_space_state
	
	combat_target = null
	# use global coordinates, not local to node
	for target : Node3D in get_tree().get_nodes_in_group("combat"):
		if target == self:
			continue
		#Check to see if they're within the correct direction
		var angle = abs(facing.angle_to(target.global_position - global_position))
		if angle > PI / 4.0:
			continue
		#Determine if they're within shot range
		var d_sq = global_position.distance_squared_to(target.global_position)
		if(d_sq > ranged_sq or(ranged_closest != null and ranged_d_sq <= d_sq)):
			continue
		
		#Raycast to see if they're a valid target
		var start = global_position + Vector3(0,1,0)
		var end = target.global_position + Vector3(0,1,0)
		var query = PhysicsRayQueryParameters3D.create(start, end, 1|2, [self])
		var result = space_state.intersect_ray(query)
		if !result or (result.collider is not PawnController and result.collider is not CombatTarget):
			return
			
		if d_sq < melee_sq:
			melee_closest = target
			melee_d_sq = d_sq
		else:
			ranged_closest = target
			ranged_d_sq = d_sq
	if melee_closest != null:
		meleeing = true
		combat_target = melee_closest
	else:
		meleeing = false
		if ranged_closest != null:
			combat_target = ranged_closest
	combat_target_changed.emit(melee_closest != null)
	
func try_lay_trap() -> void:
	if !is_on_floor():
		return
	
	if data.traps[data.active_trap].quantity < 1:
		return
		
	var square : Vector3i = (global_position - Vector3.ONE * .5).round()
	if !Game.level.is_valid_trap_square(square):
		return
	
	action_tween = create_tween()
	input_locked = true
	action_tween.tween_interval(.2)
	action_tween.tween_callback(Callable(lay_trap).bind(square, data.active_trap))
	action_tween.tween_interval(.25)
	action_tween.tween_callback(clear_action)
	
func lay_trap(square : Vector3i, idx : int) -> void:
	var type : Trap.Type = data.traps[idx].type
	var trap = trap_template.instantiate()
	trap.setup(type, facing, id)
	trap.disarmed.connect(_on_trap_disarmed)
	trap.activated.connect(_on_trap_activated)
	data.traps[idx].quantity -= 1
	trap_quantity_changed.emit(idx, data.traps[idx].quantity)
	Game.level.add_trap(trap, square)
	trap_sound.play()

func clear_action() -> void:
	input_locked = false
	action_tween = null


func detect_alert() -> void:
	detect_sound.play()
	detect_icon.visible = true
	if detect_tween != null:
		detect_tween.stop()
	detect_tween = create_tween()
	detect_tween.tween_interval(.75)
	detect_tween.tween_property(detect_icon, "visible", false, 0)
	detect_tween.tween_callback(func(): detect_tween = null)

func close_modal() -> void:
	if modal != null:
		modal.queue_free()
		modal = null

func show_remove_trap_modal() -> void:
	stop_detecting()
	modal = remove_trap_modal.instantiate()
	modal.square = current_square
	Game.level.add_child(modal)

func show_disarm_trap_modal() -> void:
	stop_detecting()
	modal = disarm_trap_modal.instantiate()
	modal.difficulty = Game.level.difficulty
	modal.square = current_square

	var trap = Game.level.traps[current_square]
	trap.disarming = true
	trap.disarm_id = Multiplayer.id
	trap.removed.connect(modal._on_trap_removed)
	trap.activated.connect(modal._on_trap_failed)
	harmed.connect(modal._on_trap_failed)
	Game.level.add_child(modal)

func start_detecting() -> void:
	detecting = true
	update_detect_region.rpc(false)

@rpc("authority")
func update_detect_region(update : bool) -> void:
	var new_square : Vector3i = (global_position - Vector3.ONE * .5).round()
	if update and new_square == current_square:
		return
	current_square = new_square
	var new_squares : Dictionary[Vector3i, bool] = {}
	for i in range(-2, 3):
		for j in range(-2, 3):
			for k in range(-2, 2):
				if abs(i) + abs(j) < 3:
					var sq = current_square + Vector3i(i, k, j)
					new_squares[sq] = true
	
	if update:
		for sq in detect_squares.keys():
			if !new_squares.has(sq):
				Game.level.detect_square(sq, false)

	var remove_list = []
	var trap_detected : bool = false
	for sq in new_squares.keys():
		if update and detect_squares.has(sq):
			continue
		if !Game.level.detect_square(sq, true):
			remove_list.append(sq)
		else:
			var trap = Game.level.get_square_trap(sq + Vector3i(0,1,0))
			if trap and trap.is_just_revealed():
				trap_detected = true
	
	if trap_detected:
		detect_alert()
	
	detect_squares = new_squares
	
	for key in remove_list:
		detect_squares.erase(key)
	
	var trap : Trap = Game.level.get_square_trap(current_square)
	if trap != null:
		if trap.trap_owner == Multiplayer.id:
			show_remove_trap_modal()
		else:
			trap.disarming = true
			show_disarm_trap_modal()

func stop_detecting() -> void:
	detecting = false
	clear_detect_region()

@rpc("authority")
func clear_detect_region() -> void:
	for sq in detect_squares.keys():
		Game.level.detect_square(sq, false)
	detect_squares = {}

@rpc("authority", "call_local", "reliable")
func setup(id : int, traps : Array, pos : Vector3) -> void:
	self.id = id
	self.global_position = pos
	var traplist : Array[PawnLevelData.TrapData] = []
	for trap in traps:
		traplist.append(PawnLevelData.TrapData.new(trap.type, trap.qty, trap.qty))
	$Data.traps = traplist
	input.set_multiplayer_authority(id)
	struggling.connect(body._on_struggle_changed)
	Game.setup_player(self)

func remove_trap_at(square) -> void:
	var trap : Trap = Game.level.traps[square]
	for i in range(len(data.traps)):
		var d = data.traps[i]
		if d.type == trap.type:
			d.quantity += 1
			trap_quantity_changed.emit(i, d.quantity)
			break
	trap.remove()
	Game.level.traps.erase(square)

func cycle_active_trap(dir) -> void:
	var prev = data.active_trap
	data.active_trap += dir
	if data.active_trap < 0:
		data.active_trap = 0
	
	if data.active_trap >= len(data.traps):
		data.active_trap = len(data.traps) - 1
	
	if prev != data.active_trap:
		trap_cycled.emit(data.active_trap)

func can_hurt() -> bool:
	return true

func hurt(damage : int) -> void:
	data.life = max(0, data.life - damage)
	if damage > 0:
		harmed.emit()
	if struggle_timer > 0:
		struggle_timer = 0
		struggling.emit(struggle_timer)
		knockdown(Vector3(0,0,-1))
	health_changed.emit(data.life, data.max_life)

func _on_trap_disarmed(type : Trap.Type) -> void:
	for i in range(len(data.traps)):
		var d = data.traps[i]
		if d.type == type:
			d.max -= 1
			trap_quantity_changed.emit(i, d.quantity)
			break
#hurt
#blast
#blast_players
#activate
#activate_trap
#fail
#on_trap_failed
func _on_trap_activated(type : Trap.Type) -> void:
	for i in range(len(data.traps)):
		var d = data.traps[i]
		if d.type == type:
			d.quantity = min(d.max, d.quantity+ 1)
			trap_quantity_changed.emit(i, d.quantity)
			break

func detonate() -> void:
	var switch_list = []
	for trap : Trap in Game.level.traps.values():
		if trap.type == Trap.Type.SWITCH and trap.trap_owner == id:
			switch_list.append(trap)
	detonate_sound.play()
	
	for trap : Trap in switch_list:
		trap.activate()

func is_poisoned() -> bool:
	return poison_time_remaining > 0

func poison(damage : int, length : float) -> void:
	if is_poisoned():
		if damage > poison_strength:
			poison_strength = damage
		if length > poison_time_remaining:
			poison_time_remaining = length
	else:
		poison_strength = damage
		poison_time_remaining = length
		poison_pulse_timer = 0
		var pshader : ShaderMaterial = body.material.next_pass
		pshader.set_shader_parameter("strength", 0.5)
		poison_status_changed.emit(true)

func fling(direction : Vector3, speed : float) -> void:
	state = State.FLUNG
	fling_direction = direction
	fling_speed = speed
	fling_sound.play()

func knockdown(direction : Vector3) -> void:
	state = State.KNOCKDOWN
	input_locked = true
	body.look_at(Vector3(0,1,0), direction)
	fling_sound.stop()
	crash_sound.play()
	var knockdown_tween = create_tween()
	knockdown_tween.tween_interval(1.5)
	knockdown_tween.tween_property(self, "state", State.NORMAL, 0)
	knockdown_tween.tween_property(self, "input_locked", false, 0)
	knockdown_tween.tween_callback(Callable(body.look_at).bind(facing, Vector3(0,1,0)))
	
func knockup(velocity : Vector3) -> void:
	if state != State.KNOCKUP:
		state = State.KNOCKUP
		input_locked = true
		if is_on_floor():
			self.velocity = Vector3.UP * .1
			move_and_slide()
	self.velocity = velocity
	
		

func pitfall(duration : float) -> void:
	struggle_timer = duration
	body.struggle_bar.max_value = struggle_timer
	body.struggle_bar.value = struggle_timer
	body.show_struggle()

func start_pitfall(square : Vector3, duration : float) -> void:
	input_locked = true
	velocity = Vector3.ZERO
	var tween = create_tween()
	tween.tween_property(self, "position", square + Vector3(.5,0,.5), 0.25)
	tween.tween_property(self, "input_locked", false, 0)
	tween.tween_callback(pitfall.bind(duration))
