@tool
extends Control

signal result_selected(path: String, cursor: Vector2, length: int)


const DialogueConstants = preload("../constants.gd")


@export var main_view: Control
@export var code_edit: CodeEdit

@onready var input: LineEdit = %Input
@onready var search_button: Button = %SearchButton
@onready var match_case_button: CheckBox = %MatchCaseButton
@onready var replace_toggle: CheckButton = %ReplaceToggle
@onready var replace_container: VBoxContainer = %ReplaceContainer
@onready var replace_input: LineEdit = %ReplaceInput
@onready var replace_selected_button: Button = %ReplaceSelectedButton
@onready var replace_all_button: Button = %ReplaceAllButton
@onready var results_container: VBoxContainer = %ResultsContainer
@onready var result_template: HBoxContainer = %ResultTemplate

var current_results: Dictionary = {}:
	set(value):
		current_results = value
		update_results_view()
		if current_results.size() == 0:
			replace_selected_button.disabled = true
			replace_all_button.disabled = true
		else:
			replace_selected_button.disabled = false
			replace_all_button.disabled = false
	get:
		return current_results

var selections: PackedStringArray = []


func prepare() -> void:
	input.grab_focus()

	var template_label = result_template.get_node("Label")
	template_label.get_theme_stylebox(&"focus").bg_color = code_edit.theme_overrides.current_line_color
	template_label.add_theme_font_override(&"normal_font", code_edit.get_theme_font(&"font"))

	replace_toggle.set_pressed_no_signal(false)
	replace_container.hide()

	$VBoxContainer/HBoxContainer/FindContainer/Label.text = DialogueConstants.translate(&"search.find")
	input.placeholder_text = DialogueConstants.translate(&"search.placeholder")
	input.text = ""
	search_button.text = DialogueConstants.translate(&"search.find_all")
	match_case_button.text = DialogueConstants.translate(&"search.match_case")
	replace_toggle.text = DialogueConstants.translate(&"search.toggle_replace")
	$VBoxContainer/HBoxContainer/ReplaceContainer/ReplaceLabel.text = DialogueConstants.translate(&"search.replace_with")
	replace_input.placeholder_text = DialogueConstants.translate(&"search.replace_placeholder")
	replace_input.text = ""
	replace_all_button.text = DialogueConstants.translate(&"search.replace_all")
	replace_selected_button.text = DialogueConstants.translate(&"search.replace_selected")

	selections.clear()
	self.current_results = {}

#region helpers


func update_results_view() -> void:
	for child in results_container.get_children():
		child.queue_free()

	for path in current_results.keys():
		var path_label: Label = Label.new()
		path_label.text = path
		# Show open files
		if main_view.open_buffers.has(path):
			path_label.text += "(*)"
		results_container.add_child(path_label)
		for path_result in current_results.get(path):
			var result_item: HBoxContainer = result_template.duplicate()

			var checkbox: CheckBox = result_item.get_node("CheckBox") as CheckBox
			var key: String = get_selection_key(path, path_result)
			checkbox.toggled.connect(func(is_pressed):
				if is_pressed:
					if not selections.has(key):
						selections.append(key)
				else:
					if selections.has(key):
						selections.remove_at(selections.find(key))
			)
			checkbox.set_pressed_no_signal(selections.has(key))
			checkbox.visible = replace_toggle.button_pressed

			var result_label: RichTextLabel = result_item.get_node("Label") as RichTextLabel
			var colors: Dictionary = code_edit.theme_overrides
			var highlight: String = ""
			if replace_toggle.button_pressed:
				var matched_word: String = "[bgcolor=" + colors.critical_color.to_html() + "][color=" + colors.text_color.to_html() + "]" + path_result.matched_text + "[/color][/bgcolor]"
				highlight = "[s]" + matched_word + "[/s][bgcolor=" + colors.notice_color.to_html() + "][color=" + colors.text_color.to_html() + "]" + replace_input.text + "[/color][/bgcolor]"
			else:
				highlight = "[bgcolor=" + colors.notice_color.to_html() + "][color=" + colors.text_color.to_html() + "]" + path_result.matched_text + "[/color][/bgcolor]"
			var text: String = path_result.text.substr(0, path_result.index) + highlight + path_result.text.substr(path_result.index + path_result.query.length())
			result_label.text = "%s: %s" % [str(path_result.line).lpad(4), text]
			result_label.gui_input.connect(func(event):
				if event is InputEventMouseButton and (event as InputEventMouseButton).button_index == MOUSE_BUTTON_LEFT and (event as InputEventMouseButton).double_click:
					result_selected.emit(path, Vector2(path_result.index, path_result.line), path_result.query.length())
			)

			results_container.add_child(result_item)


func find_in_files() -> Dictionary:
	var results: Dictionary = {}

	var q: String = input.text
	var cache = Engine.get_meta("DMCache")
	var file: FileAccess
	for path in cache.get_files():
		var path_results: Array = []
		var lines: PackedStringArray = []

		if main_view.open_buffers.has(path):
			lines = main_view.open_buffers.get(path).text.split("\n")
		else:
			file = FileAccess.open(path, FileAccess.READ)
			lines = file.get_as_text().split("\n")

		for i in range(0, lines.size()):
			var index: int = find_in_line(lines[i], q)
			while index > -1:
				path_results.append({
					line = i,
					index = index,
					text = lines[i],
					matched_text = lines[i].substr(index, q.length()),
					query = q
				})
				index = find_in_line(lines[i], q, index + q.length())

		if file != null and file.is_open():
			file.close()

		if path_results.size() > 0:
			results[path] = path_results

	return results


func get_selection_key(path: String, path_result: Dictionary) -> String:
	return "%s-%d-%d" % [path, path_result.line, path_result.index]


func find_in_line(line: String, query: String, from_index: int = 0) -> int:
	if match_case_button.button_pressed:
		return line.find(query, from_index)
	else:
		return line.findn(query, from_index)


func replace_results(only_selected: bool) -> void:
	var file: FileAccess
	var lines: PackedStringArray = []
	for path in current_results:
		if main_view.open_buffers.has(path):
			lines = main_view.open_buffers.get(path).text.split("\n")
		else:
			file = FileAccess.open(path, FileAccess.READ_WRITE)
			lines = file.get_as_text().split("\n")

		# Read the results in reverse because we're going to be modifying them as we go
		var path_results: Array = current_results.get(path).duplicate()
		path_results.reverse()
		for path_result in path_results:
			var key: String = get_selection_key(path, path_result)
			if not only_selected or (only_selected and selections.has(key)):
				lines[path_result.line] = lines[path_result.line].substr(0, path_result.index) + replace_input.text + lines[path_result.line].substr(path_result.index + path_result.matched_text.length())

		var replaced_text: String = "\n".join(lines)
		if file != null and file.is_open():
			file.seek(0)
			file.store_string(replaced_text)
			file.close()
		else:
			main_view.open_buffers.get(path).text = replaced_text
			if main_view.current_file_path == path:
				code_edit.text = replaced_text

	current_results = find_in_files()


#endregion

#region signals


func _on_search_button_pressed() -> void:
	selections.clear()
	self.current_results = find_in_files()


func _on_input_text_submitted(new_text: String) -> void:
	_on_search_button_pressed()


func _on_replace_toggle_toggled(toggled_on: bool) -> void:
	replace_container.visible = toggled_on
	if toggled_on:
		replace_input.grab_focus()
	update_results_view()


func _on_replace_input_text_changed(new_text: String) -> void:
	update_results_view()


func _on_replace_selected_button_pressed() -> void:
	replace_results(true)


func _on_replace_all_button_pressed() -> void:
	replace_results(false)


func _on_match_case_button_toggled(toggled_on: bool) -> void:
	_on_search_button_pressed()


#endregion
