@tool
extends Node
## Script for automatically copying Godot scenes and scripts without UIDs.

signal canceled
signal completed(target_path : String)

const UID_PREG_MATCH = r'uid="uid:\/\/[0-9a-z]+" '
const RUNNING_CHECK_DELAY : float = 0.25
const RESAVING_DELAY : float = 1.0
const RAW_COPY_EXTENSIONS : Array = ["gd", "md", "txt"]
const OMIT_COPY_EXTENSIONS : Array = ["uid"]
const REPLACE_CONTENT_EXTENSIONS : Array = ["gd", "tscn", "tres", "md"]

@onready var destination_dialog : FileDialog = $DestinationDialog

@export_dir var relative_path : String :
	set(value):
		relative_path = value
		if not relative_path.ends_with("/"):
			relative_path += "/"
@export var replace_strings_map : Dictionary
@export var visible : bool = true :
	set(value):
		visible = value
		if is_inside_tree():
			destination_dialog.visible = visible

func show() -> void:
	visible = true

func hide() -> void:
	visible = false

func close() -> void:
	queue_free()

func _remove_uids(content : String) -> String:
	var regex = RegEx.new()
	regex.compile(UID_PREG_MATCH)
	return regex.sub(content, "", true)

func _replace_paths(content : String, target_path : String) -> String:
	return content.replace(relative_path.trim_prefix("res://"), target_path.trim_prefix("res://"))

func _replace_strings(content : String) -> String:
	for key in replace_strings_map:
		var value : String = replace_strings_map[key]
		content = content.replace(key, value)
	return content

func _replace_content(content : String, target_path : String) -> String:
	var replaced_content : String
	replaced_content = _remove_uids(content)
	replaced_content = _replace_paths(replaced_content, target_path)
	replaced_content = _replace_strings(replaced_content)
	return replaced_content

func _replace_file_contents(file_path : String, target_path : String) -> void:
	var extension : String = file_path.get_extension()
	if extension not in REPLACE_CONTENT_EXTENSIONS:
		return
	var file = FileAccess.open(file_path, FileAccess.READ)
	if file == null:
		push_error("plugin error - null file: `%s`" % file_path)
		return
	var original_content = file.get_as_text()
	file.close()
	var replaced_content := _replace_content(original_content, target_path)
	if replaced_content == original_content: return
	file = FileAccess.open(file_path, FileAccess.WRITE)
	file.store_string(replaced_content)
	file.close()

func _save_resource(resource_path : String, resource_destination : String, whitelisted_extensions : PackedStringArray = []) -> Error:
	var extension : String = resource_path.get_extension()
	if whitelisted_extensions.size() > 0:
		if not extension in whitelisted_extensions:
			return OK
	if extension == "import":
		# skip import files
		return OK
	var file_object = load(resource_path)
	if file_object is Resource:
		var possible_extensions = ResourceSaver.get_recognized_extensions(file_object)
		if possible_extensions.has(extension):
			return ResourceSaver.save(file_object, resource_destination, ResourceSaver.FLAG_CHANGE_PATH)
		else:
			return ERR_FILE_UNRECOGNIZED
	else:
		return ERR_FILE_UNRECOGNIZED
	return OK

func _raw_copy_file_path(file_path : String, destination_path : String) -> Error:
	var dir := DirAccess.open("res://")
	var error := dir.copy(file_path, destination_path)
	return error

func _copy_file_path(file_path : String, destination_path : String, target_path : String) -> Error:
	var error : Error
	if file_path.get_extension() in OMIT_COPY_EXTENSIONS:
		return error
	if file_path.get_extension() in RAW_COPY_EXTENSIONS:
		error = _raw_copy_file_path(file_path, destination_path)
	else:
		error = _save_resource(file_path, destination_path)
		if error == ERR_FILE_UNRECOGNIZED:
			error = _raw_copy_file_path(file_path, destination_path)
	if not error:
		_replace_file_contents(destination_path, target_path)
	return error

func _copy_directory_path(dir_path : String, target_path : String) -> void:
	if not dir_path.ends_with("/"):
		dir_path += "/"
	var dir = DirAccess.open(dir_path)
	if dir:
		dir.list_dir_begin()
		var file_name = dir.get_next()
		var error : Error
		while file_name != "" and error == 0:
			var file_relative_path = dir_path.trim_prefix(relative_path)
			var destination_path = target_path + file_relative_path + file_name
			var full_file_path = dir_path + file_name
			if dir.current_is_dir():
				if not dir.dir_exists(destination_path):
					error = dir.make_dir(destination_path)
				_copy_directory_path(full_file_path, target_path)
			else:
				error = _copy_file_path(full_file_path, destination_path, target_path)
			file_name = dir.get_next()
		if error:
			push_error("plugin error - copying path: %s" % error)
	else:
		push_error("plugin error - accessing path: %s" % dir_path)

func _complete(target_path : String) -> void:
	completed.emit(target_path)
	close()

func _wait_for_scan_and_complete(target_path : String) -> void:
	var timer: Timer = Timer.new()
	var callable := func():
		if EditorInterface.get_resource_filesystem().is_scanning(): return
		timer.stop()
		_complete(target_path)
		timer.queue_free()
	timer.timeout.connect(callable)
	add_child(timer)
	timer.start(RUNNING_CHECK_DELAY)

func _delayed_saving_and_next_prompt(target_path : String) -> void:
	var timer: Timer = Timer.new()
	var callable := func():
		timer.stop()
		EditorInterface.save_all_scenes()
		EditorInterface.get_resource_filesystem().scan_sources()
		EditorInterface.get_resource_filesystem().scan()
		_wait_for_scan_and_complete(target_path)
		timer.queue_free()
	timer.timeout.connect(callable)
	add_child(timer)
	timer.start(RESAVING_DELAY)

func _copy_to_directory(target_path : String) -> void:
	if not target_path.ends_with("/"):
		target_path += "/"
	_copy_directory_path(relative_path, target_path)
	_delayed_saving_and_next_prompt(target_path)

func _on_destination_dialog_dir_selected(dir):
	_copy_to_directory(dir)

func _on_destination_dialog_canceled():
	canceled.emit()
	close()
