extends GraphEdit

var control_script
var graph_edit
var open_help
var multiple_connections
var selected_nodes = {} #used to track which nodes in the GraphEdit are selected
var copied_nodes_data = [] #stores node data on ctrl+c
var copied_connections = [] #stores all connections on ctrl+c

# Called when the node enters the scene tree for the first time.
func _ready() -> void:
	snapping_enabled = false
	show_grid = false
	zoom = 0.9

func init(main_node: Node, graphedit: GraphEdit, openhelp: Callable, multipleconnections: Window) -> void:
	control_script = main_node
	graph_edit = graphedit
	open_help = openhelp
	multiple_connections = multipleconnections

func _make_node(command: String):
	#Find node with matching name to button and create a version of it in the graph edit
	#and position it close to the origin right click to open the menu
	var effect: GraphNode = Nodes.get_node(NodePath(command)).duplicate()
	effect.name = command
	add_child(effect, true)
	effect.connect("open_help", open_help)
	effect.set_position_offset((control_script.effect_position + graph_edit.scroll_offset) / graph_edit.zoom) #set node to current mouse position in graph edit
	_register_inputs_in_node(effect) #link sliders for changes tracking
	_register_node_movement() #link nodes for tracking position changes for changes tracking

	control_script.changesmade = true

	# Remove node with UndoRedo
	control_script.undo_redo.create_action("Add Node")
	control_script.undo_redo.add_undo_method(Callable(graph_edit, "remove_child").bind(effect))
	control_script.undo_redo.add_undo_method(Callable(effect, "queue_free"))
	control_script.undo_redo.add_undo_method(Callable(self, "_track_changes"))
	control_script.undo_redo.commit_action()
	

func _on_connection_request(from_node: StringName, from_port: int, to_node: StringName, to_port: int) -> void:
	var to_graph_node = get_node(NodePath(to_node))

	# Get the type of the input port using GraphNode's built-in method
	var port_type = to_graph_node.get_input_port_type(to_port)

	# If port type is 1 and already has a connection, reject the request
	if port_type == 1:
		var connections = get_connection_list()
		var existing_connections = 0

		for conn in connections:
			if conn.to_node == to_node and conn.to_port == to_port:
				existing_connections += 1
				if existing_connections >= 1:
					var interface_settings = ConfigHandler.load_interface_settings()
					if interface_settings.disable_pvoc_warning == false:
						multiple_connections.popup_centered()
					return

	# If no conflict, allow the connection
	connect_node(from_node, from_port, to_node, to_port)
	control_script.changesmade = true

func _on_graph_edit_disconnection_request(from_node: StringName, from_port: int, to_node: StringName, to_port: int) -> void:
	disconnect_node(from_node, from_port, to_node, to_port)
	control_script.changesmade = true

func _on_graph_edit_node_selected(node: Node) -> void:
	selected_nodes[node] = true

func _on_graph_edit_node_deselected(node: Node) -> void:
	selected_nodes[node] = false

func _unhandled_key_input(event: InputEvent) -> void:
	if event is InputEventKey and event.pressed and not event.echo:
		if event.keycode == KEY_BACKSPACE:
			_on_graph_edit_delete_nodes_request(PackedStringArray(selected_nodes.keys().filter(func(k): return selected_nodes[k])))
			pass

func _on_graph_edit_delete_nodes_request(nodes: Array[StringName]) -> void:
	control_script.undo_redo.create_action("Delete Nodes (Undo only)")
	
	#get the number of inputs in the patch
	var number_of_inputs = 0
	for allnodes in get_children():
		if allnodes.get_meta("command") == "inputfile":
			number_of_inputs += 1
			
	for node in selected_nodes.keys():
		if selected_nodes[node]:
			#check if node is the output or the last input node and do nothing
			if (number_of_inputs <= 1 and node.get_meta("command") == "inputfile") or node.get_meta("command") == "outputfile":
				pass
			else:
				number_of_inputs -= 1
				# Store duplicate and state for undo
				var node_data = node.duplicate()
				var position = node.position_offset

				# Store all connections for undo
				var conns = []
				for con in get_connection_list():
					if con["to_node"] == node.name or con["from_node"] == node.name:
						conns.append(con)

				# Delete
				remove_connections_to_node(node)
				node.queue_free()
				control_script.changesmade = true

				# Register undo restore
				control_script.undo_redo.add_undo_method(Callable(self, "add_child").bind(node_data, true))
				control_script.undo_redo.add_undo_method(Callable(node_data, "set_position_offset").bind(position))
				for con in conns:
					control_script.undo_redo.add_undo_method(Callable(self, "connect_node").bind(
						con["from_node"], con["from_port"],
						con["to_node"], con["to_port"]
					))
				control_script.undo_redo.add_undo_method(Callable(self, "set_node_selected").bind(node_data, true))
				control_script.undo_redo.add_undo_method(Callable(self, "_track_changes"))
				control_script.undo_redo.add_undo_method(Callable(self, "_register_inputs_in_node").bind(node_data)) #link sliders for changes tracking
				control_script.undo_redo.add_undo_method(Callable(self, "_register_node_movement")) # link nodes for changes tracking

	# Clear selection
	selected_nodes = {}

	control_script.undo_redo.commit_action()

func set_node_selected(node: Node, selected: bool) -> void:
	selected_nodes[node] = selected
#
func remove_connections_to_node(node):
	for con in get_connection_list():
		if con["to_node"] == node.name or con["from_node"] == node.name:
			disconnect_node(con["from_node"], con["from_port"], con["to_node"], con["to_port"])
			control_script.changesmade = true
			
#copy and paste nodes with vertical offset on paste
func copy_selected_nodes():
	copied_nodes_data.clear()
	copied_connections.clear()

	# Store selected nodes and their slider values
	for node in get_children():
		# Check if the node is selected and not an 'inputfile' or 'outputfile'
		if node is GraphNode and selected_nodes.get(node, false):
			if node.get_meta("command") == "outputfile":
				continue  # Skip these nodes

			var node_data = {
				"name": node.name,
				"type": node.get_class(),
				"offset": node.position_offset,
				"slider_values": {}
			}

			for child in node.get_children():
				if child is HSlider or child is VSlider:
					node_data["slider_values"][child.name] = child.value

			copied_nodes_data.append(node_data)

	# Store connections between selected nodes
	for conn in get_connection_list():
		var from_ref = get_node_or_null(NodePath(conn["from_node"]))
		var to_ref = get_node_or_null(NodePath(conn["to_node"]))

		var is_from_selected = from_ref != null and selected_nodes.get(from_ref, false)
		var is_to_selected = to_ref != null and selected_nodes.get(to_ref, false)

		# Skip if any of the connected nodes are 'inputfile' or 'outputfile'
		if (from_ref != null and (from_ref.get_meta("command") == "inputfile" or from_ref.get_meta("command") == "outputfile")) or (to_ref != null and (to_ref.get_meta("command") == "inputfile" or to_ref.get_meta("command") == "outputfile")):
			continue

		if is_from_selected and is_to_selected:
			# Store connection as dictionary
			var conn_data = {
				"from_node": conn["from_node"],
				"from_port": conn["from_port"],
				"to_node": conn["to_node"],
				"to_port": conn["to_port"]
			}
			copied_connections.append(conn_data)

func paste_copied_nodes():
	if copied_nodes_data.is_empty():
		return

	var name_map = {}
	var pasted_nodes = []

	# Step 1: Find topmost and bottommost Y of copied nodes
	var min_y = INF
	var max_y = -INF
	for node_data in copied_nodes_data:
		var y = node_data["offset"].y
		min_y = min(min_y, y)
		max_y = max(max_y, y)

	# Step 2: Decide where to paste the group
	var base_y_offset = max_y + 350  # Pasting below the lowest node

	# Step 3: Paste nodes, preserving vertical layout
	for node_data in copied_nodes_data:
		var original_node = get_node_or_null(NodePath(node_data["name"]))
		if not original_node:
			continue

		var new_node = original_node.duplicate()
		new_node.name = node_data["name"] + "_copy_" + str(randi() % 10000)

		var relative_y = node_data["offset"].y - min_y
		new_node.position_offset = Vector2(
			node_data["offset"].x,
			base_y_offset + relative_y
		)
		

		# Restore sliders
		for child in new_node.get_children():
			if child.name in node_data["slider_values"]:
				child.value = node_data["slider_values"][child.name]

		add_child(new_node, true)
		new_node.connect("open_help", open_help)
		_register_inputs_in_node(new_node) #link sliders for changes tracking
		_register_node_movement() # link nodes for changes tracking
		name_map[node_data["name"]] = new_node.name
		pasted_nodes.append(new_node)


	# Step 4: Reconnect new nodes
	for conn_data in copied_connections:
		var new_from = name_map.get(conn_data["from_node"], null)
		var new_to = name_map.get(conn_data["to_node"], null)

		if new_from and new_to:
			connect_node(new_from, conn_data["from_port"], new_to, conn_data["to_port"])

	# Step 5: Select pasted nodes
	for pasted_node in pasted_nodes:
		set_selected(pasted_node)
		selected_nodes[pasted_node] = true
	
	control_script.changesmade = true
	
	# Remove node with UndoRedo
	control_script.undo_redo.create_action("Paste Nodes")
	for pasted_node in pasted_nodes:
		control_script.undo_redo.add_undo_method(Callable(self, "remove_child").bind(pasted_node))
		control_script.undo_redo.add_undo_method(Callable(pasted_node, "queue_free"))
		control_script.undo_redo.add_undo_method(Callable(self, "remove_connections_to_node").bind(pasted_node))
		control_script.undo_redo.add_undo_method(Callable(self, "_track_changes"))
	control_script.undo_redo.commit_action()
	

#functions for tracking changes for save state detection
func _register_inputs_in_node(node: Node):
	#tracks input to nodes sliders and codeedit to track if patch is saved
	# Track Sliders
	for slider in node.find_children("*", "HSlider", true, false):
		# Create a Callable to the correct method
		var callable = Callable(self, "_on_any_slider_changed")
		# Check if it's already connected, and connect if not
		if not slider.is_connected("value_changed", callable):
			slider.connect("value_changed", callable)
	
	for slider in node.find_children("*", "VBoxContainer", true, false):
		# Also connect to meta_changed if the slider has that signal
		if slider.has_signal("meta_changed"):
			var meta_callable = Callable(self, "_on_any_slider_meta_changed")
			if not slider.is_connected("meta_changed", meta_callable):
				slider.connect("meta_changed", meta_callable)
		
	# Track CodeEdits
	for editor in node.find_children("*", "CodeEdit", true, false):
		var callable = Callable(self, "_on_any_input_changed")
		if not editor.is_connected("text_changed", callable):
			editor.connect("text_changed", callable)
			
func _on_any_slider_meta_changed():
	control_script.changesmade = true
	print("Meta changed in slider")
	
func _register_node_movement():
	for graphnode in get_children():
		if graphnode is GraphNode:
			var callable = Callable(self, "_on_graphnode_moved")
			if not graphnode.is_connected("position_offset_changed", callable):
				graphnode.connect("position_offset_changed", callable)

func _on_graphnode_moved():
	control_script.changesmade = true
	
func _on_any_slider_changed(value: float) -> void:
	control_script.changesmade = true
	
func _on_any_input_changed():
	control_script.changesmade = true

func _track_changes():
	control_script.changesmade = true
