tool
extends MMGenBase
class_name MMGenGraph

var label : String = "Graph"
var shortdesc = ""
var longdesc = ""
var connections = []

var editable : bool = false

var transmits_seed : bool = true

signal connections_changed(removed_connections, added_connections)
signal hierarchy_changed()

func emit_hierarchy_changed():
	var top = self
	while top.get_parent() != null and top.get_parent().get_script() == get_script():
		top = top.get_parent()
	top.emit_signal("hierarchy_changed")

func fix_remotes() -> void:
	for c in get_children():
		if c is MMGenRemote:
			c.fix()

func _post_load() -> void:
	fix_remotes()


func has_randomness() -> bool:
	if !transmits_seed:
		return false
	for c in get_children():
		if c.has_randomness() and !c.is_seed_locked():
			return true
	return false

func set_position(p) -> void:
	position = p
	if !is_seed_locked():
		for c in get_children():
			c.set_position(c.position)

func get_type() -> String:
	return "graph"

func get_type_name() -> String:
	return label

func set_type_name(l) -> void:
	if l !=label:
		label = l
		emit_hierarchy_changed()

func toggle_editable() -> bool:
	editable = !editable
	if editable:
		model = null
	return true

func is_editable() -> bool:
	return editable

func get_description() -> String:
	var desc : String
	if shortdesc == "":
		desc = longdesc
	elif longdesc == "":
		desc = shortdesc
	else:
		desc = shortdesc+"\n"+longdesc
	return desc


func get_parameter_defs() -> Array:
	if has_node("gen_parameters"):
		return get_node("gen_parameters").get_parameter_defs()
	return []

func set_parameter(n : String, v) -> void:
	.set_parameter(n, v)
	if has_node("gen_parameters"):
		get_node("gen_parameters").set_parameter(n, v)

func get_input_defs() -> Array:
	if has_node("gen_inputs"):
		return get_node("gen_inputs").get_output_defs()
	return []

func get_output_defs() -> Array:
	if has_node("gen_outputs"):
		return get_node("gen_outputs").get_input_defs()
	return []

func source_changed(input_index : int) -> void:
	if has_node("gen_inputs"):
		get_node("gen_inputs").source_changed(input_index)

func all_sources_changed() -> void:
	if has_node("gen_inputs"):
		var inputs = get_node("gen_inputs")
		for i in inputs.get_input_defs().size():
			inputs.source_changed(i)

func get_port_source(gen_name: String, input_index: int) -> OutputPort:
	var rv = null
	if gen_name == "gen_inputs":
		var parent = get_parent()
		if parent != null and parent.get_script() == get_script():
			rv = parent.get_port_source(name, input_index)
	else:
		for c in connections:
			if c.to == gen_name and c.to_port == input_index:
				var src_gen = get_node(c.from)
				if src_gen != null:
					if src_gen.get_script() == get_script():
						rv = src_gen.get_port_source("gen_outputs", c.from_port)
					else:
						rv = OutputPort.new(src_gen, c.from_port)
	if rv != null and rv.generator.name == "gen_inputs":
		rv = get_port_source("gen_inputs", rv.output_index)
	return rv

func get_port_targets(gen_name: String, output_index: int) -> Array:
	var rv = []
	for c in connections:
		if c.from == gen_name and c.from_port == output_index:
			var tgt_gen = get_node(c.to)
			if tgt_gen != null:
				rv.push_back(InputPort.new(tgt_gen, c.to_port))
	return rv

func add_generator(generator : MMGenBase) -> bool:
	var name = generator.name
	if generator.name == "Material":
		if has_node("Material"):
			# Cannot create a material if it exists already
			return false
		else:
			var parent = get_parent()
			if parent != null and parent is Object and parent.get_script() == get_script():
				# Material is always in top level graph
				return false
	if has_node(name):
		var name_prefix : String
		var regex = RegEx.new()
		regex.compile("^(.*_)\\d+$")
		var result = regex.search(generator.name)
		if result:
			name_prefix = result.get_string(1)
		else:
			name_prefix = generator.name + "_"
		var index = 1
		while has_node(name):
			index += 1
			name = name_prefix + str(index)
	generator.name = name
	add_child(generator)
	if generator.get_script() == get_script():
		emit_hierarchy_changed()
	return true

func remove_generator(generator : MMGenBase) -> bool:
	if !generator.can_be_deleted():
		return false
	var new_connections = []
	var old_connections = []
	for c in connections:
		if c.from == generator.name:
			old_connections.append(c)
		elif c.to != generator.name:
			new_connections.append(c)
	connections = new_connections
	# Notify target nodes that their input vanished
	for c in old_connections:
		get_node(c.to).source_changed(c.to_port)
	remove_child(generator)
	fix_remotes()
	if generator.get_script() == get_script():
		emit_hierarchy_changed()
	generator.queue_free()
	return true

func replace_generator(old : MMGenBase, new : MMGenBase) -> void:
	new.name = old.name
	new.position = old.position
	remove_child(old)
	add_child(new)
	if old.get_script() == get_script() or new.get_script() == get_script():
		emit_hierarchy_changed()
	old.free()

func get_connected_inputs(generator) -> Array:
	var rv : Array = []
	for c in connections:
		if c.to == generator.name:
			rv.push_back(c.to_port)
	return rv

func get_connected_outputs(generator) -> Array:
	var rv : Array = []
	for c in connections:
		if c.from == generator.name:
			rv.push_back(c.from_port)
	return rv

func connect_children(from, from_port : int, to, to_port : int) -> bool:
	# check the new connection does not create a loop
	var spreadlist = [ InputPort.new(to, to_port) ]
	while !spreadlist.empty():
		var input : InputPort = spreadlist.pop_front()
		for o in input.generator.follow_input(input.input_index):
			if o.generator == from and o.output_index == from_port:
				return false
			for t in o.generator.get_parent().get_port_targets(o.generator.name, o.output_index):
				spreadlist.push_back(t)
	# disconnect target
	while true:
		var remove = -1
		for i in connections.size():
			if connections[i].to == to.name and connections[i].to_port == to_port:
				remove = i
				break
		if remove == -1:
			break
		connections.remove(remove)
	# create new connection
	connections.append({from=from.name, from_port=from_port, to=to.name, to_port=to_port})
	to.source_changed(to_port)
	return true

func disconnect_children(from, from_port : int, to, to_port : int) -> bool:
	var new_connections : Array = []
	for c in connections:
		if c.from != from.name or c.from_port != from_port or c.to != to.name or c.to_port != to_port:
			new_connections.push_back(c)
		else:
			to.source_changed(to_port)
	connections = new_connections
	return true

func reconnect_inputs(generator, reconnects : Dictionary) -> bool:
	var new_connections : Array = []
	var added_connections : Array = []
	var removed_connections : Array = []
	for c in connections:
		if c.to == generator.name and reconnects.has(c.to_port):
			removed_connections.push_back(c.duplicate(true))
			if reconnects[c.to_port] < 0:
				continue
			c.to_port = reconnects[c.to_port]
			added_connections.push_back(c.duplicate(true))
		new_connections.push_back(c)
	connections = new_connections
	if !removed_connections.empty():
		emit_signal("connections_changed", removed_connections, added_connections)
	return true

func reconnect_outputs(generator, reconnects : Dictionary) -> bool:
	var new_connections : Array = []
	var added_connections : Array = []
	var removed_connections : Array = []
	for c in connections:
		if c.from == generator.name and reconnects.has(c.from_port):
			removed_connections.push_back(c.duplicate(true))
			if reconnects[c.from_port] < 0:
				continue
			c.from_port = reconnects[c.from_port]
			added_connections.push_back(c.duplicate(true))
		new_connections.push_back(c)
	connections = new_connections
	if !removed_connections.empty():
		emit_signal("connections_changed", removed_connections, added_connections)
	return true

func _get_shader_code(uv : String, output_index : int, context : MMGenContext) -> Dictionary:
	var outputs = get_node("gen_outputs")
	if outputs != null:
		var rv = outputs._get_shader_code(uv, output_index, context)
		while rv is GDScriptFunctionState:
			rv = yield(rv, "completed")
		return rv
	return { globals=[], defs="", code="", textures={} }


func edit(node) -> void:
	node.get_parent().call_deferred("update_view", self)

func create_subgraph(gens : Array) -> MMGenGraph:
	# Remove material, gen_inputs and gen_outputs nodes
	var generators = []
	var center = Vector2(0, 0)
	var left_bound = 65535
	var right_bound = -65536
	var upper_bound = 65536
	var count = 0
	# Filter group nodes and calculate boundin box
	for g in gens:
		if g.name != "Material" and g.name != "gen_inputs" and g.name != "gen_outputs":
			generators.push_back(g)
			var p = g.position
			center += p
			count += 1
			if left_bound > p.x: left_bound = p.x
			if right_bound < p.x: right_bound = p.x
			if upper_bound > p.y: upper_bound = p.y
	if count == 0:
		return null
	center /= count
	# Create a new graph and add it to the current one
	var new_graph = get_script().new()
	new_graph.name = "graph"
	add_generator(new_graph)
	new_graph.position = center
	# Add grouped generators and keep their names
	var names : Array = []
	for g in generators:
		names.push_back(g.name)
		remove_child(g)
		new_graph.add_generator(g)
	# Create inputs and outputs generators
	var gen_inputs = MMGenIOs.new()
	gen_inputs.name = "gen_inputs"
	gen_inputs.position = Vector2(left_bound-300, center.y)
	new_graph.add_generator(gen_inputs)
	var gen_outputs = MMGenIOs.new()
	gen_outputs.name = "gen_outputs"
	gen_outputs.position = Vector2(right_bound+300, center.y)
	new_graph.add_generator(gen_outputs)
	# Process connections
	var new_graph_connections = []
	var my_new_connections = []
	var inputs = []
	var outputs = []
	for c in connections:
		var src_name = c.from+"."+str(c.from_port)
		if names.find(c.from) != -1 and names.find(c.to) != -1:
			new_graph_connections.push_back(c)
		elif names.find(c.from) != -1:
			var port_index = outputs.find(src_name)
			if port_index == -1:
				port_index = outputs.size()
				outputs.push_back(src_name)
				var type = new_graph.get_node(c.from).get_output_defs()[c.from_port].type
				gen_outputs.ports.push_back( { name="port"+str(port_index), type=type } )
			my_new_connections.push_back( { from=new_graph.name, from_port=port_index, to=c.to, to_port=c.to_port } )
			new_graph_connections.push_back( { from=c.from, from_port=c.from_port, to="gen_outputs", to_port=port_index } )
		elif names.find(c.to) != -1:
			var port_index = inputs.find(src_name)
			if port_index == -1:
				port_index = inputs.size()
				inputs.push_back(src_name)
				var type = get_node(c.from).get_output_defs()[c.from_port].type
				gen_inputs.ports.push_back( { name="port"+str(port_index), type=type } )
			my_new_connections.push_back( { from=c.from, from_port=c.from_port, to=new_graph.name, to_port=port_index } )
			new_graph_connections.push_back( { from="gen_inputs", from_port=port_index, to=c.to, to_port=c.to_port } )
		else:
			my_new_connections.push_back(c)
	connections = my_new_connections
	new_graph.connections = new_graph_connections
	var found_remote = false
	var remote_script = load("res://addons/material_maker/engine/gen_remote.gd")
	for g in generators:
		if g.get_script() == remote_script:
			g.name = "gen_parameters"
			found_remote = true
			new_graph.parameters = g.parameters.duplicate(true)
			break
	if !found_remote:
		var gen_parameters = remote_script.new()
		gen_parameters.name = "gen_parameters"
		gen_parameters.position = Vector2(center.x-200, upper_bound-300)
		new_graph.add_child(gen_parameters)
	fix_remotes()
	new_graph.fix_remotes()
	emit_hierarchy_changed()
	return new_graph


func _serialize(data: Dictionary) -> Dictionary:
	data.label = label
	data.shortdesc = shortdesc
	data.longdesc = longdesc
	data.nodes = []
	for c in get_children():
		data.nodes.append(c.serialize())
	data.connections = connections
	return data

func _deserialize(data : Dictionary) -> void:
	if data.has("label"):
		label = data.label
	if data.has("shortdesc"):
		shortdesc = data.shortdesc
	if data.has("longdesc"):
		longdesc = data.longdesc 
	var nodes = data.nodes if data.has("nodes") else []
	mm_loader.add_to_gen_graph(self, nodes, data.connections if data.has("connections") else [])
