class_name PankuModuleVariableTracker extends PankuModule
## Module to register and update some common environments.
##
## On module startup current scene root node registered as 'current' environment var,
## for more convenient access from interactive shell. Module constantly monitoring node
## tree afterwards and auto rebind 'current' environment in case of current scene changed.
## Also all user autoload singletons registered with its root node names.

const PROJECT_AUTOLOAD_PREFIX := "autoload/"
const CURRENT_SCENE_ENV := "current"
const SHELL_MODULE_NAME := "interactive_shell"
const DEFAULT_TRACKING_DELAY := 0.5

const CURRENT_REGISTERED_TIP := "[tip] Node '%s' registered as current scene, you can access it by [b]%s[/b]."
const CURRENT_REMOVED_TIP := "[tip] No current scene found, [b]%s[/b] keyword is no longer available."
const USER_AUTOLOADS_TIP := "[tip] Accessible user singleton modules: [b]%s[/b]"

var _raw_exceptions_string: String = ""
var _nodes_exception_regexp: RegEx

var _reverse_root_nodes_order: bool
var _current_scene_root:Node
var _user_singleton_files := []
var _tween_loop:Tween
var _loop_call_back:CallbackTweener


func init_module():
	get_module_opt().tracking_delay = load_module_data("tracking_delay", DEFAULT_TRACKING_DELAY)
	_reverse_root_nodes_order = load_module_data("use_last_as_current", true)
	_raw_exceptions_string = load_module_data("root_node_exceptions", _raw_exceptions_string)

	await core.get_tree().process_frame # not sure if it is necessary

	update_exceptions_regexp()
	_update_project_singleton_files()
	_setup_scene_root_tracker()
	_check_autoloads()


# Build root node exceptions regular expression
func update_exceptions_regexp() -> void:
	if _raw_exceptions_string.is_empty():
		_nodes_exception_regexp = RegEx.new() # not valid expression
		return

	_nodes_exception_regexp = RegEx.create_from_string(_raw_exceptions_string)

	if not _nodes_exception_regexp.is_valid():
		push_error("Can't parse '%s' expression for variable tracker" % _raw_exceptions_string)


# Parse project setting and collect and autoload files.
func _update_project_singleton_files() -> void:
	_user_singleton_files.clear()
	for property in ProjectSettings.get_property_list():
		if property.name.begins_with(PROJECT_AUTOLOAD_PREFIX):
			_user_singleton_files.append(ProjectSettings.get_setting(property.name).trim_prefix("*"))


# Check if given node is autoload singleton.
func _is_singleton(node: Node) -> bool:
	# Comparing scene file and script file with list of autoload files
	# from project settings. I'm not sure that approach hundred percent perfect,
	# but it works so far.
	if node.scene_file_path in _user_singleton_files:
		return true

	var script = node.get_script()
	if script and (script.get_path() in _user_singleton_files):
		return true

	return false


# Setup monitoring loop for current scene root node.
func _setup_scene_root_tracker() -> void:
	_check_current_scene()
	# The whole idea looping something in the background
	# while dev console is not even opened does not feel so right.
	# Have no idea how to make it more elegant way,
	# so lets make loop interval user controllable at least.
	var tracking_delay = get_module_opt().tracking_delay

	_tween_loop = core.create_tween()
	_loop_call_back = _tween_loop.set_loops().tween_callback(_check_current_scene).set_delay(tracking_delay)


## Set current scene root node monitoring interval.
func change_tracking_delay(delay: float) -> void:
	if _loop_call_back:
		_loop_call_back.set_delay(delay)


# Update current scene root node environment.
func _check_current_scene() -> void:
	var scene_root_found: Node = get_scene_root()

	if scene_root_found:
		if scene_root_found != _current_scene_root:
			core.gd_exprenv.register_env(CURRENT_SCENE_ENV, scene_root_found)
			_print_to_interactive_shell(CURRENT_REGISTERED_TIP % [scene_root_found.name, CURRENT_SCENE_ENV])

	else:
		if _current_scene_root:
			core.gd_exprenv.remove_env(CURRENT_SCENE_ENV)
			_print_to_interactive_shell(CURRENT_REMOVED_TIP % CURRENT_SCENE_ENV)

	_current_scene_root = scene_root_found


## Find the root node of current active scene.
func get_scene_root() -> Node:
	# Assuming current scene is the first node in tree that is not autoload singleton.
	for node in _get_valid_root_nodes():
		if not _is_singleton(node):
			return node

	return null


# Get list of tree root nodes filtered and sorted according module settings
func _get_valid_root_nodes() -> Array:
	var nodes: Array = core.get_tree().root.get_children().filter(_root_nodes_filter)

	if _reverse_root_nodes_order:
		nodes.reverse()

	return nodes


# Filter function for tree root nodes
func _root_nodes_filter(node: Node) -> bool:
	# skip panku plugin itself
	if node.name == core.SingletonName:
		return false

	# skip os window
	if node.get_meta(PankuLynxWindow.OS_WINDOW_MARKER, false):
		return false

	# skip user defined exceptions
	if _nodes_exception_regexp.is_valid() and _nodes_exception_regexp.search(node.name):
		return false

	return true


# Find all autoload singletons and bind its to environment vars.
func _check_autoloads() -> void:
	var _user_singleton_names := []

	for node in _get_valid_root_nodes():
		if _is_singleton(node):
			# register user singleton
			_user_singleton_names.append(node.name)
			core.gd_exprenv.register_env(node.name, node)

	if not _user_singleton_names.is_empty():
		_print_to_interactive_shell(USER_AUTOLOADS_TIP % ",".join(_user_singleton_names))


# Print a tip to interactive shell module, modules load order does matter.
func _print_to_interactive_shell(message: String) -> void:
	if core.module_manager.has_module(SHELL_MODULE_NAME):
		var ishell = core.module_manager.get_module(SHELL_MODULE_NAME)
		ishell.interactive_shell.output(message)


func quit_module():
	_tween_loop.kill()
	super.quit_module()
