@tool
@icon("dynamic_area_loader.svg")
class_name DynamicAreaLoader3D
extends Area3D

## @deprecated: Replaced by LevelArea
## An [Area3D] that dynamicly load a scene based on proximity.
##
## [b]Note:[/b] By default the [signal body_entered] and [signal body_exited] signals are linked
## to private [method _on_body_entered] and [method _on_body_exited] functions that check
## if the [param body] is the [member GameGlobals.player] and (un)load the area based on that.

## The signal emitted when the area to be loaded finished loading and was added to the scene tree.[br]
## You can combine this for eg. a door that only opens once the area behind it finished loading.
signal load_finished(loaded_node: Node)


## The node to load and add.[br][br]
## [b]IMPORTANT:[/b] The node needs to be marked as [b]Load as Placeholder[/b], otherwise nothing
## happens when loading (see [InstancePlaceholder]).
@export var placeholder_node: Node:
	set(value):
		placeholder_node = value
		update_configuration_warnings()
## If [code]true[/code], the actual loading will be done separately in a thread.[br]
## This prevents the game from freezing for a moment when loading a large scene.[br]
## However, loading can be slower on lower-end hardware.
@export var threaded: bool = true
## How long [i](in seconds)[/i] the area will still be loaded and inside the scene tree
## when the [PlayerCharacter] exits this area.[br]This is to prevent the [PlayerCharacter]
## from keep re-loading the area by just walking back and fourth through the load zones.
@export_range(0.0, 10.0, 0.01, "or_greater", "suffix:s") var keep_loaded_duration: float = 3.0
## Automatically load the area when entering the scene tree, if the [member GameGlobals.spawn_point]
## is set to any of these values.
@export var loaded_if_spawnpoint: Array[int] = []

## The reference to the currently loaded node that was loaded.
var loaded_node: Node


func _ready() -> void:
	if Engine.is_editor_hint():
		return

	if loaded_if_spawnpoint.has(GameGlobals.spawn_index):
		load_area(null, true)

	body_entered.connect(_on_body_entered)
	body_exited.connect(_on_body_exited)


## Instances the area defined by [member placeholder_node].[br]
## See [method load_area_threaded] if you want to load the area in a thread instead
## of having a freeze during load.[br][br]
## If [param forced] is [code]true[/code], don't check if the player is colliding when calling this function.
func load_area(custom_scene: PackedScene = null, forced: bool = false) -> void:
	assert(is_instance_valid(placeholder_node), "[placeholder_node] needs to be set.")
	assert(placeholder_node is InstancePlaceholder, "[placeholder_node] needs to be marked as 'Load As Placeholder'.")

	if is_instance_valid(loaded_node) or (not forced and not overlaps_body(GameGlobals.get_player())):
		return

	loaded_node = placeholder_node.create_instance(false, custom_scene)
	load_finished.emit(loaded_node)


## Loads the scene defined in
## [member placeholder_node] ([member InstancePlaceholder.get_instance_path]) threaded.[br]
## Calls [method load_area] with the [param custom_scene] set to the loaded scene.[br][br]
## [param forced] does the same thing as in [method load_area].
func load_area_threaded(forced: bool = false) -> void:
	assert(is_instance_valid(placeholder_node), "[placeholder_node] needs to be set.")
	assert(placeholder_node is InstancePlaceholder, "[placeholder_node] needs to be marked as 'Load As Placeholder'.")

	placeholder_node = placeholder_node as InstancePlaceholder

	var path: String = placeholder_node.get_instance_path()
	ResourceLoader.load_threaded_request(path, "", true)

	while ResourceLoader.load_threaded_get_status(path) == ResourceLoader.THREAD_LOAD_IN_PROGRESS:
		if not is_inside_tree():
			return

		await get_tree().process_frame

	var scene: PackedScene = ResourceLoader.load_threaded_get(path)
	load_area(scene, forced)


## Unloads ([method Node.queue_free]) the [member loaded_node].[br]
## If [param instantly_unload] is [code]true[/code], don't wait for the
## [member keep_loaded_duration] to end and instantly free the node.
func unload_area(instantly_unload: bool = false) -> void:
	if not is_instance_valid(loaded_node):
		return

	if not instantly_unload and keep_loaded_duration > 0.0:
		var duration: float = keep_loaded_duration

		while is_inside_tree() and duration > 0.0:
			if overlaps_body(GameGlobals.get_player()):
				return

			duration -= get_process_delta_time()
			await get_tree().process_frame

	loaded_node.queue_free()


func _on_body_entered(body: Node3D) -> void:
	if body == GameGlobals.get_player():
		if threaded:
			load_area_threaded()
		else:
			load_area()


func _on_body_exited(body: Node3D) -> void:
	if body == GameGlobals.get_player():
		unload_area()


func _get_configuration_warnings() -> PackedStringArray:
	if not is_instance_valid(placeholder_node):
		return ["'placeholder_node' is not set."]

	# TODO: Find a way to detect that in the editor.
	if placeholder_node is not InstancePlaceholder and false:
		return ["'placeholder_node' needs to be marked as 'Load As Placeholder'"]

	return []
