extends CharacterBody3D

var npc : bool = true

@export var waypoint_route : Node3D = null
var waypoints : Array

var walk_speed : float = 2.0
var run_speed : float = 5.0
var time_suspicionbuild : float = 3.0
var time_suspiciondecay : float = 2.0
var time_cooldown : float = 2.0

# var ragdolling : bool = false

var seeing_player : bool = false
var move_this_frame : bool = false
var unpause_this_frame : bool = false
var on_route : bool = false

@export var behavior_type : String = "guard"
var state : String = "default"
var current_waypoint : int = -1
var paused : bool = false
var pauseanim : String
var pauseturn : bool
var pauseangle : float
var lastseenpos : Vector3
var pathupdateinterval : float = 0.25

var player : Node

@export var appearance : String = "guard"

var current_path : PackedVector3Array
@onready var default_3d_map_rid : RID = get_world_3d().get_navigation_map()
var current_path_index : int = 0
var current_path_point : Vector3
var path_end_point : Vector3
@onready var overheadstatus : Control = $StatusSprite/SubViewport/OverheadStatus

func _ready() -> void:
	overheadstatus.set_nothing()
	overheadstatus.set_timer(-1)
	
	call_deferred("init_route")
	
	$Timers/WaypointPause.timeout.connect(timeout_waypointpause)
	$Timers/Suspicion.timeout.connect(timeout_suspicion)
	$Timers/Sight.timeout.connect(timeout_sight)
	
	if appearance == "guard":
		$CharacterArmature/Skeleton3D/head_guard.visible = true
		$CharacterArmature/Skeleton3D/body_guard.visible = true
		$CharacterArmature/Skeleton3D/feet_guard.visible = true
		$CharacterArmature/Skeleton3D/legs_guard.visible = true
	elif appearance == "barista":
		$CharacterArmature/Skeleton3D/head_barista.visible = true
		$CharacterArmature/Skeleton3D/body_barista.visible = true
		$CharacterArmature/Skeleton3D/feet_barista.visible = true
		$CharacterArmature/Skeleton3D/legs_barista.visible = true

func _physics_process(delta: float) -> void:
	seeing_player = checksight()
	ai_process()
	if move_this_frame:
		movement_process(delta)
	move_and_slide()
	animate()

func impact():
	# ragdolling = true
	state = "unconscious"
	set_collision_layer_value(1,0)
	# $CharacterArmature/Skeleton3D/ragdoll.active = true
	# $CharacterArmature/Skeleton3D/ragdoll.physical_bones_start_simulation()
	$AnimationPlayer.play("Death")
	stand_still()
	clearpath()

func ai_process():
	if unpause_this_frame:
		paused = false
		unpause_this_frame = false
	
	if state == "to_default":
		state = "default"
	elif state == "to_cooldown":
		clearpath()
		state = "cooldown"
		$Timers/Suspicion.stop()
		$Timers/Suspicion.start(time_cooldown)
	elif state == "unconscious":
		on_route = false
		$Timers/Suspicion.stop()
		$Timers/Sight.stop()
		overheadstatus.set_nothing()
		overheadstatus.set_timer(-1)
	
	if behavior_type == "guard":		
		if state == "suspicion_escalatecheck":
			on_route = false
			$Timers/Suspicion.stop()
			$Timers/Sight.stop()
			
			if seeing_player:
				setpath(lastseenpos)
				overheadstatus.set_exclamationpoint()
				overheadstatus.set_timer(-1)
				$Timers/Sight.start(pathupdateinterval)
				state = "pursuit"
			else:
				state = "default"
		elif state == "pursuit_updatepath":
			$Timers/Sight.stop()
			$Timers/Sight.start(pathupdateinterval)
			setpath(lastseenpos)
			state = "pursuit"
		elif state == "cooldown_end":
			$Timers/Suspicion.stop()
			$Timers/Sight.stop()
			on_route = true
			if len(waypoints) > 1:
				current_waypoint = nearest_waypoint()
			state = "default"
		
		if state == "default":
			overheadstatus.set_nothing()
			overheadstatus.set_timer(-1)
			
			if paused:
				$AnimationPlayer.play(pauseanim)
				if pauseturn == true:
					lerp_toward(pauseangle)
			
			if seeing_player:
				$Timers/Suspicion.stop()
				$Timers/Suspicion.start(time_suspicionbuild)
				state = "suspicion"
				if paused:
					unpause_this_frame = true
					$Timers/WaypointPause.stop()
			else:
				on_route = true
				move_this_frame = true
		elif state == "suspicion":
			clearpath()
			on_route = false
			overheadstatus.set_questionmark()
			var timerpercent : float = ((time_suspicionbuild - $Timers/Suspicion.time_left) / time_suspicionbuild) * 100
			overheadstatus.set_timer(timerpercent)
			lerp_toward(angle_toward(lastseenpos))
			
			if seeing_player:
				if $Timers/Suspicion.paused:
					$Timers/Suspicion.paused = false
				if !$Timers/Sight.is_stopped():
					$Timers/Sight.stop()
					$Timers/Sight.start(time_suspiciondecay)
			elif !seeing_player:
				if !$Timers/Suspicion.paused:
					$Timers/Suspicion.paused = true
				if $Timers/Sight.is_stopped():
					$Timers/Sight.start(time_suspiciondecay)
		elif state == "pursuit":
			if global_transform.origin.distance_to(player.global_transform.origin) <= 0.8:
				$Timers/Sight.stop()
				state = "to_cooldown"
			elif global_transform.origin.distance_to(path_end_point) <= 0.5:
				$Timers/Sight.stop()
				pauseangle = $CharacterArmature.global_rotation.y - deg_to_rad(90)
				clearpath()
				state = "pursuit_lookleft"
			else:
				on_route = false
				if !current_path.is_empty():
					move_this_frame = true
		elif state == "pursuit_lookleft":
			on_route = false
			
			if seeing_player:
				state = "suspicion_escalatecheck"
			else:
				lerp_toward(pauseangle, 0.05)
				var snappedangle = snapped(fposmod($CharacterArmature.global_rotation.y, TAU), 0.01)
				if snappedangle == snapped(fposmod(pauseangle, TAU), 0.01):
					pauseangle = $CharacterArmature.global_rotation.y + deg_to_rad(180)
					state = "pursuit_lookright"
		elif state == "pursuit_lookright":
			on_route = false
			
			if seeing_player:
				state = "suspicion_escalatecheck"
			else:
				lerp_toward(pauseangle, 0.05)
				var snappedangle = snapped(fposmod($CharacterArmature.global_rotation.y, TAU), 0.01)
				if snappedangle == snapped(fposmod(pauseangle, TAU), 0.01):
					pauseangle = $CharacterArmature.global_rotation.y + deg_to_rad(90)
					state = "pursuit_lookback"
		elif state == "pursuit_lookback":
			on_route = false
			
			if seeing_player:
				state = "suspicion_escalatecheck"
			else:
				lerp_toward(pauseangle, 0.05)
				var snappedangle = snapped(fposmod($CharacterArmature.global_rotation.y, TAU), 0.01)
				if snappedangle == snapped(fposmod(pauseangle, TAU), 0.01):
					state = "to_cooldown"
		elif state == "cooldown":
			overheadstatus.set_3dots()
			overheadstatus.subtractive = true
			var percent : float = ($Timers/Suspicion.time_left / time_suspiciondecay) * 100
			overheadstatus.set_timer(percent)

func movement_process(delta):
	if not is_on_floor():
		velocity += get_gravity() * delta
	
	if on_route and len(waypoints) == 1 and (global_transform.origin.distance_to(waypoints[0].global_transform.origin) < 0.6):
		if $CharacterArmature.global_rotation.y != waypoints[0].global_rotation.y:
			lerp_toward(waypoints[0].global_rotation.y)
		stand_still()
	elif !current_path.is_empty():
		current_path_point = current_path[current_path_index]
		if !paused:
			walk_toward(current_path_point)
		
		if global_transform.origin.distance_to(current_path_point) <= 0.5:
			current_path_index += 1
			if current_path_index >= current_path.size():
				if on_route:
					var wayedpoint = waypoints[current_waypoint]
					if wayedpoint.pausetime > 0:
						paused = true
						$Timers/WaypointPause.start(wayedpoint.pausetime)
						pauseanim = wayedpoint.animation
						if wayedpoint.turntoward == true:
							pauseturn = true
							pauseangle = wayedpoint.global_rotation.y
						else:
							pauseturn = false
					current_waypoint += 1
					if current_waypoint > len(waypoints)-1:
						current_waypoint = 0
					nextwaypoint()
				else:
					clearpath()
	elif NavigationServer3D.map_get_iteration_id(default_3d_map_rid) != 0 and current_path.is_empty() and on_route:
		nextwaypoint()
	else:
		stand_still()
		
	move_this_frame = false

func animate():
	if state == "unconscious":
		pass
	elif velocity.length() > 0:
		$AnimationPlayer.play("Walk")
	elif !paused or state != "default":
		$AnimationPlayer.play("Idle_Neutral")

func angle_toward(target : Vector3):
	return (Vector2(target.z, target.x) - Vector2($CharacterArmature.global_transform.origin.z, $CharacterArmature.global_transform.origin.x)).angle()

func walk_toward(point: Vector3):
	var direction = global_transform.origin.direction_to(point)		
	velocity.x = direction.x * walk_speed
	velocity.z = direction.z * walk_speed
	
	lerp_toward(atan2(velocity.x, velocity.z))

func stand_still():
	velocity.x = 0
	velocity.z = 0

func nextwaypoint():
	var waypoint = waypoints[current_waypoint]
	var target_position: Vector3 = waypoint.global_transform.origin
	setpath(target_position)

func setpath(target_position):
	clearpath()
	var start_position: Vector3 = global_transform.origin
	current_path = NavigationServer3D.map_get_path(
			default_3d_map_rid,
			start_position,
			target_position,
			true
		)
	path_end_point = current_path[len(current_path)-1]

func clearpath():
	current_path = []
	current_path_index = 0
	current_path_point = global_transform.origin
	velocity.x = 0
	velocity.z = 0
	
func checksight():
	var seenthings = $CharacterArmature/SightCollider.get_overlapping_bodies()
	
	if !seenthings.is_empty():
		for item in seenthings:
			if item.get_name() == "PlayerVisibilityShape":
				player = item.get_parent().get_parent()
				if sightraycheck(item) == true:
					return true
	
	return false

func sightraycheck(target):
	# var radius = target.shape_owner_get_shape(0,0).radius
	var height : float
	var tramsorm : Transform3D
		
	if target.get_parent().get_parent().get_name() == "Player" and player.crouched:
		height = target.shape_owner_get_shape(1,0).height
		tramsorm = target.shape_owner_get_transform(1).rotated(Vector3(0,1,0),target.global_rotation.y)
	else:
		height = target.shape_owner_get_shape(0,0).height
		tramsorm = target.shape_owner_get_transform(0).rotated(Vector3(0,1,0),target.global_rotation.y)
	
	var top = to_local(target.global_transform.origin - $CharacterArmature/SightCollider/SightRay.transform.origin + tramsorm.origin + Vector3(0, (height/2)-0.1, 0))
	$CharacterArmature/SightCollider/SightRay.target_position = top
	$CharacterArmature/SightCollider/SightRay.global_rotation = global_rotation
	$CharacterArmature/SightCollider/SightRay.force_raycast_update()
	if $CharacterArmature/SightCollider/SightRay.is_colliding():
		if $CharacterArmature/SightCollider/SightRay.get_collider().get_name() == "PlayerVisibilityShape":
			lastseenpos = target.global_transform.origin
			return true
	
	return false
	
func lerp_toward(angle: float, speed = 0.15):
	$CharacterArmature.global_rotation.y = lerp_angle($CharacterArmature.global_rotation.y, angle, speed)
	
func get_waypoints(path: Node3D):
	for point in path.get_children():
		waypoints.append(point)
		
func nearest_waypoint():
	var nearest : Vector3 = waypoints[0].global_transform.origin
	var nearest_index : int = 0
	for point in len(waypoints):
		if global_transform.origin.distance_to(waypoints[point].global_transform.origin) < global_transform.origin.distance_to(nearest):
			nearest = waypoints[point].global_transform.origin
			nearest_index = point
	return nearest_index

func init_route() -> void:
	if waypoint_route == null:
		var startpos = Marker3D.new()
		get_tree().current_scene.add_child(startpos)
		startpos.set_global_transform(global_transform)
		startpos.set_script(preload("res://scripts/waypoint.gd"))
		waypoints.append(startpos)
	else:
		get_waypoints(waypoint_route)

func timeout_waypointpause() -> void:
	if paused:
		unpause_this_frame = true
	
func timeout_suspicion() -> void:
	if behavior_type == "guard":
		if state == "suspicion":
			state = "suspicion_escalatecheck"
		if state == "pursuit_lookaround":
			state = "to_cooldown"
		elif state == "cooldown":
			state = "cooldown_end"

func timeout_sight() -> void:
	if behavior_type == "guard":
		if state == "suspicion":
			$Timers/Sight.stop()
			state = "to_default"
		elif state == "pursuit":
			$Timers/Sight.stop()
			$Timers/Sight.start(pathupdateinterval)
			if path_end_point.distance_to(lastseenpos) >= 2:
				state = "pursuit_updatepath"
