if (!global.framework_utils)
	global.framework_utils = require('./utils');

const BLACKLISTID = { paused: 1, groups: 1, tabs: 1 };
const D = '__';

function Message() {
	this.ismessage = true;
	this.cloned = 0;
}

Message.prototype = {

	get user() {
		return this.controller ? this.controller.user : null;
	},

	get session() {
		return this.controller ? this.controller.session : null;
	},

	get sessionid() {
		return this.controller && this.controller ? this.controller.req.sessionid : null;
	},

	get language() {
		return (this.controller ? this.controller.language : '') || '';
	},

	get ip() {
		return this.controller ? this.controller.ip : null;
	},

	get req() {
		return this.controller ? this.controller.req : null;
	},

	get res() {
		return this.controller ? this.controller.res : null;
	},

	get params() {
		return this.controller ? this.controller.params : null;
	},

	get files() {
		return this.controller ? this.controller.files : null;
	},

	get body() {
		return this.controller ? this.controller.body : null;
	},

	get query() {
		return this.controller ? this.controller.query : null;
	},

	get headers() {
		return this.controller && this.controller.req ? this.controller.req.headers : null;
	},

	get ua() {
		return this.controller && this.controller.req ? this.controller.req.ua : null;
	}
};

var MP = Message.prototype;

MP.emit = function(name, a, b, c, d, e, f, g) {

	var self = this;

	if (!self.$events)
		return self;

	var evt = self.$events[name];
	if (evt) {

		var clean = false;

		for (var e of evt) {
			if (e.once)
				clean = true;
			e.fn.call(self, a, b, c, d, e, f, g);
		}

		if (clean) {
			var index = 0;
			while (true) {
				if (!evt[index])
					break;
				if (evt[index].once)
					evt.splice(index, 1);
				else
					index++;
			}
			self.$events[name] = evt.length ? evt : undefined;
		}
	}

	return self;
};

MP.emit2 = function(name, a, b, c, d, e, f, g) {

	var self = this;

	if (!self.$events)
		return self;

	var evt = self.$events[name];
	if (evt) {

		var clean = false;

		for (var e of evt) {
			if (e.cloned < self.cloned) {
				if (e.once)
					clean = true;
				e.fn.call(self, a, b, c, d, e, f, g);
			}
		}

		if (clean) {
			var index = 0;
			while (true) {
				var e = evt[index];
				if (!e)
					break;
				if (e.cloned < self.cloned) {
					if (e.once)
						evt.splice(index, 1);
					else
						index++;
				} else
					index++;
			}
			self.$events[name] = evt.length ? evt : undefined;
		}
	}

	return self;
};

MP.on = function(name, fn, once) {
	var self = this;
	if (!self.$events)
		self.$events = {};
	var obj = { cloned: self.cloned, fn: fn, once: once };
	if (self.$events[name])
		self.$events[name].push(obj);
	else
		self.$events[name] = [obj];
	return self;
};

MP.once = function(name, fn) {
	return this.on(name, fn, true);
};

MP.removeListener = function(name, fn) {
	var self = this;
	if (self.$events) {
		var evt = self.$events[name];
		if (evt) {
			evt = evt.remove(n => n.fn === fn);
			self.$events[name] = evt.length ? evt : undefined;
		}
	}
	return self;
};

MP.removeAllListeners = function(name) {
	var self = this;
	if (self.$events) {
		if (name === true)
			self.$events = {};
		else if (name)
			self.$events[name] = undefined;
		else
			self.$events = {};
	}
	return self;
};

MP.clone = function() {

	var self = this;
	var obj = new Message();
	obj.previd = self.id;
	obj.$events = self.$events;
	obj.duration = self.duration;
	obj.repo = self.repo;
	obj.vars = self.vars;
	obj.main = self.main;
	obj.refs = self.refs;
	obj.count = self.count;
	obj.data = self.data;
	obj.used = self.used;
	obj.processed = 0;
	obj.controller = self.controller;
	obj.cloned = self.cloned + 1;
	obj.$timeoutidtotal = self.$timeoutidtotal;

	if (obj.refs.pending)
		obj.refs.pending++;
	else
		obj.refs.pending = 1;

	// additional custom variables
	obj.uid = self.uid;
	obj.reference = self.reference;
	obj.ref = self.ref;

	if (obj.$events && obj.$events.timeout) {
		var index = 0;
		while (true) {
			var e = obj.$events.timeout[index];
			if (e) {
				if ((e.cloned + 1) < obj.cloned)
					obj.$events.timeout.splice(index, 1);
				else
					index++;
			} else
				break;
		}
	}

	if (self.$timeoutid) {
		clearTimeout(self.$timeoutid);
		self.$timeoutid = null;
	}

	return obj;
};

MP.status = function(a, b, c, d) {
	this.instance.status(a, b, c, d);
	return this;
};

MP.dashboard = function(a, b, c, d) {
	this.instance.dashboard(a, b, c, d);
	return this;
};

MP.debug = function(a, b, c, d) {
	this.instance.debug(a, b, c, d);
	return this;
};

MP.throw = function(a, b, c, d) {
	this.instance.throw(a, b, c, d);
	return this;
};

MP.replace = MP.variables = function(str, data) {

	if (str.indexOf('{') !== -1) {

		str = str.args(this.vars);

		if (this.instance.main.variables) {
			if (str.indexOf('{') !== -1)
				str = str.args(this.instance.main.variables);
		}

		if (this.instance.main.variables2) {
			if (!this.instance.main.variables || str.indexOf('{') !== -1)
				str = str.args(this.instance.main.variables2);
		}

		if ((data == true || (data && typeof(data) === 'object')) && str.indexOf('{') !== -1)
			str = str.args(data == true ? this.data : data);
	}

	return str;
};

function variables(str, data) {

	if (str.indexOf('{') !== -1) {

		if (this.main.variables) {
			if (str.indexOf('{') !== -1)
				str = str.args(this.main.variables);
		}

		if (this.main.variables2) {
			if (!this.main.variables || str.indexOf('{') !== -1)
				str = str.args(this.main.variables2);
		}

		if ((data && typeof(data) === 'object') && str.indexOf('{') !== -1)
			str = str.args(data);
	}

	return str;
}

function timeouthandler(msg) {
	msg.error = 408;
	msg.$events.timeout && msg.emit('timeout', msg);
	msg.$events.timeout2 && msg.emit('timeout2', msg);
	msg.end();
}

MP.send = function(outputindex, data, clonedata) {

	var self = this;

	if (self.isdestroyed || self.main.paused || (self.instance && self.instance.isdestroyed)) {
		if (!self.isdestroyed)
			self.destroy();
		return 0;
	}

	var outputs;
	var count = 0;

	if (outputindex == null) {

		if (self.instance.connections) {
			for (var key in self.instance.connections)
				count += self.send(key);
		}

		if (!count)
			self.destroy();

		return count;
	}

	var meta = self.main.meta;
	var now = Date.now();

	outputs = self.instance.connections ? (self.instance.connections[outputindex] || EMPTYARRAY) : EMPTYARRAY;

	if (self.processed === 0) {
		self.processed = 1;
		self.main.stats.pending--;

		if (self.main.stats.pending < 0)
			self.main.stats.pending = 0;

		self.instance.stats.pending--;

		if (self.instance.stats.pending < 0)
			self.instance.stats.pending = 0;

		self.instance.stats.output++;
		self.instance.stats.duration = now - self.ts;
	}

	if (!self.main.$can(false, self.instance.id, outputindex)) {
		self.destroy();
		return count;
	}

	var tid = self.toid + D + outputindex;

	if (self.main.stats.traffic[tid]) {
		self.main.stats.traffic[tid]++;
	} else {
		self.main.stats.traffic[tid] = 1;
		self.main.stats.traffic.priority.push(tid);
	}

	for (var i = 0; i < outputs.length; i++) {
		var output = outputs[i];

		if (output.disabled || output.paused)
			continue;

		var schema = meta.flow[output.id];
		if (schema && (schema.message || schema['message_' + output.index]) && schema.component && schema.ready && self.main.$can(true, output.id, output.index)) {
			var next = meta.components[schema.component];
			// if (next && next.connected && !next.isdestroyed && !next.disabled && (!next.$inputs || next.$inputs[output.index])) {
			if (next && next.connected && !next.isdestroyed && !next.disabled) {

				var inputindex = output.index;
				var message = self.clone();

				if (data)
					message.data = data;

				if (clonedata && message.data && typeof(message.data) === 'object') {
					if (message.data instanceof Buffer) {
						var buf = Buffer.alloc(message.data.length);
						buf.copy(message.data);
						message.data = buf;
					} else
						message.data = CLONE(message.data);
				}

				message.used++;
				message.instance = schema;
				message.from = self.to;
				message.fromid = self.toid;
				message.fromcomponent = self.instance.component;
				message.to = schema;
				message.toid = output.id;
				message.output = outputindex;
				message.input = message.index = inputindex;
				message.tocomponent = schema.component;
				message.cache = schema.cache;
				message.ts = now;

				if (self.$timeout)
					message.$timeoutid = setTimeout(timeouthandler, self.$timeout, message);

				schema.stats.input++;
				schema.stats.pending++;

				self.main.stats.messages++;
				self.main.stats.pending++;
				self.main.mm++;

				if (self.$events) {
					self.$events.next && self.emit2('next', self, message);
					self.$events.something && self.emit2('something', self, message);
					self.$events.message && self.emit('message', message);
				}

				setImmediate(sendmessage, schema, message, true);
				count++;
			}
		}
	}

	if (count) {
		self.refs.pending--;
		if (self.refs.pending < 0)
			self.refs.pending = 0;
	} else
		self.destroy();

	return count;
};

MP.rewrite = function(data) {
	this.data = data;
	return this;
};

MP.totaltimeout = function(callback, time) {

	if (time == null)
		time = callback;
	else
		this.on('timeout2', callback);

	this.$timeoutidtotal && clearTimeout(this.$timeoutidtotal);
	this.$timeoutidtotal = setTimeout(timeouthandler, time, this);
	return this;
};

MP.timeout = function(callback, time) {

	if (time == null)
		time = callback;
	else
		this.on('timeout', callback);

	this.$timeout = time;
	return this;
};

MP.end = MP.destroy = function() {

	var self = this;

	if (self.isdestroyed)
		return;

	if (self.processed === 0) {
		self.processed = 1;
		self.main.stats.pending--;

		if (self.main.stats.pending < 0)
			self.main.stats.pending = 0;

		self.instance.stats.pending--;

		if (self.instance.stats.pending < 0)
			self.instance.stats.pending = 0;

		self.instance.stats.duration = Date.now() - self.ts;
		self.instance.stats.destroyed++;
	}

	if (self.$timeoutid) {
		clearTimeout(self.$timeoutid);
		self.$timeoutid = null;
	}

	if (self.$timeoutidtotal) {
		clearTimeout(self.$timeoutidtotal);
		self.$timeoutidtotal = null;
	}

	if (self.refs.pending) {
		self.refs.pending--;
		if (self.refs.pending < 0)
			self.refs.pending = 0;
	}

	if (self.$events) {
		self.$events.something && self.emit('something', self);
		self.$events.terminate && self.emit('terminate', self);
	}

	if (!self.refs.pending) {
		if (self.$events) {
			self.$events.end && self.emit('end', self);
			self.$events.destroy && self.emit('destroy', self);
		}
		if (self.main.$events)
			self.main.$events.end && self.main.emit('end', self);
	}

	self.isdestroyed = true;
	self.repo = null;
	self.main = null;
	self.from = null;
	self.to = null;
	self.data = null;
	self.instance = null;
	self.duration = null;
	self.ts = null;
	self.$events = null;
};

function Flow(name, errorhandler) {

	var t = this;
	t.strict = true;
	t.loading = 0;
	t.error = errorhandler || console.error;
	t.id = t.name = name;
	t.uid = Date.now().toString(36) + 'X';
	t.meta = {};
	t.meta.components = {};
	t.meta.flow = {};
	t.meta.cache = {};
	t.stats = { messages: 0, pending: 0, traffic: { priority: [] }, mm: 0, minutes: 0 };
	t.mm = 0;
	t.paused = false;
	t.$events = {};

	var counter = 1;

	setImmediate(function(t) {
		if (t.interval !== 0) {
			t.$interval = setInterval(function(t) {

				var is = t.mm;

				if (counter % 20 === 0) {

					t.stats.minutes++;
					t.stats.mm = t.mm;
					t.mm = 0;
					counter = 1;

					for (var key in t.meta.flow) {
						var com = t.meta.flow[key];
						com.service && com.service(t.stats.minutes);
					}

				} else
					counter++;

				t.onstats && t.onstats(t.stats);
				t.$events.stats && t.emit('stats', t.stats);

				if (is)
					t.stats.traffic = { priority: [] };

			}, t.interval || 3000, t);
		}
	}, t);

	new framework_utils.EventEmitter2(t);
}

var FP = Flow.prototype;

FP.pause = function(is) {
	var self = this;
	self.paused = is;
	for (var m in self.meta.flow) {
		var instance = self.meta.flow[m];
		if (instance && instance.pause)
			instance.pause(is);
	}
	return self;
};

FP.register = function(name, declaration, config, callback, extend) {

	var self = this;
	var type = typeof(declaration);

	if (type === 'string') {
		try {
			declaration = new Function('instance', declaration);
		} catch (e) {
			callback && callback(e);
			self.error(e, 'register', name);
			return;
		}
	}

	var cache;
	var prev = self.meta.components[name];
	if (prev) {
		cache = prev.cache;
		prev.connected = false;
		prev.disabled = true;
		prev.destroy = null;
		prev.disconnect && prev.disconnect();
	}

	var curr = { id: name, main: self, connected: true, disabled: false, cache: cache || {}, config: config || {}, stats: {}, ui: {}, iscomponent: true };
	if (extend) {
		try {
			declaration(curr, F.require);
		} catch (e) {
			self.error(e, 'register', name);
			callback && callback(e);
			return;
		}
	} else
		curr.make = declaration;

	curr.config = CLONE(curr.config || curr.options);

	var errors = new ErrorBuilder();
	var done = function() {

		self.loading--;
		self.meta.components[name] = curr;
		self.onregister && self.onregister(curr);
		self.$events.register && self.emit('register', name, curr);
		curr.install && !prev && curr.install.call(curr, curr);

		for (var key in self.meta.flow) {
			if (!BLACKLISTID[key]) {
				var f = self.meta.flow[key];
				if (f.component === curr.id)
					self.initcomponent(key, curr);
			}
		}

		self.clean();
		callback && callback(errors.length ? errors : null);
	};

	self.loading++;

	if (curr.npm && curr.npm.length) {
		curr.npm.wait(function(name, next) {
			NPMINSTALL(name, function(err) {
				if (err) {
					self.error(err, 'npm');
					errors.push(err);
				}
				next();
			});
		}, done);
	} else
		setImmediate(done);

	return curr;
};

FP.destroy = function() {
	var self = this;

	clearInterval(self.$interval);
	self.$interval = null;

	self.loading++;
	self.unload(function() {
		self.loading--;
		self.emit('destroy');
		self.meta = null;
		self.$events = null;
		delete F.flows[self.name];
	});

};

FP.cleanforce = function() {

	var self = this;

	if (!self.meta)
		return self;

	for (var key in self.meta.flow) {
		if (!BLACKLISTID[key]) {
			var instance = self.meta.flow[key];
			if (instance.connections) {

				for (var key2 in instance.connections) {

					var conns = instance.connections[key2];
					var rem = {};

					for (var conn of conns) {
						var target = self.meta.flow[conn.id];
						if (target) {
							var com = self.meta.components[target.component];
							if (com) {
								if (self.strict) {
									if (target.inputs) {
										if (!target.inputs.findItem('id', conn.index))
											rem[conn.id] = 1;
									} else if (!com.inputs || !com.inputs.findItem('id', conn.index))
										rem[conn.id] = 1;
								}
							} else
								rem[conn.id] = 1;
						} else
							rem[conn.id] = 1;
					}

					var arr = conns.remove(c => rem[c.id] === 1);
					if (arr.length)
						instance.connections[key2] = arr;
					else
						delete instance.connections[key2];
				}
			}
		}
	}

	var paused = self.meta.flow.paused;
	if (paused) {
		for (var key in paused) {
			var arr = key.split(D);
			// arr[0] type
			// arr[1] id
			// arr[2] index
			if (!self.meta.flow[arr[1]])
				delete paused[key];
		}
	}

	return self;
};

FP.unregister = function(name, callback) {

	var self = this;

	if (name == null) {
		Object.keys(self.meta.components).wait(function(key, next) {
			self.unregister(key, next);
		}, callback);
		return self;
	}

	var curr = self.meta.components[name];
	if (curr) {
		self.onunregister && self.onunregister(curr);
		self.$events.unregister && self.emit('unregister', name, curr);
		self.loading++;
		Object.keys(self.meta.flow).wait(function(key, next) {

			var instance = self.meta.flow[key];
			if (instance) {
				if (instance.component === name) {
					instance.ready = false;
					try {
						instance.isdestroyed = true;
						self.ondisconnect && self.ondisconnect(instance);
						curr.close && curr.close.call(instance);
						curr.destroy && curr.destroy.call(instance);
					} catch (e) {
						self.onerror(e, 'instance_close', instance);
					}
					delete self.meta.flow[key];
				}
			} else
				delete self.meta.flow[key];

			next();

		}, function() {
			self.loading--;
			curr.connected = false;
			curr.disabled = true;
			curr.uninstall && curr.uninstall.call(curr, curr);
			curr.destroy = null;
			curr.cache = null;
			delete self.meta.components[name];
			callback && callback();
			self.clean();
		});
	} else
		callback && callback();

	return self;
};

FP.clean = function() {
	var self = this;
	if (!self.loading)
		setTimeout2(self.name, () => self.cleanforce(), 1000);
	return self;
};

/*
FP.ondisconnect = function(instance) {
};

FP.onconnect = function(instance) {
};

FP.onregister = function(component) {
};

FP.onunregister = function(component) {
};

FP.onreconfigure = function(instance, init) {

};
*/

FP.ondashboard = function(a, b, c, d) {
	// this == instance
	this.main.$events.dashboard && this.main.emit('dashboard', this, a, b, c, d);
};

FP.onstatus = function(a, b, c, d) {
	// this == instance
	this.main.$events.status && this.main.emit('status', this, a, b, c, d);
};

FP.onerror = function(a, b, c, d) {
	// this == instance
	this.main.$events.error && this.main.emit('error', this, a, b, c, d);
};

FP.ondebug = function(a, b, c, d) {
	// this == instance
	this.main.$events.debug && this.main.emit('debug', this, a, b, c, d);
};

function newmessage(data) {
	var self = this;
	var msg = new Message();
	msg.refs = { pending: 1 };
	msg.repo = {};
	msg.vars = {};
	msg.to = msg.instance = self;
	msg.toid = self.id;
	msg.tocomponent = self.component;
	msg.data = data instanceof Message ? data.data : data;
	msg.cloned = 0;
	msg.count = 0;
	msg.instance = self;
	msg.duration = msg.ts = Date.now();
	msg.used = 1;
	msg.main = self instanceof Flow ? self : self.main;
	msg.processed = 1;
	return msg;
}

FP.ontrigger = function(outputindex, data, controller, events) {

	// this == instance

	var schema = this;
	var self = schema.main;
	var count = 0;

	if (self.paused)
		return count;

	if (schema && schema.ready && schema.component && schema.connections) {
		var instance = self.meta.components[schema.component];
		if (instance && instance.connected && !instance.disabled && self.$can(false, schema.id, outputindex)) {
			var conn = schema.connections[outputindex];
			if (conn && conn.length) {

				var ts = Date.now();
				for (var i = 0; i < conn.length; i++) {

					var m = conn[i];
					var target = self.meta.flow[m.id];
					if (!target || (!target.message && !target['message_' + m.index]) || !self.$can(true, m.id, m.index))
						continue;

					var com = self.meta.components[target.component];
					if (!com)
						continue;

					if (target.isdestroyed || (data && data.instance && data.instance.isdestroyed))
						continue;

					var ismessage = data instanceof Message;
					var message = ismessage ? data.clone() : new Message();

					if (ismessage) {

						if (data.isdestroyed)
							return 0;

						if (data.processed === 0) {
							data.processed = 1;

							data.main.stats.pending--;

							if (data.main.stats.pending < 0)
								data.main.stats.pending = 0;

							if (data.instance) {
								data.instance.stats.pending--;

								if (data.instance.stats.pending < 0)
									data.instance.stats.pending = 0;

								data.instance.stats.output++;
								data.instance.stats.duration = ts - self.ts;
							}
						}
					} else {

						message.refs = { pending: 1 };
						message.$events = events || {};
						message.repo = {};
						message.vars = {};
						message.data = data;
						message.duration = message.ts = ts;
						message.used = 1;
					}

					message.main = self;
					message.controller = controller;
					message.instance = target;

					message.from = schema;
					message.fromid = schema.id;
					message.fromcomponent = schema.component;
					message.output = outputindex;

					message.to = message.instance = target;
					message.toid = m.id;
					message.input = message.index = m.index;
					message.tocomponent = target.component;
					message.cache = target.cache;
					message.processed = 0;

					target.stats.pending++;
					target.stats.input++;
					schema.stats.output++;
					message.main.stats.pending++;
					message.main.stats.messages++;
					message.main.mm++;

					message.id = message.main.uid + message.main.stats.messages;
					message.count = message.main.stats.messages;

					if (message.fromid && !count) {
						var tid = message.fromid + D + message.output;
						if (message.main.stats.traffic[tid])
							message.main.stats.traffic[tid]++;
						else {
							message.main.stats.traffic[tid] = 1;
							message.main.stats.traffic.priority.push(tid);
						}
					}

					if (ismessage && data.$timeout)
						message.$timeoutid = setTimeout(timeouthandler, data.$timeout, message);

					if (ismessage) {
						data.next && data.emit2('next', data, message);
						data.something && data.emit2('something', data, message);
						data.message && data.emit('message', message);
					}

					count++;
					setImmediate(sendmessage, target, message, true);
				}
			}
		}
	}

	return count;
};

FP.reconfigure = function(id, config, rewrite) {
	var self = this;
	var instance = self.meta.flow[id];
	if (instance && !instance.isdestroyed) {

		if (rewrite)
			instance.config = config;
		else
			U.extend(instance.config, config);

		instance.configure && instance.configure(instance.config);
		self.onreconfigure && self.onreconfigure(instance);
	}
	return !!instance;
};

FP.unload = function(callback) {
	var self = this;
	var keys = Object.keys(self.meta.flow);
	keys.wait(function(key, next) {
		var current = self.meta.flow[key];
		if (current) {
			current.isdestroyed = true;
			self.ondisconnect && self.ondisconnect(current);
			current.close && current.close.call(current);
			current.destroy && current.destroy.call(current);
		}
		delete self.meta.flow[key];
		next();
	}, function() {
		// uninstall components
		self.unregister(null, callback);
	});
	return self;
};

FP.load = function(components, design, callback) {

	var self = this;
	if (self.loading) {
		setTimeout(() => self.load(components, design, callback), 200);
		return self;
	}

	self.loading = 10000;
	self.unload(function() {

		var keys = Object.keys(components);
		var error = new ErrorBuilder();

		keys.wait(function(key, next) {
			var body = components[key];
			if (typeof(body) === 'string' && body.indexOf('<script ') !== -1) {
				self.add(key, body, function(err) {
					err && error.push(err);
					next();
				});
			} else {
				self.register(key, body, function(err) {
					err && error.push(err);
					next();
				});
			}

		}, function() {

			// Loads design
			self.loading = 0;
			self.use(design, function(err) {
				self.loading = 0;
				err && error.push(err);
				callback && callback(err);
				self.clean();
			});

		});
	});

	return self;
};

FP.insert = function(schema, callback) {
	var self = this;
	if (callback)
		self._use(schema, callback, null, true);
	else
		return new Promise((resolve, reject) => self._use(schema, (err, res) => err ? reject(err) : resolve(res), null, true));
};

FP.remove = function(keys, callback) {
	var self = this;
	if (callback)
		self._remove(keys, callback);
	else
		return new Promise((resolve, reject) => self._remove(keys, (err, res) => err ? reject(err) : resolve(res), null, true));
};

FP._remove = function(keys, callback) {
	var self = this;

	if (!(keys instanceof Array))
		keys = Object.keys(keys);

	for (var key of keys) {

		if (BLACKLISTID[key]) {
			delete self.meta.flow[key];
			continue;
		}

		var instance = self.meta.flow[key];
		if (instance) {
			instance.ready = false;
			self.ondisconnect && self.ondisconnect(instance);
			instance.close && instance.close.call(instance);
			delete self.meta.flow[key];
		}
	}

	self.clean();
	callback && callback();
};

function use(self, schema, callback, reinit, insert) {
	self._use(schema, callback, reinit, insert);
}

FP.use = function(schema, callback, reinit) {
	var self = this;
	if (callback)
		self._use(schema, callback, reinit);
	else
		return new Promise((resolve, reject) => self._use(schema, (err, res) => err ? reject(err) : resolve(res), reinit));
};

FP._use = function(schema, callback, reinit, insert) {

	var self = this;

	if (self.loading) {
		setTimeout(use, 200, self, schema, callback, reinit, insert);
		return self;
	}

	if (typeof(schema) === 'string')
		schema = schema.parseJSON(true);
	else
		schema = CLONE(schema);

	if (typeof(callback) === 'boolean') {
		var tmp = reinit;
		reinit = callback;
		callback = tmp;
	}

	// schema.COMPONENT_ID.component = 'condition';
	// schema.COMPONENT_ID.config = {};
	// schema.COMPONENT_ID.connections = { '0': [{ id: 'COMPONENT_ID', index: '2' }] }

	var err = new ErrorBuilder();

	if (schema) {

		var keys = Object.keys(schema);
		var ts = Date.now();

		if (!insert) {
			if (self.meta.flow.paused)
				delete self.meta.flow.paused;

			if (self.meta.flow.groups)
				delete self.meta.flow.groups;

			if (self.meta.flow.tabs)
				delete self.meta.flow.tabs;
		}

		self.loading++;
		keys.wait(function(key, next) {

			if (BLACKLISTID[key]) {
				self.meta.flow[key] = schema[key];
				next();
				return;
			}

			var current = self.meta.flow[key];
			var instance = schema[key];
			var component = instance.component ? self.meta.components[instance.component] : null;

			// Component not found
			if (!component) {
				err.push(key, '"' + instance.component + '" component not found.');

				if (current) {
					current.isdestroyed = true;
					self.ondisconnect && self.ondisconnect(current);
					current.close && current.close.call(current);
					current.destroy && current.destroy.call(current);
				}

				delete self.meta.flow[key];
				next();
				return;
			}

			var fi = self.meta.flow[key];

			if (!fi || reinit) {
				self.meta.flow[key] = instance;
				var tmp = self.initcomponent(key, component);
				if (tmp)
					tmp.ts = ts;
			} else {
				fi.connections = instance.connections;
				fi.x = instance.x;
				fi.y = instance.y;
				fi.offset = instance.offset;
				fi.size = instance.size;
				fi.tab = instance.tab;
				fi.ts = ts;
				if (JSON.stringify(fi.config) !== JSON.stringify(instance.config)) {
					U.extend(fi.config, instance.config);
					fi.configure && fi.configure(fi.config);
					self.onreconfigure && self.onreconfigure(fi, true);
				}
			}

			next();

		}, function() {

			if (!insert) {
				for (var key in self.meta.flow) {
					if (!BLACKLISTID[key]) {
						var instance = self.meta.flow[key];
						if (instance.ts !== ts) {
							instance.ready = false;
							instance.isdestroyed = true;
							self.ondisconnect && self.ondisconnect(instance);
							instance.close && instance.close.call(instance);
							instance.destroy && instance.destroy.call(instance);
							delete self.meta.flow[key];
						}
					}
				}
			}

			self.loading--;
			self.cleanforce();
			self.$events.schema && self.emit('schema', self.meta.flow);
			callback && callback(err.length ? err : null);

		});

	} else {
		err.push('schema', 'Flow schema is invalid.');
		self.error(err, 'use');
		callback && callback(err);
	}

	return self;
};

FP.initcomponent = function(key, component) {

	var self = this;
	var instance = self.meta.flow[key];

	if (instance.ready) {

		// Closes old instance
		instance.ready = false;

		try {
			self.ondisconnect && self.ondisconnect(instance);
			instance.close && instance.close.call(instance);
		} catch (e) {
			self.onerror(e, 'instance_close', instance);
		}
	}

	instance.isinstance = true;
	instance.stats = { pending: 0, input: 0, output: 0, duration: 0, destroyed: 0 };
	instance.cache = {};
	instance.id = key;
	instance.module = component;
	instance.ready = false;

	if (instance.options) {
		instance.config = instance.options;
		delete instance.options;
	}

	var tmp = component.config;
	if (tmp)
		instance.config = instance.config ? U.extend(CLONE(tmp), instance.config) : CLONE(tmp);

	if (!instance.config)
		instance.config = {};

	instance.main = self;
	instance.dashboard = self.ondashboard;
	instance.status = self.onstatus;
	instance.debug = self.ondebug;
	instance.throw = self.onerror;
	instance.send = self.ontrigger;
	instance.newmessage = newmessage;
	instance.replace = variables;

	self.onconnect && self.onconnect(instance);

	try {
		component.make && component.make.call(instance, instance, instance.config);
	} catch (e) {
		self.error(e, 'instance_make', instance);
		return;
	}

	if (instance.open) {
		instance.open.call(instance, (function(instance) {
			return function() {
				if (instance) {
					instance.ready = true;
					delete instance.open;
				}
			};
		})(instance));
	} else
		instance.ready = true;

	// Notifies about the pause state
	if (self.paused && instance.pause)
		instance.pause(true);

	self.meta.flow[key] = instance;
	return instance;
};

function sendmessage(instance, message, event) {


	if (instance.isdestroyed || message.isdestroyed || instance.main.paused) {
		message.destroy();
		return;
	}

	if (event) {
		message.$events && message.$events.message && message.emit('message', message);
		message.main.$events && message.main.$events.message && message.main.emit('message', message);
	}

	try {

		var is = false;

		var key = 'message_' + message.input;

		if (instance[key]) {
			is = true;
			instance[key].call(message.instance, message);
		}

		if (instance.message) {
			instance.message.call(message.instance, message);
			is = true;
		}

		if (!is)
			message.destroy();

	} catch (e) {
		instance.main.error(e, 'instance_message', message);
		message.destroy();
	}
}

FP.$can = function(isinput, id, index) {
	var self = this;
	if (self.paused)
		return false;
	if (!self.meta.flow.paused)
		return true;
	var key = (isinput ? 'input' : 'output') + D + id + D + index;
	if (!self.meta.flow.paused[key])
		return true;
};

function trigger(self, path, data, controller, events) {
	self.trigger(path, data, controller, events);
}

// path = ID__INPUTINDEX
FP.trigger = function(path, data, controller, events) {

	var self = this;
	if (self.loading) {
		setTimeout(trigger, 200, self, path, data, controller, events);
		return;
	}

	if (self.paused)
		return;

	path = path.split(D);

	var inputindex = path.length === 1 ? 0 : path[1];

	var schema = self.meta.flow[path[0]];
	if (schema && schema.ready && schema.component && (schema.message || schema['message_' + inputindex])) {

		var instance = self.meta.components[schema.component];
		if (instance && instance.connected && !instance.disabled && self.$can(true, path[0], path[1])) {

			var ts = Date.now();
			var ismessage = data instanceof Message;
			var message = ismessage ? data.clone() : new Message();

			if (ismessage) {
				if (data.processed === 0) {
					data.processed = 1;
					data.main.stats.pending--;

					if (data.main.stats.pending < 0)
						data.main.stats.pending = 0;

					data.instance.stats.pending--;

					if (data.instance.stats.pending < 0)
						data.instance.stats.pending = 0;

					data.instance.stats.output++;
					data.instance.stats.duration = ts - self.ts;
				}
			} else {
				message.refs = { pending: 1 };
				message.$events = events || {};
				message.repo = {};
				message.data = data;
				message.duration = message.ts = ts;
				message.used = 1;
			}

			message.controller = controller;
			message.instance = schema;
			message.main = self;
			message.to = schema;
			message.toid = path[0];
			message.input = message.index = inputindex;
			message.tocomponent = instance.id;
			message.cache = instance.cache;

			message.processed = 0;

			schema.stats.input++;
			schema.stats.pending++;

			message.main.stats.pending++;
			message.main.stats.messages++;
			message.main.mm++;

			message.id = message.main.uid + message.main.stats.messages;
			message.count = message.main.stats.messages;

			if (message.fromid) {
				var tid = message.fromid + D + message.output;
				if (message.main.stats.traffic[tid])
					message.main.stats.traffic[tid]++;
				else {
					message.main.stats.traffic[tid] = 1;
					message.main.stats.traffic.priority.push(tid);
				}
			} else {
				message.from = null;
				message.fromid = null;
				message.fromcomponent = null;
				message.output = null;
			}

			setImmediate(sendmessage, schema, message, true);
			return message;
		}
	}
};

FP.trigger2 = function(path, data, controller) {

	var self = this;

	if (self.paused)
		return;

	var events = {};
	var obj;

	path = path.split(D);

	var counter = 0;
	for (var key in self.meta.flow) {
		var flow = self.meta.flow[key];
		if (flow.component === path[0])
			obj = self.trigger(key + D + (path.length === 1 ? 0 : path[1]), data, controller, events, counter++);
	}

	return obj;
};

FP.clear = function() {
	var self = this;
	self.meta.flow = {};
	return self;
};

FP.make = function(fn) {
	var self = this;
	fn.call(self, self);
	return self;
};

FP.find = function(id) {
	return this.meta.flow[id];
};

FP.send = function(path, body) {
	var self = this;
	if (!self.paused && self.meta && self.meta.flow) {
		path = path.split(D);
		var instance = self.meta.flow[path[0]];
		if (instance)
			instance.send(path[1], body);
		return !!instance;
	}
};

FP.add = function(name, body, callback) {

	var self = this;
	var meta = body.parseComponent({ readme: '<readme>', settings: '<settings>', css: '<style>', be: '<script total>', be2: '<script node>', js: '<script>', html: '<body>', schema: '<schema>', template: '<template>' });
	var node = (meta.be || meta.be2 || '');

	meta.id = name;
	meta.checksum = HASH(node).toString(36);

	var component = self.meta.components[name];
	if (component && component.ui && component.ui.checksum === meta.checksum) {
		component.ui = meta;
		component.ts = Date.now();
		callback && callback();
	} else {

		var fn;

		try {
			fn = new Function('exports', 'require', node);
		} catch (e) {
			self.error(e, 'add', name);
			callback && callback(e);
			return null;
		}

		delete meta.be;
		delete meta.be2;
		component = self.register(meta.id, fn, null, callback, true);
		if (component)
			component.ui = meta;
		else
			return null;
	}

	component.ui.raw = body;
	return component;
};

FP.instances = function() {

	var self = this;
	var arr = [];

	for (var key in self.meta.flow) {
		if (!BLACKLISTID[key]) {
			var instance = self.meta.flow[key];
			if (instance.ready)
				arr.push(instance);
		}
	}

	return arr;
};

FP.export_instance = function(id) {

	var self = this;
	var instance = self.meta.flow[id];
	if (instance) {

		if (BLACKLISTID[id])
			return CLONE(instance);

		var tmp = {};
		tmp.x = instance.x;
		tmp.y = instance.y;
		tmp.size = instance.size;
		tmp.offset = instance.offset;
		tmp.stats = CLONE(instance.stats);
		tmp.connections = CLONE(instance.connections);
		tmp.id = instance.id;
		tmp.config = CLONE(instance.config);
		tmp.component = instance.component;
		tmp.connected = true;
		tmp.note = instance.note;
		tmp.tab = instance.tab;
		tmp.reference = instance.reference;
		tmp.meta = instance.meta;

		if (instance.outputs)
			tmp.outputs = instance.outputs;

		if (instance.inputs)
			tmp.inputs = instance.inputs;

		return tmp;
	}
};

FP.export_component = function(id) {
	var self = this;
	var com = self.meta.components[id];
	if (com) {
		var obj = {};
		obj.id = com.id;
		obj.name = com.name;
		obj.title = com.title;
		obj.meta = com.meta;
		obj.type = com.type;
		obj.css = com.ui.css;
		obj.js = com.ui.js;
		obj.icon = com.icon;
		obj.color = com.color;
		obj.config = com.config;
		obj.html = com.ui.html;
		obj.readme = com.ui.readme;
		obj.template = com.ui.template;
		obj.settings = com.ui.settings;
		obj.inputs = com.inputs;
		obj.outputs = com.outputs;
		obj.group = com.group;
		obj.version = com.version;
		obj.author = com.author;
		obj.permissions = com.permissions;
		return obj;
	}
};

FP.export = function(type) {

	var self = this;

	if (type === 'components')
		return self.components(true);

	var output = {};

	for (var key in self.meta.flow)
		output[key] = self.export_instance(key);

	return output;
};

FP.components = function(prepare_export) {

	var self = this;
	var arr = [];

	for (var key in self.meta.components) {
		if (prepare_export)
			arr.push(self.export_component(key));
		else
			arr.push(self.meta.components[key]);
	}

	return arr;
};

exports.make = function(name, errorhandler) {
	return new Flow(name, errorhandler);
};

exports.prototypes = function() {
	var obj = {};
	obj.Message = Message.prototype;
	obj.FlowStream = Flow.prototype;
	return obj;
};