;(function(global) {
  "use strict";

var Kefir = global.Kefir;
if ((typeof Kefir === 'undefined') &&
    (typeof require !== 'undefined')) Kefir = require('kefir');
if (!Kefir) throw new Error('Kefir.js (https://github.com/rpominov/kefir) is required for Rpd to work');

var VERSION = 'v3.0.0-alpha';

var Rpd = (function() {

injectKefirEmitter();

// Rpd.NOTHING, Rpd.ID_LENGTH, ...

//var PATCH_PROPS = [ 'title', '*handle' ];
var nodetypes = {}; var NODE_PROPS = [ 'title', '*inlets', '*outlets', 'prepare', 'process', 'tune', '*handle' ];
var channeltypes = {}; var INLET_PROPS = [ 'label', 'default', 'hidden', 'cold', 'readonly', 'allow', 'accept', 'adapt', 'tune', 'show', '*handle' ];
                       var OUTLET_PROPS = [ 'label', 'tune', 'show', '*handle' ];
                       var CHANNEL_PROPS = INLET_PROPS;
var noderenderers = {}; var NODE_RENDERER_PROPS = [ 'prepare', 'size', 'first', 'always' ];
var channelrenderers = {}; var CHANNEL_RENDERER_PROPS = [ 'prepare', 'show', 'edit' ];
var nodedescriptions = {};
var styles = {};
var nodetypeicons = {};
var toolkiticons = {};

var renderer_registry = {};

var event_types = { 'network/add-patch': [ 'patch' ] };
var rpdEvent = create_event_map(event_types);
var rpdEvents = create_events_stream(event_types, rpdEvent, 'network', Rpd);

rpdEvent['network/add-patch'].onValue(function(patch) { rpdEvents.plug(patch.events); });

var rendering;

function ƒ(v) { return function() { return v; } }

function create_rendering_stream() {
    var rendering = Kefir.emitter();
    rendering.map(function(rule) {
        return {
            rule: rule,
            func: function(patch) {
                patch.render(rule.aliases, rule.targets, rule.config)
            }
        }
    }).scan(function(prev, curr) {
        if (prev) rpdEvent['network/add-patch'].offValue(prev.func);
        rpdEvent['network/add-patch'].onValue(curr.func);
        return curr;
    }, null).last().onValue(function(last) {
        rpdEvent['network/add-patch'].offValue(last.func);
    });
    return rendering;
}

function /*Rpd.*/renderNext(aliases, targets, conf) {
    if (!rendering) rendering = create_rendering_stream();
    rendering.emit({ aliases: aliases, targets: targets, config: conf });
}

function /*Rpd.*/stopRendering() {
    if (rendering) {
        rendering.end();
        rendering = null;
    }
}

function /*Rpd.*/addPatch(arg0, arg1, arg2) {
    return addClosedPatch(arg0, arg1).open(arg2);
}

function /*Rpd.*/addClosedPatch(arg0, arg1) {
    var name = !is_object(arg0) ? arg0 : undefined; var def = arg1 || arg0;
    var instance = new Patch(name, def);
    rpdEvent['network/add-patch'].emit(instance);
    return instance;
}

// =============================================================================
// ================================== Patch ====================================
// =============================================================================

function Patch(name, def) {
    this.id = short_uid();
    this.name = name;
    this.def = def || {};

    var patch = this;

    var event_types = {
        'patch/is-ready':      [ ],
        'patch/open':          [ 'parent' ],
        'patch/close':         [ ],
        'patch/move-canvas':   [ 'position' ],
        'patch/resize-canvas': [ 'size' ],
        'patch/set-inputs':    [ 'inputs' ],
        'patch/set-outputs':   [ 'outputs' ],
        'patch/project':       [ 'node', 'target' ],
        'patch/refer':         [ 'node', 'target' ],
        'patch/add-node':      [ 'node' ],
        'patch/remove-node':   [ 'node' ]
    };
    this.event = create_event_map(event_types);
    this.events = create_events_stream(event_types, this.event, 'patch', this);

    if (this.def.handle) subscribe(this.events, this.def.handle);

    // this stream controls the way patch events reach the assigned renderer
    this.renderQueue = Kefir.emitter();
    var renderStream = Kefir.combine([ this.events ],
                  [ this.renderQueue.scan(function(storage, rule) {
                        var alias = rule.alias, target = rule.target, configuration = rule.config;
                        var renderer = storage[alias];
                        if (!renderer) {
                            renderer = {};
                            renderer.produce = renderer_registry[alias](patch);
                            renderer.handlers = [];
                            storage[alias] = renderer;
                        }
                        if (renderer.produce) {
                            var handler = renderer.produce(target, configuration);
                            if (handler) {
                                renderer.handlers.push(
                                    (typeof handler === 'function') ? handler
                                                                    : function(event) {
                                                                          if (handler[event.type]) handler[event.type](event);
                                                                      } );
                            }
                        }
                        return storage;
                    }, {}) ]);
    // we need to wait for first renderer and then push there events happened before, then proceed
    renderStream = renderStream.bufferBy(this.renderQueue).take(1).flatten().concat(renderStream);
    renderStream.onValue(function(value) {
                    var event = value[0], renderers = value[1];
                    var aliases = Object.keys(renderers);
                    var renderer, handlers;
                    for (var i = 0, il = aliases.length; i < il; i++) {
                        renderer = renderers[aliases[i]]; handlers = renderer.handlers;
                        for (var j = 0, jl = handlers.length; j < jl; j++) {
                            handlers[j](inject_render(clone_obj(event), aliases[i]));
                        }
                    }
                });

    // projections are connections between different patches; patch inlets looking in the outer
    // world are called "inputs" here, and outlets looking in the outer world are, correspondingly,
    // called "outputs"
    this.projections = Kefir.emitter();
    Kefir.combine(
        [ this.projections ],
        [ this.event['patch/set-inputs'],
          this.event['patch/set-outputs'] ]
    ).onValue(function(value) {
        var node = value[0], inputs = value[1], outputs = value[2];
        var inlet, outlet, input, output;
        for (var i = 0; i < inputs.length; i++) {
            inlet = node.addInlet(inputs[i].type, inputs[i].alias, inputs[i].def, inputs[i].render);
            inlet.event['inlet/update'].onValue((function(input) {
                return function(value) { input.receive(value); };
            })(inputs[i]));
        } // use inlet.onUpdate?
        for (i = 0; i < outputs.length; i++) {
            outlet = node.addOutlet(outputs[i].type, outputs[i].alias, outputs[i].def, outputs[i].render);
            outputs[i].event['outlet/update'].onValue((function(outlet) {
                return function(value) { outlet.send(value); };
            })(outlet));
        } // use output.onUpdate?
        patch.event['patch/project'].emit({ node: node, target: node.patch });
        node.patch.event['patch/refer'].emit({ node: node, target: patch });
    });

    this.nodesToRemove = Kefir.emitter();

    this.event['patch/is-ready'].emit();
}
Patch.prototype.render = function(aliases, targets, config) {
    aliases = Array.isArray(aliases) ? aliases : [ aliases ];
    targets = Array.isArray(targets) ? targets : [ targets ];
    for (var i = 0, il = aliases.length, alias; i < il; i++) {
        for (var j = 0, jl = targets.length, target; j < jl; j++) {
            alias = aliases[i]; target = targets[j];
            if (!renderer_registry[alias]) report_system_error(this, 'patch', 'Renderer \'' + alias + '\' is not registered');
            this.renderQueue.emit({ alias: alias, target: target, config: config });
        }
    }
    return this;
}
Patch.prototype.addNode = function(type, arg1, arg2, arg3) {
    var patch = this;

    var def = arg2 ? arg2 : (is_object(arg1) ? (arg1 || {}) : {});
    var title = is_object(arg1) ? undefined : arg1;
    if (title) def.title = title;
    var render = (arg1 && is_object(arg1)) ? arg2 : arg3;

    var node = new Node(type, this, def, render, function(node) {
        patch.events.plug(node.events);
        patch.event['patch/add-node'].emit(node);

        node.turnOn();
    });

    return node;
}
Patch.prototype.removeNode = function(node) {
    node.turnOff();
    this.event['patch/remove-node'].emit(node);
    this.events.unplug(node.events);
    return this;
}
Patch.prototype.open = function(parent) {
    this.event['patch/open'].emit(parent);
    return this;
}
Patch.prototype.close = function() {
    this.event['patch/close'].emit();
    return this;
}
Patch.prototype.inputs = function(list) {
    this.event['patch/set-inputs'].emit(list);
    return this;
}
Patch.prototype.outputs = function(list) {
    this.event['patch/set-outputs'].emit(list);
    return this;
}
Patch.prototype.project = function(node) {
    this.projections.emit(node);
    return this;
}
Patch.prototype.moveCanvas = function(x, y) {
    this.event['patch/move-canvas'].emit([x, y]);
    return this;
}
Patch.prototype.resizeCanvas = function(width, height) {
    this.event['patch/resize-canvas'].emit([width, height]);
    return this;
}

// =============================================================================
// ================================= Node ======================================
// =============================================================================

function Node(type, patch, def, render, callback) {
    this.type = type || 'core/basic';
    this.toolkit = extract_toolkit(type);
    this.id = short_uid();
    this.patch = patch;

    var event_types = {
        'node/turn-on':       [ ],
        'node/is-ready':      [ ],
        'node/process':       [ 'inlets', 'outlets' ],
        'node/turn-off':      [ ],
        'node/add-inlet':     [ 'inlet' ],
        'node/remove-inlet':  [ 'inlet' ],
        'node/add-outlet':    [ 'outlet' ],
        'node/remove-outlet': [ 'outlet' ],
        'node/move':          [ 'position' ],
        'node/configure':    [ 'props' ]
    };
    this.event = create_event_map(event_types);
    this.events = create_events_stream(event_types, this.event, 'node', this);

    var type_def = adapt_to_obj(nodetypes[this.type], this);
    if (!type_def) report_system_error(this, 'node', 'Node type \'' + this.type + '\' is not registered!');
    this.def = join_definitions(NODE_PROPS, def, type_def);

    this.render = join_render_definitions(NODE_RENDERER_PROPS, render,
                      prepare_render_obj(noderenderers[this.type], this));

    if (callback) callback(this);

    var node = this;

    if (this.def.handle) subscribe(this.events, this.def.handle);

    if (this.def.process) {

        var process_f = this.def.process;

        var process = Kefir.combine([

            // when new inlet was added, start monitoring its updates
            // as an active stream
            this.event['node/add-inlet'].flatMap(function(inlet) {
                var updates = inlet.event['inlet/update'].map(function(value) {
                    return { inlet: inlet, value: value };
                });;
                if (node.def.tune) updates = node.def.tune.bind(node)(updates);
                return updates;
            })

        ],
        [
            // collect all the existing outlets aliases as a passive stream
            this.event['node/add-outlet'].scan(function(storage, outlet) {
                storage[outlet.alias] = outlet;
                return storage;
            }, {})

        ])

        // do not fire any event until node is ready, then immediately fire them one by one, if any occured;
        // later events are fired after node/is-ready, corresponding to their time of firing, as usual
        process = process.bufferBy(this.event['node/is-ready']).take(1).flatten().concat(process);

        process = process.scan(function(storage, update) {
            // update[0] is inlet value update, update[1] is a list of outlets
            var inlet = update[0].inlet;
            var alias = inlet.alias;
            storage.inlets.prev[alias] = storage.inlets.cur[alias];
            storage.inlets.cur[alias] = update[0].value;
            storage.outlets = update[1];
            storage.source = inlet;
            return storage;
        }, { inlets: { prev: {}, cur: {} }, outlets: {} }).changes();

        // filter cold inlets, so the update data will be stored, but process event won't fire
        process = process.filter(function(data) { return !data.source.def.cold; });

        process.onValue(function(data) {
            // call a node/process event using collected inlet values
            var outlets_vals = process_f.bind(node)(data.inlets.cur, data.inlets.prev);
            node.event['node/process'].emit({ inlets: data.inlets.cur, outlets: outlets_vals });
            // send the values provided from a `process` function to corresponding outlets
            var outlets = data.outlets;
            for (var outlet_name in outlets_vals) {
                if (outlets[outlet_name]) {
                    if (outlets_vals[outlet_name] instanceof Kefir.Stream) {
                        outlets[outlet_name].stream(outlets_vals[outlet_name]);
                    } else {
                        outlets[outlet_name].send(outlets_vals[outlet_name]);
                    }
                };
            }
        });

    }

    // only inlets / outlets described in type definition are stored inside
    // (since they are constructed once), it lets user easily connect to them

    if (this.def.inlets) {
        this.inlets = {};
        for (var alias in this.def.inlets) {
            var conf = this.def.inlets[alias];
            var inlet = this.addInlet(conf.type, alias, conf);
            this.inlets[alias] = inlet;
        }
    }

    if (this.def.outlets) {
        this.outlets = {};
        for (var alias in this.def.outlets) {
            var conf = this.def.outlets[alias];
            var outlet = this.addOutlet(conf.type, alias, conf);
            if(conf.type )
            this.outlets[alias] = outlet;
        }
    }

    if (this.def.prepare) this.def.prepare.bind(this)(this.inlets, this.outlets);

    this.event['node/is-ready'].emit();
}
Node.prototype.turnOn = function() {
    this.event['node/turn-on'].emit();
    return this;
}
Node.prototype.turnOff = function() {
    this.event['node/turn-off'].emit();
    return this;
}
Node.prototype.addInlet = function(type, alias, arg2, arg3, arg4) {
    var def = arg3 ? arg3 : (is_object(arg2) ? (arg2 || {}) : {});
    var label = is_object(arg2) ? undefined : arg2;
    if (label) def.label = label;
    var render = (arg2 && is_object(arg2)) ? arg3 : arg4;

    var inlet = new Inlet(type, this, alias, def, render);
    this.events.plug(inlet.events);

    this.event['node/add-inlet'].emit(inlet);
    inlet.toDefault();
    return inlet;
}
Node.prototype.addOutlet = function(type, alias, arg2, arg3, arg4) {
    console.log('here2');
    var def = arg3 ? arg3 : (is_object(arg2) ? (arg2 || {}) : {});
    var label = is_object(arg2) ? undefined : arg2;
    if (label) def.label = label;
    var render = (arg2 && is_object(arg2)) ? arg3 : arg4;

    var outlet = new Outlet(type, this, alias, def, render);
    this.events.plug(outlet.events);

    var options = {
        formatContainer: function(cont) {
            cont.className = 'tree';
            return cont;
        },
        formatProperty: function(prop) {
            var strong = document.createElement('strong');
            var div = document.createElement('div');
            strong.appendChild(prop);
            return strong;
        },
        formatValue: function(value) {
            var connector = document.createElement('td');
            connector.classList.add('rpd-connector');
            connector.appendChild(value);

            return connector;
        }
    };

    var flattenObject = function(ob) {
        var toReturn = {};

        for (var i in ob) {
            if (!ob.hasOwnProperty(i)) continue;
            if ((typeof ob[i]) == 'object') {
                var flatObject = flattenObject(ob[i]);
                for (var x in flatObject) {
                    if (!flatObject.hasOwnProperty(x)) continue;
                    if(x==='type') {
                        toReturn[i] = flatObject[x];
                    } else {
                        toReturn[i + '.' + x] = flatObject[x];
                    }
                }
            } else {
                toReturn[i] = ob[i];
            }
        }
        return toReturn;
    };
    
    if(type === 'core/object') {
        var json = {};
        json[alias] = arg2;
        var obj = flattenObject(json);
        var keysAlt = Object.keys(obj);

        keysAlt.sort()

        if(keysAlt.length > 0) {
            var first = keysAlt[0];
            var split = first.split('.');
            if(keysAlt.length > 1) {
                outlet = new Outlet('core/any', this, split[0], def, render, 1, false, split[0], alias);
                this.outlets[split[0]] = outlet;
            }
            else {
                outlet = new Outlet('core/any', this, split[0], def, render, 1, true, split[0], alias);
                this.outlets[split[0]] = outlet;
            }
            this.events.plug(outlet.events);
            this.event['node/add-outlet'].emit(outlet);
            for(var i=1;i<keysAlt.length;i++) {
                var key = keysAlt[i];
                var arr = key.split('.');
                var last = json;
                for(var j=0;j<arr.length;j++) {
                    var k = arr[j]
                    last = last[k]
                }
                if(Object.keys(last).length > 1) {
                    outlet = new Outlet('core/any', this, key, def, render, arr.length, false, arr[arr.length-1], alias + ' ' + arr[arr.length-1]);
                    this.outlets[key] = outlet;
                } else {
                    outlet = new Outlet('core/any', this, key, def, render, arr.length, true, arr[arr.length-1], alias + ' ' + arr[arr.length-2]);
                    this.outlets[key] = outlet;
                }
                this.events.plug(outlet.events);
                this.event['node/add-outlet'].emit(outlet);
            }
        } else {
            // this.event['node/add-outlet'].emit(outlet);
        }
    } else {
        this.event['node/add-outlet'].emit(outlet);
    }
    outlet.toDefault();
    return outlet;
}
Node.prototype.removeInlet = function(inlet) {
    this.event['node/remove-inlet'].emit(inlet);
    this.events.unplug(inlet.events);
    return this;
}
Node.prototype.removeOutlet = function(outlet) {
    this.event['node/remove-outlet'].emit(outlet);
    this.events.unplug(outlet.events);
    return this;
}
Node.prototype.move = function(x, y) {
    this.event['node/move'].emit([ x, y ]);
    return this;
}
Node.prototype.configure = function(props) {
    this.event['node/configure'].emit(props);
    return this;
}

// =============================================================================
// ================================== Inlet ====================================
// =============================================================================

function Inlet(type, node, alias, def, render) {
    this.type = type || 'core/any';
    this.toolkit = extract_toolkit(type);
    this.id = short_uid();
    this.alias = alias;
    this.node = node;

    var type_def = adapt_to_obj(channeltypes[this.type], this);
    if (!type_def) report_system_error(this, 'inlet', 'Channel type \'' + this.type + '\' is not registered!');
    this.def = join_definitions(INLET_PROPS, def, type_def);

    if (!this.alias) report_error(this, 'inlet', 'Inlet should have an alias');

    this.value = Kefir.pool();

    this.render = join_render_definitions(CHANNEL_RENDERER_PROPS, render,
                      prepare_render_obj(channelrenderers[this.type], this));

    var event_types = {
        'inlet/update': [ 'value' ]
    };
    this.event = create_event_map(event_types);
    var orig_updates = this.event['inlet/update'];
    var updates = orig_updates.merge(this.value);
    if (this.def.tune) updates = this.def.tune.bind(this)(updates);
    if (this.def.accept) updates = updates.flatten(function(v) {
        if (this.def.accept(v)) { return [v]; } else {
            orig_updates.error(make_silent_error(this, 'inlet')); return [];
        }
    }.bind(this));
    if (this.def.adapt) updates = updates.map(this.def.adapt);
    // rewrite with the modified stream
    this.event['inlet/update'] = updates.onValue(function(){});
    this.events = create_events_stream(event_types, this.event, 'inlet', this);

    if (this.def.handle) subscribe(this.events, this.def.handle);
}
Inlet.prototype.receive = function(value) {
    this.value.plug(Kefir.constant(value));
    return this;
}
Inlet.prototype.stream = function(stream) {
    this.value.plug(stream);
    return this;
}
Inlet.prototype.toDefault = function() {
    if (is_defined(this.def.default)) {
        if (this.def.default instanceof Kefir.Stream) {
            this.stream(this.def.default);
        } else this.receive(this.def.default);
    }
    return this;
}
Inlet.prototype.allows = function(outlet) {
    if (this.type === 'core/any') return true;
    if (outlet.type === this.type) return true;
    if (!this.def.allow && (outlet.type !== this.type)) return false;
    if (this.def.allow) {
        var matched = false;
        this.def.allow.forEach(function(allowedType) {
            if (outlet.type === allowedType) { matched = true; };
        });
        return matched;
    }
    return true;
}

// =============================================================================
// ================================= Outlet ====================================
// =============================================================================

function Outlet(type, node, alias, def, render, space, conn, label, parent) {
    this.type = type || 'core/any';
    this.toolkit = extract_toolkit(type);
    this.id = short_uid();
    this.alias = alias;
    this.label = label;
    this.node = node;
    this.space = space;
    this.showConnector = conn !== undefined ? conn : true;
    this.parent = parent;
    this.links = [];

    var type_def = adapt_to_obj(channeltypes[this.type], this);
    if (!type_def) report_system_error(this, 'outlet', 'Channel type \'' + this.type + '\' is not registered!');
    this.def = join_definitions(OUTLET_PROPS, def, type_def);

    if (!this.alias) report_error(this, 'outlet', 'Outlet should have an alias');

    this.value = Kefir.pool();

    this.render = join_render_definitions(CHANNEL_RENDERER_PROPS, render,
                      prepare_render_obj(channelrenderers[this.type], this));

    // outlets values are not editable

    var event_types = {
        'outlet/update':     [ 'value' ],
        'outlet/connect':    [ 'link', 'inlet' ],
        'outlet/disconnect': [ 'link' ]
    };
    this.event = create_event_map(event_types);
    var orig_updates = this.event['outlet/update'];
    var updates = orig_updates.merge(this.value);
    // rewrite with the modified stream
    this.event['outlet/update'] = updates.onValue(function(v){});
    this.events = create_events_stream(event_types, this.event, 'outlet', this);

    if (this.def.handle) subscribe(this.events, this.def.handle);

    // re-send last value on connection
    var outlet = this;
    Kefir.combine([ this.event['outlet/connect'] ],
                  [ this.event['outlet/update'] ])
         .onValue(function(update) {
             outlet.value.plug(Kefir.constant(update[1]));
         });

}
Outlet.prototype.connect = function(inlet) {
    if (!inlet.allows(this)) {
        report_error(this, 'outlet', 'Outlet of type \'' + this.type + '\' is not allowed to connect to inlet of type \'' + inlet.type + '\'');
    }
    var link = new Link(this, inlet);
    this.events.plug(link.events);
    this.value.onValue(link.receiver);

    var f = this.links.find(function (link) {
        return link.inlet.alias == inlet.alias && link.inlet.node.id == inlet.node.id;
    });

    if(!f) {
        this.links.push(link);
        this.event['outlet/connect'].emit({ link: link, inlet: inlet });
    }
    return link;
}
Outlet.prototype.disconnect = function(link) {
    this.links = this.links.filter(function (linked) {
        return linked.id !== link.id;
    });
    this.event['outlet/disconnect'].emit(link);
    this.value.offValue(link.receiver);
    this.events.unplug(link.events);
    return this;
}
Outlet.prototype.send = function(value) {
    this.value.plug(Kefir.constant(value));
    return this;
}
Outlet.prototype.stream = function(stream) {
    this.value.plug(stream);
    return this;
}
Outlet.prototype.toDefault = function() {
    if (is_defined(this.def.default)) {
        if (this.def.default instanceof Kefir.Stream) {
            this.stream(this.def.default);
        } else this.send(this.def.default);
    }
    return this;
}

// =============================================================================
// ================================= Link ======================================
// =============================================================================

function Link(outlet, inlet, label) {
    this.id = short_uid();

    this.name = label || '';

    this.outlet = outlet;
    this.inlet = inlet;

    this.value = Kefir.emitter();

    var link = this;

    this.receiver = (outlet.node.id !== inlet.node.id)
        ? function(x) { link.pass(x); }
        : function(x) {
            // this avoids stack overflow on recursive connections
            setTimeout(function() { link.pass(x); }, 0);
        };

    var event_types = {
        'link/enable':  [ ],
        'link/disable': [ ],
        'link/pass':    [ 'value' ]
    };

    this.event = create_event_map(event_types);
    var orig_updates = this.event['link/pass'];
    var updates = orig_updates.merge(this.value);
    // rewrite with the modified stream
    this.event['link/pass'] = updates.onValue(function(v){});
    this.events = create_events_stream(event_types, this.event, 'link', this);

    this.enabled = Kefir.merge([ this.event['link/disable'].map(ƒ(false)),
                                 this.event['link/enable'].map(ƒ(true)) ]).toProperty(ƒ(true));

    this.event['link/pass'].filterBy(this.enabled).onValue(function(value) {
        inlet.receive(value);
    });

    // re-send last value on enable
    Kefir.combine([ this.event['link/enable'] ],
                  [ this.event['link/pass'] ])
         .onValue(function(event) {
              link.pass(event[1]);
          });
}
Link.prototype.pass = function(value) {
    this.value.emit(value);
    return this;
}
Link.prototype.enable = function() {
    this.event['link/enable'].emit();
    return this;
}
Link.prototype.disable = function() {
    this.event['link/disable'].emit();
    return this;
}
Link.prototype.disconnect = function() {
    this.outlet.disconnect(this);
    return this;
}

// =============================================================================
// ================================== utils ====================================
// =============================================================================

function injectKefirEmitter() {
    Kefir.emitter = function() {
        var e, stream = Kefir.stream(function(_e) {
            e = _e; return function() { e = undefined; }
        });
        stream.emit = function(x) { e && e.emit(x); return this; }
        stream.error = function(x) { e && e.error(x); return this; }
        stream.end = function() { e && e.end(); return this; }
        stream.emitEvent = function(x) { e && e.emitEvent(x); return this; }
        return stream.setName('emitter');
    }
}

function join_definitions(keys, src1, src2) {
    var trg = {}; src1 = src1 || {}; src2 = src2 || {};
    var key;
    for (var i = 0, il = keys.length; i < il; i++) {
        key = keys[i];
        if (key[0] !== '*') {
            if (!(key in src1) && !(key in src2)) continue;
            trg[key] = is_defined(src1[key]) ? src1[key] : src2[key];
        } else {
            key = key.slice(1);
            if (!(key in src1) && !(key in src2)) continue;
            trg[key] = {};
            var src2_keys = src2[key] ? Object.keys(src2[key]) : [];
            for (var j = 0, jl = src2_keys.length; j < jl; j++) {
                trg[key][src2_keys[j]] = src2[key][src2_keys[j]];
            }
            var src1_keys = src1[key] ? Object.keys(src1[key]) : [];
            for (var j = 0, jl = src1_keys.length; j < jl; j++) {
                trg[key][src1_keys[j]] = src1[key][src1_keys[j]];
            }
        }
    }
    return trg;
}

function clone_obj(src) {
    // this way is not a deep-copy and actually not cloning at all, but that's ok,
    // since we use it few times for events, which are simple objects and the objects they
    // pass, should be the same objects they got; just events by themselves should be different.
    var res = {}; var keys = Object.keys(src);
    for (var i = 0, il = keys.length; i < il; i++) {
        res[keys[i]] = src[keys[i]];
    }
    return res;
}

function is_object(val) {
    return (typeof val === 'object');
}

function is_defined(val) {
    return (typeof val !== 'undefined');
}

function adapt_to_obj(val, subj) {
    if (!val) return null;
    if (typeof val === 'function') return val(subj);
    return val;
}

function prepare_render_obj(template, subj) {
    if (!template) return {};
    var render_obj = {};
    for (var render_type in template) {
        render_obj[render_type] = adapt_to_obj(template[render_type], subj);
    }
    return render_obj;
}

function join_render_definitions(keys, user_render, type_render) {
    if (!user_render) return type_render;
    var result = {};
    for (var render_type in type_render) {
        result[render_type] = join_definitions(keys, user_render[render_type], type_render[render_type]);
    }
    for (var render_type in user_render) {
        if (!result[render_type] && !type_render[render_type]) result[render_type] = user_render[render_type];
    }
    return result;
}

function create_event_map(conf) {
    var map = {}; var types = Object.keys(conf);
    for (var i = 0, type; i < types.length; i++) {
        type = types[i];
        map[type] = Kefir.emitter();
    }
    return map;
}

function adapt_events(type, spec) {
    if (spec.length === 0) return function() { return { type: type } };
    if (spec.length === 1) return function(value) { var evt = {}; evt.type = type; evt[spec[0]] = value; return evt; };
    if (spec.length > 1)   return function(event) { event = clone_obj(event); event.type = type; return event; };
}
function create_events_stream(conf, event_map, subj_as, subj) {
    var stream = Kefir.pool(); var types = Object.keys(conf);
    for (var i = 0; i < types.length; i++) {
        stream.plug(event_map[types[i]]
                         .map(adapt_events(types[i], conf[types[i]]))
                         .map(function(evt) {
                             evt[subj_as] = subj;
                             return evt;
                         }));
    }
    return stream;
}

function subscribe(events, handlers) {
    events.filter(function(event) { return handlers[event.type]; })
          .onValue(function(event) {
              handlers[event.type](event);
          });
}

function make_silent_error(subject, subject_name) {
    return make_error(subject, subject_name, null, false, true);
}

function make_error(subject, subject_name, message, is_system, is_silent) {
    return { type: subject_name + '/error', system: is_system || false,
             subject: subject, message: message, silent: is_silent || false };
}

function report_error(subject, subject_name, message, is_system) {
    rpdEvents.plug(Kefir.constantError(make_error(subject, subject_name, message, is_system)));
}

function report_system_error(subject, subject_name, message) {
    report_error(subject, subject_name, message, true);
}

function short_uid() {
    return ("0000" + (Math.random() * Math.pow(36,4) << 0).toString(36)).slice(-4);
}

function inject_render(update, alias) {
    var type = update.type;
    if ((type === 'patch/add-node') || (type === 'node/process')) {
        update.render = update.node.render[alias] || {};
    } else if ((type === 'node/add-inlet')  || (type === 'inlet/update')) {
        update.render = update.inlet.render[alias] || {};
    } else if ((type === 'node/add-outlet')  || (type === 'outlet/update')) {
        update.render = update.outlet.render[alias] || {};
    }
    return update;
}

function get_style(name, renderer) {
    if (!name) report_system_error(null, 'network', 'Unknown style requested: \'' + name + '\'');
    if (!styles[name]) report_system_error(null, 'network', 'Style \'' + name + '\' is not registered');
    var style = styles[name][renderer];
    if (!style) report_system_error(null, 'network', 'Style \'' + name + '\' has no definition for \'' + renderer + '\' renderer');
    return style;
}

function extract_toolkit(type) {
    var slashPos = type.indexOf('/');
    return (slashPos >= 0) ? type.substring(0, slashPos) : '';
}

// =============================================================================
// =========================== registration ====================================
// =============================================================================

function nodetype(type, def) {
    nodetypes[type] = def || {};
}

function channeltype(type, def) {
    channeltypes[type] = def || {};
}

function renderer(alias, f) {
    renderer_registry[alias] = f;
}

function noderenderer(type, alias, data) {
    if (!nodetypes[type]) report_system_error(null, 'network', 'Node type \'' + type + '\' is not registered');
    if (!noderenderers[type]) noderenderers[type] = {};
    noderenderers[type][alias] = data;
}

function channelrenderer(type, alias, data) {
    if (!channeltypes[type]) report_system_error(null, 'network', 'Channel type \'' + type + '\' is not registered');
    if (!channelrenderers[type]) channelrenderers[type] = {};
    channelrenderers[type][alias] = data;
}

function nodedescription(type, description) {
    nodedescriptions[type] = description;
}

function style(name, renderer, func) {
    if (!styles[name]) styles[name] = {};
    styles[name][renderer] = func;
}

function toolkiticon(toolkit, icon) {
    toolkiticons[toolkit] = icon;
}

function nodetypeicon(type, icon) {
    nodetypeicons[type] = icon;
}

nodetype('core/basic', {});
nodetype('core/reference', {});
channeltype('core/any', {});

// =============================================================================
// ========================== stringification ==================================
// =============================================================================

var stringify = {};
stringify.patch = function(patch) {
    return '[ Patch' + (patch.name ? (' \'' + patch.name + '\'') : ' <Unnamed>') + ' ]';
};
stringify.node = function(node) {
    return '[ Node (' + node.type + ')' +
              (node.def ? (node.def.title ? (' \'' + node.def.title + '\'') : ' <Untitled>') : ' <Unprepared>') +
              ' #' + node.id +
              ' ]';
};
stringify.outlet = function(outlet) {
    return '[ Outlet (' + outlet.type + ')' +
              (outlet.alias ? (' \'' + outlet.alias + '\'') : ' <Unaliased>') +
              (outlet.def ? (outlet.def.label ? (' \'' + outlet.def.label + '\'') : ' <Unlabeled>') : ' <Unprepared>') +
              ' #' + outlet.id +
              ' ]';
};
stringify.inlet = function(inlet) {
    return '[ Inlet (' + inlet.type + ')' +
              (inlet.alias ? (' \'' + inlet.alias + '\'') : ' <Unaliased>') +
              (inlet.def ? (inlet.def.label ? (' \'' + inlet.def.label + '\'') : ' <Unlabeled>') : ' <Unprepared>') +
              ' #' + inlet.id +
              (inlet.def.hidden ? ' (hidden)' : '') +
              (inlet.def.cold ? ' (cold)' : '') +
              ' ]';
};
stringify.link = function(link) {
    return '[ Link (' + link.type + ')' +
              ' \'' + link.name + '\'' +
              ' #' + link.id +
              ' ' + stringify.outlet(link.outlet) + ' ->' +
              ' ' + stringify.inlet(link.inlet) +
              ' ]';
};

function autoStringify(value) {
    if (value instanceof Patch)  { return stringify.patch(value); }
    if (value instanceof Node)   { return stringify.node(value); }
    if (value instanceof Inlet)  { return stringify.inlet(value); }
    if (value instanceof Outlet) { return stringify.outlet(value); }
    if (value instanceof Link)   { return stringify.link(value); }
    return '<?> ' + value;
}

// =============================================================================
// =============================== export ======================================
// =============================================================================

return {

    'VERSION': VERSION,

    '_': { 'Patch': Patch, 'Node': Node, 'Inlet': Inlet, 'Outlet': Outlet, 'Link': Link },

    'unit': ƒ,
    'not': function(value) { return !value; },

    'event': rpdEvent,
    'events': rpdEvents,

    'addPatch': addPatch,
    'addClosedPatch': addClosedPatch,
    'renderNext': renderNext,
    'stopRendering': stopRendering,

    'nodetype': nodetype,
    'channeltype': channeltype,
    'nodedescription': nodedescription,

    'renderer': renderer, 'styles': styles, 'style': style,
    'noderenderer': noderenderer,
    'channelrenderer': channelrenderer,

    'toolkiticon': toolkiticon,
    'nodetypeicon': nodetypeicon,

    'import': {}, 'export': {},

    'allNodeTypes': nodetypes,
    'allChannelTypes': channeltypes,
    'allNodeRenderers': noderenderers,
    'allChannelRenderers': channelrenderers,
    'allNodeDescriptions': nodedescriptions,
    'allNodeTypeIcons': nodetypeicons,
    'allToolkitIcons': toolkiticons,

    'getStyle': get_style,
    'reportError': report_error,
    'reportSystemError': report_system_error,

    'short_uid': short_uid,

    'stringify': stringify,
    'autoStringify': autoStringify

}

})();

if (typeof define === 'function' && define.amd) {
    define([], function() { return Rpd; });
    global.Rpd = Rpd;
} else if (typeof module === 'object' && typeof exports === 'object') {
    module.exports = Rpd;
    Rpd.Rpd = Rpd;
} else {
    global.Rpd = Rpd;
}

}(this));
