@tool
extends Node
class_name VisualizerTools
## Crawls a Godot project and parses all GDScript files to build a project map.

var _editor_plugin: EditorPlugin = null
var _scene_tools_ref: Node = null

func set_editor_plugin(plugin: EditorPlugin) -> void:
	_editor_plugin = plugin

func set_scene_tools_ref(scene_tools: Node) -> void:
	_scene_tools_ref = scene_tools

func map_project(args: Dictionary) -> Dictionary:
	"""Crawl the entire project and build a structural map of all scripts."""
	var root_path: String = str(args.get(&"root", "res://"))
	var include_addons: bool = bool(args.get(&"include_addons", false))

	if not root_path.begins_with("res://"):
		root_path = "res://" + root_path

	# Collect all .gd files
	var script_paths: Array = []
	_collect_scripts(root_path, script_paths, include_addons)

	if script_paths.is_empty():
		return {&"ok": false, &"error": "No GDScript files found in " + root_path}

	# Parse each script
	var nodes: Array = []
	var class_map: Dictionary = {}  # class_name -> path

	for path: String in script_paths:
		var info: Dictionary = _parse_script(path)
		nodes.append(info)
		if info.get(&"class_name", "") != "":
			class_map[info[&"class_name"]] = path

	# Build edges
	var edges: Array = []
	for node: Dictionary in nodes:
		var from_path: String = node[&"path"]

		# extends relationship (resolve class_name to path)
		var extends_class: String = node.get(&"extends", "")
		if extends_class in class_map:
			edges.append({&"from": from_path, &"to": class_map[extends_class], &"type": "extends"})

		# preload/load references
		for ref: String in node.get(&"preloads", []):
			if ref.ends_with(".gd"):
				edges.append({&"from": from_path, &"to": ref, &"type": "preload"})

		# signal connections
		for conn: Dictionary in node.get(&"connections", []):
			var target: String = conn.get(&"target", "")
			if target in class_map:
				edges.append({&"from": from_path, &"to": class_map[target], &"type": "signal", &"signal_name": conn.get(&"signal", "")})

	return {
		&"ok": true,
		&"project_map": {
			&"nodes": nodes,
			&"edges": edges,
			&"total_scripts": nodes.size(),
			&"total_connections": edges.size()
		}
	}

func _collect_scripts(path: String, results: Array, include_addons: bool) -> void:
	"""Recursively collect all .gd files."""
	var dir := DirAccess.open(path)
	if dir == null:
		return

	dir.list_dir_begin()
	var name := dir.get_next()
	while name != "":
		if name.begins_with("."):
			name = dir.get_next()
			continue

		var full_path := path.path_join(name)

		if dir.current_is_dir():
			if name == "addons" and not include_addons:
				name = dir.get_next()
				continue
			_collect_scripts(full_path, results, include_addons)
		elif name.ends_with(".gd"):
			results.append(full_path)

		name = dir.get_next()
	dir.list_dir_end()

func _parse_script(path: String) -> Dictionary:
	"""Parse a GDScript file and extract its structure."""
	var file := FileAccess.open(path, FileAccess.READ)
	if file == null:
		return {&"path": path, &"error": "Cannot open file"}

	var content: String = file.get_as_text()
	file.close()

	var lines: PackedStringArray = content.split("\n")
	var line_count: int = lines.size()

	var description := ""
	var extends_class := ""
	var class_name_str := ""
	var variables: Array = []
	var functions: Array = []
	var signals_list: Array = []
	var preloads: Array = []
	var connections: Array = []

	# Regex patterns
	var re_desc := RegEx.new()
	re_desc.compile("^##\\s*@desc:\\s*(.+)")

	var re_extends := RegEx.new()
	re_extends.compile("^extends\\s+(\\w+)")

	var re_class_name := RegEx.new()
	re_class_name.compile("^class_name\\s+(\\w+)")

	# Match: @export var name: Type = value  OR  var name: Type  OR  var name = value
	# Captures: 1=@export, 2=@onready, 3=name, 4=type, 5=default
	var re_var := RegEx.new()
	re_var.compile("^(@export(?:\\([^)]*\\))?\\s+)?(@onready\\s+)?var\\s+(\\w+)\\s*(?::\\s*(\\w+))?(?:\\s*=\\s*(.+))?")

	# Match: func name(params) -> ReturnType:
	var re_func := RegEx.new()
	re_func.compile("^func\\s+(\\w+)\\s*\\(([^)]*)\\)\\s*(?:->\\s*(\\w+))?")

	# Match: signal name(params)
	var re_signal := RegEx.new()
	re_signal.compile("^signal\\s+(\\w+)(?:\\(([^)]*)\\))?")

	var re_preload := RegEx.new()
	re_preload.compile("(?:preload|load)\\s*\\(\\s*\"(res://[^\"]+)\"\\s*\\)")

	# Match: obj.signal.connect(...) pattern (Godot 4 style)
	var re_connect_obj := RegEx.new()
	re_connect_obj.compile("(\\w+)\\.(\\w+)\\.connect\\s*\\(")
	
	# Match: signal.connect(...) pattern (direct signal)
	var re_connect_direct := RegEx.new()
	re_connect_direct.compile("^\\s*(\\w+)\\.connect\\s*\\(")
	
	# Map of variable names to their types (for resolving signal connections)
	var var_type_map: Dictionary = {}

	# First pass: extract metadata and find function boundaries
	var func_starts: Array = []  # [{line_idx, name}]

	for i: int in range(line_count):
		var line: String = lines[i]
		var stripped: String = line.strip_edges()

		# Description tag (check first 15 lines)
		if i < 15 and description.is_empty():
			var m := re_desc.search(stripped)
			if m:
				description = m.get_string(1)
				continue

		# extends
		if extends_class.is_empty():
			var m := re_extends.search(stripped)
			if m:
				extends_class = m.get_string(1)
				continue

		# class_name
		if class_name_str.is_empty():
			var m := re_class_name.search(stripped)
			if m:
				class_name_str = m.get_string(1)
				continue

		# Variables - only match top-level (not indented)
		# Skip lines that start with tab or spaces (inside functions)
		if not line.begins_with("\t") and not line.begins_with(" "):
			var m_var := re_var.search(stripped)
			if m_var:
				var exported: bool = m_var.get_string(1).strip_edges() != ""
				var onready: bool = m_var.get_string(2).strip_edges() != ""
				var var_name: String = m_var.get_string(3)
				var var_type: String = m_var.get_string(4).strip_edges()
				var default_val: String = m_var.get_string(5).strip_edges()

				# Try to infer type from default value if no explicit type
				if var_type.is_empty() and not default_val.is_empty():
					var_type = _infer_type(default_val)
				
				# Track variable types for signal connection resolution
				if not var_type.is_empty():
					var_type_map[var_name] = var_type

				variables.append({
					&"name": var_name,
					&"type": var_type,
					&"exported": exported,
					&"onready": onready,
					&"default": default_val
				})

		# Functions
		var m_func := re_func.search(stripped)
		if m_func:
			var func_name: String = m_func.get_string(1)
			var return_type: String = m_func.get_string(3).strip_edges()
			func_starts.append({&"line_idx": i, &"name": func_name})
			functions.append({
				&"name": func_name,
				&"params": m_func.get_string(2).strip_edges(),
				&"return_type": return_type,
				&"line": i + 1,
				&"body": ""  # filled in second pass
			})

		# Signals
		var m_sig := re_signal.search(stripped)
		if m_sig:
			signals_list.append({
				&"name": m_sig.get_string(1),
				&"params": m_sig.get_string(2).strip_edges() if m_sig.get_string(2) else ""
			})

		# Preload/load references
		var m_preload := re_preload.search(stripped)
		if m_preload:
			preloads.append(m_preload.get_string(1))

		# Signal connections (Godot 4 style)
		# Pattern: obj.signal.connect(...) - e.g. wave_manager.wave_started.connect(...)
		var m_conn_obj := re_connect_obj.search(stripped)
		if m_conn_obj:
			var obj_name: String = m_conn_obj.get_string(1)
			var signal_name: String = m_conn_obj.get_string(2)
			var target_type: String = var_type_map.get(obj_name, "")
			connections.append({
				&"object": obj_name,
				&"signal": signal_name,
				&"target": target_type,
				&"line": i + 1
			})
		else:
			# Pattern: signal.connect(...) - e.g. body_entered.connect(...)
			var m_conn_direct := re_connect_direct.search(stripped)
			if m_conn_direct:
				connections.append({
					&"signal": m_conn_direct.get_string(1),
					&"target": extends_class,  # Direct signal likely from parent class
					&"line": i + 1
				})

	# Second pass: extract function bodies
	for fi: int in range(func_starts.size()):
		var start_idx: int = func_starts[fi][&"line_idx"]
		var end_idx: int
		if fi + 1 < func_starts.size():
			end_idx = func_starts[fi + 1][&"line_idx"]
		else:
			end_idx = line_count

		# Find actual end: look backwards from next func to skip blank lines
		while end_idx > start_idx + 1 and lines[end_idx - 1].strip_edges().is_empty():
			end_idx -= 1

		# Also check for top-level declarations (var, signal, @export, class_name, etc.)
		# that would end the function body
		for check_idx in range(start_idx + 1, end_idx):
			var check_line: String = lines[check_idx]
			# If line is not indented and not empty and not a comment, it's not part of the function
			if not check_line.is_empty() and not check_line.begins_with("\t") and not check_line.begins_with(" ") and not check_line.begins_with("#"):
				end_idx = check_idx
				break

		var body_lines: PackedStringArray = PackedStringArray()
		for li: int in range(start_idx, end_idx):
			body_lines.append(lines[li])

		var body: String = "\n".join(body_lines)
		# Cap body size to avoid huge payloads
		if body.length() > 3000:
			body = body.substr(0, 3000) + "\n# ... (truncated)"

		functions[fi][&"body"] = body
		functions[fi][&"body_lines"] = end_idx - start_idx

	# Determine folder
	var folder: String = path.get_base_dir()
	var filename: String = path.get_file()

	return {
		&"path": path,
		&"filename": filename,
		&"folder": folder,
		&"class_name": class_name_str,
		&"extends": extends_class,
		&"description": description,
		&"line_count": line_count,
		&"variables": variables,
		&"functions": functions,
		&"signals": signals_list,
		&"preloads": preloads,
		&"connections": connections
	}

func _infer_type(default_val: String) -> String:
	"""Try to infer GDScript type from a default value."""
	if default_val == "true" or default_val == "false":
		return "bool"
	if default_val.is_valid_int():
		return "int"
	if default_val.is_valid_float():
		return "float"
	if default_val.begins_with("\"") or default_val.begins_with("'"):
		return "String"
	if default_val.begins_with("Vector2"):
		return "Vector2"
	if default_val.begins_with("Vector3"):
		return "Vector3"
	if default_val.begins_with("Color"):
		return "Color"
	if default_val.begins_with("["):
		return "Array"
	if default_val.begins_with("{"):
		return "Dictionary"
	if default_val == "null":
		return "Variant"
	if default_val.ends_with(".new()"):
		return default_val.replace(".new()", "")
	return ""


func map_scenes(args: Dictionary) -> Dictionary:
	"""Crawl the project and build a map of all scenes."""
	var root_path: String = str(args.get(&"root", "res://"))
	var include_addons: bool = bool(args.get(&"include_addons", false))

	if not root_path.begins_with("res://"):
		root_path = "res://" + root_path

	# Collect all .tscn files
	var scene_paths: Array = []
	_collect_scenes(root_path, scene_paths, include_addons)

	if scene_paths.is_empty():
		return {&"ok": true, &"scene_map": {&"scenes": [], &"total_scenes": 0}}

	# Parse each scene
	var scenes: Array = []
	for path: String in scene_paths:
		var info: Dictionary = _parse_scene(path)
		scenes.append(info)

	# Build edges between scenes (instantiation, preloads)
	var edges: Array = []
	for scene: Dictionary in scenes:
		var from_path: String = scene[&"path"]
		for instance: String in scene.get(&"instances", []):
			edges.append({&"from": from_path, &"to": instance, &"type": "instance"})

	return {
		&"ok": true,
		&"scene_map": {
			&"scenes": scenes,
			&"edges": edges,
			&"total_scenes": scenes.size()
		}
	}


func _collect_scenes(path: String, results: Array, include_addons: bool) -> void:
	"""Recursively collect all .tscn files."""
	var dir := DirAccess.open(path)
	if dir == null:
		return

	dir.list_dir_begin()
	var name := dir.get_next()
	while name != "":
		if name.begins_with("."):
			name = dir.get_next()
			continue

		var full_path := path.path_join(name)

		if dir.current_is_dir():
			if name == "addons" and not include_addons:
				name = dir.get_next()
				continue
			_collect_scenes(full_path, results, include_addons)
		elif name.ends_with(".tscn"):
			results.append(full_path)

		name = dir.get_next()
	dir.list_dir_end()


func _parse_scene(path: String) -> Dictionary:
	"""Parse a scene file and extract its structure."""
	var file := FileAccess.open(path, FileAccess.READ)
	if file == null:
		return {&"path": path, &"error": "Cannot open file"}

	var content: String = file.get_as_text()
	file.close()

	var scene_name: String = path.get_file().replace(".tscn", "")
	var root_type: String = ""
	var nodes: Array = []
	var instances: Array = []
	var scripts: Array = []

	# Parse .tscn format
	var lines: PackedStringArray = content.split("\n")
	var current_node: Dictionary = {}
	
	var re_ext_resource := RegEx.new()
	re_ext_resource.compile('\\[ext_resource.*path="([^"]+)".*type="([^"]+)"')
	
	var re_node := RegEx.new()
	re_node.compile('\\[node name="([^"]+)".*type="([^"]+)"')
	
	var re_node_instance := RegEx.new()
	re_node_instance.compile('\\[node name="([^"]+)".*instance=ExtResource\\("([^"]+)"\\)')
	
	var re_script := RegEx.new()
	re_script.compile('script = ExtResource\\("([^"]+)"\\)')

	var ext_resources: Dictionary = {}  # id -> {path, type}
	
	for line: String in lines:
		# External resources
		var m_ext := re_ext_resource.search(line)
		if m_ext:
			var res_path: String = m_ext.get_string(1)
			var res_type: String = m_ext.get_string(2)
			# Extract ID from the line
			var id_match := RegEx.create_from_string('id="([^"]+)"').search(line)
			if id_match:
				ext_resources[id_match.get_string(1)] = {&"path": res_path, &"type": res_type}
				if res_type == "PackedScene":
					instances.append(res_path)
				elif res_type == "Script":
					scripts.append(res_path)
			continue

		# Regular nodes
		var m_node := re_node.search(line)
		if m_node:
			var node_name: String = m_node.get_string(1)
			var node_type: String = m_node.get_string(2)
			if root_type.is_empty():
				root_type = node_type
			nodes.append({&"name": node_name, &"type": node_type})
			continue

		# Instance nodes
		var m_inst := re_node_instance.search(line)
		if m_inst:
			var node_name: String = m_inst.get_string(1)
			nodes.append({&"name": node_name, &"type": "Instance"})

	return {
		&"path": path,
		&"name": scene_name,
		&"root_type": root_type,
		&"nodes": nodes,
		&"instances": instances,
		&"scripts": scripts,
		&"node_count": nodes.size()
	}


# ============================================================================
# INTERNAL FILE MODIFICATION FUNCTIONS (not exposed as MCP tools)
# These are called directly by the visualizer for inline editing
# ============================================================================

func _internal_map_scenes(args: Dictionary) -> Dictionary:
	"""Internal wrapper for map_scenes."""
	return map_scenes(args)


func _internal_refresh_map(args: Dictionary) -> Dictionary:
	"""Refresh the project map."""
	return map_project(args)


func _internal_create_script_file(args: Dictionary) -> Dictionary:
	"""Create a new script file."""
	var script_path: String = args.get(&"path", "")
	var extends_type: String = args.get(&"extends", "Node")
	var class_name_str: String = args.get(&"class_name", "")
	
	if script_path.is_empty():
		return {&"ok": false, &"error": "No path provided"}
	
	if not script_path.begins_with("res://"):
		script_path = "res://" + script_path
	
	if not script_path.ends_with(".gd"):
		script_path += ".gd"
	
	# Check if file already exists
	if FileAccess.file_exists(script_path):
		return {&"ok": false, &"error": "File already exists: " + script_path}
	
	# Create directory if needed
	var dir_path: String = script_path.get_base_dir()
	if not DirAccess.dir_exists_absolute(dir_path):
		var err := DirAccess.make_dir_recursive_absolute(dir_path)
		if err != OK:
			return {&"ok": false, &"error": "Failed to create directory"}
	
	# Build script content
	var content := ""
	if not class_name_str.is_empty():
		content += "class_name " + class_name_str + "\n"
	content += "extends " + extends_type + "\n"
	content += "\n\n"
	content += "func _ready() -> void:\n"
	content += "\tpass\n"
	
	# Write file
	var file := FileAccess.open(script_path, FileAccess.WRITE)
	if file == null:
		return {&"ok": false, &"error": "Cannot create file: " + script_path}
	
	file.store_string(content)
	file.close()
	
	return {&"ok": true, &"path": script_path}


func _internal_modify_variable(args: Dictionary) -> Dictionary:
	"""Add, update, or delete a variable in a script file."""
	var script_path: String = args.get(&"path", "")
	var action: String = args.get(&"action", "")  # "add", "update", "delete"
	var old_name: String = args.get(&"old_name", "")
	var new_name: String = args.get(&"name", "")
	var var_type: String = args.get(&"type", "")
	var default_val: String = args.get(&"default", "")
	var exported: bool = args.get(&"exported", false)
	var onready: bool = args.get(&"onready", false)
	
	if script_path.is_empty():
		return {&"ok": false, &"error": "No script path provided"}
	
	var file := FileAccess.open(script_path, FileAccess.READ)
	if file == null:
		return {&"ok": false, &"error": "Cannot open file: " + script_path}
	
	var content: String = file.get_as_text()
	file.close()
	
	var lines: Array = Array(content.split("\n"))
	var modified := false
	
	if action == "delete":
		# Find and remove the variable declaration
		var pattern := RegEx.new()
		pattern.compile("^(@export(?:\\([^)]*\\))?\\s+)?(?:@onready\\s+)?var\\s+" + old_name + "\\s*(?::|=|$)")
		for i: int in range(lines.size() - 1, -1, -1):
			if pattern.search(lines[i].strip_edges()):
				lines.remove_at(i)
				modified = true
				break
	
	elif action == "update":
		# Find and update the variable declaration
		var pattern := RegEx.new()
		pattern.compile("^(@export(?:\\([^)]*\\))?\\s+)?(@onready\\s+)?var\\s+" + old_name + "\\s*(?::\\s*\\w+)?(?:\\s*=\\s*.+)?$")
		for i: int in range(lines.size()):
			var m := pattern.search(lines[i].strip_edges())
			if m:
				# Use onready from args, not from matched pattern
				var new_line := _build_var_line(new_name, var_type, default_val, exported, onready)
				lines[i] = new_line
				modified = true
				break
	
	elif action == "add":
		# Find position to insert (after last variable, before first function)
		var insert_pos := _find_var_insert_position(lines, exported)
		var new_line := _build_var_line(new_name, var_type, default_val, exported, false)
		lines.insert(insert_pos, new_line)
		modified = true
	
	if modified:
		var new_content := "\n".join(PackedStringArray(lines))
		var write_file := FileAccess.open(script_path, FileAccess.WRITE)
		if write_file == null:
			return {&"ok": false, &"error": "Cannot write to file: " + script_path}
		write_file.store_string(new_content)
		write_file.close()
		return {&"ok": true, &"action": action, &"variable": new_name}
	
	return {&"ok": false, &"error": "Variable not found: " + old_name}


func _internal_modify_signal(args: Dictionary) -> Dictionary:
	"""Add, update, or delete a signal in a script file."""
	var script_path: String = args.get(&"path", "")
	var action: String = args.get(&"action", "")
	var old_name: String = args.get(&"old_name", "")
	var new_name: String = args.get(&"name", "")
	var params: String = args.get(&"params", "")
	
	if script_path.is_empty():
		return {&"ok": false, &"error": "No script path provided"}
	
	var file := FileAccess.open(script_path, FileAccess.READ)
	if file == null:
		return {&"ok": false, &"error": "Cannot open file: " + script_path}
	
	var content: String = file.get_as_text()
	file.close()
	
	var lines: Array = Array(content.split("\n"))
	var modified := false
	
	if action == "delete":
		var pattern := RegEx.new()
		pattern.compile("^signal\\s+" + old_name + "(?:\\s*\\(|$)")
		for i: int in range(lines.size() - 1, -1, -1):
			if pattern.search(lines[i].strip_edges()):
				lines.remove_at(i)
				modified = true
				break
	
	elif action == "update":
		var pattern := RegEx.new()
		pattern.compile("^signal\\s+" + old_name + "(?:\\s*\\([^)]*\\))?$")
		for i: int in range(lines.size()):
			if pattern.search(lines[i].strip_edges()):
				var new_line := "signal " + new_name
				if not params.is_empty():
					new_line += "(" + params + ")"
				lines[i] = new_line
				modified = true
				break
	
	elif action == "add":
		var insert_pos := _find_signal_insert_position(lines)
		var new_line := "signal " + new_name
		if not params.is_empty():
			new_line += "(" + params + ")"
		lines.insert(insert_pos, new_line)
		modified = true
	
	if modified:
		var new_content := "\n".join(PackedStringArray(lines))
		var write_file := FileAccess.open(script_path, FileAccess.WRITE)
		if write_file == null:
			return {&"ok": false, &"error": "Cannot write to file: " + script_path}
		write_file.store_string(new_content)
		write_file.close()
		return {&"ok": true, &"action": action, &"signal": new_name}
	
	return {&"ok": false, &"error": "Signal not found: " + old_name}


func _internal_modify_function(args: Dictionary) -> Dictionary:
	"""Update a function's body in a script file."""
	var script_path: String = args.get(&"path", "")
	var func_name: String = args.get(&"name", "")
	var new_body: String = args.get(&"body", "")
	
	if script_path.is_empty() or func_name.is_empty():
		return {&"ok": false, &"error": "Missing path or function name"}
	
	var file := FileAccess.open(script_path, FileAccess.READ)
	if file == null:
		return {&"ok": false, &"error": "Cannot open file: " + script_path}
	
	var content: String = file.get_as_text()
	file.close()
	
	var lines: Array = Array(content.split("\n"))
	
	# Find the function
	var re_func := RegEx.new()
	re_func.compile("^func\\s+" + func_name + "\\s*\\(")
	
	var func_start := -1
	var func_end := -1
	
	for i: int in range(lines.size()):
		if func_start == -1:
			if re_func.search(lines[i].strip_edges()):
				func_start = i
		elif func_start != -1:
			# Find end of function (next top-level declaration or end of file)
			var stripped: String = lines[i].strip_edges()
			if not stripped.is_empty() and not lines[i].begins_with("\t") and not lines[i].begins_with(" ") and not stripped.begins_with("#"):
				func_end = i
				break
	
	if func_start == -1:
		return {&"ok": false, &"error": "Function not found: " + func_name}
	
	if func_end == -1:
		func_end = lines.size()
	
	# Remove trailing empty lines from function body
	while func_end > func_start + 1 and lines[func_end - 1].strip_edges().is_empty():
		func_end -= 1
	
	# Replace function body
	var new_lines := Array(new_body.split("\n"))
	
	# Remove old function lines
	for i: int in range(func_end - 1, func_start - 1, -1):
		lines.remove_at(i)
	
	# Insert new function lines
	for i: int in range(new_lines.size()):
		lines.insert(func_start + i, new_lines[i])
	
	var new_content := "\n".join(PackedStringArray(lines))
	var write_file := FileAccess.open(script_path, FileAccess.WRITE)
	if write_file == null:
		return {&"ok": false, &"error": "Cannot write to file: " + script_path}
	write_file.store_string(new_content)
	write_file.close()
	
	return {&"ok": true, &"function": func_name}


func _internal_modify_function_delete(args: Dictionary) -> Dictionary:
	"""Delete a function from a script file."""
	var script_path: String = args.get(&"path", "")
	var func_name: String = args.get(&"name", "")
	
	if script_path.is_empty() or func_name.is_empty():
		return {&"ok": false, &"error": "Missing path or function name"}
	
	var file := FileAccess.open(script_path, FileAccess.READ)
	if file == null:
		return {&"ok": false, &"error": "Cannot open file: " + script_path}
	
	var content: String = file.get_as_text()
	file.close()
	
	var lines: Array = Array(content.split("\n"))
	
	# Find the function
	var re_func := RegEx.new()
	re_func.compile("^func\\s+" + func_name + "\\s*\\(")
	
	var func_start := -1
	var func_end := -1
	
	for i: int in range(lines.size()):
		if func_start == -1:
			if re_func.search(lines[i].strip_edges()):
				func_start = i
		elif func_start != -1:
			# Find end of function (next top-level declaration or end of file)
			var stripped: String = lines[i].strip_edges()
			if not stripped.is_empty() and not lines[i].begins_with("\t") and not lines[i].begins_with(" ") and not stripped.begins_with("#"):
				func_end = i
				break
	
	if func_start == -1:
		return {&"ok": false, &"error": "Function not found: " + func_name}
	
	if func_end == -1:
		func_end = lines.size()
	
	# Remove trailing empty lines before the function
	while func_end > func_start + 1 and lines[func_end - 1].strip_edges().is_empty():
		func_end -= 1
	
	# Remove the function lines
	for i: int in range(func_end - 1, func_start - 1, -1):
		lines.remove_at(i)
	
	# Remove extra blank lines that might be left
	# (but keep at least one blank line between declarations)
	
	var new_content := "\n".join(PackedStringArray(lines))
	var write_file := FileAccess.open(script_path, FileAccess.WRITE)
	if write_file == null:
		return {&"ok": false, &"error": "Cannot write to file: " + script_path}
	write_file.store_string(new_content)
	write_file.close()
	
	return {&"ok": true, &"deleted": func_name}


func _internal_find_usages(args: Dictionary) -> Dictionary:
	"""Find all usages of a variable, signal, or function across all scripts."""
	var name: String = args.get(&"name", "")
	var item_type: String = args.get(&"type", "")  # "variable", "signal", "function"
	var root_path: String = args.get(&"root", "res://")
	
	if name.is_empty():
		return {&"ok": false, &"error": "No name provided"}
	
	# Collect all scripts
	var script_paths: Array = []
	_collect_scripts(root_path, script_paths, false)
	
	var usages: Array = []
	
	# Build regex pattern based on type
	var pattern := RegEx.new()
	if item_type == "signal":
		# Match: signal_name.emit() or signal_name.connect() or .signal_name.
		pattern.compile("\\b" + name + "\\b")
	else:
		# Match word boundary for variables and functions
		pattern.compile("\\b" + name + "\\b")
	
	for path: String in script_paths:
		var file := FileAccess.open(path, FileAccess.READ)
		if file == null:
			continue
		
		var content: String = file.get_as_text()
		file.close()
		
		var lines: PackedStringArray = content.split("\n")
		for i: int in range(lines.size()):
			var line: String = lines[i]
			if pattern.search(line):
				# Skip the declaration line itself
				if item_type == "variable" and RegEx.create_from_string("^\\s*(@export)?\\s*var\\s+" + name + "\\b").search(line):
					continue
				if item_type == "signal" and RegEx.create_from_string("^\\s*signal\\s+" + name + "\\b").search(line):
					continue
				if item_type == "function" and RegEx.create_from_string("^\\s*func\\s+" + name + "\\s*\\(").search(line):
					continue
				
				usages.append({
					&"file": path,
					&"line": i + 1,
					&"code": line.strip_edges()
				})
	
	return {&"ok": true, &"usages": usages, &"count": usages.size()}


# Helper functions for file modification

func _build_var_line(name: String, type: String, default: String, exported: bool, onready: bool) -> String:
	var line := ""
	if exported:
		line += "@export "
	if onready:
		line += "@onready "
	line += "var " + name
	if not type.is_empty():
		line += ": " + type
	if not default.is_empty():
		line += " = " + default
	return line


func _find_var_insert_position(lines: Array, exported: bool) -> int:
	"""Find the best position to insert a new variable."""
	var last_var_line := -1
	var first_func_line := -1
	var after_class_decl := 0
	
	var re_var := RegEx.new()
	re_var.compile("^(@export)?\\s*(@onready)?\\s*var\\s+")
	var re_func := RegEx.new()
	re_func.compile("^func\\s+")
	var re_class := RegEx.new()
	re_class.compile("^(class_name|extends)\\s+")
	
	for i: int in range(lines.size()):
		var stripped: String = lines[i].strip_edges()
		if re_class.search(stripped):
			after_class_decl = i + 1
		if re_var.search(stripped):
			last_var_line = i
		if re_func.search(stripped) and first_func_line == -1:
			first_func_line = i
			break
	
	# Insert after last variable, or before first function, or after class declarations
	if last_var_line != -1:
		return last_var_line + 1
	if first_func_line != -1:
		return first_func_line
	return max(after_class_decl, 2)  # At least after extends/class_name


func _find_signal_insert_position(lines: Array) -> int:
	"""Find the best position to insert a new signal."""
	var last_signal_line := -1
	var first_var_line := -1
	var after_class_decl := 0
	
	var re_signal := RegEx.new()
	re_signal.compile("^signal\\s+")
	var re_var := RegEx.new()
	re_var.compile("^(@export)?\\s*var\\s+")
	var re_class := RegEx.new()
	re_class.compile("^(class_name|extends)\\s+")
	
	for i: int in range(lines.size()):
		var stripped: String = lines[i].strip_edges()
		if re_class.search(stripped):
			after_class_decl = i + 1
		if re_signal.search(stripped):
			last_signal_line = i
		if re_var.search(stripped) and first_var_line == -1:
			first_var_line = i
	
	# Insert after last signal, or before first var, or after class declarations
	if last_signal_line != -1:
		return last_signal_line + 1
	if first_var_line != -1:
		return first_var_line
	return max(after_class_decl, 2)


# ============================================================================
# SCENE VISUALIZER INTERNAL FUNCTIONS
# These are called by the visualizer for scene view functionality
# ============================================================================

func _internal_get_scene_hierarchy(args: Dictionary) -> Dictionary:
	"""Get scene hierarchy for visualizer."""
	if _scene_tools_ref and _scene_tools_ref.has_method("get_scene_hierarchy"):
		return _scene_tools_ref.get_scene_hierarchy(args)
	return {&"ok": false, &"error": "Scene tools not available"}


func _internal_get_scene_node_properties(args: Dictionary) -> Dictionary:
	"""Get node properties for visualizer panel."""
	if _scene_tools_ref and _scene_tools_ref.has_method("get_scene_node_properties"):
		return _scene_tools_ref.get_scene_node_properties(args)
	return {&"ok": false, &"error": "Scene tools not available"}


func _internal_set_scene_node_property(args: Dictionary) -> Dictionary:
	"""Set node property from visualizer panel."""
	if _scene_tools_ref and _scene_tools_ref.has_method("set_scene_node_property"):
		return _scene_tools_ref.set_scene_node_property(args)
	return {&"ok": false, &"error": "Scene tools not available"}


func _internal_add_scene_node(args: Dictionary) -> Dictionary:
	"""Add a node to a scene from visualizer."""
	if _scene_tools_ref and _scene_tools_ref.has_method("add_node"):
		return _scene_tools_ref.add_node(args)
	return {&"ok": false, &"error": "Scene tools not available"}


func _internal_remove_scene_node(args: Dictionary) -> Dictionary:
	"""Remove a node from a scene from visualizer."""
	if _scene_tools_ref and _scene_tools_ref.has_method("remove_node"):
		return _scene_tools_ref.remove_node(args)
	return {&"ok": false, &"error": "Scene tools not available"}


func _internal_rename_scene_node(args: Dictionary) -> Dictionary:
	"""Rename a node in a scene from visualizer."""
	if _scene_tools_ref and _scene_tools_ref.has_method("rename_node"):
		return _scene_tools_ref.rename_node(args)
	return {&"ok": false, &"error": "Scene tools not available"}


func _internal_move_scene_node(args: Dictionary) -> Dictionary:
	"""Move/reorder a node in a scene from visualizer."""
	if _scene_tools_ref and _scene_tools_ref.has_method("move_node"):
		return _scene_tools_ref.move_node(args)
	return {&"ok": false, &"error": "Scene tools not available"}


# Simpler aliases for context menu actions
func _internal_add_node(args: Dictionary) -> Dictionary:
	"""Add a node to a scene."""
	if _scene_tools_ref and _scene_tools_ref.has_method("add_node"):
		return _scene_tools_ref.add_node(args)
	return {&"ok": false, &"error": "Scene tools not available"}


func _internal_remove_node(args: Dictionary) -> Dictionary:
	"""Remove a node from a scene."""
	if _scene_tools_ref and _scene_tools_ref.has_method("remove_node"):
		return _scene_tools_ref.remove_node(args)
	return {&"ok": false, &"error": "Scene tools not available"}


func _internal_rename_node(args: Dictionary) -> Dictionary:
	"""Rename a node in a scene."""
	if _scene_tools_ref and _scene_tools_ref.has_method("rename_node"):
		return _scene_tools_ref.rename_node(args)
	return {&"ok": false, &"error": "Scene tools not available"}


func _internal_move_node(args: Dictionary) -> Dictionary:
	"""Move/reorder a node in a scene."""
	if _scene_tools_ref and _scene_tools_ref.has_method("move_node"):
		return _scene_tools_ref.move_node(args)
	return {&"ok": false, &"error": "Scene tools not available"}


func _internal_duplicate_node(args: Dictionary) -> Dictionary:
	"""Duplicate a node in a scene."""
	if _scene_tools_ref and _scene_tools_ref.has_method("duplicate_node"):
		return _scene_tools_ref.duplicate_node(args)
	return {&"ok": false, &"error": "Scene tools not available"}


func _internal_reorder_node(args: Dictionary) -> Dictionary:
	"""Reorder a node within its parent (change sibling index)."""
	if _scene_tools_ref and _scene_tools_ref.has_method("reorder_node"):
		return _scene_tools_ref.reorder_node(args)
	return {&"ok": false, &"error": "Scene tools not available"}
