@tool
extends Node
class_name ModToolUtils


# Utility functions used across the ModTool.


# ! Not used currently. This can overwrite existing text very easily if the wrong script is shown in the text editor.
static func reload_script(script: Script, mod_tool_store: ModToolStore) -> void:
	var pending_reloads := mod_tool_store.pending_reloads

	if script and script.resource_path in pending_reloads:
		var source_code_from_disc := FileAccess.open(script.resource_path, FileAccess.READ).get_as_text()

		var script_editor := EditorInterface.get_script_editor()
		var text_edit: CodeEdit = script_editor.get_current_editor().get_base_editor()

		var column := text_edit.get_caret_column()
		var row := text_edit.get_caret_line()
		var scroll_position_h := text_edit.get_h_scroll_bar().value
		var scroll_position_v := text_edit.get_v_scroll_bar().value

		text_edit.text = source_code_from_disc
		text_edit.set_caret_column(column)
		text_edit.set_caret_line(row)
		text_edit.scroll_horizontal = scroll_position_h
		text_edit.scroll_vertical = scroll_position_v

		text_edit.tag_saved_version()

		pending_reloads.erase(script.resource_path)


# Takes a file path and an array of file extensions [.txt, .tscn, ..]
static func is_file_extension(path: String, excluded_extensions: PackedStringArray) -> bool:
	var is_extension := false

	for extension in excluded_extensions:
		var file_name := path.get_file()
		if(extension in file_name):
			is_extension = true
			break
		else:
			is_extension = false

	return is_extension


# Returns the content of the file from the given path as a string.
static func file_get_as_text(path: String) -> String:
	var file_access := FileAccess.open(path, FileAccess.READ)
	var content := file_access.get_as_text()
	file_access.close()
	return content


# Copies a file from a given src to the specified dst path.
# src = path/to/file.extension
# dst = other/path/to/file.extension
static func file_copy(src: String, dst: String) -> void:
	var dst_dir := dst.get_base_dir()

	if not DirAccess.dir_exists_absolute(dst_dir):
		DirAccess.make_dir_recursive_absolute(dst_dir)

	DirAccess.copy_absolute(src, dst)


# Log error messages
static func output_error(message) -> void:
	printerr("ModTool Error: " + str(message))


static func output_info(message) -> void:
	print("ModTool: " + str(message))


static func save_to_manifest_json(manifest_data: ModManifest, path_manifest: String) -> bool:
	var is_success := _ModLoaderFile._save_string_to_file(
		manifest_data.to_json(),
		path_manifest
	)

	if is_success:
		output_info("Successfully saved manifest.json file!")

	return is_success


static func make_dir_recursive(dst_dir: String) -> bool:
	var error := DirAccess.make_dir_recursive_absolute(dst_dir)
	if not error == OK:
		output_error("Failed creating directory at %s with error \"%s\"" % [dst_dir, error_string(error)])
		return false
	return true


# Takes a directory path to get removed.
# https://www.davidepesce.com/2019/11/04/essential-guide-to-godot-filesystem-api/
static func remove_recursive(path: String) -> void:
	var directory := DirAccess.open(path)

	if not directory:
		print("Error removing " + path)
		return

	# List directory content
	directory.list_dir_begin()
	var file_name := directory.get_next()
	while file_name != "":
		if directory.current_is_dir():
			remove_recursive(path + "/" + file_name)
		else:
			directory.remove(file_name)
		file_name = directory.get_next()

	# Remove current path
	directory.remove(path)


static func check_for_hooked_script(script_paths: Array[String], mod_tool_store: ModToolStore) -> int:
	var count := 0

	for script_path in script_paths:
		if mod_tool_store.hooked_scripts.has(script_path):
			count += 1

	return count


static func quote_string(string: String) -> String:
	var settings: EditorSettings = EditorInterface.get_editor_settings()
	if settings.get_setting("text_editor/completion/use_single_quotes"):
		return "'%s'" % string
	return "\"%s\"" % string


static func script_has_method(script_path: String, method: String) -> bool:
	var script: Script = load(script_path)

	for script_method in script.get_script_method_list():
		if script_method.name == method:
			return true

	if method in script.source_code:
		return true

	return false


static func get_index_at_method_end(method_name: String, text: String) -> int:
	var starting_index := text.rfind(method_name)

	# Find the end of the method
	var next_method_line_index := text.find("func ", starting_index)
	var method_end := -1

	if next_method_line_index == -1:
		# Backtrack empty lines from the end of the file
		method_end = text.length() -1
	else:
		# Get the line before the next function line
		method_end = text.rfind("\n", next_method_line_index)

	# Backtrack to the last non-empty line
	var last_non_empty_line_index := method_end
	while last_non_empty_line_index > starting_index:
		last_non_empty_line_index -= 1
		# Remove spaces, tabs and newlines (whitespace) to check if the line really is empty
		if text[last_non_empty_line_index].rstrip("\t\n "):
			break # encountered a filled line

	return last_non_empty_line_index +1

# Slightly modified version of:
# https://gist.github.com/willnationsdev/00d97aa8339138fd7ef0d6bd42748f6e
# Removed .import from the extension filter.
# p_match is a string that filters the list of files.
# If p_match_is_regex is false, p_match is directly string-searched against the FILENAME.
# If it is true, a regex object compiles p_match and runs it against the FILEPATH.
static func get_flat_view_dict(
	p_dir := "res://",
 	p_match := "",
	p_match_file_extensions: Array[StringName] = [],
	p_match_is_regex := false,
	include_empty_dirs := false,
	ignored_dirs: Array[StringName] = []
) -> PackedStringArray:
	var data: PackedStringArray = []
	var regex: RegEx

	if p_match_is_regex:
		regex = RegEx.new()
		var _compile_error: int = regex.compile(p_match)
		if not regex.is_valid():
			return data

	var dirs := [p_dir]
	var first := true
	while not dirs.is_empty():
		var dir_name : String = dirs.back()
		var dir := DirAccess.open(dir_name)
		dirs.pop_back()

		if dir_name.lstrip("res://").get_slice("/", 0) in ignored_dirs:
			continue

		if dir:
			var _dirlist_error: int = dir.list_dir_begin()
			var file_name := dir.get_next()
			if include_empty_dirs and not dir_name == p_dir:
				data.append(dir_name)
			while file_name != "":
				if not dir_name == "res://":
					first = false
				# ignore hidden, temporary, or system content
				if not file_name.begins_with(".") and not file_name.get_extension() == "tmp":
					# If a directory, then add to list of directories to visit
					if dir.current_is_dir():
						dirs.push_back(dir.get_current_dir() + "/" + file_name)
					# If a file, check if we already have a record for the same name
					else:
						var path := dir.get_current_dir() + ("/" if not first else "") + file_name
						# grab all
						if not p_match and not p_match_file_extensions:
							data.append(path)
						# grab matching strings
						elif not p_match_is_regex and p_match and file_name.contains(p_match):
							data.append(path)
						# garb matching file extension
						elif p_match_file_extensions and file_name.get_extension() in p_match_file_extensions:
							data.append(path)
						# grab matching regex
						elif p_match_is_regex:
							var regex_match := regex.search(path)
							if regex_match != null:
								data.append(path)
				# Move on to the next file in this directory
				file_name = dir.get_next()
			# We've exhausted all files in this directory. Close the iterator.
			dir.list_dir_end()
	return data
