// Copyright (C) 2018-2024, Tellusim Technologies Inc. All rights reserved
// https://tellusim.com/

#include <core/TellusimLog.h>
#include <core/TellusimSet.h>
#include <format/TellusimXml.h>

#include <interface/tooltip/include/TellusimControlTooltip.h>
#include "../include/TellusimControlFlow.h"

/*
 */
namespace Tellusim {
	
	/*
	 */
	static const char *control_flow_shape_names[] = {
		"circle", "square", "dialond", "triangle",
	};
	TS_STATIC_ASSERT(TS_COUNTOF(control_flow_shape_names) == ControlFlow::NumShapes);
	
	/*
	 */
	ControlFlow::ControlFlow(Control *parent) : ControlBase(parent) {
		
		// selection style
		setSelectionStyle(StrokeStyle(3.0f, Color(0.6f, 0.8f, 1.0f, 0.75f)));
		
		// proto style
		setProtoColor(Color(0.7f, 0.8f));
		setProtoStrokeStyle(StrokeStyle(3.0f, Color::black));
		setProtoGradientStyle(GradientStyle(2.0f, Vector2f(0.2f, 1.0f), Color::gray, Color::black));
	}
	
	ControlFlow::~ControlFlow() {
		
	}
	
	/*
	 */
	void ControlFlow::clearFlow() {
		clearNodes();
		clearProtos();
		clearTypes();
	}
	
	/*
	 */
	void ControlFlow::load_stroke_style(const Xml &xml, StrokeStyle &style) {
		
	}
	
	void ControlFlow::load_gradient_style(const Xml &xml, GradientStyle &style) {
		
	}
	
	/*
	 */
	void ControlFlow::save_stroke_style(Xml &xml, const StrokeStyle &style) {
		Xml child = Xml(&xml, "stroke");
		if(style.width != 0.0f) child.setData("width", style.width);
		if(style.offset != 0.0f) child.setData("offset", style.offset);
		if(style.color != Color::white) child.setData("color", style.color.c, 4);
	}
	
	void ControlFlow::save_gradient_style(Xml &xml, const GradientStyle &style) {
		Xml child = Xml(&xml, "gradient");
		if(style.radius != 0.0f) child.setData("radius", style.radius);
		if(style.length != 0.0f) child.setData("length", style.length);
		if(style.center != Vector2f::zero) child.setData("center", style.center.v, 2);
		if(style.axis != Vector2f::zero) child.setData("axis", style.axis.v, 2);
		if(style.color_0 != Color::white) child.setData("color_0", style.color_0.c, 4);
		if(style.color_1 != Color::black) child.setData("color_1", style.color_1.c, 4);
	}
	
	/*
	 */
	bool ControlFlow::load(const Xml &xml, bool merge) {
		
		// clear flow
		if(!merge) clearFlow();
		
		// types merge
		Map<uint32_t, uint32_t> type_indices;
		
		// protos merge
		struct ProtoIndices {
			uint32_t proto_index;
			Map<uint32_t, uint32_t> input_indices;
			Map<uint32_t, uint32_t> output_indices;
		};
		Map<uint32_t, ProtoIndices> proto_indices;
		
		// node indices
		struct NodeIndices {
			uint32_t node_index;
			uint32_t proto_index;
		};
		Map<uint32_t, NodeIndices> node_indices;
		
		// load flow
		for(const Xml &child : xml.getChildren()) {
			const String &name = child.getName();
			
			// load types
			if(name == "types") {
				for(const Xml &types_xml : child.getChildren()) {
					const String &name = types_xml.getName();
					if(name == "type") {
						const String &name = types_xml.getAttribute("name");
						uint32_t src_index = types_xml.getAttribute("index", Maxu32);
						uint32_t type_index = findType(name);
						if(type_index == Maxu32) {
							type_index = addType(name);
							type_indices.append(src_index, type_index);
							for(const Xml &type_xml : types_xml.getChildren()) {
								const String &name = type_xml.getName();
								if(name == "text") setTypeText(type_index, type_xml.getData());
								else if(name == "info") setTypeInfo(type_index, type_xml.getData());
								else if(name == "value") setTypeValue(type_index, type_xml.getData());
								else if(name == "color") type_xml.getData(types[type_index].color.c, 4);
								else if(name == "stroke") load_stroke_style(type_xml, types[type_index].stroke_style);
								else if(name == "mask") {
									uint64_t mask = 0;
									Array<String> masks(64u);
									masks.resize(type_xml.getData(masks));
									for(const String &name : masks) {
										uint32_t index = findType(name);
										if(index < 64) mask |= (1ull << index);
										else if(index != Maxu32) TS_LOGF(Warning, "ControlFlow::load(): can't apply mask for \"%s\"\n", name.get());
										else TS_LOGF(Warning, "ControlFlow::load(): unknown type \"%s\"\n", name.get());
									}
									setTypeMask(type_index, mask);
								}
								else if(name == "shape") {
									Shape shape = NumShapes;
									const String &name = type_xml.getData();
									for(uint32_t i = 0; i < NumShapes; i++) {
										if(control_flow_shape_names[i] == name) {
											shape = (Shape)i;
											break;
										}
									}
									if(shape < NumShapes) setTypeShape(type_index, shape);
									else TS_LOGF(Warning, "ControlFlow::load(): unknown shape \"%s\"\n", name.get());
								}
								else if(name == "connection") {
									for(const Xml &connection_xml : type_xml.getChildren()) {
										const String &name = connection_xml.getName();
										if(name == "width") setTypeConnectionWidth(type_index, connection_xml.getDataf32());
										else if(name == "color") connection_xml.getData(types[type_index].connection_color.c, 4);
										else if(name == "stroke") load_stroke_style(connection_xml, types[type_index].connection_style);
										else TS_LOGF(Warning, "ControlFlow::load(): unknown connection node \"%s\"\n", name.get());
									}
								}
								else {
									TS_LOGF(Warning, "ControlFlow::load(): unknown type node \"%s\"\n", name.get());
								}
							}
						} else {
							type_indices.append(src_index, type_index);
						}
					}
					else {
						TS_LOGF(Warning, "ControlFlow::load(): unknown types node \"%s\"\n", name.get());
					}
				}
			}
			
			// load protos
			else if(name == "protos") {
				for(const Xml &protos_xml : child.getChildren()) {
					const String &name = protos_xml.getName();
					if(name == "proto") {
						const String &name = protos_xml.getAttribute("name");
						uint32_t src_index = protos_xml.getAttribute("index", Maxu32);
						uint32_t proto_index = findProto(name);
						if(proto_index == Maxu32) {
							proto_index = addProto(name);
							auto proto_indices_it = proto_indices.append(src_index);
							proto_indices_it->data.proto_index = proto_index;
							for(const Xml &proto_xml : protos_xml.getChildren()) {
								const String &name = proto_xml.getName();
								if(name == "text") setProtoText(proto_index, proto_xml.getData());
								else if(name == "info") setProtoInfo(proto_index, proto_xml.getData());
								else if(name == "source") setProtoSource(proto_index, proto_xml.getData());
								else if(name == "color") proto_xml.getData(protos[proto_index].color.c, 4);
								else if(name == "stroke") load_stroke_style(proto_xml, protos[proto_index].stroke_style);
								else if(name == "gradient") load_gradient_style(proto_xml, protos[proto_index].gradient_style);
								else if(name == "inputs") {
									for(const Xml &inputs_xml : proto_xml.getChildren()) {
										const String &name = inputs_xml.getName();
										if(name == "input") {
											uint32_t type_index = inputs_xml.getAttribute("type", Maxu32);
											auto type_index_it = type_indices.find(type_index);
											if(!type_index_it) {
												TS_LOGF(Warning, "ControlFlow::load(): unknown input type %u\n", type_index);
												continue;
											}
											uint32_t src_index = inputs_xml.getAttribute("index", Maxu32);
											uint32_t input_index = addProtoInput(proto_index, "", type_index_it->data);
											proto_indices_it->data.input_indices.append(src_index, input_index);
											for(const Xml &input_xml : inputs_xml.getChildren()) {
												const String &name = input_xml.getName();
												if(name == "name") setProtoInputName(proto_index, input_index, input_xml.getData());
												else if(name == "text") setProtoInputText(proto_index, input_index, input_xml.getData());
												else if(name == "info") setProtoInputInfo(proto_index, input_index, input_xml.getData());
												else if(name == "value") setProtoInputValue(proto_index, input_index, input_xml.getData());
												else if(name == "multi") setMultiProtoInput(proto_index, input_index, input_xml.getDataBool());
												else TS_LOGF(Warning, "ControlFlow::load(): unknown input node \"%s\"\n", name.get());
											}
										}
										else {
											TS_LOGF(Warning, "ControlFlow::load(): unknown inputs node \"%s\"\n", name.get());
										}
									}
								}
								else if(name == "outputs") {
									for(const Xml &outputs_xml : proto_xml.getChildren()) {
										const String &name = outputs_xml.getName();
										if(name == "output") {
											uint32_t type_index = outputs_xml.getAttribute("type", Maxu32);
											auto type_index_it = type_indices.find(type_index);
											if(!type_index_it) {
												TS_LOGF(Warning, "ControlFlow::load(): unknown output type %u\n", type_index);
												continue;
											}
											uint32_t src_index = outputs_xml.getAttribute("index", Maxu32);
											uint32_t output_index = addProtoOutput(proto_index, "", type_index_it->data);
											proto_indices_it->data.output_indices.append(src_index, output_index);
											for(const Xml &output_xml : outputs_xml.getChildren()) {
												const String &name = output_xml.getName();
												if(name == "name") setProtoOutputName(proto_index, output_index, output_xml.getData());
												else if(name == "text") setProtoOutputText(proto_index, output_index, output_xml.getData());
												else if(name == "info") setProtoOutputInfo(proto_index, output_index, output_xml.getData());
												else if(name == "value") setProtoOutputValue(proto_index, output_index, output_xml.getData());
												else if(name == "multi") setMultiProtoOutput(proto_index, output_index, output_xml.getDataBool());
												else TS_LOGF(Warning, "ControlFlow::load(): unknown output node \"%s\"\n", name.get());
											}
										}
										else {
											TS_LOGF(Warning, "ControlFlow::load(): unknown outputs node \"%s\"\n", name.get());
										}
									}
								}
								else {
									TS_LOGF(Warning, "ControlFlow::load(): unknown proto node \"%s\"\n", name.get());
								}
							}
						}
						// merge proto
						else {
							auto proto_indices_it = proto_indices.append(src_index);
							proto_indices_it->data.proto_index = proto_index;
							for(const Xml &proto_xml : protos_xml.getChildren()) {
								const String &name = proto_xml.getName();
								if(name == "inputs") {
									for(const Xml &inputs_xml : proto_xml.getChildren()) {
										const String &name = inputs_xml.getName();
										if(name == "input") {
											uint32_t type_index = inputs_xml.getAttribute("type", Maxu32);
											auto type_index_it = type_indices.find(type_index);
											if(!type_index_it) {
												TS_LOGF(Warning, "ControlFlow::load(): unknown input type %u\n", type_index);
												continue;
											}
											String name = inputs_xml.getData("name");
											if(!name) name = inputs_xml.getData("text");
											if(!name) name = getTypeName(type_index_it->data);
											uint32_t input_index = findProtoInput(proto_index, name);
											if(input_index == Maxu32) {
												TS_LOGF(Warning, "ControlFlow::load(): unknown proto input \"%s\"\n", name.get());
												continue;
											}
											if(getProtoInputType(proto_index, input_index) != type_index_it->data) {
												TS_LOGF(Warning, "ControlFlow::load(): invalid proto input type \"%s\"\n", name.get());
												continue;
											}
											uint32_t src_index = inputs_xml.getAttribute("index", Maxu32);
											proto_indices_it->data.input_indices.append(src_index, input_index);
										}
										else {
											TS_LOGF(Warning, "ControlFlow::load(): unknown inputs node \"%s\"\n", name.get());
										}
									}
								}
								else if(name == "outputs") {
									for(const Xml &outputs_xml : proto_xml.getChildren()) {
										const String &name = outputs_xml.getName();
										if(name == "output") {
											uint32_t type_index = outputs_xml.getAttribute("type", Maxu32);
											auto type_index_it = type_indices.find(type_index);
											if(!type_index_it) {
												TS_LOGF(Warning, "ControlFlow::load(): unknown output type %u\n", type_index);
												continue;
											}
											String name = outputs_xml.getData("name");
											if(!name) name = outputs_xml.getData("text");
											if(!name) name = getTypeName(type_index_it->data);
											uint32_t output_index = findProtoOutput(proto_index, name);
											if(output_index == Maxu32) {
												TS_LOGF(Warning, "ControlFlow::load(): unknown proto output \"%s\"\n", name.get());
												continue;
											}
											if(getProtoOutputType(proto_index, output_index) != type_index_it->data) {
												TS_LOGF(Warning, "ControlFlow::load(): invalid proto output type \"%s\"\n", name.get());
												continue;
											}
											uint32_t src_index = outputs_xml.getAttribute("index", Maxu32);
											proto_indices_it->data.output_indices.append(src_index, output_index);
										}
										else {
											TS_LOGF(Warning, "ControlFlow::load(): unknown outputs node \"%s\"\n", name.get());
										}
									}
								}
							}
						}
					}
					else {
						TS_LOGF(Warning, "ControlFlow::load(): unknown protos node \"%s\"\n", name.get());
					}
				}
			}
			
			// load nodes
			else if(name == "nodes") {
				for(const Xml &nodes_xml : child.getChildren()) {
					const String &name = nodes_xml.getName();
					if(name == "node") {
						uint32_t proto_index = nodes_xml.getAttribute("proto", Maxu32);
						auto proto_index_it = proto_indices.find(proto_index);
						if(!proto_index_it) {
							TS_LOGF(Warning, "ControlFlow::load(): unknown proto type %u\n", proto_index);
							continue;
						}
						uint32_t src_index = nodes_xml.getAttribute("index", Maxu32);
						uint32_t node_index = addNode(proto_index_it->data.proto_index);
						auto node_index_it = node_indices.append(src_index);
						node_index_it->data.node_index = node_index;
						node_index_it->data.proto_index = proto_index;
						for(const Xml &node_xml : nodes_xml.getChildren()) {
							const String &name = node_xml.getName();
							if(name == "value") setNodeValue(node_index, node_xml.getData());
							if(name == "static") setNodeStatic(node_index, node_xml.getDataBool());
							else if(name == "position") {
								Vector2f position = Vector2f::zero;
								if(node_xml.getData(position.v, 2) == 2) setNodePosition(node_index, position);
							}
						}
						const Node &node = nodes[node_index];
						if(node.load_func) node.load_func(this, &nodes_xml, node_index);
					}
					else {
						TS_LOGF(Warning, "ControlFlow::load(): unknown nodes node \"%s\"\n", name.get());
					}
				}
			}
			
			// load connections
			else if(name == "connections") {
				for(const Xml &connections_xml : child.getChildren()) {
					const String &name = connections_xml.getName();
					if(name == "output") {
						uint32_t output_node = connections_xml.getAttribute("node", Maxu32);
						auto output_node_it = node_indices.find(output_node);
						if(!output_node_it) {
							TS_LOGF(Warning, "ControlFlow::load(): unknown output node %u\n", output_node);
							continue;
						}
						auto output_proto_it = proto_indices.find(output_node_it->data.proto_index);
						if(!output_proto_it) {
							TS_LOGF(Warning, "ControlFlow::load(): unknown output proto %u\n", output_node);
							continue;
						}
						uint32_t output_index = connections_xml.getAttribute("index", Maxu32);
						auto output_it = output_proto_it->data.output_indices.find(output_index);
						if(!output_it) {
							TS_LOGF(Warning, "ControlFlow::load(): unknown output index %u\n", output_index);
							continue;
						}
						for(const Xml &output_xml : connections_xml.getChildren()) {
							const String &name = output_xml.getName();
							if(name == "input") {
								uint32_t input_node = output_xml.getAttribute("node", Maxu32);
								auto input_node_it = node_indices.find(input_node);
								if(!input_node_it) {
									TS_LOGF(Warning, "ControlFlow::load(): unknown input node %u\n", input_node);
									continue;
								}
								auto input_proto_it = proto_indices.find(input_node_it->data.proto_index);
								if(!input_proto_it) {
									TS_LOGF(Warning, "ControlFlow::load(): unknown input proto %u\n", input_node);
									continue;
								}
								uint32_t input_index = output_xml.getAttribute("index", Maxu32);
								auto input_it = input_proto_it->data.input_indices.find(input_index);
								if(!input_it) {
									TS_LOGF(Warning, "ControlFlow::load(): unknown input index %u\n", input_index);
									continue;
								}
								addOutputConnection(output_node_it->data.node_index, output_it->data, input_node_it->data.node_index, input_it->data);
							}
							else {
								TS_LOGF(Warning, "ControlFlow::load(): unknown outputs node \"%s\"\n", name.get());
							}
						}
					}
					else {
						TS_LOGF(Warning, "ControlFlow::load(): unknown connections node \"%s\"\n", name.get());
					}
				}
			}
			else {
				TS_LOGF(Warning, "ControlFlow::load(): unknown node \"%s\"\n", name.get());
			}
		}
		return true;
	}
	
	bool ControlFlow::load(const char *name, bool merge) {
		Xml xml;
		if(!xml.load(name) || !load(xml, merge)) {
			TS_LOGF(Error, "ControlFlow::load(): can't load \"%s\" file\n", name);
			return false;
		}
		return true;
	}
	
	bool ControlFlow::load(const String &name, bool merge) {
		Xml xml;
		if(!xml.load(name) || !load(xml, merge)) {
			TS_LOGF(Error, "ControlFlow::load(): can't load \"%s\" file\n", name.get());
			return false;
		}
		return true;
	}
	
	/*
	 */
	void ControlFlow::save(Xml &xml, bool merge) const {
		
		// node dependencies
		Set<uint32_t> type_indices;
		Set<uint32_t> proto_indices;
		if(merge) {
			for(const Node &node : nodes) {
				if(node.index == Maxu32) continue;
				if(protos[node.proto].index == Maxu32) continue;
				proto_indices.append(node.proto);
				for(const NodeInput &input : node.inputs) {
					if(types[input.type].index == Maxu32) continue;
					type_indices.append(input.type);
				}
				for(const NodeOutput &output : node.outputs) {
					if(types[output.type].index == Maxu32) continue;
					type_indices.append(output.type);
				}
			}
		}
		
		// save types
		if(type_indices || (types && !merge)) {
			Xml parent = Xml(&xml, "types");
			for(const Type &type : types) {
				if(type.index == Maxu32) continue;
				if(merge && !type_indices.find(type.index)) continue;
				Xml child = Xml(&parent, "type");
				child.setAttribute("name", type.name);
				child.setAttribute("index", type.index);
				if(type.text) child.setData("text", type.text);
				if(type.info) child.setData("info", type.info);
				if(type.value) child.setData("value", type.value);
				if(type.mask) {
					Array<const char*> masks;
					uint64_t mask = type.mask;
					for(const Type &type : types) {
						if(!(mask & (1ull << type.index))) continue;
						masks.append(type.name.get());
					}
					child.setData("mask", masks.get(), masks.size());
				}
				if(type.shape != getTypeShape()) child.setData("shape", control_flow_shape_names[type.shape]);
				if(type.color != getTypeColor()) child.setData("color", type.color.c, 4);
				if(type.stroke_style != getTypeStrokeStyle()) save_stroke_style(child, type.stroke_style);
				if(type.gradient_style != getTypeGradientStyle()) save_gradient_style(child, type.gradient_style);
				if(type.connection_width != getConnectionWidth() || type.connection_color != getConnectionColor() || type.connection_style != getConnectionStyle()) {
					Xml connection = Xml(&child, "connection");
					if(type.connection_width != getConnectionWidth()) connection.setData("width", type.connection_width);
					if(type.connection_color != getConnectionColor()) connection.setData("color", type.connection_color.c, 4);
					if(type.connection_style != getConnectionStyle()) save_stroke_style(connection, type.connection_style);
				}
			}
		}
		
		// save protos
		if(proto_indices || (protos && !merge)) {
			Xml parent = Xml(&xml, "protos");
			for(const Proto &proto : protos) {
				if(proto.index == Maxu32) continue;
				if(merge && !proto_indices.find(proto.index)) continue;
				Xml child = Xml(&parent, "proto");
				child.setAttribute("name", proto.name);
				child.setAttribute("index", proto.index);
				if(proto.text) child.setData("text", proto.text);
				if(proto.info) child.setData("info", proto.info);
				if(proto.source) child.setData("source", proto.source);
				if(proto.color != getProtoColor()) child.setData("color", proto.color.c, 4);
				if(proto.stroke_style != getProtoStrokeStyle()) save_stroke_style(child, proto.stroke_style);
				if(proto.gradient_style != getProtoGradientStyle()) save_gradient_style(child, proto.gradient_style);
				if(proto.inputs) {
					Xml inputs = Xml(&child, "inputs");
					for(const ProtoInput &input : proto.inputs) {
						if(input.index == Maxu32) continue;
						Xml child = Xml(&inputs, "input");
						const Type &type = types[input.type];
						child.setAttribute("type", input.type);
						child.setAttribute("index", input.index);
						if(input.name) child.setData("name", input.name);
						if(input.text && input.text != type.text) child.setData("text", input.text);
						if(input.info && input.info != type.info) child.setData("info", input.info);
						if(input.value && input.value != type.value) child.setData("value", input.value);
						if(input.is_multi) child.setData("multi", input.is_multi);
					}
				}
				if(proto.outputs) {
					Xml outputs = Xml(&child, "outputs");
					for(const ProtoOutput &output : proto.outputs) {
						if(output.index == Maxu32) continue;
						Xml child = Xml(&outputs, "output");
						const Type &type = types[output.type];
						child.setAttribute("type", output.type);
						child.setAttribute("index", output.index);
						if(output.name) child.setData("name", output.name);
						if(output.text && output.text != type.text) child.setData("text", output.text);
						if(output.info && output.info != type.info) child.setData("info", output.info);
						if(output.value && output.value != type.value) child.setData("value", output.value);
						if(output.is_multi) child.setData("multi", output.is_multi);
					}
				}
			}
		}
		
		// save nodes
		if(nodes) {
			Xml parent = Xml(&xml, "nodes");
			for(const Node &node : nodes) {
				if(node.index == Maxu32) continue;
				Xml child = Xml(&parent, "node");
				child.setAttribute("proto", node.proto);
				child.setAttribute("index", node.index);
				if(node.value) child.setData("value", node.value);
				if(node.is_static) child.setData("static", node.is_static);
				if(node.position != Vector2f::zero) child.setData("position", node.position.v, 2);
				if(node.save_func) node.save_func(this, &child, node.index);
			}
		}
		
		// save connections
		if(nodes) {
			Xml parent = Xml(&xml, "connections");
			for(const Node &node : nodes) {
				if(node.index == Maxu32) continue;
				for(const NodeOutput &output : node.outputs) {
					if(!output.connections) continue;
					Xml child = Xml(&parent, "output");
					child.setAttribute("node", node.index);
					child.setAttribute("index", output.index);
					for(const OutputConnection &connection : output.connections) {
						Xml input = Xml(&child, "input");
						input.setAttribute("node", connection.node);
						input.setAttribute("index", connection.input);
					}
				}
			}
		}
	}
	
	bool ControlFlow::save(const char *name, bool merge) const {
		Xml xml("flow");
		save(xml, merge);
		if(!xml.save(name)) {
			TS_LOGF(Error, "ControlFlow::save(): can't save \"%s\" file\n", name);
			return false;
		}
		return true;
	}
	
	bool ControlFlow::save(const String &name, bool merge) const {
		Xml xml("flow");
		save(xml, merge);
		if(!xml.save(name)) {
			TS_LOGF(Error, "ControlFlow::save(): can't save \"%s\" file\n", name.get());
			return false;
		}
		return true;
	}
	
	/*****************************************************************************\
	 *
	 * ControlFlow Types
	 *
	\*****************************************************************************/
	
	/*
	 */
	void ControlFlow::clearTypes() {
		types.clear();
		type_names.clear();
	}
	
	void ControlFlow::reserveTypes(uint32_t num_types) {
		types.reserve(num_types);
	}
	
	/*
	 */
	uint32_t ControlFlow::addType(const char *name, const Color &color, Shape shape, uint64_t mask) {
		Type type;
		type.name = name;
		type.mask = mask;
		type.index = types.size();
		type.shape = shape;
		type.color = color;
		type.stroke_style = getTypeStrokeStyle();
		type.gradient_style = getTypeGradientStyle();
		type.connection_color = color;
		type.connection_width = getConnectionWidth();
		type.connection_style = getConnectionStyle();
		type_names.append(type.name, type.index);
		types.append(type);
		return type.index;
	}
	
	uint32_t ControlFlow::addType(const String &name, const Color &color, Shape shape, uint64_t mask) {
		return addType(name.get(), color, shape, mask);
	}
	
	/*
	 */
	uint32_t ControlFlow::findType(const char *name) const {
		auto it = type_names.find(name);
		if(it) return it->data;
		return Maxu32;
	}
	
	uint32_t ControlFlow::findType(const String &name) const {
		return findType(name.get());
	}
	
	/*
	 */
	void ControlFlow::removeType(uint32_t index) {
		Type &type = types[index];
		auto it = type_names.find(type.name);
		if(it) type_names.remove(it);
		type.name.clear();
		type.index = Maxu32;
	}
	
	/*
	 */
	void ControlFlow::setTypeName(uint32_t index, const char *name) {
		Type &type = types[index];
		auto it = type_names.find(type.name);
		if(it) type_names.remove(it);
		type.name = name;
		type_names.append(type.name, type.index);
	}
	
	void ControlFlow::setTypeName(uint32_t index, const String &name) {
		setTypeName(index, name.get());
	}
	
	/*****************************************************************************\
	 *
	 * ControlFlow Protos
	 *
	\*****************************************************************************/
	
	/*
	 */
	void ControlFlow::clearProtos() {
		protos.clear();
		proto_names.clear();
	}
	
	void ControlFlow::reserveProtos(uint32_t num_protos) {
		protos.reserve(num_protos);
	}
	
	/*
	 */
	uint32_t ControlFlow::addProto(const char *name, uint32_t input_type, uint32_t output_type) {
		Proto proto;
		proto.name = name;
		proto.text = name;
		proto.index = protos.size();
		proto.color = getProtoColor();
		proto.stroke_style = getProtoStrokeStyle();
		proto.gradient_style = getProtoGradientStyle();
		if(input_type != Maxu32) {
			ProtoInput input;
			input.type = input_type;
			input.index = proto.inputs.size();
			input.text = getTypeText(input_type);
			input.info = types[input_type].info;
			proto.inputs.append(input);
		}
		if(output_type != Maxu32) {
			ProtoOutput output;
			output.type = output_type;
			output.index = proto.outputs.size();
			output.text = getTypeText(output_type);
			output.info = types[output_type].info;
			proto.outputs.append(output);
		}
		proto_names.append(proto.name, proto.index);
		update_proto_tree = true;
		protos.append(proto);
		return proto.index;
	}
	
	uint32_t ControlFlow::addProto(const String &name, uint32_t input, uint32_t output) {
		return addProto(name.get(), input, output);
	}
	
	/*
	 */
	uint32_t ControlFlow::addProto(const char *name, const InitializerList<uint32_t> &inputs, const InitializerList<uint32_t> &outputs) {
		Proto proto;
		proto.name = name;
		proto.text = name;
		proto.index = protos.size();
		proto.color = getProtoColor();
		proto.stroke_style = getProtoStrokeStyle();
		proto.gradient_style = getProtoGradientStyle();
		for(uint32_t input_type : inputs) {
			ProtoInput input;
			input.type = input_type;
			input.index = proto.inputs.size();
			input.text = getTypeText(input_type);
			input.info = types[input_type].info;
			proto.inputs.append(input);
		}
		for(uint32_t output_type : outputs) {
			ProtoOutput output;
			output.type = output_type;
			output.index = proto.outputs.size();
			output.text = getTypeText(output_type);
			output.info = types[output_type].info;
			proto.outputs.append(output);
		}
		proto_names.append(proto.name, proto.index);
		update_proto_tree = true;
		protos.append(proto);
		return proto.index;
	}
	
	uint32_t ControlFlow::addProto(const String &name, const InitializerList<uint32_t> &inputs, const InitializerList<uint32_t> &outputs) {
		return addProto(name.get(), inputs, outputs);
	}
	
	/*
	 */
	uint32_t ControlFlow::findProto(const char *name) const {
		auto it = proto_names.find(name);
		if(it) return it->data;
		for(const Proto &proto : protos) {
			if(proto.text == name) return proto.index;
		}
		return Maxu32;
	}
	
	uint32_t ControlFlow::findProto(const String &name) const {
		return findProto(name.get());
	}
	
	/*
	 */
	void ControlFlow::removeProto(uint32_t index) {
		Proto &proto = protos[index];
		auto it = proto_names.find(proto.name);
		if(it) proto_names.remove(it);
		proto.name.clear();
		proto.index = Maxu32;
		update_proto_tree = true;
	}
	
	/*
	 */
	void ControlFlow::setProtoName(uint32_t index, const char *name) {
		Proto &proto = protos[index];
		auto it = proto_names.find(proto.name);
		if(it) proto_names.remove(it);
		proto.name = name;
		proto_names.append(proto.name, proto.index);
	}
	
	void ControlFlow::setProtoName(uint32_t index, const String &name) {
		setProtoName(index, name.get());
	}
	
	/*
	 */
	void ControlFlow::clearProtoInputs(uint32_t proto) {
		protos[proto].inputs.clear();
	}
	
	void ControlFlow::reserveProtoInputs(uint32_t proto, uint32_t num_inputs) {
		protos[proto].inputs.reserve(num_inputs);
	}
	
	/*
	 */
	uint32_t ControlFlow::addProtoInput(uint32_t proto, const char *name, uint32_t type) {
		ProtoInput input;
		input.name = name;
		input.type = type;
		input.info = types[type].info;
		input.value = types[type].value;
		input.index = protos[proto].inputs.size();
		protos[proto].inputs.append(input);
		return input.index;
	}
	
	uint32_t ControlFlow::addProtoInput(uint32_t proto, const String &name, uint32_t type) {
		return addProtoInput(proto, name.get(), type);
	}
	
	/*
	 */
	uint32_t ControlFlow::findProtoInput(uint32_t proto, const char *name) const {
		const auto &inputs = protos[proto].inputs;
		for(const ProtoInput &input : inputs) {
			if(input.name == name) return input.index;
		}
		for(const ProtoInput &input : inputs) {
			if(input.text == name) return input.index;
		}
		for(const ProtoInput &input : inputs) {
			if(types[input.type].name == name) return input.index;
		}
		for(const ProtoInput &input : inputs) {
			if(types[input.type].text == name) return input.index;
		}
		return Maxu32;
	}
	
	uint32_t ControlFlow::findProtoInput(uint32_t proto, const String &name) const {
		return findProtoInput(proto, name.get());
	}
	
	/*
	 */
	Array<uint32_t> ControlFlow::findProtoInputs(uint32_t proto, const char *name) const {
		Array<uint32_t> ret;
		for(const ProtoInput &input : protos[proto].inputs) {
			if(input.name == name || input.text == name || types[input.type].name == name || types[input.type].text == name) ret.append(input.index);
		}
		return move(ret);
	}
	
	Array<uint32_t> ControlFlow::findProtoInputs(uint32_t proto, const String &name) const {
		return move(findProtoInputs(proto, name.get()));
	}
	
	/*
	 */
	void ControlFlow::removeProtoInput(uint32_t proto, uint32_t index) {
		ProtoInput &input = protos[proto].inputs[index];
		input.name.clear();
		input.index = Maxu32;
	}
	
	/*
	 */
	bool ControlFlow::setProtoInputValue(uint32_t proto, const char *name, const char *value) {
		uint32_t index = findProtoInput(proto, name);
		if(index == Maxu32) {
			TS_LOGF(Error, "ControlFlow::setProtoInputValue(): can't find \"%s\" input\n", name);
			return false;
		}
		setProtoInputValue(proto, index, value);
		return true;
	}
	
	bool ControlFlow::setProtoInputValue(uint32_t proto, const char *name, const String &value) {
		return setProtoInputValue(proto, name, value.get());
	}
	
	bool ControlFlow::setProtoInputValue(uint32_t proto, const String &name, const String &value) {
		return setProtoInputValue(proto, name.get(), value.get());
	}
	
	const String &ControlFlow::getProtoInputValue(uint32_t proto, const char *name) const {
		uint32_t index = findProtoInput(proto, name);
		if(index == Maxu32) {
			TS_LOGF(Error, "ControlFlow::getProtoInputValue(): can't find \"%s\" input\n", name);
			return String::null;
		}
		return getProtoInputValue(proto, index);
	}
	
	const String &ControlFlow::getProtoInputValue(uint32_t proto, String &name) const {
		return getProtoInputValue(proto, name.get());
	}
	
	/*
	 */
	void ControlFlow::clearProtoOutputs(uint32_t proto) {
		protos[proto].outputs.clear();
	}
	
	void ControlFlow::reserveProtoOutputs(uint32_t proto, uint32_t num_outputs) {
		protos[proto].outputs.reserve(num_outputs);
	}
	
	/*
	 */
	uint32_t ControlFlow::addProtoOutput(uint32_t proto, const char *name, uint32_t type) {
		ProtoOutput output;
		output.name = name;
		output.type = type;
		output.info = types[type].info;
		output.value = types[type].value;
		output.index = protos[proto].outputs.size();
		protos[proto].outputs.append(output);
		return output.index;
	}
	
	uint32_t ControlFlow::addProtoOutput(uint32_t proto, const String &name, uint32_t type) {
		return addProtoOutput(proto, name.get(), type);
	}
	
	/*
	 */
	uint32_t ControlFlow::findProtoOutput(uint32_t proto, const char *name) const {
		const auto &outputs = protos[proto].outputs;
		for(const ProtoOutput &output : outputs) {
			if(output.name == name) return output.index;
		}
		for(const ProtoOutput &output : outputs) {
			if(output.text == name) return output.index;
		}
		for(const ProtoOutput &output : outputs) {
			if(types[output.type].name == name) return output.index;
		}
		for(const ProtoOutput &output : outputs) {
			if(types[output.type].text == name) return output.index;
		}
		return Maxu32;
	}
	
	uint32_t ControlFlow::findProtoOutput(uint32_t proto, const String &name) const {
		return findProtoOutput(proto, name.get());
	}
	
	/*
	 */
	Array<uint32_t> ControlFlow::findProtoOutputs(uint32_t proto, const char *name) const {
		Array<uint32_t> ret;
		for(const ProtoOutput &output : protos[proto].outputs) {
			if(output.name == name || output.text == name || types[output.type].name == name || types[output.type].text == name) ret.append(output.index);
		}
		return move(ret);
	}
	
	Array<uint32_t> ControlFlow::findProtoOutputs(uint32_t proto, const String &name) const {
		return move(findProtoOutputs(proto, name.get()));
	}
	
	/*
	 */
	void ControlFlow::removeProtoOutput(uint32_t proto, uint32_t index) {
		ProtoOutput &output = protos[proto].outputs[index];
		output.name.clear();
		output.index = Maxu32;
	}
	
	/*
	 */
	bool ControlFlow::setProtoOutputValue(uint32_t proto, const char *name, const char *value) {
		uint32_t index = findProtoOutput(proto, name);
		if(index == Maxu32) {
			TS_LOGF(Error, "ControlFlow::setProtoOutputValue(): can't find \"%s\" input\n", name);
			return false;
		}
		setProtoOutputValue(proto, index, value);
		return true;
	}
	
	bool ControlFlow::setProtoOutputValue(uint32_t proto, const char *name, const String &value) {
		return setProtoOutputValue(proto, name, value.get());
	}
	
	bool ControlFlow::setProtoOutputValue(uint32_t proto, const String &name, const String &value) {
		return setProtoOutputValue(proto, name.get(), value.get());
	}
	
	const String &ControlFlow::getProtoOutputValue(uint32_t proto, const char *name) const {
		uint32_t index = findProtoOutput(proto, name);
		if(index == Maxu32) {
			TS_LOGF(Error, "ControlFlow::getProtoOutputValue(): can't find \"%s\" input\n", name);
			return String::null;
		}
		return getProtoOutputValue(proto, index);
	}
	
	const String &ControlFlow::getProtoOutputValue(uint32_t proto, String &name) const {
		return getProtoOutputValue(proto, name.get());
	}
	
	/*****************************************************************************\
	 *
	 * ControlFlow Nodes
	 *
	\*****************************************************************************/
	
	/*
	 */
	void ControlFlow::clearNodes() {
		for(Node &node : nodes) {
			if(node.index == Maxu32) continue;
			removeNode(node.index);
		}
		nodes.clear();
	}
	
	void ControlFlow::reserveNodes(uint32_t num_nodes) {
		nodes.reserve(num_nodes);
	}
	
	/*
	 */
	uint32_t ControlFlow::addNode(uint32_t proto_index, const Vector2f &position, const char *value, bool is_static) {
		
		// node proto
		const Proto &proto = protos[proto_index];
		
		// create node
		Node node;
		node.value = value;
		node.proto = proto_index;
		node.index = nodes.size();
		node.is_static = is_static;
		node.position = position;
		
		// create node dialog
		node.dialog = ControlDialog(&flow_grid);
		node.dialog.setPosition(Vector3f(position, 0.0f));
		node.dialog.setAlign(AlignCenter | AlignOverlap);
		node.dialog.setResizable(false);
		node.dialog.setConstrained(false);
		node.dialog.setColor(proto.color);
		node.dialog.setStrokeStyle(proto.stroke_style);
		node.dialog.setGradientStyle(proto.gradient_style);
		if(proto.gradient_style) node.dialog.setMode(CanvasElement::ModeGradient);
		node.dialog.setUpdatedCallback(makeFunction(node_updated_callback, ControlDialog::null, this, node.index));
		node.dialog.setPressedCallback(makeFunction(node_pressed_callback, ControlRect::null, 0.0f, 0.0f, this, node.index));
		node.dialog.setReleasedCallback(makeFunction(node_released_callback, ControlRect::null, 0.0f, 0.0f, this, node.index));
		if(proto.info) tooltip->addTooltip(node.dialog, proto.info);
		
		// create node text
		node.title_text = ControlText(&node.dialog, proto.text);
		node.title_text.setMargin(spacing.x, spacing.x, spacing.y, 0.0f);
		node.title_text.setAlign(AlignTop | AlignCenterX);
		node.title_text.setFontName("sansb.ttf");
		
		// create main grid
		node.main_grid = ControlGrid(&node.dialog, 3, spacing.x);
		node.main_grid.setAlign(AlignExpand);
		
		// create input grid
		node.input_grid = ControlGrid(&node.main_grid, 2, spacing.x, spacing.y);
		node.input_grid.setAlign(AlignLeftBottom);
		
		// create node grid
		node.node_grid = ControlGrid(&node.main_grid);
		node.node_grid.setAlign(AlignTop);
		
		// create output grid
		node.output_grid = ControlGrid(&node.main_grid, 2, spacing.x, spacing.y);
		node.output_grid.setAlign(AlignRightBottom);
		
		// create inputs
		node.inputs.reserve(proto.inputs.size());
		for(const ProtoInput &proto_input : proto.inputs) {
			NodeInput node_input;
			node_input.value = proto_input.value;
			node_input.type = proto_input.type;
			node_input.proto = proto_index;
			node_input.index = node.inputs.size();
			
			// input socket
			node_input.socket = makeAutoPtr(create_socket(&node.input_grid, types[proto_input.type]));
			node_input.socket->setAlign(Control::AlignLeft | Control::AlignCenterY);
			node_input.socket->setPressedCallback(makeFunction(input_pressed_callback, nullptr, 0.0f, 0.0f, this, node.index, node_input.index));
			node_input.socket->setReleasedCallback(makeFunction(input_released_callback, nullptr, 0.0f, 0.0f, this, node.index, node_input.index));
			node_input.socket->setClicked2Callback(makeFunction(input_clicked2_callback, nullptr, this, node.index, node_input.index));
			if(proto_input.info) tooltip->addTooltip(node_input.socket.ref(), proto_input.info);
			
			// input text
			node_input.text = ControlText(&node.input_grid, proto_input.text);
			node_input.text.setAlign(Control::AlignLeft | Control::AlignCenterY);
			
			// add input
			node.inputs.append(node_input);
		}
		
		// create outputs
		node.outputs.reserve(proto.outputs.size());
		for(const ProtoOutput &proto_output : proto.outputs) {
			NodeOutput node_output;
			node_output.value = proto_output.value;
			node_output.type = proto_output.type;
			node_output.proto = proto_index;
			node_output.index = node.outputs.size();
			
			// output text
			node_output.text = ControlText(&node.output_grid, proto_output.text);
			node_output.text.setAlign(Control::AlignRight | Control::AlignCenterY);
			
			// output socket
			node_output.socket = makeAutoPtr(create_socket(&node.output_grid, types[proto_output.type]));
			node_output.socket->setAlign(Control::AlignRight | Control::AlignCenterY);
			node_output.socket->setPressedCallback(makeFunction(output_pressed_callback, nullptr, 0.0f, 0.0f, this, node.index, node_output.index));
			node_output.socket->setReleasedCallback(makeFunction(output_released_callback, nullptr, 0.0f, 0.0f, this, node.index, node_output.index));
			node_output.socket->setClicked2Callback(makeFunction(output_clicked2_callback, nullptr, this, node.index, node_output.index));
			if(proto_output.info) tooltip->addTooltip(node_output.socket.ref(), proto_output.info);
			
			// add output
			node.outputs.append(node_output);
		}
		
		// update node
		update_nodes.append(node.index);
		
		// add node
		nodes.append(node);
		
		// input create callbacks
		for(const ProtoInput &proto_input : proto.inputs) {
			const Type &type = types[proto_input.type];
			if(type.create_func) type.create_func(this, node.inputs[proto_input.index].socket.get(), type.index);
			if(proto_input.create_func) proto_input.create_func(this, node.input_grid, node.index, proto_input.index);
		}
		
		// output create callbacks
		for(const ProtoOutput &proto_output : proto.outputs) {
			const Type &type = types[proto_output.type];
			if(type.create_func) type.create_func(this, node.inputs[proto_output.index].socket.get(), type.index);
			if(proto_output.create_func) proto_output.create_func(this, node.output_grid, node.index, proto_output.index);
		}
		
		// proto create callback
		if(proto.create_func) proto.create_func(this, node.node_grid, node.index);
		
		return node.index;
	}
	
	uint32_t ControlFlow::addNode(uint32_t proto_index, const Vector2f &position, const String &value, bool is_static) {
		return addNode(proto_index, position, value.get(), is_static);
	}
	
	/*
	 */
	uint32_t ControlFlow::findNode(const char *name) const {
		for(const Node &node : nodes) {
			if(node.index == Maxu32) continue;
			if(protos[node.proto].name == name) return node.index;
		}
		for(const Node &node : nodes) {
			if(node.index == Maxu32) continue;
			if(protos[node.proto].text == name) return node.index;
		}
		return Maxu32;
	}
	
	uint32_t ControlFlow::findNode(const String &name) const {
		return findNode(name.get());
	}
	
	/*
	 */
	Array<uint32_t> ControlFlow::findNodes(const char *name) const {
		Array<uint32_t> ret;
		for(const Node &node : nodes) {
			if(node.index == Maxu32) continue;
			const Proto &proto = protos[node.proto];
			if(proto.name == name || proto.text == name) ret.append(node.index);
		}
		return move(ret);
	}
	
	Array<uint32_t> ControlFlow::findNodes(const String &name) const {
		return move(findNodes(name.get()));
	}
	
	/*
	 */
	void ControlFlow::removeNode(uint32_t node_index) {
		
		// clear node
		Node &node = nodes[node_index];
		if(node.index == Maxu32) return;
		clearConnections(node_index);
		node.index = Maxu32;
		
		// remove socket elements
		for(NodeInput &input : node.inputs) {
			if(input.socket) removed_elements.append(input.socket.release());
		}
		for(NodeOutput &output : node.outputs) {
			if(output.socket) removed_elements.append(output.socket.release());
		}
		
		// release node
		if(node.dialog) {
			flow_grid.removeChild(node.dialog);
			removed_controls.append(node.dialog);
			node.dialog.clearPtr();
		}
		
		// change mode
		mode = ModeClear;
	}
	
	/*
	 */
	void ControlFlow::setNodePosition(uint32_t node_index, const Vector2f &position) {
		Node &node = nodes[node_index];
		if(node.index == Maxu32) return;
		Vector2f offset = position - node.position;
		node.dialog.setPosition(Vector3f(position, 0.0f));
		for(NodeInput &input : node.inputs) {
			input.socket->setOffset(input.socket->getOffset() + Vector3f(offset, 0.0f));
		}
		for(NodeOutput &output : node.outputs) {
			output.socket->setOffset(output.socket->getOffset() + Vector3f(offset, 0.0f));
		}
		update_nodes.append(node_index);
		node.position = position;
	}
	
	/*
	 */
	void ControlFlow::updateNode(uint32_t node_index, bool inverse) {
		const Node &node = nodes[node_index];
		const Proto &proto = protos[node.proto];
		if(proto.update_func) proto.update_func(this, node.index, inverse);
		if(node.update_func) node.update_func(this, node.index, inverse);
	}
	
	/*
	 */
	void ControlFlow::cloneNodes(Array<uint32_t> &node_indices) {
		
		// create nodes
		Array<uint32_t> old_node_indices = node_indices;
		for(uint32_t i = 0; i < node_indices.size(); i++) {
			uint32_t index = node_indices[i];
			const Node &node = nodes[index];
			if(node.is_static) continue;
			if(node.index == Maxu32) continue;
			node_indices[i] = addNode(node.proto, node.position);
		}
		
		// create connections
		for(uint32_t i = 0; i < old_node_indices.size(); i++) {
			uint32_t index = old_node_indices[i];
			const Node &node = nodes[index];
			if(node.is_static) continue;
			if(node.index == Maxu32) continue;
			for(const NodeInput &input : node.inputs) {
				for(const InputConnection &connection : input.connections) {
					uint32_t index = old_node_indices.findIndex(connection.node);
					if(index != Maxu32) addInputConnection(node_indices[i], input.index, node_indices[index], connection.output);
				}
			}
			for(const NodeOutput &output : node.outputs) {
				for(const OutputConnection &connection : output.connections) {
					uint32_t index = old_node_indices.findIndex(connection.node);
					if(index != Maxu32) addOutputConnection(node_indices[i], output.index, node_indices[index], connection.input);
				}
			}
		}
	}
	
	/*
	 */
	void ControlFlow::setInputValue(uint32_t node_index, uint32_t index, const char *value) {
		Node &node = nodes[node_index];
		NodeInput &input = node.inputs[index];
		if(input.value != value) {
			input.value = value;
			const Type &type = types[input.type];
			const ProtoInput &proto_input = protos[node.proto].inputs[index];
			if(type.update_func) type.update_func(this, input.text, input.value, input.type);
			if(proto_input.update_func) proto_input.update_func(this, node_index, index);
		}
	}
	
	void ControlFlow::setInputValue(uint32_t node, uint32_t index, const String &value) {
		setInputValue(node, index, value.get());
	}
	
	/*
	 */
	bool ControlFlow::setInputValue(uint32_t node, const char *name, const char *value) {
		uint32_t index = findInput(node, name);
		if(index == Maxu32) {
			TS_LOGF(Error, "ControlFlow::setInputValue(): can't find \"%s\" input\n", name);
			return false;
		}
		setInputValue(node, index, value);
		return true;
	}
	
	bool ControlFlow::setInputValue(uint32_t node, const char *name, const String &value) {
		return setInputValue(node, name, value.get());
	}
	
	bool ControlFlow::setInputValue(uint32_t node, const String &name, const String &value) {
		return setInputValue(node, name.get(), value.get());
	}
	
	const String &ControlFlow::getInputValue(uint32_t node, const char *name) const {
		uint32_t index = findInput(node, name);
		if(index == Maxu32) {
			TS_LOGF(Error, "ControlFlow::getInputValue(): can't find \"%s\" input\n", name);
			return String::null;
		}
		return getInputValue(node, index);
	}
	
	const String &ControlFlow::getInputValue(uint32_t node, String &name) const {
		return getInputValue(node, name.get());
	}
	
	/*
	 */
	void ControlFlow::setOutputValue(uint32_t node_index, uint32_t index, const char *value) {
		Node &node = nodes[node_index];
		NodeOutput &output = node.outputs[index];
		if(output.value != value) {
			output.value = value;
			const Type &type = types[output.type];
			const ProtoOutput &proto_output = protos[node.proto].outputs[index];
			if(type.update_func) type.update_func(this, output.text, output.value, output.type);
			if(proto_output.update_func) proto_output.update_func(this, node_index, index);
		}
	}
	
	void ControlFlow::setOutputValue(uint32_t node, uint32_t index, const String &value) {
		setOutputValue(node, index, value.get());
	}
	
	/*
	 */
	bool ControlFlow::setOutputValue(uint32_t node, const char *name, const char *value) {
		uint32_t index = findOutput(node, name);
		if(index == Maxu32) {
			TS_LOGF(Error, "ControlFlow::setOutputValue(): can't find \"%s\" output\n", name);
			return false;
		}
		setOutputValue(node, index, value);
		return true;
	}
	
	bool ControlFlow::setOutputValue(uint32_t node, const char *name, const String &value) {
		return setOutputValue(node, name, value.get());
	}
	
	bool ControlFlow::setOutputValue(uint32_t node, const String &name, const String &value) {
		return setOutputValue(node, name.get(), value.get());
	}
	
	const String &ControlFlow::getOutputValue(uint32_t node, const char *name) const {
		uint32_t index = findOutput(node, name);
		if(index == Maxu32) {
			TS_LOGF(Error, "ControlFlow::getOutputValue(): can't find \"%s\" output\n", name);
			return String::null;
		}
		return getOutputValue(node, index);
	}
	
	const String &ControlFlow::getOutputValue(uint32_t node, String &name) const {
		return getOutputValue(node, name.get());
	}
	
	/*****************************************************************************\
	 *
	 * ControlFlow Connections
	 *
	\*****************************************************************************/
	
	/*
	 */
	void ControlFlow::clearConnections(uint32_t node_index) {
		Node &node = nodes[node_index];
		if(node.index == Maxu32) return;
		for(uint32_t i = 0; i < node.inputs.size(); i++) {
			removeInputConnection(node_index, i);
		}
		for(uint32_t i = 0; i < node.outputs.size(); i++) {
			removeOutputConnection(node_index, i);
		}
	}
	
	/*
	 */
	uint32_t ControlFlow::addInputConnection(uint32_t node, uint32_t input, uint32_t output_node, uint32_t output_index) {
		const Node &input_node = nodes[node];
		if(input_node.index == Maxu32) return Maxu32;
		const NodeInput &node_input = input_node.inputs[input];
		addOutputConnection(output_node, output_index, node, input);
		return node_input.connections.size() - 1;
	}
	
	uint32_t ControlFlow::addInputConnection(uint32_t node, const char *input, uint32_t output_node, const char *output_name) {
		uint32_t input_index = findInput(node, input);
		if(input_index == Maxu32) {
			TS_LOGF(Error, "ControlFlow::addInputConnection(): can't find \"%s\" input\n", input);
			return Maxu32;
		}
		uint32_t output_index = findOutput(output_node, output_name);
		if(output_index == Maxu32) {
			TS_LOGF(Error, "ControlFlow::addInputConnection(): can't find \"%s\" output\n", output_name);
			return false;
		}
		return addInputConnection(node, input_index, output_node, output_index);
	}
	
	uint32_t ControlFlow::addInputConnection(uint32_t node, const String &input, uint32_t output_node, const String &output_name) {
		return addInputConnection(node, input.get(), output_node, output_name.get());
	}
	
	
	/*
	 */
	bool ControlFlow::isInputConnected(uint32_t node, uint32_t input, uint32_t output_node, uint32_t output_index) const {
		for(const InputConnection &input_connection : nodes[node].inputs[input].connections) {
			if(input_connection.node != output_node) continue;
			if(input_connection.output != output_index) continue;
			return true;
		}
		return false;
	}
	
	bool ControlFlow::isInputConnected(uint32_t node, const char *input, uint32_t output_node, const char *output_name) const {
		uint32_t input_index = findInput(node, input);
		if(input_index == Maxu32) {
			TS_LOGF(Error, "ControlFlow::isInputConnected(): can't find \"%s\" input\n", input);
			return false;
		}
		uint32_t output_index = findOutput(output_node, output_name);
		if(output_index == Maxu32) {
			TS_LOGF(Error, "ControlFlow::isInputConnected(): can't find \"%s\" output\n", output_name);
			return false;
		}
		return isInputConnected(node, input_index, output_node, output_index);
	}
	
	bool ControlFlow::isInputConnected(uint32_t node, const String &input, uint32_t output_node, const String &output_name) const {
		return isInputConnected(node, input.get(), output_node, output_name.get());
	}
	
	/*
	 */
	void ControlFlow::removeInputConnection(uint32_t node, uint32_t input, uint32_t output_node, uint32_t output_index) {
		auto &input_connections = nodes[node].inputs[input].connections;
		for(uint32_t i = input_connections.size(); i > 0; i--) {
			InputConnection &input_connection = input_connections[i - 1];
			if(input_connection.node != output_node && output_node != Maxu32) continue;
			if(input_connection.output != output_index && output_index != Maxu32) continue;
			auto &output_connections = nodes[input_connection.node].outputs[input_connection.output].connections;
			for(uint32_t j = output_connections.size(); j > 0; j--) {
				OutputConnection &output_connection = output_connections[j - 1];
				if(output_connection.node == node && output_connection.input == input) {
					removed_elements.append(output_connection.strip.release());
					output_connections.remove(j - 1);
				}
			}
			input_connections.remove(i - 1);
			is_changed = true;
		}
	}
	
	/*
	 */
	ControlElement *ControlFlow::getInputConnectionStrip(uint32_t node, uint32_t input, uint32_t index) {
		const InputConnection &connection = nodes[node].inputs[input].connections[index];
		auto &connections = nodes[connection.node].outputs[connection.output].connections;
		for(uint32_t i = 0; i < connections.size(); i++) {
			if(connections[i].input == input) return connections[i].strip.get();
		}
		return nullptr;
	}
	
	const ControlElement *ControlFlow::getInputConnectionStrip(uint32_t node, uint32_t input, uint32_t index) const {
		const InputConnection &connection = nodes[node].inputs[input].connections[index];
		const auto &connections = nodes[connection.node].outputs[connection.output].connections;
		for(uint32_t i = 0; i < connections.size(); i++) {
			if(connections[i].input == input) return connections[i].strip.get();
		}
		return nullptr;
	}
	
	/*
	 */
	uint32_t ControlFlow::addOutputConnection(uint32_t output_node_index, uint32_t output_index, uint32_t input_node_index, uint32_t input_index) {
		
		// connection nodes
		Node &output_node = nodes[output_node_index];
		Node &input_node = nodes[input_node_index];
		if(output_node.index == Maxu32) return Maxu32;
		if(input_node.index == Maxu32) return Maxu32;
		
		// output node
		const Proto &output_proto = protos[output_node.proto];
		NodeOutput &node_output = output_node.outputs[output_index];
		const ProtoOutput &proto_output = output_proto.outputs[output_index];
		if(!proto_output.is_multi) removeOutputConnection(output_node_index, output_index);
		
		// input node
		const Proto &input_proto = protos[input_node.proto];
		NodeInput &node_input = input_node.inputs[input_index];
		const ProtoInput &proto_input = input_proto.inputs[input_index];
		if(!proto_input.is_multi) removeInputConnection(input_node_index, input_index);
		
		// create output connection
		OutputConnection output_connection;
		output_connection.node = input_node_index;
		output_connection.input = input_index;
		output_connection.strip = makeAutoPtr(create_strip(types[proto_output.type]));
		node_output.connections.append(output_connection);
		
		// create input connection
		InputConnection input_connection;
		input_connection.node = output_node_index;
		input_connection.output = output_index;
		node_input.connections.append(input_connection);
		
		// update node
		update_nodes.append(output_node_index);
		
		// changed flag
		is_changed = true;
		
		// return connection index
		return node_output.connections.size() - 1;
	}
	
	uint32_t ControlFlow::addOutputConnection(uint32_t node, const char *output, uint32_t input_node, const char *input_name) {
		uint32_t output_index = findOutput(node, output);
		if(output_index == Maxu32) {
			TS_LOGF(Error, "ControlFlow::addOutputConnection(): can't find \"%s\" output\n", output);
			return Maxu32;
		}
		uint32_t input_index = findInput(input_node, input_name);
		if(input_index == Maxu32) {
			TS_LOGF(Error, "ControlFlow::addOutputConnection(): can't find \"%s\" input\n", input_name);
			return Maxu32;
		}
		return addOutputConnection(node, output_index, input_node, input_index);
	}
	
	uint32_t ControlFlow::addOutputConnection(uint32_t node, const String &output, uint32_t input_node, const String &input_name) {
		return addOutputConnection(node, output.get(), input_node, input_name.get());
	}
	
	/*
	 */
	bool ControlFlow::isOutputConnected(uint32_t node, uint32_t output, uint32_t input_node, uint32_t input_index) const {
		for(const OutputConnection &output_connection : nodes[node].outputs[output].connections) {
			if(output_connection.node != input_node) continue;
			if(output_connection.input != input_index) continue;
			return true;
		}
		return false;
	}
	
	bool ControlFlow::isOutputConnected(uint32_t node, const char *output, uint32_t input_node, const char *input_name) const {
		uint32_t output_index = findOutput(node, output);
		if(output_index == Maxu32) {
			TS_LOGF(Error, "ControlFlow::isOutputConnected(): can't find \"%s\" output\n", output);
			return false;
		}
		uint32_t input_index = findInput(input_node, input_name);
		if(input_index == Maxu32) {
			TS_LOGF(Error, "ControlFlow::isOutputConnected(): can't find \"%s\" input\n", input_name);
			return false;
		}
		return isOutputConnected(node, output_index, input_node, input_index);
	}
	
	bool ControlFlow::isOutputConnected(uint32_t node, const String &output, uint32_t input_node, const String &input_name) const {
		return isOutputConnected(node, output.get(), input_node, input_name.get());
	}
	
	/*****************************************************************************\
	 *
	 * ControlFlow Traversal
	 *
	\*****************************************************************************/
	
	/*
	 */
	void ControlFlow::removeOutputConnection(uint32_t node, uint32_t output, uint32_t input_node, uint32_t input_index) {
		auto &output_connections = nodes[node].outputs[output].connections;
		for(uint32_t i = output_connections.size(); i > 0; i--) {
			OutputConnection &output_connection = output_connections[i - 1];
			if(output_connection.node != input_node && input_node != Maxu32) continue;
			if(output_connection.input != input_index && input_index != Maxu32) continue;
			auto &input_connections = nodes[output_connection.node].inputs[output_connection.input].connections;
			for(uint32_t j = input_connections.size(); j > 0; j--) {
				InputConnection &input_connection = input_connections[j - 1];
				if(input_connection.node == node && input_connection.output == output) {
					input_connections.remove(j - 1);
				}
			}
			removed_elements.append(output_connection.strip.release());
			output_connections.remove(i - 1);
			is_changed = true;
		}
	}
	
	/*
	 */
	void ControlFlow::propagateInputValue(uint32_t node, uint32_t index) {
		const NodeInput &input = nodes[node].inputs[index];
		String value = getProtoInputValue(nodes[node].proto, index);
		for(const InputConnection &connection : input.connections) {
			value = getOutputValue(connection.node, connection.output);
			break;
		}
		setInputValue(node, index, value);
	}
	
	void ControlFlow::propagateInputValues(uint32_t node) {
		for(uint32_t i = 0; i < nodes[node].inputs.size(); i++) {
			propagateInputValue(node, i);
		}
	}
	
	/*
	 */
	void ControlFlow::propagateOutputValue(uint32_t node, uint32_t index) {
		const NodeOutput &output = nodes[node].outputs[index];
		for(const OutputConnection &connection : output.connections) {
			setInputValue(connection.node, connection.input, output.value);
		}
	}
	
	void ControlFlow::propagateOutputValues(uint32_t node) {
		for(uint32_t i = 0; i < nodes[node].outputs.size(); i++) {
			propagateOutputValue(node, i);
		}
	}
	
	/*
	 */
	void ControlFlow::traverseNodes(uint32_t node, const TraversalCallback &func) {
		
		// update frame
		frame++;
		
		// traverse nodes
		Array<Vector2i> node_indices;
		node_indices.append(Vector2i(node, Maxu32));
		for(uint32_t i = 0; i < node_indices.size(); i++) {
			Vector2i indices = node_indices[i];
			Node &node = nodes[indices.x];
			if(node.index == Maxu32) continue;
			if(node.frame == frame) continue;
			node.frame = frame;
			
			// update node
			updateNode(indices.x, false);
			
			// traverse callback
			if(func) func(this, indices.x, indices.y);
			else propagateOutputValues(indices.x);
			
			// node outputs
			for(const NodeOutput &output : node.outputs) {
				for(const OutputConnection &connection : output.connections) {
					node_indices.append(Vector2i(connection.node, indices.x));
				}
			}
		}
	}
	
	void ControlFlow::itraverseNodes(uint32_t node, const TraversalCallback &func) {
		
		// update frame
		frame++;
		
		// inverse traversal
		Array<Vector2i> node_indices;
		Array<Vector2i> traverse_indices;
		node_indices.append(Vector2i(node, Maxu32));
		for(uint32_t i = 0; i < node_indices.size(); i++) {
			Vector2i indices = node_indices[i];
			Node &node = nodes[indices.x];
			if(node.index == Maxu32) continue;
			if(node.frame == frame) continue;
			node.frame = frame;
			
			// node inputs
			traverse_indices.append(indices);
			for(const NodeInput &input : node.inputs) {
				for(const InputConnection &connection : input.connections) {
					node_indices.append(Vector2i(connection.node, indices.x));
				}
			}
		}
		
		// traverse nodes
		for(uint32_t i = traverse_indices.size(); i > 0; i--) {
			const Vector2i &indices = traverse_indices[i - 1];
			
			// traverse callback
			if(func) func(this, indices.x, indices.y);
			else propagateInputValues(indices.x);
			
			// update node
			updateNode(indices.x, true);
		}
	}
	
	/*****************************************************************************\
	 *
	 * ControlFlow Utils
	 *
	\*****************************************************************************/
	
	/*
	 */
	Vector2f ControlFlow::get_position(const ControlRoot &root) const {
		return floor(root.getMouse() / flow_area.getScale() - flow_area.getControlsOffset() - flow_area.getControlsSize() * 0.5f + 0.5f);
	}
	
	/*
	 */
	TS_INLINE const ControlFlow::Type &ControlFlow::get_node_input_type(uint32_t node, uint32_t input) const {
		const Proto &proto = protos[nodes[node].proto];
		return types[proto.inputs[input].type];
	}
	
	TS_INLINE const ControlFlow::Type &ControlFlow::get_node_output_type(uint32_t node, uint32_t output) const {
		const Proto &proto = protos[nodes[node].proto];
		return types[proto.outputs[output].type];
	}
	
	TS_INLINE bool ControlFlow::is_compatible_types(const Type &type_0, const Type &type_1) const {
		if((1ull << type_0.index) & type_1.mask) return true;
		if(type_0.index == type_1.index) return true;
		return false;
	}
	
	/*
	 */
	ControlElement *ControlFlow::create_socket(Control *parent, const Type &type) {
		
		Canvas canvas = getCanvas();
		CanvasElement canvas_element;
		
		// create circle shape
		if(type.shape == ShapeCircle) {
			CanvasEllipse ellipse = CanvasEllipse(canvas);
			ellipse.setStrokeStyle(type.stroke_style);
			ellipse.setGradientStyle(type.gradient_style);
			if(type.gradient_style) ellipse.setMode(CanvasElement::ModeGradient);
			ellipse.setRadius(getRadius());
			canvas_element = ellipse;
		}
		// create square shape
		else if(type.shape == ShapeSquare) {
			CanvasRect rect = CanvasRect(canvas);
			rect.setStrokeStyle(type.stroke_style);
			rect.setGradientStyle(type.gradient_style);
			if(type.gradient_style) rect.setMode(CanvasElement::ModeGradient);
			rect.setSize(getRadius() * 2.0f, getRadius() * 2.0f);
			canvas_element = rect;
		}
		// create diamond shape
		else if(type.shape == ShapeDiamond) {
			CanvasShape shape = CanvasShape(canvas);
			shape.setStrokeStyle(type.stroke_style);
			shape.setGradientStyle(type.gradient_style);
			if(type.gradient_style) shape.setMode(CanvasElement::ModeGradient);
			shape.addPosition(0.0f,  getRadius()); shape.addPosition(0.0f,  getRadius()); shape.addPosition(-getRadius(), 0.0f);
			shape.addPosition(-getRadius(), 0.0f); shape.addPosition(0.0f, -getRadius()); shape.addPosition(0.0f, -getRadius());
			shape.addPosition( getRadius(), 0.0f); shape.addPosition( getRadius(), 0.0f); shape.addPosition(0.0f,  getRadius());
			shape.addPosition(0.0f,  getRadius()); shape.addPosition(-getRadius(), 0.0f); shape.addPosition(0.0f,  getRadius());
			canvas_element = shape;
		}
		// create triangle shape
		else if(type.shape == ShapeTriangle) {
			CanvasTriangle triangle = CanvasTriangle(canvas);
			triangle.setStrokeStyle(type.stroke_style);
			triangle.setGradientStyle(type.gradient_style);
			if(type.gradient_style) triangle.setMode(CanvasElement::ModeGradient);
			triangle.setPosition0(0.0f, getRadius());
			triangle.setPosition1(-getRadius(), -getRadius() * 0.75f);
			triangle.setPosition2( getRadius(), -getRadius() * 0.75f);
			canvas_element = triangle;
		}
		else {
			TS_ASSERT(0 && "ControlFlow::create_socket(): unknown shape");
		}
		
		// element parameters
		canvas_element.setColor(type.color);
		
		// create element
		ControlRoot root = getRoot();
		ControlElement *element = new ControlElement(parent, canvas_element);
		element->setInputMargin(root.getPanelRadius(), getSpacing().y * 0.5f);
		element->setAlign(AlignCenter);
		return element;
	}
	
	/*
	 */
	ControlElement *ControlFlow::create_strip(const Type &type) {
		
		// create strip
		Canvas canvas = getCanvas();
		CanvasStrip strip = CanvasStrip(canvas);
		strip.setWidth(type.connection_width);
		strip.setColor(type.connection_color);
		strip.setStrokeStyle(type.connection_style);
		
		// create element
		ControlElement *element = new ControlElement(&flow_panel, strip);
		element->setAlign(AlignOverlap);
		return element;
	}
	
	void ControlFlow::create_strip(ControlElement *element, const Vector2f &p0, const Vector2f &p3, bool clear) {
		CanvasStrip strip = CanvasStrip(element->getElement());
		float32_t radius = min(getRadius() * 16.0f, length(p3 - p0) * 0.5f);
		Vector2f p1 = Vector2f(p0.x + radius, p0.y);
		Vector2f p2 = Vector2f(p3.x - radius, p3.y);
		if(clear) strip.clearPositions();
		strip.createCubic(p0, p1, p2, p3, 0.1f);
	}
	
	/*
	 */
	void ControlFlow::clear_spatial() {
		spatial_indices.clear();
		spatial_nodes.clear();
	}
	
	void ControlFlow::create_input_spatial(uint32_t node_index, uint32_t input_index) {
		
		// check spatial tree
		if(spatial_nodes) return;
		
		// create output indices
		const Type &input_type = get_node_input_type(node_index, input_index);
		for(const Node &node : nodes) {
			if(node.index == Maxu32) continue;
			for(const NodeOutput &output : node.outputs) {
				const Type &output_type = types[output.type];
				if(!is_compatible_types(output_type, input_type)) continue;
				if(isInputConnected(node_index, input_index, node.index, output.index)) continue;
				NodeIndex &node_index = spatial_indices.append();
				node_index.node = node.index;
				node_index.index = output.index;
			}
		}
		
		// create spatial tree
		spatial_nodes.resize(max(spatial_indices.size() * 2, 4u));
		for(uint32_t i = 0; i < spatial_indices.size(); i++) {
			const NodeIndex &node_index = spatial_indices[i];
			Spatial::Node2f &spatial_node = spatial_nodes[spatial_indices.size() + i];
			Vector2f position = getOutputSocket(node_index.node, node_index.index)->getOffset().xy;
			spatial_node.bound.set(position - getRadius(), position + getRadius());
		}
		Spatial::create<float32_t>(spatial_nodes.get(), spatial_indices.size());
	}
	
	void ControlFlow::create_output_spatial(uint32_t node_index, uint32_t output_index) {
		
		// check spatial tree
		if(spatial_nodes) return;
		
		// create input indices
		const Type &output_type = get_node_output_type(node_index, output_index);
		for(const Node &node : nodes) {
			if(node.index == Maxu32) continue;
			for(const NodeInput &input : node.inputs) {
				const Type &input_type = types[input.type];
				if(!is_compatible_types(output_type, input_type)) continue;
				if(isOutputConnected(node_index, output_index, node.index, input.index)) continue;
				NodeIndex &node_index = spatial_indices.append();
				node_index.node = node.index;
				node_index.index = input.index;
			}
		}
		
		// create spatial tree
		spatial_nodes.resize(max(spatial_indices.size() * 2, 4u));
		for(uint32_t i = 0; i < spatial_indices.size(); i++) {
			const NodeIndex &node_index = spatial_indices[i];
			Spatial::Node2f &spatial_node = spatial_nodes[spatial_indices.size() + i];
			Vector2f position = getInputSocket(node_index.node, node_index.index)->getOffset().xy;
			spatial_node.bound.set(position - getRadius(), position + getRadius());
		}
		Spatial::create<float32_t>(spatial_nodes.get(), spatial_indices.size());
	}
	
	/*****************************************************************************\
	 *
	 * ControlFlow Callbacks
	 *
	\*****************************************************************************/
	
	/*
	 */
	void ControlFlow::flow_pressed_callback(ControlRect rect, float32_t x, float32_t y, ControlFlow *self) {
		
		// create region rect
		ControlRoot root = rect.getRoot();
		ControlRect &region_rect = self->region_rect;
		if(root.getKeyboardKey(KeyCtrl) || region_rect.isEnabled()) {
			ControlRoot root = rect.getRoot();
			Vector2f position = root.getMouse() - self->flow_area.getControlsSize() * 0.5f;
			if(region_rect.isEnabled()) {
				region_rect.setSize(abs(position - self->region_position));
				region_rect.setPosition(Vector3f((position + self->region_position) * 0.5f, 0.0f));
				CanvasRect canvas_rect = region_rect.getCanvasRect();
				canvas_rect.setOrder(canvas_rect.getOrder() + Maxu16);
			} else {
				region_rect.setEnabled(true);
				region_rect.setPosition(Vector3f(position, 0.0f));
				region_rect.setSize(Vector2f::zero);
				self->flow_grid.raiseChild(region_rect);
				self->region_position = position;
				self->mode = ModeClear;
			}
			return;
		}
		
		// disable callback if nothing is selected
		if(!self->selected_nodes) {
			root.clearCurrentControl();
			self->mode = ModeClear;
		}
	}
	
	void ControlFlow::flow_released_callback(ControlRect rect, float32_t x, float32_t y, ControlFlow *self) {
		
		// clear selected nodes
		self->add_select_action();
		self->selected_nodes.clear();
		self->current_node_index = Maxu32;
		
		// select nodes by region rect
		if(self->region_rect.isEnabled()) {
			Vector2f rect_hsize = self->region_rect.getSize() * 0.5f;
			Vector2f rect_min = self->region_rect.getPosition().xy - rect_hsize;
			Vector2f rect_max = self->region_rect.getPosition().xy + rect_hsize;
			for(Node &node : self->nodes) {
				if(node.index == Maxu32) continue;
				Vector2f dialog_hsize = node.dialog.getRect().getSize() * 0.5f;
				Vector2f dialog_min = node.position - dialog_hsize;
				Vector2f dialog_max = node.position + dialog_hsize;
				if(rect_min.x > dialog_min.x || rect_max.x < dialog_max.x) continue;
				if(rect_min.y > dialog_min.y || rect_max.y < dialog_max.y) continue;
				self->selected_nodes.append(node.index);
				self->current_node_index = node.index;
			}
			if(self->selected_nodes) {
				ControlRoot root = rect.getRoot();
				root.setCurrentControl(self->nodes[self->current_node_index].dialog);
				self->mode = ModeNodePressed;
			}
			self->region_rect.setEnabled(false);
		}
	}
	
	/*
	 */
	void ControlFlow::node_updated_callback(ControlDialog dialog, ControlFlow *self, uint32_t node_index) {
		
		// disable callback
		ControlRoot root = dialog.getRoot();
		if(root.getKeyboardKey(KeyCtrl)) {
			Node &node = self->nodes[node_index];
			dialog.setPosition(Vector3f(node.position, 0.0f));
			root.clearCurrentControl();
			return;
		}
		
		// clone selected nodes
		if(!self->is_clone && root.getKeyboardKey(KeyShift, true)) {
			uint32_t index = self->selected_nodes.findIndex(node_index);
			self->cloneNodes(self->selected_nodes);
			node_index = self->selected_nodes[index];
			root.setCurrentControl(self->nodes[node_index].dialog);
			self->current_node_index = node_index;
			self->mode = ModeNodePressed;
			self->add_create_action();
			self->is_clone = true;
			self->is_move = true;
			return;
		}
		
		// move action
		if(!self->is_move) {
			self->add_move_action();
			self->is_move = true;
		}
		
		// node movement
		Node &node = self->nodes[node_index];
		Vector2f offset = dialog.getPosition().xy - node.position;
		dialog.setPosition(Vector3f(node.position, 0.0f));
		
		// move selected nodes
		Vector2f rect_min = node.position;
		Vector2f rect_max = node.position;
		for(uint32_t index : self->selected_nodes) {
			Node &node = self->nodes[index];
			if(node.index == Maxu32) continue;
			self->setNodePosition(index, node.position + offset);
			Vector2f hsize = node.dialog.getRect().getSize() * 0.5f;
			rect_min = min(rect_min, node.position - hsize);
			rect_max = max(rect_max, node.position + hsize);
		}
		Vector2f rect_size = rect_max - rect_min;
		
		// resize flow
		ControlArea &flow_area = self->flow_area;
		float32_t scale = flow_area.getScale();
		Rect view_rect = flow_area.getViewRect() / scale - flow_area.getControlsOffset() - flow_area.getControlsSize() * 0.5f;
		Vector2f mouse_position = dialog.getMousePosition();
		
		// horizontal resize
		if(view_rect.getWidth() > rect_size.x) {
			if(view_rect.left > rect_min.x) {
				float32_t offset = floor(view_rect.left - rect_min.x + 0.5f);
				float64_t value = flow_area.getHorizontalValue() - offset * scale;
				dialog.setMousePosition(mouse_position - Vector2f(offset, 0.0f));
				if(value < 0.0) self->flow_resize.x -= (float32_t)value - rect_size.x;
				flow_area.setHorizontalValue(max(value, 0.0));
			} else if(view_rect.right < rect_max.x) {
				float32_t offset = floor(rect_max.x - view_rect.right + 0.5f);
				float64_t value = flow_area.getHorizontalValue() + offset * scale;
				float64_t max_value = flow_area.getHorizontalRange() - flow_area.getHorizontalFrame();
				dialog.setMousePosition(mouse_position + Vector2f(offset, 0.0f));
				if(value > max_value) self->flow_resize.x += (float32_t)(value - max_value) + rect_size.x;
				flow_area.setHorizontalValue(min(value, max_value));
			}
		}
		
		// vertical resize
		if(view_rect.getHeight() > rect_size.y) {
			if(view_rect.bottom > rect_min.y) {
				float32_t offset = floor(view_rect.bottom - rect_min.y + 0.5f);
				float64_t value = flow_area.getVerticalValue() - offset * scale;
				dialog.setMousePosition(mouse_position - Vector2f(0.0f, offset));
				if(value < 0.0) self->flow_resize.y -= (float32_t)value - rect_size.y;
				flow_area.setVerticalValue(max(value, 0.0));
			} else if(view_rect.top < rect_max.y) {
				float32_t offset = floor(rect_max.y - view_rect.top + 0.5f);
				float64_t value = flow_area.getVerticalValue() + offset * scale;
				float64_t max_value = flow_area.getVerticalRange() - flow_area.getVerticalFrame();
				dialog.setMousePosition(mouse_position + Vector2f(0.0f, offset));
				if(value > max_value) self->flow_resize.y += (float32_t)(value - max_value) + rect_size.y;
				flow_area.setVerticalValue(min(value, max_value));
			}
		}
	}
	
	void ControlFlow::node_pressed_callback(ControlRect rect, float32_t x, float32_t y, ControlFlow *self, uint32_t node_index) {
		
		// multi-selection
		ControlRoot root = rect.getRoot();
		if(root.getKeyboardKey(KeyCtrl)) {
			self->mode = ModeNodePressed;
			self->current_node_index = node_index;
			if(!self->selected_nodes.find(node_index)) {
				self->add_select_action();
				self->selected_nodes.append(node_index);
			}
		}
		
		// new node selection
		if(self->mode != ModeNodePressed || !self->selected_nodes.find(node_index)) {
			self->add_select_action();
			self->mode = ModeNodePressed;
			self->current_node_index = node_index;
			self->selected_nodes.copy({ node_index });
		}
	}
	
	void ControlFlow::node_released_callback(ControlRect rect, float32_t x, float32_t y, ControlFlow *self, uint32_t node_index) {
		self->is_clone = false;
		self->is_move = false;
	}
	
	/*
	 */
	void ControlFlow::input_pressed_callback(ControlElement *element, float32_t x, float32_t y, ControlFlow *self, uint32_t node, uint32_t input) {
		self->mode = ModeInputPressed;
		self->current_node_index = node;
		self->current_input_index = input;
		self->current_output_index = Maxu32;
		self->mouse_position = element->getOffset().xy + Vector2f(x, y);
		self->create_input_spatial(node, input);
	}
	
	void ControlFlow::input_released_callback(ControlElement *element, float32_t x, float32_t y, ControlFlow *self, uint32_t node, uint32_t input) {
		self->mode = ModeInputReleased;
		self->current_node_index = node;
		self->current_input_index = input;
	}
	
	void ControlFlow::input_clicked2_callback(ControlElement *element, ControlFlow *self, uint32_t node, uint32_t input) {
		self->add_connect_action(Maxu32, Maxu32, node, input);
		self->removeInputConnection(node, input);
	}
	
	/*
	 */
	void ControlFlow::output_pressed_callback(ControlElement *element, float32_t x, float32_t y, ControlFlow *self, uint32_t node, uint32_t output) {
		self->mode = ModeOutputPressed;
		self->current_node_index = node;
		self->current_input_index = Maxu32;
		self->current_output_index = output;
		self->mouse_position = element->getOffset().xy + Vector2f(x, y);
		self->create_output_spatial(node, output);
	}
	
	void ControlFlow::output_released_callback(ControlElement *element, float32_t x, float32_t y, ControlFlow *self, uint32_t node, uint32_t output) {
		self->mode = ModeOutputReleased;
		self->current_node_index = node;
		self->current_output_index = output;
	}
	
	void ControlFlow::output_clicked2_callback(ControlElement *element, ControlFlow *self, uint32_t node, uint32_t output) {
		self->add_connect_action(node, output, Maxu32, Maxu32);
		self->removeOutputConnection(node, output);
	}
	
	/*
	 */
	void ControlFlow::proto_changed_callback(ControlEdit edit, ControlFlow *self) {
		String name = self->proto_edit.getText().lower();
		for(const Proto &proto : self->protos) {
			if(proto.item == Maxu32) continue;
			self->proto_tree.setItemHidden(proto.item, (name && !proto.text_lower.contains(name.get()) && !proto.text_lower.match(name.get())));
		}
	}
	
	/*
	 */
	bool ControlFlow::proto_dragged_callback(ControlTree tree, uint32_t item, ControlFlow *self) {
		self->dragged_tree_item = item;
		return true;
	}
	
	void ControlFlow::proto_dropped_callback(ControlTree tree, uint32_t item, ControlFlow *self) {
		for(const Proto &proto : self->protos) {
			if(proto.item == self->dragged_tree_item) {
				ControlRoot root = tree.getRoot();
				Vector2f position = root.getMouse();
				if(self->flow_area.getViewRect().inside(position)) {
					uint32_t node = self->addNode(proto.index, self->get_position(root));
					self->mode = ModeNodePressed;
					self->current_node_index = node;
					self->selected_nodes.copy({ node });
					self->add_create_action();
				}
				break;
			}
		}
		self->dragged_tree_item = Maxu32;
	}
	
	/*
	 */
	void ControlFlow::remove_clicked_callback(ControlButton button, ControlFlow *self) {
		self->add_remove_action();
		for(uint32_t index : self->selected_nodes) {
			if(self->isNodeStatic(index)) continue;
			self->removeNode(index);
		}
		self->current_node_index = Maxu32;
		self->selected_nodes.clear();
		self->mode = ModeClear;
	}
	
	/*****************************************************************************\
	 *
	 * ControlFlow Unredo
	 *
	\*****************************************************************************/
	
	/*
	 */
	void ControlFlow::add_move_action() {
		Action *action = new Action(ActionMove);
		action->indices = selected_nodes;
		for(uint32_t node : selected_nodes) {
			action->positions.append(nodes[node].position);
		}
		add_action(action);
	}
	
	void ControlFlow::add_select_action() {
		Action *action = new Action(ActionSelect);
		action->indices = selected_nodes;
		add_action(action);
	}
	
	void ControlFlow::add_create_action() {
		Action *action = new Action(ActionCreate);
		for(uint32_t index : selected_nodes) {
			Node &node = nodes[index];
			if(node.is_static) continue;
			if(node.index == Maxu32) continue;
			add_indices(action->create_indices, node);
			action->indices.append(index);
		}
		add_action(action);
	}
	
	void ControlFlow::add_remove_action() {
		Action *action = new Action(ActionRemove);
		for(uint32_t index : selected_nodes) {
			Node &node = nodes[index];
			if(node.is_static) continue;
			if(node.index == Maxu32) continue;
			add_indices(action->remove_indices, node);
			add_node(action, node);
		}
		action->indices.swap(selected_nodes);
		add_action(action);
	}
	
	void ControlFlow::add_connect_action(uint32_t node, uint32_t output, uint32_t input_node, uint32_t input_index) {
		Action *action = new Action(ActionConnect);
		if(node == Maxu32 || (input_node != Maxu32 && !protos[nodes[input_node].proto].inputs[input_index].is_multi)) {
			for(const InputConnection &input_connection : nodes[input_node].inputs[input_index].connections) {
				for(const OutputConnection &output_connection : nodes[input_connection.node].outputs[input_connection.output].connections) {
					if(output_connection.node == input_node && output_connection.input == input_index) {
						action->remove_indices.append(Vector4u(input_connection.node, input_connection.output, input_node, input_index));
					}
				}
			}
		}
		if(input_node == Maxu32 || (node != Maxu32 && !protos[nodes[node].proto].outputs[output].is_multi)) {
			for(const OutputConnection &output_connection : nodes[node].outputs[output].connections) {
				for(const InputConnection &input_connection : nodes[output_connection.node].inputs[output_connection.input].connections) {
					if(input_connection.node == node && input_connection.output == output) {
						action->remove_indices.append(Vector4u(node, output, output_connection.node, output_connection.input));
					}
				}
			}
		}
		if(node != Maxu32 && input_node != Maxu32) {
			action->create_indices.append(Vector4u(node, output, input_node, input_index));
		}
		if(action->create_indices || action->remove_indices) {
			action->indices.swap(selected_nodes);
			add_action(action);
		}
		else delete action;
	}
	
	/*
	 */
	void ControlFlow::add_action(Action *action) {
		undo_actions.append(makeAutoPtr(action));
		for(auto &action : redo_actions) {
			for(Node &node : action->nodes) {
				for(NodeInput &input : node.inputs) {
					if(input.socket) removed_elements.append(input.socket.release());
				}
				for(NodeOutput &output : node.outputs) {
					if(output.socket) removed_elements.append(output.socket.release());
				}
				if(node.dialog) {
					flow_grid.removeChild(node.dialog);
					removed_controls.append(node.dialog);
					node.dialog.clearPtr();
				}
			}
		}
		redo_actions.release();
	}
	
	/*
	 */
	void ControlFlow::add_indices(Array<Vector4u> &indices, const Node &node) {
		for(const NodeInput &input : node.inputs) {
			for(const InputConnection &connection : input.connections) {
				indices.append(Vector4u(connection.node, connection.output, node.index, input.index));
			}
		}
		for(const NodeOutput &output : node.outputs) {
			for(const OutputConnection &connection : output.connections) {
				indices.append(Vector4u(node.index, output.index, connection.node, connection.input));
			}
		}
	}
	
	void ControlFlow::add_node(Action *action, Node &node) {
		TS_ASSERT(node.index != Maxu32 && "ControlFlow::add_node(): invalid node index");
		clearConnections(node.index);
		action->nodes.append(node);
		node.dialog.setStrokeStyle(node.stroke_style);
		node.dialog.setEnabled(false);
		node.dialog.clearPtr();
		node.update_func.clear();
		node.load_func.clear();
		node.save_func.clear();
		node.index = Maxu32;
	}
	
	/*
	 */
	void ControlFlow::swap_action(Action *action) {
		if(action->type == ActionMove) {
			for(uint32_t i = 0; i < action->indices.size(); i++) {
				uint32_t node = action->indices[i];
				Vector2f position = nodes[node].position;
				setNodePosition(node, action->positions[i]);
				action->positions[i] = position;
			}
		}
		else if(action->type == ActionSelect) {
			action->indices.swap(selected_nodes);
		}
		else if(action->type == ActionCreate) {
			for(uint32_t i = 0; i < action->indices.size(); i++) {
				add_node(action, nodes[action->indices[i]]);
			}
			action->type = ActionRemove;
			action->create_indices.swap(action->remove_indices);
			mode = ModeClear;
		}
		else if(action->type == ActionRemove) {
			for(uint32_t i = 0; i < action->indices.size(); i++) {
				Node &node = nodes[action->indices[i]];
				node = action->nodes[i];
				node.dialog.setEnabled(true);
			}
			for(const Vector4u &index : action->remove_indices) {
				addOutputConnection(index.x, index.y, index.z, index.w);
			}
			action->type = ActionCreate;
			action->create_indices.swap(action->remove_indices);
			action->nodes.release();
			old_selected_nodes.clear();
			selected_nodes = action->indices;
			mode = ModeNodePressed;
		}
		else if(action->type == ActionConnect) {
			for(const Vector4u &index : action->create_indices) {
				removeOutputConnection(index.x, index.y, index.z, index.w);
			}
			for(const Vector4u &index : action->remove_indices) {
				addOutputConnection(index.x, index.y, index.z, index.w);
			}
			action->indices.swap(selected_nodes);
			action->create_indices.swap(action->remove_indices);
		}
		else {
			TS_ASSERT(0 && "ControlFlow::swap_action(): unknown action");
		}
	}
	
	/*
	 */
	void ControlFlow::undo() {
		if(!undo_actions) return;
		AutoPtr<Action> action = undo_actions.back();
		undo_actions.removeBack();
		swap_action(action.get());
		redo_actions.append(action);
	}
	
	/*
	 */
	void ControlFlow::redo() {
		if(!redo_actions) return;
		AutoPtr<Action> action = redo_actions.back();
		redo_actions.removeBack();
		swap_action(action.get());
		undo_actions.append(action);
	}
	
	/*****************************************************************************\
	 *
	 * ControlFlow
	 *
	\*****************************************************************************/
	
	/*
	 */
	bool ControlFlow::create(Control *controls_root, Control *tooltip_root) {
		
		// parent control
		Control parent = getParent();
		
		// create main grid
		if(!controls_root) controls_root = &parent;
		main_grid = ControlGrid(controls_root);
		main_grid.setAlign(AlignExpand);
		
		// create flow area
		flow_area = ControlArea(&main_grid);
		flow_area.setAbsolute(true);
		flow_area.setScalable(true);
		flow_area.setAlign(AlignExpand);
		flow_area.setFrameAlign(AlignCenter);
		flow_area.setScaleRange(0.5f, 2.0f);
		
		// create flow panel
		flow_panel = ControlPanel(&flow_area);
		flow_panel.setPressedCallback(makeFunction(flow_pressed_callback, ControlRect::null, 0.0f, 0.0f, this));
		flow_panel.setReleasedCallback(makeFunction(flow_released_callback, ControlRect::null, 0.0f, 0.0f, this));
		flow_panel.setColor(Color(0.1f, 1.0f));
		flow_panel.setSize(4096.0f, 2048.0f);
		flow_panel.setAlign(AlignCenter);
		flow_panel.setMargin(0.0f, 0.0f);
		flow_panel.setRadius(0.0f);
		
		// create flow grid
		flow_grid = ControlGrid(&flow_area);
		flow_grid.setAlign(AlignCenter | AlignExpand | AlignOverlap);
		
		// create proto dialog
		proto_dialog = ControlDialog(&main_grid);
		proto_dialog.setAlign(AlignRight | AlignExpandY | AlignOverlap | AlignSpacer);
		proto_dialog.setSize(128.0f, 256.0f);
		proto_dialog.setRadius(0.0f);
		
		// create proto edit
		proto_edit = ControlEdit(&proto_dialog);
		proto_edit.setChangedCallback(makeFunction(proto_changed_callback, ControlEdit::null, this));
		proto_edit.setAlign(AlignExpandX);
		
		// create proto area
		proto_area = ControlArea(&proto_dialog);
		proto_area.setVerticalEnabled(true, true);
		proto_area.setHorizontalEnabled(true, true);
		proto_area.setAlign(AlignExpand);
		
		// create proto tree
		proto_tree = ControlTree(&proto_area);
		proto_tree.setDraggedCallback(makeFunction(proto_dragged_callback, ControlTree::null, Maxu32, this));
		proto_tree.setDroppedCallback(makeFunction(proto_dropped_callback, ControlTree::null, Maxu32, this));
		proto_tree.setAlign(AlignExpand);
		
		// create remove button
		remove_button = ControlButton(&proto_dialog, "Remove");
		remove_button.setClickedCallback(makeFunction(remove_clicked_callback, ControlButton::null, this));
		remove_button.setAlign(AlignExpandX);
		
		// create mouse strip
		Canvas canvas = getCanvas();
		mouse_strip = makeAutoPtr(new ControlElement(&flow_panel, CanvasStrip(canvas)));
		mouse_strip->setAlign(AlignOverlap | AlignSpacer);
		
		// create region rect
		region_rect = ControlRect(&flow_grid);
		region_rect.setColor(Color(0.5f, 0.5f));
		region_rect.setStrokeStyle(StrokeStyle(1.0f, Color(1.0f, 0.5f)));
		region_rect.setAlign(AlignCenter | AlignOverlap | AlignSpacer);
		region_rect.setEnabled(false);
		
		// create tooltip
		Control root = getRoot();
		if(tooltip_root == nullptr) tooltip_root = &root;
		tooltip = makeAutoPtr(new ControlTooltip(tooltip_root));
		tooltip->addTooltip(proto_edit, "Filter by name");
		tooltip->addTooltip(remove_button, "Remove selected nodes");
		
		return true;
	}
	
	/*
	 */
	bool ControlFlow::update(ControlRoot &root, const Rect &region, const Rect &view, uint32_t scale) {
		
		bool ret = false;
		
		// update frame
		frame++;
		
		// update proto tree
		if(update_proto_tree) {
			proto_tree.clearItems();
			for(Proto &proto : protos) {
				proto.text_lower = proto.text.lower();
				if(proto.text) proto.item = proto_tree.addItem(proto.text);
				else proto.item = Maxu32;
			}
			proto_changed_callback(proto_edit, this);
			update_proto_tree = false;
		}
		
		// update flow scale
		if(old_scale != flow_area.getScale()) {
			for(Node &node : nodes) {
				if(node.index == Maxu32) continue;
				update_nodes.append(node.index);
			}
			old_scale = flow_area.getScale();
			ret = true;
		}
		
		// process keyboard
		Control input_control = root.getInputControl();
		if(flow_grid == input_control || flow_grid.isChild(input_control)) {
			if(root.getKeyboardKey(KeyDelete, true) || root.getKeyboardKey(KeyBackspace, true)) {
				remove_clicked_callback(remove_button, this);
			}
		}
		
		// changed callback
		if(is_changed && changed_func) {
			changed_func(this);
			is_changed = false;
		}
		
		// update selected nodes style
		for(uint32_t index : old_selected_nodes) {
			if(selected_nodes.find(index)) continue;
			Node &node = nodes[index];
			if(node.index == Maxu32) continue;
			node.dialog.setStrokeStyle(node.stroke_style);
		}
		for(uint32_t index : selected_nodes) {
			if(old_selected_nodes.find(index)) continue;
			Node &node = nodes[index];
			if(node.index == Maxu32) continue;
			node.stroke_style = node.dialog.getStrokeStyle();
			node.dialog.setStrokeStyle(getSelectionStyle());
		}
		old_selected_nodes = selected_nodes;
		
		// show remove button
		if((mode == ModeNodePressed) && current_node_index < nodes.size() && !isNodeStatic(current_node_index)) {
			remove_button.setEnabled(true);
		} else {
			remove_button.setEnabled(false);
		}
		
		// resize flow panel
		Rect flow_rect = flow_panel.getRect();
		if(update_nodes && !selected_nodes) {
			Vector2f hsize = flow_rect.getSize() * 0.5f;
			for(uint32_t index : update_nodes) {
				Node &node = nodes[index];
				if(node.index == Maxu32) continue;
				flow_resize = max(flow_resize, floor(abs(node.position) - hsize + 0.5f));
			}
		}
		flow_resize = max(flow_resize, floor(flow_area.getViewRect().getSize() / flow_area.getScale() - flow_rect.getSize() + 0.5f));
		if(flow_resize.x > 0.0f || flow_resize.y > 0.0f) {
			flow_panel.setSize(flow_rect.getSize() + flow_resize * 2.0f);
			for(Node &node : nodes) {
				if(node.index == Maxu32) continue;
				for(NodeInput &input : node.inputs) {
					input.socket->setOffset(input.socket->getOffset() + Vector3f(flow_resize, 0.0f));
				}
				for(NodeOutput &output : node.outputs) {
					output.socket->setOffset(output.socket->getOffset() + Vector3f(flow_resize, 0.0f));
				}
				update_nodes.append(node.index);
			}
		}
		flow_resize = Vector2f::zero;
		
		// update mode
		if(mode == ModeNodePressed) {
			if(old_mode != mode) proto_changed_callback(proto_edit, this);
		}
		else if(mode == ModeInputPressed) {
			
			// mouse strip parameters
			const Type &type = get_node_input_type(current_node_index, current_input_index);
			CanvasStrip strip = CanvasStrip(mouse_strip->getElement());
			strip.setWidth(type.connection_width);
			strip.setColor(type.connection_color);
			strip.setStrokeStyle(type.connection_style);
			
			// mouse output position
			current_output_index = Spatial::closestIntersection<float32_t>(mouse_position, spatial_nodes.get());
			if(current_output_index < spatial_indices.size()) {
				const NodeIndex &index = spatial_indices[current_output_index];
				Vector2f output_position = getOutputSocket(index.node, index.index)->getOffset().xy;
				if(index.node != current_node_index && length(output_position - mouse_position) < getThreshold()) {
					create_strip(mouse_strip.get(), output_position, mouse_position);
				} else {
					current_output_index = Maxu32;
				}
			}
			
			// create strip
			Vector2f input_position = getInputSocket(current_node_index, current_input_index)->getOffset().xy;
			create_strip(mouse_strip.get(), mouse_position, input_position, (current_output_index >= spatial_indices.size()));
		}
		else if(mode == ModeOutputPressed) {
			
			// mouse strip parameters
			const Type &type = get_node_output_type(current_node_index, current_output_index);
			CanvasStrip strip = CanvasStrip(mouse_strip->getElement());
			strip.setWidth(type.connection_width);
			strip.setColor(type.connection_color);
			strip.setStrokeStyle(type.connection_style);
			
			// create strip
			Vector2f output_position = getOutputSocket(current_node_index, current_output_index)->getOffset().xy;
			create_strip(mouse_strip.get(), output_position, mouse_position);
			
			// mouse input position
			current_input_index = Spatial::closestIntersection<float32_t>(mouse_position, spatial_nodes.get());
			if(current_input_index < spatial_indices.size()) {
				const NodeIndex &index = spatial_indices[current_input_index];
				Vector2f input_position = getInputSocket(index.node, index.index)->getOffset().xy;
				if(index.node != current_node_index && length(input_position - mouse_position) < getThreshold()) {
					create_strip(mouse_strip.get(), mouse_position, input_position, false);
				} else {
					current_input_index = Maxu32;
				}
			}
		}
		else if(mode == ModeInputReleased) {
			
			// create input connection
			if(current_output_index < spatial_indices.size()) {
				const NodeIndex &index = spatial_indices[current_output_index];
				add_connect_action(index.node, index.index, current_node_index, current_input_index);
				addOutputConnection(index.node, index.index, current_node_index, current_input_index);
				mode = ModeClear;
			}
			// create output proto tree
			else {
				String name = proto_edit.getText().lower();
				const Type &input_type = get_node_input_type(current_node_index, current_input_index);
				for(const Proto &proto : protos) {
					bool is_hidden = true;
					if(proto.item == Maxu32) continue;
					for(const ProtoOutput &proto_output : proto.outputs) {
						if(!is_compatible_types(types[proto_output.type], input_type)) continue;
						is_hidden = false;
						break;
					}
					if(!is_hidden) is_hidden = (name && !proto.text_lower.contains(name.get()) && !proto.text_lower.match(name.get()));
					proto_tree.setItemHidden(proto.item, is_hidden);
				}
				mode = ModeClear;
				ret = true;
			}
		}
		else if(mode == ModeOutputReleased) {
			
			// create output connection
			if(current_input_index < spatial_indices.size()) {
				const NodeIndex &index = spatial_indices[current_input_index];
				add_connect_action(current_node_index, current_output_index, index.node, index.index);
				addOutputConnection(current_node_index, current_output_index, index.node, index.index);
				mode = ModeClear;
			}
			// create input proto tree
			else {
				String name = proto_edit.getText().lower();
				const Type &output_type = get_node_output_type(current_node_index, current_output_index);
				for(const Proto &proto : protos) {
					bool is_hidden = true;
					if(proto.item == Maxu32) continue;
					for(const ProtoInput &proto_input : proto.inputs) {
						if(!is_compatible_types(output_type, types[proto_input.type])) continue;
						is_hidden = false;
						break;
					}
					if(!is_hidden) is_hidden = (name && !proto.text_lower.contains(name.get()) && !proto.text_lower.match(name.get()));
					proto_tree.setItemHidden(proto.item, is_hidden);
				}
				mode = ModeClear;
				ret = true;
			}
		}
		// clear mode
		else if(mode == ModeClear) {
			CanvasStrip strip = CanvasStrip(mouse_strip->getElement());
			current_node_index = Maxu32;
			current_input_index = Maxu32;
			current_output_index = Maxu32;
			strip.clearPositions();
			clear_spatial();
			mode = ModeNone;
			ret = true;
		}
		old_mode = mode;
		
		// create node strips
		for(uint32_t index : old_update_nodes) {
			Node &node = nodes[index];
			if(node.frame == frame) continue;
			if(node.index == Maxu32) continue;
			node.frame = frame;
			
			// create node outputs
			for(NodeOutput &node_output : node.outputs) {
				if(node_output.frame == frame) continue;
				node_output.frame = frame;
				
				// update output connections
				ControlElement *output_socket = node_output.socket.get();
				Vector2f output_position = output_socket->getOffset().xy;
				for(OutputConnection &output_connection : node_output.connections) {
					if(output_connection.frame == frame) continue;
					ControlElement *input_socket = getInputSocket(output_connection.node, output_connection.input);
					Vector2f input_position = input_socket->getOffset().xy;
					create_strip(output_connection.strip.get(), output_position, input_position);
					output_connection.frame = frame;
				}
			}
			
			// create node inputs
			for(NodeInput &node_input : node.inputs) {
				if(node_input.frame == frame) continue;
				node_input.frame = frame;
				
				// update input connections
				ControlElement *input_socket = node_input.socket.get();
				Vector2f input_position = input_socket->getOffset().xy;
				for(InputConnection &input_connection : node_input.connections) {
					if(input_connection.frame == frame) continue;
					input_connection.frame = frame;
					
					// find output connection
					NodeOutput &node_output = nodes[input_connection.node].outputs[input_connection.output];
					for(OutputConnection &output_connection : node_output.connections) {
						if(output_connection.frame == frame) continue;
						if(output_connection.node != node.index) continue;
						if(output_connection.input != node_input.index) continue;
						ControlElement *output_socket = node_output.socket.get();
						Vector2f output_position = output_socket->getOffset().xy;
						create_strip(output_connection.strip.get(), output_position, input_position);
						output_connection.frame = frame;
					}
				}
			}
		}
		
		// remove elements
		Canvas canvas = getCanvas();
		for(ControlElement *element : removed_elements) {
			CanvasElement canvas_element = element->getElement();
			canvas.removeElement(canvas_element);
			flow_panel.removeChild(*element);
			delete element;
		}
		removed_elements.clear();
		
		// remove controls
		for(Control &control : removed_controls) {
			control.destroyPtr();
		}
		removed_controls.clear();
		
		// clear update nodes
		ret |= (bool)update_nodes;
		old_update_nodes = update_nodes;
		update_nodes.clear();
		return ret;
	}
}
