@tool
extends EditorPlugin


const DialogueConstants = preload("res://addons/dialogue_manager/constants.gd")
const DialogueImportPlugin = preload("res://addons/dialogue_manager/import_plugin.gd")
const DialogueTranslationParserPlugin = preload("res://addons/dialogue_manager/editor_translation_parser_plugin.gd")
const DialogueSettings = preload("res://addons/dialogue_manager/components/settings.gd")
const MainView = preload("res://addons/dialogue_manager/views/main_view.tscn")


var import_plugin: DialogueImportPlugin
var translation_parser_plugin: DialogueTranslationParserPlugin
var main_view

var dialogue_file_cache: Dictionary = {}


func _enter_tree() -> void:
	add_autoload_singleton("DialogueManager", "res://addons/dialogue_manager/dialogue_manager.gd")
	add_custom_type("DialogueLabel", "RichTextLabel", preload("res://addons/dialogue_manager/dialogue_label.gd"), _get_plugin_icon())
	
	if Engine.is_editor_hint():
		DialogueSettings.prepare()
		
		import_plugin = DialogueImportPlugin.new()
		import_plugin.editor_plugin = self
		add_import_plugin(import_plugin)
		
		translation_parser_plugin = DialogueTranslationParserPlugin.new()
		add_translation_parser_plugin(translation_parser_plugin)
		
		main_view = MainView.instantiate()
		main_view.editor_plugin = self
		get_editor_interface().get_editor_main_screen().add_child(main_view)
		_make_visible(false)
		
		update_dialogue_file_cache()
		get_editor_interface().get_resource_filesystem().filesystem_changed.connect(_on_filesystem_changed)
		get_editor_interface().get_file_system_dock().files_moved.connect(_on_files_moved)
		get_editor_interface().get_file_system_dock().file_removed.connect(_on_file_removed)
		
		add_tool_menu_item("Create copy of dialogue example balloon...", _copy_dialogue_balloon)


func _exit_tree() -> void:
	remove_autoload_singleton("DialogueManager")
	remove_custom_type("DialogueLabel")
	
	remove_import_plugin(import_plugin)
	import_plugin = null
	
	remove_translation_parser_plugin(translation_parser_plugin)
	translation_parser_plugin = null
	
	if is_instance_valid(main_view):
		main_view.queue_free()
	
	get_editor_interface().get_resource_filesystem().filesystem_changed.disconnect(_on_filesystem_changed)
	get_editor_interface().get_file_system_dock().files_moved.disconnect(_on_files_moved)
	
	remove_tool_menu_item("Create copy of dialogue example balloon...")


func _has_main_screen() -> bool:
	return true


func _make_visible(next_visible: bool) -> void:
	if is_instance_valid(main_view):
		main_view.visible = next_visible


func _get_plugin_name() -> String:
	return "Dialogue"


func _get_plugin_icon() -> Texture2D:
	return load("res://addons/dialogue_manager/assets/icon.svg")


func _handles(object) -> bool:
	return object is DialogueResource


func _edit(object) -> void:
	if is_instance_valid(main_view) and is_instance_valid(object):
		main_view.open_resource(object)


func _apply_changes() -> void:
	if is_instance_valid(main_view):
		main_view.apply_changes()


func _build() -> bool:
	# Ignore errors in other files if we are just running the test scene
	if DialogueSettings.get_user_value("is_running_test_scene", true): return true
	
	var can_build: bool = true
	var is_first_file: bool = true
	for dialogue_file in dialogue_file_cache.values():
		if dialogue_file.errors.size() > 0:
			# Open the first file
			if is_first_file:
				get_editor_interface().edit_resource(load(dialogue_file.path))
				main_view.show_build_error_dialog()
				is_first_file = false
			push_error("You have %d error(s) in %s" % [dialogue_file.errors.size(), dialogue_file.path])
			can_build = false
	return can_build


## Keep track of known files and their dependencies
func add_to_dialogue_file_cache(path: String, resource_path: String, parse_results: DialogueManagerParseResult) -> void:
	dialogue_file_cache[path] = {
		path = path,
		resource_path = resource_path,
		dependencies = Array(parse_results.imported_paths).filter(func(d): return d != path),
		errors = []
	}
	
	save_dialogue_cache()
	recompile_dependent_files(path)


## Keep track of compile errors
func add_errors_to_dialogue_file_cache(path: String, errors: Array[Dictionary]) -> void:
	if dialogue_file_cache.has(path):
		dialogue_file_cache[path]["errors"] = errors
	else:
		dialogue_file_cache[path] = { 
			path = path,
			errors = errors 
		}
		
	save_dialogue_cache()
	recompile_dependent_files(path)


## Update references to a moved file
func update_import_paths(from_path: String, to_path: String) -> void:
	# Update its own reference in the cache
	if dialogue_file_cache.has(from_path):
		dialogue_file_cache[to_path] = dialogue_file_cache[from_path].duplicate()
		dialogue_file_cache.erase(from_path)
	
	# Reopen the file if it's already open
	if main_view.current_file_path == from_path:
		main_view.current_file_path = ""
		main_view.open_file(to_path)
	
	# Update any other files that import the moved file
	var dependents = dialogue_file_cache.values().filter(func(d): return from_path in d.dependencies)
	for dependent in dependents:
		dependent.dependencies.erase(from_path)
		dependent.dependencies.append(to_path)
		
		# Update the live buffer
		if main_view.current_file_path == dependent.path:
			main_view.code_edit.text = main_view.code_edit.text.replace(from_path, to_path)
			main_view.pristine_text = main_view.code_edit.text

		# Open the file and update the path
		var file: FileAccess = FileAccess.open(dependent.path, FileAccess.READ)
		var text = file.get_as_text().replace(from_path, to_path)
		
		file = FileAccess.open(dependent.path, FileAccess.WRITE)
		file.store_string(text)
	
	save_dialogue_cache()


## Rebuild any files that depend on this path
func recompile_dependent_files(path: String) -> void:
	# Rebuild any files that depend on this one
	var dependents = dialogue_file_cache.values().filter(func(d): return path in d.dependencies)
	for dependent in dependents:
		if dependent.has("path") and dependent.has("resource_path"):
			import_plugin.compile_file(dependent.path, dependent.resource_path, false)


## Make sure the cache points to real files
func update_dialogue_file_cache() -> void:
	var cache: Dictionary = {}
	
	# Open our cache file if it exists
	if FileAccess.file_exists(DialogueConstants.CACHE_PATH):
		var file: FileAccess = FileAccess.open(DialogueConstants.CACHE_PATH, FileAccess.READ)
		cache = JSON.parse_string(file.get_as_text())
	
	# Scan for dialogue files
	var current_files: PackedStringArray = _get_dialogue_files_in_filesystem()
	
	# Add any files to POT generation
	var files_for_pot: PackedStringArray = ProjectSettings.get_setting("internationalization/locale/translations_pot_files", [])
	var files_for_pot_changed: bool = false
	for path in current_files:
		if not files_for_pot.has(path):
			files_for_pot.append(path)
			files_for_pot_changed = true
	
	# Remove any files that don't exist any more
	for path in cache.keys():
		if not path in current_files:
			cache.erase(path)
			DialogueSettings.remove_recent_file(path)
			
			# Remove missing files from POT generation
			if files_for_pot.has(path):
				files_for_pot.remove_at(files_for_pot.find(path))
				files_for_pot_changed = true
	
	# Update project settings if POT changed
	if files_for_pot_changed:
		ProjectSettings.set_setting("internationalization/locale/translations_pot_files", files_for_pot)
		ProjectSettings.save()
	
	dialogue_file_cache = cache


## Persist the cache
func save_dialogue_cache() -> void:
	var file: FileAccess = FileAccess.open(DialogueConstants.CACHE_PATH, FileAccess.WRITE)
	file.store_string(JSON.stringify(dialogue_file_cache))


## Recursively find any dialogue files in a directory
func _get_dialogue_files_in_filesystem(path: String = "res://") -> PackedStringArray:
	var files: PackedStringArray = []
	
	if DirAccess.dir_exists_absolute(path):
		var dir = DirAccess.open(path)
		dir.list_dir_begin()
		var file_name = dir.get_next()
		while file_name != "":
			var file_path: String = (path + "/" + file_name).simplify_path()
			if dir.current_is_dir():
				if not file_name in [".godot", ".tmp"]:
					files.append_array(_get_dialogue_files_in_filesystem(file_path))
			elif file_name.get_extension() == "dialogue":
				files.append(file_path)
			file_name = dir.get_next()
	
	return files


### Callbacks


func _copy_dialogue_balloon() -> void:
	var scale: float = get_editor_interface().get_editor_scale()
	var directory_dialog: FileDialog = FileDialog.new()
	var label: Label = Label.new()
	label.text = "Dialogue balloon files will be copied into chosen directory."
	directory_dialog.get_vbox().add_child(label)
	directory_dialog.file_mode = FileDialog.FILE_MODE_OPEN_DIR
	directory_dialog.min_size = Vector2(600, 500) * scale
	directory_dialog.dir_selected.connect(func(path):
		var file: FileAccess = FileAccess.open("res://addons/dialogue_manager/example_balloon/example_balloon.tscn", FileAccess.READ)
		var file_contents: String = file.get_as_text().replace("res://addons/dialogue_manager/example_balloon/example_balloon.gd", path + "/balloon.gd")
		file = FileAccess.open(path + "/balloon.tscn", FileAccess.WRITE)
		file.store_string(file_contents)
		file.close()
		
		file = FileAccess.open("res://addons/dialogue_manager/example_balloon/small_example_balloon.tscn", FileAccess.READ)
		file_contents = file.get_as_text().replace("res://addons/dialogue_manager/example_balloon/example_balloon.gd", path + "/balloon.gd")
		file = FileAccess.open(path + "/small_balloon.tscn", FileAccess.WRITE)
		file.store_string(file_contents)
		file.close()
		
		file = FileAccess.open("res://addons/dialogue_manager/example_balloon/example_balloon.gd", FileAccess.READ)
		file_contents = file.get_as_text()
		file = FileAccess.open(path + "/balloon.gd", FileAccess.WRITE)
		file.store_string(file_contents)
		file.close()
		
		get_editor_interface().get_resource_filesystem().scan()
		get_editor_interface().get_file_system_dock().call_deferred("navigate_to_path", path + "/balloon.tscn")
		
		directory_dialog.queue_free()
	)
	get_editor_interface().get_base_control().add_child(directory_dialog)
	directory_dialog.popup_centered()


### Signals


func _on_filesystem_changed() -> void:
	update_dialogue_file_cache()


func _on_files_moved(old_file: String, new_file: String) -> void:
	update_import_paths(old_file, new_file)
	DialogueSettings.move_recent_file(old_file, new_file)


func _on_file_removed(file: String) -> void:
	recompile_dependent_files(file)
	if is_instance_valid(main_view):
		main_view.close_file(file)
